├── .gitignore ├── code ├── ec-sim-zs │ ├── .gitignore │ ├── generate_default.sh │ ├── convert_open.sh │ ├── utils │ │ ├── blocktree.py │ │ └── driver.py │ └── main.go ├── vrf-chain-sim │ ├── test.py │ ├── improved_biasable_block_withholding.py │ ├── biasable_block_whithholding.py │ ├── find_pattern2.py │ ├── frozen_vrf_biasability.py │ └── find_pattern.py ├── other-sims │ ├── utils.py │ ├── generate_chain.py │ ├── epochboundary_large.py │ ├── cq_closed_form.py │ ├── max_tot_null.py │ ├── pure_withholding.py │ ├── continuous_hs.py │ ├── case2.py │ ├── case2_mp.py │ ├── simple_withholding.py │ ├── case4.py │ ├── case4_mp.py │ ├── epoch_boundary_end.py │ ├── case1_maxTotalCatchup.py │ ├── finality_fixedlength.py │ ├── early_chain_quality_sim.py │ ├── case3_mp.py │ ├── headstart.py │ ├── probaofcatchingup.py │ ├── probasucessfulHS.py │ ├── catchup_epoch_boundary.py │ ├── headstart_henri.py │ ├── canadversarycatchup.py │ ├── lookfwd.py │ ├── ec_markov.py │ ├── grinding.py │ ├── grinding.go │ ├── epochboundary.py │ ├── general_grinding.py │ └── ec_withhold.py ├── attacks │ ├── forking-factor.py │ └── attack-calcs.py ├── ec-sim-w │ ├── utils.go │ └── main.go ├── beacon-sim │ └── leaderelection.py └── README.md ├── COPYRIGHT ├── LICENSE-APACHE ├── research-notes ├── tools.md ├── interfaces.md ├── attacks.md ├── glossary.md ├── randomness.md ├── waiting.md └── open-questions.md ├── CONTRIBUTING.md ├── LICENSE-MIT ├── specs └── ec │ ├── MC.tla │ └── ec.tla └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.png 3 | *.dot 4 | -------------------------------------------------------------------------------- /code/ec-sim-zs/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | ec-sim-zs 4 | output/* 5 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | This library is dual-licensed under Apache 2.0 and MIT terms. 2 | -------------------------------------------------------------------------------- /code/ec-sim-zs/generate_default.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | go build && ./ec-sim-zs 3 | -------------------------------------------------------------------------------- /code/ec-sim-zs/convert_open.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | dot -Tpng $1 -o chain.png && open chain.png 3 | -------------------------------------------------------------------------------- /code/vrf-chain-sim/test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def new_node(n,slot,weight,parent=0): 4 | return { 5 | 'n': n, 6 | 'slot': slot, 7 | 'weight':weight, 8 | 'parent':parent 9 | } 10 | 11 | nnode = new_node (1,2,3,4) 12 | 13 | print(nnode['weight']) 14 | 15 | list_oh_weight = {i: 0 for i in range(10)} 16 | 17 | print(list_oh_weight) 18 | 19 | l =[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] 20 | frozen = 5 21 | vrf_ca = [l[i] for i in range(len(l)) if i%frozen == 0] 22 | print(l==[l[0]]+l[1:]) 23 | print(2%1) 24 | print(vrf_ca) -------------------------------------------------------------------------------- /code/other-sims/utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy as np 3 | # -> Pr[ X = k ] = Binomial(k, s * f, e / s) s number of sectors 4 | def binomial(k,n,p): 5 | return math.comb(int(n),int(k)) * np.power(p,k) * np.power((1-p),(n-k)) 6 | 7 | # Pr[ X < k] 8 | def binomial_cdf(upto,n,p): 9 | return sum([binomial(k,n,p) for k in range(upto)]) 10 | 11 | def poisson(k,rate): 12 | return rate**k * np.exp(-rate) / math.factorial(k) 13 | 14 | # Pr[ X < upto] 15 | def poisson_cdf(upto,rate): 16 | return sum([poisson(k,rate) for k in range(upto)]) 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /code/other-sims/generate_chain.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | ntot = 10000 4 | height = 20 5 | p = 1./float(1*ntot) 6 | sim = 4 7 | qs = [.10, .25, .33, .49] 8 | # qs = [k/100.0 for k in range (10, 32, 5)] 9 | 10 | for s in range(sim): 11 | for q in qs: 12 | nh = round((1-q)*ntot) 13 | na = round(q*ntot) 14 | ch = np.random.binomial(nh, p, height) 15 | ca = np.random.binomial(na, p, height) 16 | print "attacker power is {q}".format(q = q) 17 | print "honest is {ch}".format(ch = ch) 18 | print "advers is {ca}".format(ca = ca) 19 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Copyright 2019 by the Filecoin contributors. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /research-notes/tools.md: -------------------------------------------------------------------------------- 1 | ##tools 2 | 3 | author: @sternhenri 4 | 5 | --------- 6 | 7 | - Slashing 8 | - Can help with: double spending, nothing at stake 9 | - Eg provable deviations get slashed, punish all orphans (casper), 10 | - Choice of randomness 11 | - Can help with: *-grinding 12 | - Eg checkpointing, or far-away lookback, or multi-chain 13 | - Confirmation times/freezing time 14 | - Can help with: *-grinding, double spend 15 | - Weighting function 16 | - Can help with: *-grinding, Selfish mining (support needed for blocks) 17 | - Well-defined provable deviations 18 | - Finality 19 | - Can help with double spending, nothing at stake 20 | - Through Checkpointing, can help with costless sim attack -------------------------------------------------------------------------------- /research-notes/interfaces.md: -------------------------------------------------------------------------------- 1 | ##Interfaces 2 | 3 | author: @sternhenri 4 | 5 | ----------- 6 | 7 | **EC** 8 | 9 | (randomness, distribution, weighting fn) -> (~1 provable leader) 10 | 11 | Must guarantee: 12 | 13 | - eventual leader (on expectation one) found proportional to distrib 14 | - locally predictable, only at given epoch. (ie secret leader) 15 | 16 | **SPC** 17 | 18 | (network members, pledged storage) -> (power table -> regular proofs of storage from all members) 19 | 20 | Must guarantee: 21 | 22 | - Pledged and used storage is reflected in leader choice 23 | - Chosen leader does not influence power table 24 | 25 | **FIL** 26 | 27 | (Storage, computation) -> (FIL) 28 | 29 | Must guarantee: 30 | 31 | - All dealt storage is accounted for at regular intervals -------------------------------------------------------------------------------- /research-notes/attacks.md: -------------------------------------------------------------------------------- 1 | ## Attacks 2 | 3 | author: @sternhenri 4 | 5 | ----------- 6 | 7 | We must keep in mind these attacks (effect): 8 | 9 | - Block Grinding (soundness) 10 | - Power table Grinding (soundness) 11 | - Challenge grinding (soundness) 12 | - Attack affecting multi-parent consensus (where chain weighting is involved): grind for a while to gain advantage for chain weight in time 13 | - Double Spending (soundness) 14 | - Nothing at stake (consistency/convergence) 15 | - Selfish Mining/Withheld blocks (convergence) 16 | - Exponential Forking - Mining atop all subtrees in a “Heaviest chain” protocol (a la GHOST) 17 | - Costless simulation attack, or posterior corruption or long term attack -- Coercing past stakeholders far after they have sold out of currency, and rewriting history from farback -------------------------------------------------------------------------------- /code/other-sims/epochboundary_large.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import math 3 | import numpy as np 4 | import utils as u 5 | 6 | 7 | # probability that if attacker chooses a time t \in T, it falls on 50% of the 8 | # population node, or right before: 9 | # Pr[node_i chooses t ] = d / T for a delay d 10 | # Pr[attacker targets 50%] = binomial(h/2,h,d/T) 11 | # Pr[ 50% < target < 60% ] = binomialCDF(60%) - binomialCDF(50%) 12 | 13 | n=100 14 | att=n*(1.0/3.0) 15 | hon=n - att 16 | T = 80 17 | delay = 10 18 | pr_node = delay / T 19 | target = int(0.5 * hon) 20 | pr50 = u.binomial(target,hon,pr_node) 21 | upto = int(0.60 * hon) 22 | # Pr[ 50% < target < 60% ] 23 | pr_to60 = u.binomial_cdf(upto,hon,pr_node) - u.binomial_cdf(target,hon,pr_node) 24 | print("pr50: {}".format(pr50)) 25 | print("pr[50->60]: {}".format(pr_to60)) 26 | 27 | 28 | -------------------------------------------------------------------------------- /code/other-sims/cq_closed_form.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.special as spe 3 | from scipy.stats import binom 4 | 5 | # User Params 6 | k = 10000 # number of epochs 7 | alpha = .3 8 | e = 5.0 9 | negl = 2**-50 10 | 11 | # Modeling Params 12 | miners = 10000 13 | hon = miners * (1-alpha) 14 | adv = miners * alpha 15 | p = e/miners 16 | 17 | def advWinsLTE(x): 18 | return binom.cdf(x, adv*k, p) 19 | 20 | def advWinsGT(x): 21 | return 1 - advWinsLTE(x) 22 | 23 | def advWinsAtMost(): 24 | for i in range(0, int(k*e+1)): 25 | if advWinsGT(i) <= negl: 26 | return i 27 | 28 | def chainQual(adv): 29 | advRatio = adv/(k*e) 30 | mu = 1 - advRatio 31 | return mu 32 | 33 | i = advWinsAtMost() 34 | print "With e = {e} -- over {ep} epochs, adv with power {pow_} wins more than {num} blocks with proba {proba}".format(e=e, ep=k, pow_=alpha, num=i, proba=negl) 35 | print "Chain qual mu = {mu}".format(mu=chainQual(i)) 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Filecoin consensus Research 2 | ===== 3 | 4 | **Disclaimer:** While we work hard to document our work as it progresses, research progress may not be fully reflected here for some time, or may be worked out out-of-band. 5 | 6 | In general, we try to focus on a single set of problems at a time, but we welcome any contributions across the [open problems]() we have! 7 | 8 | ## Consensus Questions? 9 | 10 | You may want to check out the [usual sources]() for an answer. 11 | 12 | Still haven't found an answer to your question? Open an issue, and tag it with `question`. 13 | 14 | ## Problems with consensus? 15 | 16 | Have you found a problem with the Protocol? 17 | 18 | Open an issue and tag it with `bug`. We would love to engage with you on it, and potentially start working together to solve it. 19 | 20 | ## Ideas for our `unsolved-problems`? 21 | 22 | How exciting! Engage on the relevant issue and let's take our collaboration forward from there! 23 | 24 | ## New Ideas? 25 | 26 | Share them with the community by opening up a new issue and tagging it with `idea/brainstorm`. 27 | -------------------------------------------------------------------------------- /research-notes/glossary.md: -------------------------------------------------------------------------------- 1 | ##Glossary 2 | 3 | author: @sternhenri, @zenground0 4 | 5 | ----------- 6 | 7 | - Seed -- What we use as input to generate randomness in a round of leader election 8 | - Grinding -- 9 | - Ticket -- Implementation choice as seed. 10 | - Slot -- Period over which a leader is elected. 0 or 1 blocks can be generated in a slot. 11 | - Round -- Same as slot. 12 | - Epoch -- Period over which a same seed is used as a source of randomness for a leader. Multiple of slots (1 to n). 13 | - PoST -- A primitive built from proof of replication (using VDF under the hood) and SNARKs that cryptographically verifies that specific data was stored for a given amount of time. 14 | Candidate VDF for leader election. 15 | - VDF -- A cryptographic primitive that takes a verifiable number of computations to compute but can be verified in a constant number of steps. Full definition online. 16 | Running a PoST can be thought of as a VDF for leader election. 17 | - Lookback-parameter -- the number of blocks (or slots, or units of time) traversed back in a blockchain to fetch the value of a seed or a committee -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /code/other-sims/max_tot_null.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | import numpy as np 3 | from scipy.stats import binom as bi 4 | import math 5 | 6 | ## We are looking for a number of rounds n such that adv will have at least 7 | ## k null rounds within n with near certainty. 8 | 9 | targetNulls = 22 # drawn from selfish mining sim results 10 | nullProb = .27 11 | 12 | s = 2**-30 13 | 14 | def chainLen(): 15 | # This is a simple CDF of a binomial (sum of binom probas up to x). 16 | # each round has a proba p = advNullRound() of being null for the adversary 17 | # We are looking for n such that n trials will yield at least targ successes with high proba 18 | # which is to say n s.t. 1 - CDF(targ, n, p) > 1 - s => CDF(targ, n, p) < s 19 | totalRounds = targetNulls - 1 20 | sumToTargN = 1 21 | while sumToTargN >= s: 22 | totalRounds += 1 23 | sumToTargN = bi.cdf(targetNulls - 1, totalRounds, nullProb) 24 | return totalRounds 25 | 26 | print "A chain of len {chainLen} will have at least {nulls} null rounds with high proba".format(chainLen =chainLen(), nulls=targetNulls) 27 | 28 | -------------------------------------------------------------------------------- /code/other-sims/pure_withholding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import utils as u 3 | import numpy as np 4 | 5 | # This scripts computes the advantage of running the selfish mining attack 6 | # number of blocks 7 | sims = 5 8 | numRounds = 1000 9 | e=5.0 10 | att=.33 11 | rat=1-att 12 | miners = 10000 13 | ratMin = rat * miners 14 | attMin = att * miners 15 | p = e/miners 16 | 17 | # note we are assuming very worst case, in which adv knows exactly when to release 18 | # which is completely unrealistic 19 | maxLen = 0 20 | for _ in range(sims): 21 | ratBlocks = np.random.binomial(ratMin, p, numRounds) 22 | advBlocks = np.random.binomial(attMin, p, numRounds) 23 | gap = [ratBlocks[x] - advBlocks[x] for x in range(numRounds)] 24 | 25 | # goal is to search for the length of the longest series of contiguous numbers 26 | # in the array such that their sum is negative 27 | for i in range(numRounds): 28 | for j in range(i, numRounds): 29 | _sum = sum(gap[i:j]) 30 | if _sum < 0: 31 | if (j - i) > maxLen: 32 | maxLen = j- i 33 | 34 | print maxLen 35 | -------------------------------------------------------------------------------- /code/other-sims/continuous_hs.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | 4 | nh=66667 5 | na=33333 6 | ntot=na+nh 7 | heights=range(20000,100001,1000) 8 | p=5./float(1*ntot) 9 | 10 | sim=10000 11 | 12 | ec =[] 13 | praos = [] 14 | 15 | start_time = time.time() 16 | 17 | for height in heights: 18 | win_ec = 0 19 | for i in range(sim): 20 | ch = np.random.binomial(nh, p, height) 21 | ca = np.random.binomial(na, p, height) 22 | # result of flipping a coin nha times, tested 1000 times. 23 | # is this sim of praos overly optimistic: it glosses over potential fork when multiple honest wins 24 | # put another way, it represents choice between longest honest and longest adv, and not between longest honests 25 | # praosh=[1 if ch[i]>0 else 0 for i in range(len(ch))] 26 | # praosa=[1 if ca[i]>0 else 0 for i in range(len(ca))] 27 | evench = [ch[i] if i%2 == 0 else 0 for i in range(len(ch))] 28 | oddch = [ch[i] if i%2 == 1 else 0 for i in range(len(ch))] 29 | chain_A = [sum(ca[:-1])+ sum(oddch)] 30 | chain_B = [sum(ca[1:])+ sum(evench) + ch[1]] 31 | 32 | if chain_A==chain_B: win_ec+=1 33 | print win_ec 34 | ec.append(float(win_ec)/float(sim)) 35 | 36 | 37 | print ec 38 | 39 | print("--- %s seconds ---" % (time.time() - start_time)) -------------------------------------------------------------------------------- /code/other-sims/case2.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | nh=66667 4 | na=33333 5 | ntot=na+nh 6 | heights=[20] 7 | e=5. 8 | p=e/float(1*ntot) 9 | 10 | sim=1000000 11 | 12 | ec =[] 13 | praos = [] 14 | 15 | ###### Simple block withholding with headstart 16 | 17 | for height in heights: #try different height to get the probability of success of each attack 18 | win_ec = 0 19 | #win_praos = 0 20 | for i in range(sim): 21 | ch = np.random.binomial(nh, p, height) 22 | ca = np.random.binomial(na, p, height) 23 | # result of flipping a coin nha times, tested 1000 times. 24 | # is this sim of praos overly optimistic: it glosses over potential fork when multiple honest wins 25 | # put another way, it represents choice between longest honest and longest adv, and not between longest honests 26 | #praosh=[1 if ch[i]>0 else 0 for i in range(len(ch))] 27 | #praosa=[1 if ca[i]>0 else 0 for i in range(len(ca))] 28 | w_a = ch[0]+sum(ca)#num,ber of blocks created by adversary + headstart 29 | w_h = sum(ch) 30 | #wpraos_h = sum(praosh) 31 | #wpraos_a = sum(praosa) 32 | 33 | if w_a>=w_h: win_ec+=1 34 | #if wpraos_a>=wpraos_h: win_praos+=1 35 | #print win_ec, win_praos 36 | ec.append(float(win_ec)/float(sim)) 37 | #praos.append(float(win_praos)/float(sim)) 38 | 39 | print(ec, praos) -------------------------------------------------------------------------------- /specs/ec/MC.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE MC ---- 2 | EXTENDS ec, TLC 3 | 4 | \* MV CONSTANT declarations@modelParameterConstants 5 | CONSTANTS 6 | M1, M2, M3 7 | ---- 8 | 9 | \* MV CONSTANT definitions miners 10 | const_1556327768723618000 == 11 | {M1, M2, M3} 12 | ---- 13 | 14 | \* SYMMETRY definition 15 | symm_1556327768723619000 == 16 | Permutations(const_1556327768723618000) 17 | ---- 18 | 19 | \* CONSTANT definitions @modelParameterConstants:1NumMiners 20 | const_1556327768723620000 == 21 | 3 22 | ---- 23 | 24 | \* CONSTANT definitions @modelParameterConstants:2USE_RANDOM 25 | const_1556327768723621000 == 26 | TRUE 27 | ---- 28 | 29 | \* CONSTRAINT definition @modelParameterContraint:0 30 | constr_1556327768723622000 == 31 | ModelBoundedLeaders 32 | ---- 33 | \* SPECIFICATION definition @modelBehaviorSpec:0 34 | spec_1556327768723623000 == 35 | Spec 36 | ---- 37 | \* INVARIANT definition @modelCorrectnessInvariants:0 38 | inv_1556327768723624000 == 39 | FairMining 40 | ---- 41 | \* INVARIANT definition @modelCorrectnessInvariants:1 42 | inv_1556327768723625000 == 43 | BoundedLeaders 44 | ---- 45 | \* INVARIANT definition @modelCorrectnessInvariants:2 46 | inv_1556327768723626000 == 47 | NoProgress 48 | ---- 49 | ============================================================================= 50 | -------------------------------------------------------------------------------- /code/attacks/forking-factor.py: -------------------------------------------------------------------------------- 1 | # These functions are used for rapidly calculating the values of closed form 2 | # expressions explored in the attacks.tex document to drive research. 3 | # 4 | # This is intended to be run with python 2. 5 | 6 | import scipy.special 7 | import math 8 | import matplotlib.pyplot as plt 9 | 10 | """ 11 | Initial Condition has 1 block (genesis), "rational" miner mines block on heaviest winning chain. 12 | In Round n, what are the expected number of network forks with weight > 3/4 n, with n the number of rounds considered (with genesis the first). 13 | With weight as win: add 1, lose: add nothing. And Weight a proxy for expected return. 14 | """ 15 | 16 | THRESHOLD = 0.75 17 | ROUNDS = 10000 18 | LOOKBACK = 1 19 | MINERS = 3 20 | POWER = 1/MINERS 21 | 22 | def simulate(forkingFactor=THRESHOLD, rounds=ROUNDS, lookback=1): 23 | 24 | 25 | def simulate_round(forks): 26 | 27 | 28 | ## Matplotlib plotting 29 | def plotForks(): 30 | for a in [0.01, 0.05, 0.1]: 31 | data = [] 32 | for n in range(1, 500): 33 | data.append(ForkWin(n,a)) 34 | plt.plot(range(1,500), data) 35 | plt.xlabel("Fork Length (rounds)") 36 | plt.ylabel("Success Probability") 37 | plt.ylim(top=0.01) 38 | plt.xlim(right=100) 39 | plt.legend([r'$\alpha=0.01$', r'$\alpha=0.05$', r'$\alpha=0.1$'], loc='upper right') 40 | plt.title("Upper bound on fork probabilities") 41 | plt.savefig("small-probs.png") 42 | plt.show() 43 | 44 | if __name__ == '__main__': 45 | plotForks() 46 | -------------------------------------------------------------------------------- /code/other-sims/case2_mp.py: -------------------------------------------------------------------------------- 1 | import multiprocessing as mp 2 | import numpy as np 3 | import time 4 | 5 | nh=66667 6 | na=33333 7 | ntot=na+nh 8 | height=20 9 | e=5. 10 | p=e/float(1*ntot) 11 | 12 | 13 | ec =[] 14 | praos = [] 15 | print "e = ", e 16 | Num_of_sim_per_proc = 10000 17 | 18 | start_time = time.time() 19 | 20 | # This script simulates the worst length of n consecutive "small headstart" attacks (case 3) 21 | 22 | 23 | def simu(sim): 24 | win_ec = 0 25 | #win_praos = 0 26 | np.random.seed() 27 | for i in range(sim): 28 | ch = np.random.binomial(nh, p, height) 29 | ca = np.random.binomial(na, p, height) 30 | # result of flipping a coin nha times, tested 1000 times. 31 | # is this sim of praos overly optimistic: it glosses over potential fork when multiple honest wins 32 | # put another way, it represents choice between longest honest and longest adv, and not between longest honests 33 | #praosh=[1 if ch[i]>0 else 0 for i in range(len(ch))] 34 | #praosa=[1 if ca[i]>0 else 0 for i in range(len(ca))] 35 | w_a = ch[0]+sum(ca)#num,ber of blocks created by adversary + headstart 36 | w_h = sum(ch) 37 | #wpraos_h = sum(praosh) 38 | #wpraos_a = sum(praosa) 39 | 40 | if w_a>=w_h: win_ec+=1 41 | #if wpraos_a>=wpraos_h: win_praos+=1 42 | #print win_ec, win_praos 43 | return float(win_ec)/float(sim) 44 | #praos.append(float(win_praos)/float(sim)) 45 | 46 | 47 | #we rune the simulations in parallel: 48 | pool = mp.Pool(mp.cpu_count()) 49 | print mp.cpu_count() 50 | results = pool.map(simu, [Num_of_sim_per_proc]*mp.cpu_count()) 51 | pool.close() 52 | 53 | 54 | print results, np.average(results) 55 | print("--- %s seconds ---" % (time.time() - start_time)) -------------------------------------------------------------------------------- /code/ec-sim-w/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "sort" 6 | "sync" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | var colors = []*color.Color{ 12 | color.New(color.BgCyan, color.FgBlack), 13 | color.New(color.BgHiYellow, color.FgBlack), 14 | color.New(color.BgMagenta, color.FgBlack), 15 | color.New(color.BgGreen, color.FgBlack), 16 | color.New(color.BgHiYellow, color.FgBlack), 17 | color.New(color.BgHiRed, color.FgBlack), 18 | color.New(color.BgHiMagenta, color.FgBlack), 19 | } 20 | 21 | func sortHashSet(h [][32]byte) { 22 | sort.Slice(h, func(i, j int) bool { 23 | return bytes.Compare(h[i][:], h[j][:]) < 0 24 | }) 25 | } 26 | 27 | func keyForParentSet(parents [][32]byte) string { 28 | sortHashSet(parents) 29 | 30 | s := "" 31 | for _, p := range parents { 32 | s += string(p[:]) 33 | } 34 | 35 | return s 36 | } 37 | 38 | // hashPrefs is a helper function that returns the prefixes of each of the 39 | // given hashes, for pretty printing 40 | func hashPrefs(h [][32]byte) [][]byte { 41 | var out [][]byte 42 | for _, h := range h { 43 | c := make([]byte, 4) 44 | copy(c, h[:]) 45 | out = append(out, c) 46 | } 47 | return out 48 | } 49 | 50 | type Blockstore struct { 51 | blocks map[[32]byte]*Block 52 | lk sync.Mutex 53 | } 54 | 55 | func newBlockstore() *Blockstore { 56 | return &Blockstore{ 57 | blocks: make(map[[32]byte]*Block), 58 | } 59 | } 60 | 61 | func (bs *Blockstore) Put(blk *Block) { 62 | bs.lk.Lock() 63 | defer bs.lk.Unlock() 64 | bs.blocks[blk.Hash()] = blk 65 | } 66 | 67 | func (bs *Blockstore) Get(h [32]byte) *Block { 68 | bs.lk.Lock() 69 | defer bs.lk.Unlock() 70 | b, ok := bs.blocks[h] 71 | if !ok { 72 | panic("couldnt find block") 73 | } 74 | return b 75 | } 76 | -------------------------------------------------------------------------------- /code/other-sims/simple_withholding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import utils as u 3 | import numpy as np 4 | 5 | # This scripts computes the advantage of running the lookfwd attack according to 6 | # the paper formula (lookfwd.pdf) 7 | # number of blocks 8 | sims = 10000 9 | numRounds = 5000 10 | e=5.0 11 | att=.33 12 | rat=1-att 13 | miners = 10000 14 | ratMin = rat * miners 15 | attMin = att * miners 16 | p = e/miners 17 | 18 | # note we are assuming very worst case, in which adv knows exactly when to release 19 | # which is completely unrealistic 20 | maxLen = 0 21 | for _ in range(sims): 22 | ratBlocks = np.random.binomial(ratMin, p, numRounds) 23 | advBlocks = np.random.binomial(attMin, p, numRounds) 24 | 25 | attacking = False 26 | atkLen = 0 27 | 28 | # track longest possible run in the 5k rounds 29 | # adv stops as soon as neg gap returns to 0 30 | gap = 0 31 | for rnd in range(numRounds): 32 | rndDiff = ratBlocks[rnd] - advBlocks[rnd] 33 | 34 | if attacking: 35 | newGap = gap + rndDiff 36 | # adv wins on ties (so >, not >=) 37 | if newGap > 0: 38 | # stop attack (note this is a loss, but we assume perfect knowledge) 39 | gap = 0 40 | attacking = False 41 | if atkLen - 1 > maxLen: 42 | maxLen = atkLen - 1 43 | atkLen = 0 44 | else: 45 | # continue attack 46 | gap = newGap 47 | atkLen += 1 48 | else: 49 | # not attacking and miner miners more, can start attack 50 | if rndDiff < 0: 51 | # do HS, ie adv can coopt ratBlocks and mine over full tipset next round 52 | # which makes gap be full advBlocks, not just diff 53 | gap = ratBlocks[rnd] 54 | attacking = True 55 | atkLen = 1 56 | 57 | 58 | print maxLen 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /code/other-sims/case4.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from math import floor 4 | import multiprocessing as mp 5 | 6 | nh=67 7 | na=33 8 | ntot=na+nh 9 | heights=range(250,251,10) 10 | e=1 11 | p=float(e)/float(1*ntot) 12 | 13 | sim=1000000 14 | 15 | ec =[] 16 | num=1 17 | if e==1: num=77 18 | if e==5: num=54 19 | 20 | start_time = time.time() 21 | 22 | # Step 1: Init multiprocessing.Pool() 23 | pool = mp.Pool(mp.cpu_count()) 24 | 25 | 26 | for height in heights: #the adversary tries to keep maintaining two chain of same weight and 27 | #length "height", we try different heights and see the probability of succeeding. Ig this probability is 28 | #small enough, we consider this height a good finality candidate. 29 | win_ec = 0 30 | longestfork =[] 31 | for i in range(sim): 32 | ch = np.random.binomial(nh, p, height) 33 | ca = np.random.binomial(na, p, height) 34 | # result of flipping a coin nha times, tested height times. (i.e. number of leaders 35 | # at ach slot for both adversary and honest players) 36 | j=0 37 | w_h = 0 38 | w_a = 0 39 | praos_h=0 40 | praos_a=0 41 | j=0 42 | while ca[j]>0 and j=w_h and w_a>0: 49 | win_ec+=1 50 | longestfork.append(j) 51 | #print np.average(longestfork) 52 | ec.append(float(win_ec)/float(sim)) 53 | # longestfork.sort() 54 | 55 | # print ec,np.average(longestfork), np.median(longestfork),max(longestfork), sum(longestfork[-54:]) 56 | 57 | #before sorting, we group them by groups of num 58 | stop = int(floor(sim/num)*num) #need to stop before the end of the longest fork 59 | #if it is not a multiple of num 60 | 61 | groupedfork=[ sum(longestfork[x:x+num]) for x in range(0, stop, num)] 62 | 63 | 64 | print ec, np.average(groupedfork), np.median(groupedfork), max(groupedfork), len(groupedfork) 65 | 66 | print("--- %s seconds ---" % (time.time() - start_time)) -------------------------------------------------------------------------------- /research-notes/randomness.md: -------------------------------------------------------------------------------- 1 | ##Randomness 2 | 3 | author: @sternhenri, @zenground0 4 | 5 | ----------- 6 | 7 | **Random oracle:** 8 | Let’s assume we have a perfect randomness beacon 9 | It spits out a single seed at every slot 10 | It generates them instantly 11 | They are all independent 12 | All miners get them instantly 13 | 14 | Every miner uses that to see if they’ve won at each slot 15 | It gets us: 16 | Same random seed for all miners at time t (ie no grinding via forks) 17 | Recovery from attack at every slot (derived from independent seeds) 18 | Random number is verifiable by all parties at every slot 19 | 20 | **Need-to-have:** 21 | 2 - Should not use more than 2 CPUs at any time 22 | 4 - Should not be globally predictable 23 | 5 - Should be immediately globally verifiable 24 | 6 - Should be able to recover from an attack 25 | Refresh randomness if there is bias 26 | Successful attack at t, has no impact on t+1 27 | 7 - Nodes should be able to participate sporadically (need to be online always vs need to be online at some time) 28 | 29 | **Nice-to-have:** 30 | A - Should prevent grinding 31 | B - Should not be locally predictable beyond 1-recent 32 | C - each seed is independent from past ones 33 | 1 - Should be “easy” to resume/catch-up after interruption 34 | 35 | (from: https://docs.google.com/presentation/d/1wL9wWvQ0nlOYxQhmzg8uiEyX2B77e6ruZQulFMKatV8/edit#slide=id.g4690a1b346_2_50) 36 | 37 | **Problems:** 38 | Global predictability -- prepared DOS attacks against leader 39 | 40 | - Tool: VRF to calculate randomness input to leader election given public seed 41 | 42 | Local predictability of leader election -- predictable selfish mining / double spending, fork grinding 43 | 44 | - Tool: VDFs to prevent knowing in advance, sampling from recent past 45 | 46 | Power over seed -- leads to grinding 47 | 48 | - Tool: Epoch-based seed selection, lookback parameters, off-chain randomness 49 | 50 | Other tooling: 51 | 52 | - Lookback parameters (for committee or seed) 53 | - Duration of seed use (epochs) 54 | - MPC 55 | - SW timestamp ordering -------------------------------------------------------------------------------- /research-notes/waiting.md: -------------------------------------------------------------------------------- 1 | ## Waiting is irationnal 2 | 3 | author: @zenground0 4 | 5 | -------- 6 | 7 | One the key assumptions behind the design of Filecoin consensus is that rational miners will not necessarily wait to release blocks in the protocol specified block time without some PoW-style hardware-enforced reason to wait. Waiting could be enforced by a PoST (though this appears to be deprecated) or a VDF. 8 | 9 | The intuition behind this does seem reasonable: miners are incentivized to get their blocks included in the chain and can get away with fudging timestamps to rush block propagation. This could eventually lead to a situation where high bandwidth highly connected miners are disproportionally represented in the chain which has centralizing effects on the network. 10 | 11 | However we should be aware that this assumption is not considered by most other PoS consensus protocols out there. This can be explained in part because some of these protocols are not considering security in a rational actor model (i.e. Snow White and Algorand), but this isn't true across the board. 12 | 13 | It would be great to articulate the "waiting can be irrational argument" as it has significant impact on consensus design. 14 | 15 | Rough idea: one place to start is by examining how the sleepy consensus model responds to added rationality assumptions. One of the assumptions of SC/SW is that all alert node clocks are synchronized within delta, another is that the proportion of alert nodes is higher than the proportion of adversary nodes at any time. 16 | 17 | One modification to the model is to consider nodes that occasionally lag more than delta from other honest nodes but out of rational interest do NOT reject chains with future timestamps in the hope of catching themselves up and winning block rewards. 1. Is this a rational strategy for nodes? 2. How would this group of not too powerful deviators (not too powerful because they experience delay and maybe we don't give them the full set of powers given to adversary in the sleepy model) affect the security guarantees of sleepy consensus? -------------------------------------------------------------------------------- /code/other-sims/case4_mp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from math import floor 4 | import multiprocessing as mp 5 | 6 | 7 | #sim=10000 8 | 9 | ec =[] 10 | num=1 11 | e = 1 12 | print "e = ", e 13 | Num_of_sim_per_proc = 100000 14 | 15 | start_time = time.time() 16 | 17 | #This simulation computes at is the worst length of n successives attack 4 (adversary split the honest power 18 | #as much as possible and selfish mine in parallel) 19 | 20 | def simu(sim): 21 | nh=67 22 | na=33 23 | ntot=na+nh 24 | height = 250#the height is chosen to avoid infinite loop, in practice a selfish mining 25 | #attack will not last 250 except with negligible probabilities 26 | p=float(e)/float(1*ntot) 27 | if e==1: num=17 28 | if e==5: num=100 29 | #num corresponds to the number of iterations of the attack that the adversary can perform 30 | #(calculated previously) 31 | win_ec = 0 32 | longestfork =[] 33 | np.random.seed()#initialise random seed for different processors 34 | for i in range(sim): 35 | ch = np.random.binomial(nh, p, height) 36 | ca = np.random.binomial(na, p, height) 37 | # result of flipping a coin nha times, tested height times. (i.e. number of leaders 38 | # at ach slot for both adversary and honest players) 39 | j=0 40 | w_h = 0 41 | w_a = 0 42 | j=0 43 | while ca[j]>0 and j=w_h and w_a>0: 48 | win_ec+=1 49 | longestfork.append(j)#length of the attack 50 | stop = int(floor(sim/num)*num) #need to stop before the end of the longest fork 51 | # #if it is not a multiple of num 52 | groupedfork=[ sum(longestfork[x:x+num]) for x in range(0, stop, num)] #we grouped the num 53 | #successive attacks toigether and sums them up to get the length of num successives attacks 54 | return max(groupedfork) 55 | 56 | pool = mp.Pool(mp.cpu_count()) 57 | print mp.cpu_count() 58 | results = pool.map(simu, [Num_of_sim_per_proc]*mp.cpu_count()) 59 | pool.close() 60 | 61 | print results, max(results) 62 | print("--- %s seconds ---" % (time.time() - start_time)) -------------------------------------------------------------------------------- /code/other-sims/epoch_boundary_end.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import numpy as np 3 | from scipy.stats import binom as bi 4 | 5 | ## Take two forks that yield a number of blocks i, j respectively in a given epoch with i >= j 6 | ## Epoch boundary will stop on two conditions 7 | ## 1 - the adversary produces no blocks in that round. in that case, even if i == j, normal 8 | ## FCR will apply at next round given a consistent view of network by all participants 9 | ## 2 - the adversary produces k blocks, with k < i - j. In this case, that means that the 10 | ## adversary's delayed propagation tactics will not suffice to split the network given 11 | ## the regular entropy in the network. 12 | ## 13 | ## We are looking to model the likelihood of these events, taking a worst case which means: 14 | ## - the two forks have equal power behind them (meaning perfect precision on the part of 15 | ## the adversary when creating the split) 16 | ## - the adversary never gets slashed (ie adv wins on both chains with different sybils 17 | ## throughout the duration of the attack) 18 | ## 19 | ## That is, take Y a variable describing adversarial wins, and X being wins in either fork, 20 | ## we are looking for 21 | ## pr(Y = 0) + sum_k=1^inf pr(Y = k) * sum_j=0^inf pr(X = j) * pr(X > i) with i = j + k 22 | 23 | e = 5.0 24 | alpha = 1.0/3 25 | rat = 1 - alpha 26 | miners = 10000 27 | 28 | advMiners = alpha * miners 29 | ratMiners = rat * miners 30 | p = e / miners 31 | 32 | def advNullRound(): 33 | return bi.pmf(0, advMiners, p) 34 | 35 | def advLessThanForkGap(): 36 | # we cut it off at 30 without sacrificing much precision: whole network has 37 | # a 2.5E-14 chance of mining 30 blocks with e - 5 38 | cutoff = 30 39 | 40 | _sum = 0 41 | for k in range(1, cutoff + 1): 42 | 43 | advPr = bi.pmf(k, advMiners, p) 44 | for j in range(cutoff + 1): 45 | fork1Pr = bi.pmf(j, ratMiners/2.0, p) 46 | fork2Pr = 1- bi.cdf(j + k, ratMiners/2.0, p) 47 | 48 | _sum += advPr * fork1Pr * fork2Pr 49 | 50 | return _sum 51 | 52 | print advNullRound() + advLessThanForkGap() 53 | -------------------------------------------------------------------------------- /research-notes/open-questions.md: -------------------------------------------------------------------------------- 1 | ## Notes on Open Problems 2 | 3 | author: @sternhenri 4 | ------ 5 | 6 | **Formally defining the EC/SPC Interface** 7 | There are multiple candidate constructions for defining the interface between EC and SPC, we would like to work on a formal treatment of these interfaces and the tradeoffs amongst them. This work is relevant to making Filecoin consensus more modular. 8 | 9 | **Efficient 51% block signing via all-to-all communications** 10 | This problem deals with finality in SPC systems and attempts to prove that blocks that have been signed by 51% of storage in the chain can be deemed committed. 11 | 12 | **Proof-of-Space before SEAL** 13 | We are interested in exploring the ways in which miners could contribute to SPC security ahead of SEALING their pledged sectors. We have certain ideas on how to do this, but would like to explore them for use in the Filecoin protocol. 14 | 15 | **PFT** 16 | Power-Fault Tolerance reframes Byzantine Fault Tolerance in terms of the power or influence miners have over a network, for instance computation in Ethereum, Hashing power (or electricity) in Bitcoin, Storage in Filecoin. As the space grows, we believe that a formal treatment of consensus as a function of power rather than individual machines may become more appropriate to understanding the guarantees and limitations of various security models and consensus algorithms. 17 | 18 | The team is working on early formalization of insights granted to us through work on Filecoin but is eager to find collaborators to pursue this work with. 19 | 20 | **SPC** 21 | Storage Power Consensus can be thought of as the intermediate layer of consensus in the Filecoin system, it sits atop the Filecoin protocol and build and maintains a power table which provably accounts for miners’ storage in the network, and then invokes a consensus algorithm (such as EC or SPC) to elect a leader at every round. 22 | 23 | In this sense, SPC uses PoS to approxime a useful Proof-of-Work. We are interested in developing both formal treatments of the assumptions and guarantees SPC makes in a generic setting, as well as heuristics for SPC in the context of Filecoin. 24 | 25 | This is a project the team is actively working on, and for which we seek interested collaborators or may even put out RFPs in the future. 26 | 27 | -------------------------------------------------------------------------------- /code/other-sims/case1_maxTotalCatchup.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from math import floor 4 | import multiprocessing as mp 5 | 6 | 7 | ## In this sumlation, we compute what is the length of n consecutive 8 | ## selfish mining attacks to get the value of maxTotalCatchUp 9 | 10 | nh=67 11 | na=33 12 | ntot=na+nh 13 | e=5 14 | print "e = ", e 15 | Num_of_sim_per_proc = 100000 16 | p=float(e)/float(ntot) 17 | min_length=10 18 | 19 | ## Takes the number of cycles num and return the worst TotalCatchup 20 | ## of num consecutives catchup (of length at least min_length) 21 | 22 | ec =[] 23 | praos = [] 24 | ### Put NumCycle here 25 | if e==5: num=4 26 | if e==1: num=81 #number of allowed consecutive attacks computed previously 27 | start_time = time.time() 28 | height=150#the height is chosen to avoid infinite loop, in practice a selfish mining 29 | #attack will not last 150 except with negligible probabilities 30 | 31 | def simu(sim): 32 | longestfork =[] 33 | win_ec = 0 34 | np.random.seed()#initialise random seed for different processors 35 | for i in range(sim): 36 | ch = np.random.binomial(nh, p, height) 37 | ca = np.random.binomial(na, p, height) 38 | # result of flipping a coin nha times, tested height times. 39 | 40 | j=0 41 | win =1 42 | sumh = ch[0]#this is the begining of the selfish mining 43 | suma = ca[0] 44 | ind = 1 45 | while sumh>suma and ind=min_length: 53 | #win = 1 54 | longestfork.append(ind) 55 | stop = int(floor(sim/num)*num) #need to stop before the end of the longest fork 56 | # #if it is not a multiple of num 57 | groupedfork=[ sum(longestfork[x:x+num]) for x in range(0, stop, num)]# we grouped the num 58 | #successive attacks together and sums them up to get the length of num successives attacks 59 | return max(groupedfork) 60 | 61 | pool = mp.Pool(mp.cpu_count()) 62 | print mp.cpu_count() 63 | results = pool.map(simu, [Num_of_sim_per_proc]*mp.cpu_count()) 64 | pool.close() 65 | 66 | print results, max(results) 67 | 68 | 69 | 70 | print("--- %s seconds ---" % (time.time() - start_time)) 71 | 72 | 73 | -------------------------------------------------------------------------------- /code/other-sims/finality_fixedlength.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import pandas as pd 4 | 5 | qs=[k/100.0 for k in range(2, 54, 2)] 6 | # nh=48 7 | # na=52 8 | ntot=10000 9 | sim=10000 10 | heights = range(5,105,10) 11 | blocks_back = range(5,105, 10) 12 | d={} 13 | height_max=95 14 | success_ec_tot=[] 15 | success_ec_without_tot=[] 16 | success_praos_tot=[] 17 | for q in qs: 18 | nh=round((1-q)*ntot) 19 | na=round(q*ntot) 20 | p=1./float(1*ntot) 21 | success_ec = [0]*len(heights) 22 | success_ec_without = [0]*len(heights) 23 | success_praos = [0]*len(heights) 24 | for i in range(sim): 25 | ch = np.random.binomial(nh, p, height_max) 26 | ca = np.random.binomial(na, p, height_max) 27 | praosh=[1 if ch[i]>0 else 0 for i in range(len(ch))] 28 | praosa=[1 if ca[i]>0 else 0 for i in range(len(ca))] 29 | for idx ,height in enumerate(heights): 30 | # result of flipping a coin nha times, tested 1000 times. 31 | # is this sim of praos overly optimistic: it glosses over potential fork when multiple honest wins 32 | # put another way, it represents choice between longest honest and longest adv, and not between longest honests 33 | w_a = sum(ca[:height])+ch[0] 34 | w_h = sum(ch[:height]) 35 | wpraos_h = sum(praosh[:height]) 36 | wpraos_a = sum(praosa[:height]) 37 | w_without_headstart_a = sum(ca[:height]) 38 | w_without_headstart_h = sum(ch[:height]) 39 | if w_a>=w_h: success_ec[idx] += 1 40 | if wpraos_a>=wpraos_h: success_praos[idx] += 1 41 | if w_without_headstart_a>w_without_headstart_h: success_ec_without[idx] += 1 42 | 43 | success_ec_tot.append([i/float(sim) for i in success_ec]) 44 | success_praos_tot.append([i/float(sim) for i in success_praos]) 45 | success_ec_without_tot.append([i/float(sim) for i in success_ec_without]) 46 | # print "EC: {suc};".format(suc=[i/float(sim) for i in success_ec]) 47 | # print "Praos: {suc};".format(suc=[i/float(sim) for i in success_praos]) 48 | # print "EC without tipset: {suc};".format(suc=[i/float(sim) for i in success_ec_without]) 49 | 50 | 51 | print "EC" 52 | df = pd.DataFrame(success_ec_tot, columns=blocks_back, index=qs) 53 | print df 54 | 55 | print "EC without HeadStart" 56 | df = pd.DataFrame(success_ec_without_tot, columns=blocks_back, index=qs) 57 | print df 58 | 59 | print "EC without TipSets" 60 | df = pd.DataFrame(success_praos_tot, columns=blocks_back, index=qs) 61 | print df 62 | -------------------------------------------------------------------------------- /code/other-sims/early_chain_quality_sim.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | nh=70 4 | na=30 5 | ntot=na+nh 6 | height=13 7 | p=1./float(ntot) 8 | diff_praos=[] 9 | diff_ec=[] 10 | sim=100000 11 | NumOfAttacks=1 12 | diff_headstart=[] 13 | adv=[] 14 | hon=[] 15 | adv_sucess=[] 16 | h=[] 17 | adv_abs=[] 18 | success=0 19 | j_chain=[] 20 | for i in range(sim): 21 | adv_chain=0 22 | hon_chain=0 23 | for k in range(NumOfAttacks): 24 | #For Praos for a chain to grow we need to have at least one player 25 | #elected leader -> this raises the chain by one 26 | #at each height add one if at least one is elected 27 | #honest chain 28 | #not excatly, for each height we need to count one if there is one success! 29 | ch = np.random.binomial(nh, p, height) 30 | ca = np.random.binomial(na, p, height) 31 | # result of flipping a coin nha times, tested 1000 times. 32 | # praosh=[1 for i in range(len(ch)) if ch[i]>0 ] 33 | # praosa=[1 for i in range(len(ca)) if ca[i]>0 ] 34 | 35 | # diff_praos.append(sum(praosh)-sum(praosa)) 36 | # #for ghost the chain grows by the number of elected leader 37 | # #at each height add the number of elected leader 38 | 39 | diff_ec.append(sum(ch)-sum(ca)) 40 | 41 | j=0 42 | for j in range(len(ch)): 43 | if ch[j]>0 and ca[j]>0: 44 | break 45 | if jsuma and ind loosing 49 | win =0 50 | break 51 | j=1 52 | if ind sum(ca): 23 | return 0 24 | else: 25 | s=sum(ca) 26 | ca = [x for x in ca if x != 0] 27 | n=len(ca) 28 | l1 = [s-i for i in range(ca[-1]+1) if s-i>=num] 29 | l = np.array(l1.copy()) 30 | for j in range(1,n): 31 | for i in range(1,ca[-1-j]+1): 32 | ll = np.array(l1)-i 33 | ll = [x for x in ll if x>=num] 34 | l=np.concatenate((l,ll),axis =0) 35 | l1 = l.copy() 36 | # dict_of_weight = {i: 0 for i in range(sum(ca)+1)} 37 | # for elt in l: 38 | # dict_of_weight[elt]+=1 39 | # return dict_of_weight 40 | #assert len(l) == np.prod(np.array(ca)+1) 41 | ct = len(l) 42 | # if ct>10: 43 | # print(ca,num,l,len(l)) 44 | return ct 45 | 46 | 47 | 48 | 49 | def simu(sim): 50 | np.random.seed()#initialise random seed for different processors 51 | # we consider two variant of the attack: 52 | # 1. the unrealistic "worst case" scenario where the honest chain is completely split due to synchrony errors 53 | wh_unrealistic = [] 54 | # 2. the synchronous case where the honest chain is never split (+ our adversary does not perform an epoch boundary) 55 | wh_sync = [0] 56 | # we could take the average of both depending on our threat model 57 | wa = [] 58 | for i in range(sim): 59 | ch = np.random.binomial(nh, p, height) 60 | ca = np.random.binomial(na, p, height) 61 | h_sync = sum(ch) 62 | a_max = sum(ca) #heaviest chain that adversary can create 63 | if unrealistic: h_unrealistic = sum([1 if ch[i]>0 else 0 for i in range(len(ch))]) 64 | #diff = a_max-h_sync 65 | new_ca = np.concatenate(([ca[0]+ch[0]],ca[1:]),axis =0) 66 | winners = count_possibilities(new_ca,h_sync) 67 | wa.append(winners) 68 | return np.average(wa) 69 | 70 | 71 | 72 | pool = mp.Pool(mp.cpu_count()) 73 | print(mp.cpu_count()) 74 | results = pool.map(simu, [Num_of_sim_per_proc]*mp.cpu_count()) 75 | pool.close() 76 | 77 | print(results, np.average(results)) 78 | print("--- %s seconds ---" % (time.time() - start_time)) -------------------------------------------------------------------------------- /code/other-sims/headstart.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This scripts computes the maximum probability that an attacker can pull off a 4 | # successfull headstart attacks. 5 | # Intuition: For the attack to work, the sum of (1) its blocks Bj at round J and 6 | # (2) its block B_j+1 at round j+1 needs to be greater than the honest number 7 | # of blocks at round J+1. Since both honests and attacker are the same honest 8 | # blocks at round J, they don’t make a difference in the chain weight. 9 | # **Analysis**: 10 | # Describes number of blocks found in a block by using Poisson dist. 11 | # Hj = # of honest blocks at round j 12 | # Xj = # of malicious blocks at round j 13 | # We want to find the maximum probability as follow: 14 | # SUM: For all k1, k2: Pr[ Hj+1 < k1 + k2 | Xj = k1, Xj+1 = k2 ] * Pr[ Xj = k1 ] * Pr[ Xj+1 = k2] 15 | # Inner loop: Pr[ Hj+1 < k1 + k2] = CDF_Poisson(k1+k2-1) 16 | 17 | import numpy as np 18 | import math 19 | import utils 20 | 21 | 22 | s=100000 23 | att=0.1 24 | honest=1-att 25 | e=5 26 | 27 | def poisson(k1,k2): 28 | ## pr. honest finds less blocks 29 | honest_pr = utils.poisson_cdf(k1 + k2,honest* e) 30 | ## attacker number of blocks at first round 31 | malicious_1 = utils.poisson(k1,att*e) 32 | ## attacker number of blocks at second round 33 | malicious_2 = utils.poisson(k2,att*e) 34 | return honest_pr,malicious_1,malicious_2 35 | 36 | def binomial(k1,k2): 37 | ## pr. honest finds less blocks 38 | honest_pr = utils.binomial_cdf(k1 + k2,honest*s,e/s) 39 | ## attacker number of blocks at first round 40 | malicious_1 = utils.binomial(k1,att*s,e/s) 41 | ## attacker number of blocks at second round 42 | malicious_2 = utils.binomial(k2,att*s,e/s) 43 | return honest_pr,malicious_1,malicious_2 44 | 45 | 46 | def compute(prob_function): 47 | max_pr = 0 48 | sum_pr = 0 49 | max_k = 20 50 | for k1 in range(1,max_k): 51 | for k2 in range(0, max_k): 52 | (honest_pr,malicious_1,malicious_2) = prob_function(k1,k2) 53 | if k1 + k2 < honest*e: 54 | continue 55 | total = honest_pr * malicious_1 * malicious_2 56 | if total > max_pr: 57 | max_pr = total 58 | sum_pr += total 59 | print("max probability: {}".format(max_pr)) 60 | print("sum probability: {}".format(sum_pr)) 61 | print("chain quality reduction: {}".format(sum_pr/3)) 62 | 63 | print(" --- using poisson approximation ---") 64 | compute(poisson) 65 | print(" --- using binomial approximation ---") 66 | compute(binomial) 67 | -------------------------------------------------------------------------------- /code/other-sims/probaofcatchingup.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from math import floor, log 4 | 5 | nh=67 6 | na=33 7 | ntot=na+nh 8 | heights=range(100,101,50) 9 | p=5./float(1*ntot) 10 | 11 | sim=10000 12 | min_length=10 13 | 14 | ec =[] 15 | praos = [] 16 | 17 | start_time = time.time() 18 | 19 | hs=1 20 | longestfork =[] 21 | for height in heights: 22 | win_ec = 0 23 | for i in range(sim): 24 | ch = np.random.binomial(nh, p, height) 25 | ca = np.random.binomial(na, p, height) 26 | # result of flipping a coin nha times, tested 1000 times. 27 | # print ch 28 | # print ca 29 | j=0 30 | win =1 31 | sumh = ch[0] 32 | suma = ca[0] 33 | ind = 1 34 | while sumh>suma and ind1 : 43 | sumh = ch[j]/2+ch[j+1]#at round j the power of honest miners is still split between two chains 44 | #at round j+1 it goes all back to one chain 45 | 46 | suma = ca[j-1]-1 +ca[j+1]#the adversary used all the blocks it withheld in period j-1 47 | #(all of them minus 1 that it used to maintain the forks) 48 | 49 | ind = j+2 50 | while sumh>suma and indsuma: #we have reach the end of the attack 56 | #and adversary has not catch up 57 | win =0 58 | break 59 | if ind =min_length: win = 1 61 | longestfork.append(ind) 62 | 63 | if win ==1: 64 | if ind>= min_length: win_ec+=1 65 | longestfork.append(j) 66 | #print np.average(longestfork) 67 | ec.append(float(win_ec)/float(sim)) 68 | longestfork.sort() 69 | 70 | print ec, np.average(longestfork), np.median(longestfork), np.average(longestfork[-33:]),max(longestfork) 71 | 72 | n = log(2**-30)/log(ec[0]) 73 | print(n) 74 | print("--- %s seconds ---" % (time.time() - start_time)) 75 | 76 | 77 | 78 | # longestfork =[] 79 | # for height in heights: 80 | # win_ec = 0 81 | # for i in range(sim): 82 | # ch = np.random.binomial(nh, p, height) 83 | # ca = np.random.binomial(na, p, height) 84 | # # result of flipping a coin nha times, tested 1000 times. 85 | # # print ch 86 | # # print ca 87 | # j=0 88 | # win =1 89 | # sumh = ch[0] 90 | # suma = ca[0] 91 | # if ch[0]>ca[0] and ca[0]+ca[1]>=ch[0]+ch[1]: 92 | # win_ec+=1 93 | 94 | # #print np.average(longestfork) 95 | # ec.append(float(win_ec)/float(sim)) 96 | # print ec -------------------------------------------------------------------------------- /code/other-sims/probasucessfulHS.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from math import floor 4 | 5 | nh=67 6 | na=33 7 | ntot=na+nh 8 | heights=range(100,101,50) 9 | e=1 10 | p=float(e)/float(1*ntot) 11 | 12 | sim=10000000 13 | 14 | ec =[] 15 | praos = [] 16 | 17 | start_time = time.time() 18 | 19 | num=1 20 | if e==1: num=77 21 | if e==5: num=43 22 | 23 | hs=1 24 | longestfork =[] 25 | # for height in heights: 26 | # win_ec = 0 27 | # for i in range(sim): 28 | # ch = np.random.binomial(nh, p, height) 29 | # ca = np.random.binomial(na, p, height) 30 | # # result of flipping a coin nha times, tested 1000 times. 31 | # # print ch 32 | # # print ca 33 | # j=0 34 | # win =1 35 | # sumh = ch[0]+ch[1] 36 | # suma = ch[0]+ca[0]+ca[1] 37 | # ind = 1 38 | # while sumh>suma and indsuma and indsum(vec): 31 | return 0 32 | else: 33 | list_of_nodes = [[new_node(-1,0,)]] 34 | for ind,v in enumerate(vec): 35 | list_of_nodes_at_slot_ind = [] 36 | for i in range(v+1): 37 | for node in list_of_nodes[ind]: #take all the nodes from slot before i.e. ind-1 38 | weight = node['weight'] + i 39 | nnode = new_node(ind,weight) 40 | list_of_nodes_at_slot_ind.append(nnode) 41 | list_of_nodes.append(list_of_nodes_at_slot_ind) 42 | ct = 0 43 | for node in list_of_nodes[-1]: 44 | if node['weight']>=num: 45 | ct+=1 46 | 47 | return ct 48 | 49 | 50 | 51 | 52 | 53 | def simu(sim): 54 | np.random.seed()#initialise random seed for different processors 55 | # we consider two variant of the attack: 56 | # 1. the unrealistic "worst case" scenario where the honest chain is completely split due to synchrony errors 57 | wh_unrealistic = [] 58 | # 2. the synchronous case where the honest chain is never split (+ our adversary does not perform an epoch boundary) 59 | wh_sync = [0] 60 | # we could take the average of both depending on our threat model 61 | wa = [] 62 | 63 | for i in range(sim): 64 | ch = np.random.binomial(nh, p, height) 65 | ca = np.random.binomial(na, p, height) 66 | ca = np.array(ca)+1 # we add one to consider the option of "null blocks" 67 | h_sync = sum(ch) 68 | a_max = sum(ca) #heaviest chain that adversary can create 69 | if unrealistic: h_unrealistic = sum([1 if ch[i]>0 else 0 for i in range(len(ch))]) 70 | #diff = a_max-h_sync 71 | winners = count_possibilities(ca,h_sync) 72 | wa.append(winners) 73 | return max(wa) 74 | 75 | 76 | 77 | pool = mp.Pool(mp.cpu_count()) 78 | print(mp.cpu_count()) 79 | results = pool.map(simu, [Num_of_sim_per_proc]*mp.cpu_count()) 80 | pool.close() 81 | 82 | print(results, max(results)) 83 | print("--- %s seconds ---" % (time.time() - start_time)) -------------------------------------------------------------------------------- /code/vrf-chain-sim/find_pattern2.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from math import floor 4 | import multiprocessing as mp 5 | import scipy.special 6 | #Initialize parameters 7 | Num_of_sim_per_proc = 1 8 | start_time = time.time() 9 | e = 5. 10 | alpha = 0.33 11 | ntot = 100 12 | na = int(ntot*alpha) 13 | nh = ntot - na 14 | height = 5 #height of the attack 15 | p=float(e)/float(1*ntot) 16 | unrealistic = 0 #do we want to compute the worst case or just the synchronous case? 17 | def multinomial(lst): 18 | res, i = 1, sum(lst) 19 | i0 = lst.index(max(lst)) 20 | for a in lst[:i0] + lst[i0+1:]: 21 | for j in range(1,a+1): 22 | res *= i 23 | res //= j 24 | i -= 1 25 | return res 26 | ## use multinomial coefficient 27 | def new_node(slot,weight): 28 | return { 29 | 'slot': slot, 30 | 'weight':weight 31 | } 32 | 33 | 34 | def print_weight(vec):#given a vector of number of election won at each slot, how many 35 | # "situations" gives a chain weight (i.e. sum of blocks) higher than some number 36 | list_of_nodes = [[new_node(-1,0,)]] 37 | for ind,v in enumerate(vec): 38 | list_of_nodes_at_slot_ind = [] 39 | for i in range(v+1): 40 | for node in list_of_nodes[ind]: #take all the nodes from slot before i.e. ind-1 41 | weight = node['weight'] + i 42 | nnode = new_node(ind,weight) 43 | list_of_nodes_at_slot_ind.append(nnode) 44 | list_of_nodes.append(list_of_nodes_at_slot_ind) 45 | dict_of_weight = {i: 0 for i in range(sum(vec)+1)} 46 | for elt in list_of_nodes[-1]: 47 | w = elt['weight'] 48 | dict_of_weight[w]+=1 49 | return dict_of_weight 50 | 51 | 52 | def count(ca): 53 | #create first list with (s=sum(ca_i), s-1, s-2, ..., s-ca_n) 54 | s=sum(ca) 55 | print(ca) 56 | ca = [x for x in ca if x != 0] 57 | n=len(ca) 58 | l1 = [s-i for i in range(ca[-1]+1)] 59 | l = np.array(l1.copy()) 60 | for j in range(1,n): 61 | for i in range(1,ca[-1-j]+1): 62 | ll = np.array(l1)-i 63 | l=np.concatenate((l,ll),axis =0) 64 | l1 = l.copy() 65 | dict_of_weight = {i: 0 for i in range(sum(ca)+1)} 66 | for elt in l: 67 | dict_of_weight[elt]+=1 68 | return dict_of_weight 69 | 70 | def simu(sim): 71 | np.random.seed()#initialise random seed for different processors 72 | wa = [] 73 | for i in range(sim): 74 | ca = np.random.binomial(na, p, height) 75 | winners = print_weight(ca) 76 | wa.append(winners) 77 | #ca = np.array(ca)+1 78 | #tot = np.prod(ca) 79 | cc = count(ca) 80 | return wa[0], cc, wa[0] == cc 81 | 82 | 83 | pool = mp.Pool(1) 84 | #print(mp.cpu_count()) 85 | results = pool.map(simu, [Num_of_sim_per_proc]) 86 | pool.close() 87 | 88 | print(results) 89 | print("--- %s seconds ---" % (time.time() - start_time)) -------------------------------------------------------------------------------- /code/other-sims/catchup_epoch_boundary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import utils as u 3 | import numpy as np 4 | 5 | # This scripts computes the advantage of running the lookfwd attack according to 6 | # the paper formula (lookfwd.pdf) 7 | # number of blocks 8 | sims = 10000 9 | e=5.0 10 | att=.33 11 | rat=1-att 12 | miners = 1000 13 | ratMin = rat * miners 14 | attMin = att * miners 15 | p = e/miners 16 | 17 | s = 2**-30 18 | 19 | # multiDraw is not a good approximation since it redraws the full chain 20 | # at every epoch of the attempted catchup. This is tantamount to the 21 | # adversary redrawing every past epoch at every epoch, which is not the 22 | # right model and increases their chances (since more entropy) 23 | def multiDraw(): 24 | caughtUp = 0 25 | maxDistToCU = 0 26 | for _ in range(sims): 27 | ## we can use the number from block withholding sim here as max 28 | for distCatchup in range(1, 250): 29 | ratBlocks = sum(np.random.binomial(ratMin, p, distCatchup)) 30 | advBlocks = sum(np.random.binomial(attMin, p, distCatchup)) 31 | 32 | if advBlocks >= ratBlocks: 33 | caughtUp += 1 34 | maxDistToCU = max(distCatchup, maxDistToCU) 35 | break 36 | 37 | print caughtUp 38 | pCatchUp = float(caughtUp)/sims 39 | print "pr(catchup) = " + str(pCatchUp) 40 | 41 | r = 1 42 | while pCatchUp**r > s: 43 | r += 1 44 | 45 | print "secure after " + str(r) + " cycles of at most " + str(maxDistToCU) + " epochs" 46 | 47 | 48 | def singleDraw(): 49 | caughtUp = 0 50 | maxDistToCU = 0 51 | CUDists = [] 52 | 53 | for _ in range(sims): 54 | # count rat blocks found in null adversarial period 55 | ratAdvance = 0 56 | # ratAdvance = np.random.binomial(ratMin/2, p, 1)[0] 57 | 58 | maxCatchup = 250 59 | ratBlocks = np.random.binomial(ratMin, p, maxCatchup) 60 | advBlocks = np.random.binomial(attMin, p, maxCatchup) 61 | for distCatchup in range(1, maxCatchup): 62 | if sum(advBlocks[:distCatchup]) >= sum(ratBlocks[:distCatchup]) + ratAdvance: 63 | caughtUp += 1 64 | maxDistToCU = max(distCatchup, maxDistToCU) 65 | CUDists.append(distCatchup) 66 | break 67 | 68 | print caughtUp 69 | pCatchUp = float(caughtUp)/sims 70 | print "pr(catchup) = " + str(pCatchUp) 71 | 72 | r = 1 73 | while pCatchUp**r > s: 74 | r += 1 75 | 76 | print "secure after " + str(r) + " cycles of at most " + str(maxDistToCU) + " epochs" 77 | 78 | CUDists.sort(reverse=True) 79 | avg = np.average(CUDists[:r]) 80 | 81 | print "looking at worst " + str(r) + " dists, we can take " + str(avg) 82 | 83 | singleDraw() 84 | 85 | 86 | -------------------------------------------------------------------------------- /code/beacon-sim/leaderelection.py: -------------------------------------------------------------------------------- 1 | import networkx as nx 2 | import numpy as np 3 | import itertools 4 | 5 | nh=67 #number of honest players 6 | na=33#number of adversarial players 7 | ntot=na+nh #total number of players 8 | p=1./float(ntot) #proba for one leader to be elected 9 | Kmax=40 #length of the attack 10 | grind_max=30 #how many "grinds" we allow 11 | sim=100 #number of simulations 12 | forks=[] 13 | 14 | 15 | 16 | #This is the "no grinding" strategy 17 | forks=[] 18 | for i in range(sim): 19 | nogrinding_fork_length=0 20 | slot_number=0 21 | ca = np.random.binomial(na, p, 1)[0]#each player toss a coin that succeed with probability p 22 | #(i.e. each player is elected with probability p) 23 | while slot_number0:#there were leaders elected, one block is created 25 | nogrinding_fork_length+=1 26 | #no leaders elected everyone moves to next slot 27 | slot_number+=1 28 | ca = np.random.binomial(na, p, 1)[0]#each player toss 29 | #a new coin for that round 30 | forks.append(nogrinding_fork_length) 31 | 32 | 33 | print "Length of Fork without grinding: {f}.".format(f=np.average(forks)) 34 | 35 | ### Case where adversary is grinding 36 | 37 | ##this is how to add the next "set of nodes" we need to do it for every node until 38 | ## we reach KMax 39 | def grind(n): 40 | index_parent = n 41 | lgth = G.node[n]['length'] 42 | c = G.node[n]['num_winner'] 43 | slot = G.node[n]['slot'] 44 | global ct 45 | global current_list 46 | if slot0 and jmax_l: max_l = G.node[n]['length']#we choose the longest fork 69 | grind(n) #add all the blocks created for grinding 70 | 71 | forks.append(max_l) 72 | 73 | print "Length of adversarial fork with grinding: {f}".format(f=np.average(forks)) 74 | 75 | 76 | 77 | #what happens to the rest of the player? (not grinding) 78 | forks=[] 79 | for i in range(sim): 80 | honest_fork_length=0 81 | slot_number=0 82 | ch = np.random.binomial(nh, p, 1)[0]#honest players each toss a coin to see if elected leaders 83 | 84 | while slot_number0:#there were leaders elected, one block is created 86 | honest_fork_length+=1 87 | #no leaders elected everyone moves to next slot 88 | slot_number+=1 89 | ch = np.random.binomial(nh, p, 1)[0]#each honest leaders toss 90 | #a new coin for that round 91 | forks.append(honest_fork_length) 92 | 93 | 94 | #longest chain case: 95 | print "Rest of player Fork: {f}.".format(f=np.average(forks)) -------------------------------------------------------------------------------- /code/vrf-chain-sim/frozen_vrf_biasability.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from math import floor 4 | import multiprocessing as mp 5 | import scipy.special 6 | #Initialize parameters 7 | Num_of_sim_per_proc = 100000 8 | height = 6 #height of the attack 9 | 10 | start_time = time.time() 11 | e = 5. 12 | alpha = 0.33 13 | ntot = 1000 14 | na = int(ntot*alpha) 15 | nh = ntot - na 16 | p=float(e)/float(1*ntot) 17 | unrealistic = 0 #do we want to compute the worst case or just the synchronous case? 18 | 19 | #number of blocks after which we update the vrf chain 20 | frozen = 1 21 | 22 | 23 | def count_possibilities(ca,num): 24 | #create first list with (s=sum(ca_i), s-1, s-2, ..., s-ca_n) 25 | #print("num = ",num, "sum ca inside fct = ", sum(ca)) 26 | if num>sum(ca): 27 | return 0 28 | else: 29 | s=sum(ca) 30 | vrf_ca = [ca[i] for i in range(len(ca)) if i%frozen == 0] 31 | #vrf_ca is the vector of all the blocks that count for leader election 32 | 33 | ss= sum([ca[i] for i in range(len(ca)) if i%frozen != 0]) 34 | # the adversary includes in its chain all the blocks that do not count 35 | # for the ticket chain, thus we add these blocks to the weight 36 | ca = [x for x in vrf_ca if x != 0] 37 | # ca is now the list of all blocks that count for vrf 38 | n=len(ca) 39 | l1 = [s-i for i in range(ca[-1]+1) if s-i>=num] 40 | l = np.array(l1.copy()) 41 | for j in range(1,n): 42 | for i in range(1,ca[-1-j]+1): 43 | ll = np.array(l1)-i 44 | ll = [x for x in ll if x>=num] 45 | l=np.concatenate((l,ll),axis =0) 46 | l1 = l.copy() 47 | # dict_of_weight = {i: 0 for i in range(sum(ca)+1)} 48 | # for elt in l: 49 | # dict_of_weight[elt]+=1 50 | # return dict_of_weight 51 | #assert len(l) == np.prod(np.array(ca)+1) 52 | ct = len(l) 53 | # if ct>10: 54 | # print(ca,num,l,len(l)) 55 | return ct 56 | 57 | 58 | 59 | 60 | def simu(sim): 61 | np.random.seed()#initialise random seed for different processors 62 | # we consider two variant of the attack: 63 | # 1. the unrealistic "worst case" scenario where the honest chain is completely split due to synchrony errors 64 | wh_unrealistic = [] 65 | # 2. the synchronous case where the honest chain is never split (+ our adversary does not perform an epoch boundary) 66 | wh_sync = [0] 67 | # we could take the average of both depending on our threat model 68 | wa = [] 69 | nn =0 70 | for i in range(sim): 71 | ch = np.random.binomial(nh, p, height) 72 | ca = np.random.binomial(na, p, height) 73 | #print("ca = ",ca, " sum ca with HS = ", sum(ca)+ch[0] , "sum ch",sum(ch)) 74 | h_sync = sum(ch) 75 | if unrealistic: h_unrealistic = sum([1 if ch[i]>0 else 0 for i in range(len(ch))]) 76 | #diff = a_max-h_sync 77 | new_ca = np.concatenate(([ca[0]+ch[0]],ca[1:]),axis =0) 78 | winners = count_possibilities(new_ca,h_sync) 79 | # count number of times where adversary could reset 80 | wa.append(winners) 81 | if ch[0]+sum(ca)>=sum(ch): nn +=1 82 | return np.average(wa) 83 | 84 | 85 | 86 | pool = mp.Pool(mp.cpu_count()) 87 | #print(mp.cpu_count()) 88 | results = pool.map(simu, [Num_of_sim_per_proc]*mp.cpu_count()) 89 | #results = pool.map(simu, [Num_of_sim_per_proc]*1) 90 | pool.close() 91 | 92 | print(results,np.average(results)) 93 | print("--- %s seconds ---" % (time.time() - start_time)) -------------------------------------------------------------------------------- /code/attacks/attack-calcs.py: -------------------------------------------------------------------------------- 1 | # These functions are used for rapidly calculating the values of closed form 2 | # expressions explored in the attacks.tex document to drive research. 3 | # 4 | # This is intended to be run with python 2. 5 | 6 | import scipy.special 7 | import math 8 | import matplotlib.pyplot as plt 9 | 10 | 11 | ###**Predictable Selfish Mining Attack Probabilities**## 12 | 13 | # ElectionWinis the exact probability that an attacker with power a 14 | # wins m out of n successive elections where the final election must be won. 15 | def ElectionWin(n, m, a): 16 | return a**m * scipy.special.binom(n-1, m-1) * (1.0 - a)**(n-m) 17 | 18 | # Success is a (loose) upper bound on the probability that an attacker with 19 | # power a beats the honest chain in an instance where the attacker knows that 20 | # they will win m out of n successive elections and follows the strategy of 21 | # witholding until the success in the final round. Bound is loose because 22 | # chernoff bound is not tight and because we give attacker probability 1 23 | # in cases where chernoff bound is not useful. 24 | def Success(n, m, a): 25 | if n == 0: # handle div-by-zero 26 | return 0.0 27 | mu = n * (1.0 - a) 28 | delta = 1 - (m / (n * (1.0 - a))) 29 | if delta < 0: # can't apply bound if delta < 0 30 | return 1.0 31 | chernoff = math.exp(-1 * mu * delta**2 / 2.0) 32 | return min(1.0, chernoff) # min might not be necessary with the check on delta 33 | 34 | # ForkWin is a (loose) upper bound on the probability that an attacker with 35 | # power a can successfully mine a fork of n rounds. Bound is not tight because 36 | # we use union bound on all values of m when in reality events with m1 out 37 | # of n successes overlap with events having m2 out of n successes. 38 | def ForkWin(n, a): 39 | win = 0.0 40 | for m in range(1, n+1): 41 | win += ElectionWin(n, m, a) * Success(n, m, a) 42 | return win 43 | 44 | # ForksPerYear is an upper bound on expected forks per year based on ForkWin 45 | # Probability and 30 second block times. This is an upper bound because it 46 | # applies an aggressive union bound over every block when actually there is 47 | # a lot of dependence. For example if round x has no forks with 9 out of 48 | # 10 attacker wins then then the chances of round x + 1 having 9 out of 10 49 | # wins is much lower. 50 | def ForksPerYear(n, a): 51 | blocksPerYear = 2.0*60.0*24.0*365.0 52 | return ForkWin(n, a) * blocksPerYear 53 | 54 | 55 | # PDayLongFork determines probability of attacker with power a running a 56 | # fork for a day's worth of blocks. 57 | def PThreeHourFork(a): 58 | blocksPerYear = 2.0*60.0*24.0*365.0 59 | return ForkWin(360, a) * 360 60 | 61 | 62 | 63 | ## Matplotlib plotting 64 | def plotForks(): 65 | for a in [0.01, 0.05, 0.1]: 66 | data = [] 67 | for n in range(1, 500): 68 | data.append(ForkWin(n,a)) 69 | plt.plot(range(1,500), data) 70 | plt.xlabel("Fork Length (rounds)") 71 | plt.ylabel("Success Probability") 72 | plt.ylim(top=0.01) 73 | plt.xlim(right=100) 74 | plt.legend([r'$\alpha=0.01$', r'$\alpha=0.05$', r'$\alpha=0.1$'], loc='upper right') 75 | plt.title("Upper bound on fork probabilities") 76 | plt.savefig("small-probs.png") 77 | plt.show() 78 | 79 | if __name__ == '__main__': 80 | plotForks() 81 | 82 | -------------------------------------------------------------------------------- /code/other-sims/headstart_henri.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.special as spe 3 | from scipy.stats import binom 4 | 5 | # User Params 6 | k = 10000 # number of epochs 7 | alpha = 0.1 8 | e = 5.0 9 | negl = 2**-5 10 | numSims = 1000 11 | 12 | # Modeling Params 13 | miners = 10000 14 | hon = miners * (1-alpha) 15 | adv = miners * alpha 16 | p = e/miners 17 | 18 | def chernoff(): 19 | # using honest miners since > than attacker (ie give largest bound) 20 | expectedVal = p * hon 21 | for i in range(int(e), int(e*100)): 22 | sigma = i / expectedVal -1 23 | likelihood = np.exp(-(sigma**2 / (2+sigma))*expectedVal) 24 | if likelihood < negl: 25 | return i 26 | 27 | 28 | # let H1, H2 be the first and second epoch LE results for honest parties 29 | # and A1, A2, be those for adversarial. 30 | # We are looking for Pr(|H1| + |H2| <= |H1| + |O1| + |O2|): the condition 31 | # in which the adversary will launch an attack. 32 | 33 | 34 | # Method 1: sum probabilities for each event, using chernoff to get rid of long tail 35 | def sumProbas(): 36 | tailBound = 2*chernoff() 37 | print "binding at {ch}".format(ch=tailBound) 38 | win = 0 39 | rat = 0 40 | honBinoms = {} 41 | advBinoms = {} 42 | 43 | for h in range (0, tailBound + 1): 44 | honBinoms[h] = binom.pmf(h, hon, p) 45 | advBinoms[h] = binom.pmf(h, adv, p) 46 | 47 | # representing the first and second honest runs separately, both adv runs together 48 | for h0 in range (1, tailBound + 1): 49 | for h1 in range(0, tailBound + 1): 50 | honPr = honBinoms[h0] * honBinoms[h1] 51 | for a0 in range(1, tailBound + 1): 52 | for a1 in range(0, tailBound + 1): 53 | advPr = advBinoms[a0] * advBinoms[a1] 54 | advWins = h0 + a0 + a1 55 | if advWins >= h0 + hon*p: 56 | prod = honPr *advPr 57 | rat += prod 58 | if advWins >= h0 + h1: 59 | win += prod 60 | return win, rat 61 | 62 | # Method 2: actually simulate using binomial series 63 | def binomialRuns(): 64 | successfulAtk = 0 65 | rationalAtk = 0 66 | ratLoss = 0 67 | luckyWin = 0 68 | luckyLoss = 0 69 | for i in range(numSims): 70 | ch = np.random.binomial(hon, p, 2) 71 | ca = np.random.binomial(adv, p, 2) 72 | 73 | honWins = ch[0] + ch[1] 74 | advWins = ch[0] + ca[0] + ca[1] 75 | if ch[0] > 0 and ca[0] > 0: 76 | if advWins >= ch[0] + hon*p: 77 | # extra condition without which a rational adv would not run the attack 78 | rationalAtk += 1 79 | if advWins >= honWins: 80 | successfulAtk += 1 81 | else: 82 | ratLoss += 1 83 | else: 84 | if advWins >= honWins: 85 | luckyWin += 1 86 | else: 87 | luckyLoss += 1 88 | 89 | 90 | return successfulAtk/float(numSims), rationalAtk/float(numSims), ratLoss/float(numSims), luckyWin/float(numSims), luckyLoss/float(numSims) 91 | 92 | sum_, rat_ = sumProbas() 93 | suc, rat, loss, w, l = binomialRuns() 94 | print "Method 1: likelihood of successful atk is: {runs}. Likelihood of rational atk is: {rat}".format(runs=sum_, rat=rat) 95 | print "Method 2: likelihood of successful atk is: {runs}. Likelihood of rational atk is: {rat}. Loss is {l}. Lucky {w}. Unlucky {ll}".format(runs=suc, rat=rat, l=loss, w=w, ll=l) 96 | -------------------------------------------------------------------------------- /code/other-sims/canadversarycatchup.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from math import floor 4 | 5 | nh=60 6 | na=40 7 | ntot=na+nh 8 | heights=range(250,151,10) 9 | p=5./float(1*ntot) 10 | 11 | sim=1000000 12 | 13 | ec =[] 14 | 15 | 16 | start_time = time.time() 17 | 18 | ## Here we consider the epoch boundary attack with a Byzantine (i.e. not rational 19 | ## fully malicious adversary). 20 | ## The idea is that whenever the adversary is elected leader it could potentially 21 | ## maintain two chains of same weight growing, but as soon as it is not 22 | ## elected leader anymore, then the honest players will have to converge to the same 23 | ## chain, assuming perfect synchronicity. 24 | ## however when the adversary is not elected leader (and stops the epoch boundary attack) 25 | ## it could still try and maintain the chain that has been abandoned by the honest players 26 | ## if the adversary gets luck then the abandoned chain can catch up with the other chain 27 | ## and thus the adversary can continue the epoch boundary attack. 28 | 29 | for height in heights: #the adversary tries to keep maintaining two chain of same weight and 30 | #length "height", we try different heights and see the probability of succeeding. Ig this probability is 31 | #small enough, we consider this height a good finality candidate. 32 | win_ec = 0 33 | #longestfork =[] 34 | for i in range(sim): 35 | ch = np.random.binomial(nh, p, height) 36 | ca = np.random.binomial(na, p, height) 37 | # result of flipping a coin nha times, tested height times. (i.e. number of leaders 38 | # at ach slot for both adversary and honest players) 39 | j=0 40 | win =1 41 | while jsuma and indsuma: #we have reach the end of the attack 53 | #and adversary has not catch up 54 | win =0 55 | break 56 | else: #adversary has catch up and can continue the forks until 57 | #it is not elected leader again 58 | j= ind 59 | 60 | #if the adversary did not catch up, we try to see 61 | #if it has a better chance by instead, trying to 62 | #perform a headstart attack from a round before 63 | if j>0 and win == 0 : 64 | if ch[j]/2 +ch[j+1] - (ca[j-1]-1+ca[j+1]) < ch[j+1]- ca[j+1]: #check if the advantage, 65 | #i.e. diff between abandoned and honest chain, 66 | #is better in this case (if not no need to try) 67 | sumh = ch[j]/2 +ch[j+1]#at round j the power of honest miners is still split between two chains 68 | #at round j+1 it goes all back to one chain 69 | suma = ca[j-1]-1+ca[j+1]#the adversary used all the blocks it withheld in period j-1 70 | #(all of them minus 1 that it used to maintain the forks) 71 | ind = j+2 72 | while sumh>suma and indsuma: #we have reach the end of the attack 78 | #and adversary has not catch up 79 | win =0 80 | break 81 | else: #adversary has catch up and can continue the forks until 82 | #it is not elected leader again 83 | j= ind 84 | else: 85 | j+=1 86 | if win ==1: 87 | win_ec+=1 88 | #longestfork.append(j) 89 | #print np.average(longestfork) 90 | ec.append(float(win_ec)/float(sim)) 91 | 92 | print ec 93 | 94 | print("--- %s seconds ---" % (time.time() - start_time)) -------------------------------------------------------------------------------- /code/vrf-chain-sim/find_pattern.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | from math import floor 4 | import multiprocessing as mp 5 | import scipy.special 6 | #Initialize parameters 7 | Num_of_sim_per_proc = 1 8 | start_time = time.time() 9 | e = 5. 10 | alpha = 0.33 11 | ntot = 100 12 | na = int(ntot*alpha) 13 | nh = ntot - na 14 | height = 5 #height of the attack 15 | p=float(e)/float(1*ntot) 16 | unrealistic = 0 #do we want to compute the worst case or just the synchronous case? 17 | def multinomial(lst): 18 | res, i = 1, sum(lst) 19 | i0 = lst.index(max(lst)) 20 | for a in lst[:i0] + lst[i0+1:]: 21 | for j in range(1,a+1): 22 | res *= i 23 | res //= j 24 | i -= 1 25 | return res 26 | ## use multinomial coefficient 27 | def new_node(slot,weight): 28 | return { 29 | 'slot': slot, 30 | 'weight':weight 31 | } 32 | 33 | 34 | def print_weight(vec):#given a vector of number of election won at each slot, how many 35 | # "situations" gives a chain weight (i.e. sum of blocks) higher than some number 36 | list_of_nodes = [[new_node(-1,0,)]] 37 | for ind,v in enumerate(vec): 38 | list_of_nodes_at_slot_ind = [] 39 | for i in range(v+1): 40 | for node in list_of_nodes[ind]: #take all the nodes from slot before i.e. ind-1 41 | weight = node['weight'] + i 42 | nnode = new_node(ind,weight) 43 | list_of_nodes_at_slot_ind.append(nnode) 44 | list_of_nodes.append(list_of_nodes_at_slot_ind) 45 | dict_of_weight = {i: 0 for i in range(sum(vec)+1)} 46 | for elt in list_of_nodes[-1]: 47 | w = elt['weight'] 48 | dict_of_weight[w]+=1 49 | return dict_of_weight 50 | 51 | 52 | def count_n2(ca): 53 | num = len([x for x in ca if x > 1]) #count number of slot with more than 2 slots 54 | n1 = len([x for x in ca if x != 0]) 55 | if n1>1: 56 | num += scipy.special.binom(n1, 2) 57 | return num 58 | def count_n3(ca): 59 | num = 0 60 | n3 = len([x for x in ca if x > 2]) #count number of slot 3 blocks 61 | n2 = len([x for x in ca if x > 1]) 62 | num += n3 63 | n1 = len([x for x in ca if x != 0]) 64 | if n1>2: 65 | num += scipy.special.binom(n1, 3) # 1 1 1 66 | # 2 1 67 | if n1>0: 68 | #num += scipy.special.binom(n2, 3) 69 | num +=n2*(n1-1) 70 | return num 71 | def count_n5(ca): 72 | num = 0 73 | n5 = len([x for x in ca if x > 4]) 74 | n4 = len([x for x in ca if x > 3]) 75 | n3 = len([x for x in ca if x > 2]) #count number of slot 3 blocks 76 | n2 = len([x for x in ca if x > 1]) 77 | n1 = len([x for x in ca if x != 0]) 78 | num += n5 # 5 79 | if n1>4: 80 | num += scipy.special.binom(n1, 5) # 1 1 1 1 1 81 | # 2 3 82 | if n2>1: 83 | num += n3*(n2-1) 84 | # 4 1 85 | if n1>0: 86 | #num += scipy.special.binom(n2, 3) 87 | num +=n4*(n1-1) 88 | # 2 1 1 1 89 | num+=n2*(scipy.special.binom(n1-1, 3)) 90 | # 2 2 1 91 | if n1>1 and n2>1: 92 | num += (n1-2)*scipy.special.binom(n2, 2) 93 | #1 1 3 94 | if n3>0: 95 | num+=n3*scipy.special.binom(n1-1,2) 96 | return num 97 | 98 | def count_k(k,ca): 99 | #n_i = len([x for x in ca if x >= k]) 100 | n = lambda i: len([x for x in ca if x >= i]) 101 | n1 = n(1) 102 | #sqr_fun = lambda x: x * x 103 | #n(i) 104 | num = n(k)+scipy.special.binom(n1,k) 105 | for j in range(1,k-1): 106 | num+= n(k-j)*scipy.special.binom(n1-1,j) 107 | return num 108 | 109 | 110 | 111 | 112 | def count_n4(ca): 113 | num = 0 114 | n4 = len([x for x in ca if x > 3]) 115 | n3 = len([x for x in ca if x > 2]) #count number of slot 3 blocks 116 | n2 = len([x for x in ca if x > 1]) 117 | n1 = len([x for x in ca if x != 0]) 118 | num += n4 # 4 119 | if n1>3: 120 | num += scipy.special.binom(n1, 4) # 1 1 1 1 121 | # 2 2 122 | if n2>1: 123 | num += scipy.special.binom(n2, 2) 124 | # 3 1 125 | if n1>0: 126 | #num += scipy.special.binom(n2, 3) 127 | num +=n3*(n1-1) 128 | # 2 1 1 129 | num+=n2*(scipy.special.binom(n1-1, 2)) 130 | return num 131 | def count_n1(ca): 132 | return len([x for x in ca if x != 0]) 133 | 134 | def simu(sim): 135 | np.random.seed()#initialise random seed for different processors 136 | wa = [] 137 | for i in range(sim): 138 | ca = np.random.binomial(na, p, height) 139 | winners = print_weight(ca) 140 | wa.append(winners) 141 | #ca = np.array(ca)+1 142 | #tot = np.prod(ca) 143 | return wa, count_n2(ca), count_k(2,ca), count_n3(ca), count_k(3,ca),count_n4(ca), count_k(4,ca) 144 | 145 | 146 | 147 | pool = mp.Pool(1) 148 | #print(mp.cpu_count()) 149 | results = pool.map(simu, [Num_of_sim_per_proc]) 150 | pool.close() 151 | 152 | print(results) 153 | print("--- %s seconds ---" % (time.time() - start_time)) -------------------------------------------------------------------------------- /code/other-sims/lookfwd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import utils as u 3 | 4 | # This scripts computes the advantage of running the lookfwd attack according to 5 | # the paper formula (lookfwd.pdf) 6 | # number of blocks 7 | s = 10000 8 | e=5 9 | att=1/3 10 | honest=0 11 | rat=1-att-honest 12 | # target success rate of the rational player 13 | rate_success=0.9 14 | 15 | def ratio(e,att,trials=e): 16 | honest=1-att 17 | p0 = u.poisson(0,honest*e) 18 | b = u.poisson(1, att*e) 19 | ratio = 1 / (1 - b * p0 * trials) 20 | return ratio 21 | 22 | # prob. attacker wins more than 0 times 23 | def att_prb(e,f): 24 | return 1 - u.poisson_cdf(0,e*att) 25 | 26 | # prob. honest wins 0 times 27 | def honest_prb(e,f): 28 | return u.poisson(0,e*f) 29 | 30 | def attacks_happens(e,att,honest): 31 | return att_prb(e,att) * honest_prb(e,honest) 32 | 33 | def lookfwd_writeup(e,att,honest): 34 | print("LOOKFWD writeup computations:") 35 | print("-> ratio for e={} is {}".format(e,ratio(e,att))) 36 | print("-> ratio for e={} is {}".format(3,ratio(e,att,3))) 37 | print("-> attacks happens {} fraction of the time".format(attacks_happens(e,att,honest))) 38 | # e = 5, attacker fraction = 0.3333333333333333 39 | # -> ratio is 1.05948988933682 40 | # -> attacks happens 0.035673993347252374 fraction of the time 41 | 42 | 43 | # lookfwd_attack looks at the feasibility of the attack if there is a rational 44 | # miner that is trying to mine on the first round (where attacker is not sole 45 | # miner) if it sees the attacker "dropped" too many blocks from that round when 46 | # grinding. Rational player must be able to grind more than the chain of the 47 | # attacker at the third round. 48 | # Description: 49 | # Round 1: attacker grinds on all blocks presents, evicting them one by one 50 | # Round 2: attacker is sole winner (so expected number of blocks acc. to power) 51 | # Round 3: two chains: 52 | # C1: attacker + honest chain 53 | # C2: rational player that mines on top of round 1, bypassing round 2 54 | def lookfwd_attack(s, e,att,rat,honest,rate_success): 55 | print("Attack LOOKFWD computations") 56 | ## Find the minimum number of blocks that attacker has to keep on the parent 57 | ## tipset such that rational player is unlikely to mine a heavier chain 58 | def find_minimum(s,e,att,rat,honest,rate_success): 59 | exph = e * honest 60 | expa = e * att 61 | expr = e * rat 62 | # = e I know 63 | exp_round = exph + expa + expr 64 | print("-> expected number of honest blocks: {}".format(exph)) 65 | print("-> expected number of attacker blocks: {}".format(expa)) 66 | print("-> expected number of rational blocks: {}".format(expr)) 67 | print("-> expected number of blocks in a round: {}".format(exp_round)) 68 | 69 | ## find probability that rational player will find more blocks when attacker 70 | ## only uses i blocks to mine on from the first round 71 | ## minimum_inclusion is the minimum number of blocks attacker must 72 | ## include 73 | ## Note that this sim does not include subtle behaviors that account for expected win/loss by miners whose 74 | ## blocks were grinded, etc. 75 | minimum_inclusion = int(exp_round) 76 | for blocks_kept in range(0,minimum_inclusion): 77 | round1 = blocks_kept 78 | round2 = expa 79 | round3 = expa + exph 80 | print("\t* Attacker number of blocks in chain: {} -> {:.3f} -> {:.3f}".format(round1,round2,round3)) 81 | # minimum number of blocks rational must mine at round 3 to get heavier 82 | # chain > round2 + round3 - "diff between round1 and expected # round1" 83 | blocks_discarded = exp_round - round1 84 | minrat = round2 + round3 - blocks_discarded 85 | # Pr[rational mines more than minrat] = 1 - Pr[rat mines < min_rat] 86 | pr = 1 - u.binomial_cdf(int(minrat), s*rat, e/s) 87 | print("\t Rational player needs to find >= {:.3f} blocks".format(minrat)) 88 | print("\t Rational probability of having more blocks: {:.3f}".format(pr)) 89 | if pr < rate_success: 90 | return blocks_kept 91 | ## if we reach here, that means there is no drop beyond minimum_inclusion that yields 92 | ## rational mining on adv chain 93 | return minimum_inclusion 94 | 95 | ## minimum number of blocks attacker has to keep 96 | min_keeping = find_minimum(s,e,att,rat,honest,rate_success) 97 | togrind_on = e - min_keeping 98 | print("-> minimum numbers of blocks to keep: {}".format(min_keeping)) 99 | print("-> maximum numbers of blocks to grind on: {}".format(togrind_on)) 100 | adv = ratio(e,att,togrind_on) 101 | original = ratio(e,att) 102 | print("-> ratio of the attacker: {:.4f} vs original {:4f}".format(adv,original)) 103 | 104 | print("e = {}, attacker fraction = {}".format(e,att)) 105 | lookfwd_writeup(e,att,honest) 106 | lookfwd_attack(s,e,att,rat,honest,rate_success) 107 | -------------------------------------------------------------------------------- /code/README.md: -------------------------------------------------------------------------------- 1 | # Code 2 | 3 | --- 4 | 5 | This section of the repository holds most of the simulation code used as part of work on Filecoin's Expected Consensus. 6 | 7 | Below, the list of sims. 8 | 9 | #### Sims in use 10 | 11 | - [ec-withhold](./other-sims/ec_withhold.py) -- Main Monte-Carlo simulation for EC, modeling finality against withholding attack (as in [BTC section 11](https://bitcoin.org/bitcoin.pdf)). 12 | - Generates results for block withholding (probability a longer adversarial chain was generated starting x blocks back, for variable x) and chain quality (portion of adversarial:honest blocks mined as compared to their relative power). 13 | - Used for setting recommended conf time in EC, under various settings. 14 | - Jump to the [list of sim settings](#monte-carlo-settings) 15 | - [markov](./other-sims/ec-markov.py) -- Markov Chain closed-form for EC, as described in [confirmation time observable](https://observablehq.com/d/432bc3aeac0ca166). 16 | - Models gap between honest and adversarial chain as markov states and maps evolution of the gap over a number of rounds. 17 | - Works with weighting-fn 18 | - Works with lbp 19 | - Works with headstart 20 | - [generate-chain](./other-sims/generate-chain.py) -- simply outputs short runs of binomial tosses for a set number of miners with equal probability of winning. 21 | - Helpful to visualize how many blocks can be found per round on expectation in EC, e.g. for visualizing how often null blocks can be found (counterintuitively high). 22 | - [ec-sim-zs](./ec-sim-zs/) -- Golang sim and viz of EC. 23 | - Ultra-aggressive (irrational) mining strategy: mine every available fork, release if heavier (no weighting fn) than your own. 24 | - Includes editable lbp 25 | - Useful for head change visualisation. 26 | - It helps graph the impact of having a randomness lookback on a miner's ability to NaS 27 | 28 | ### Other Sims 29 | 30 | - [ec-sim-w](./ec-sim-w/) -- Golang early sim of how EC works (not to spec). 31 | - [attacks](./attacks) -- Two sims helpful for calculating closed-form forking factor and bounds for EC, related to an (incomplete) [analysis of attacks in EC](https://www.overleaf.com/project/5be983c5db30c7318939372d). 32 | 33 | ### Monte-Carlo Settings 34 | 35 | The list below lays out the different parameter-sets that can be used with the [Monte-Carlo sim](./other-sims/ec-withhold.py) to simulate various results for EC. 36 | 37 | In short, the sim uses binomials to estimate number of blocks per round for EC for honest and adversarial miners over series of rounds. It models what an adversary might do by running standard selfish mining, and keeps track of 38 | 39 | - how often they mine blocks (vs expected from their power) -- quality 40 | - longest run of block withholding in series -- confirmation/finality 41 | 42 | #### Params list 43 | 44 | Below, the list of parameters to be changed as part of experiments. The full list of settings is printed out after a run for reproducibility. 45 | 46 | - store_output -- bool -- True: will store output of run in json under ./monte/; False: only command line output 47 | - alphas -- array, values in [0, .5] -- set of adversarial powers to simulate. 48 | - lookahead -- integer, value >= 1 -- gives power to adversary to simulate randomness lookback. 49 | - rounds_back -- array, values in N+ -- use if looking for outputs of likelihood of successful attack for specific number of rounds back 50 | - e_blocks_per_round -- array, values in N+ -- expected number of blocks per round. Default: 1 51 | - num_sims -- integer, value in N+ -- number of sims to run from which to sample results. Key in that it implies precision for results, eg 100 sims means % level precision, 1,000 means first decimal, 10,000 means second decimal... Sim slows linearly in num_sims. 52 | - target_conf -- array, values in [0,1] -- target security parameter we want: for how far back is proba of a competing chain less than target_conf likely to be found 53 | - Sim-to-run -- array, values in {Sim.EC, Sim.NOHS, Sim.NOTS} -- what model of consensus to run sim over. 54 | - NOTS -- No TipSets. Ignores potential multiple honest blocks found in a round. 55 | - NOHS -- No [HeadStart](https://github.com/filecoin-project/consensus/issues/22). Idealized EC (optimistic). 56 | - EC -- TipSets with HeadStart. 57 | - Wt_fn -- bool -- use number of blocks as measure of weight (false) or use [custom weight fn](https://observablehq.com/d/3812cd65c054082d) (true). The following params make up custom wt fn: 58 | - powerAtStart -- int, N+ -- PB size of network at start of sim. Default: 5000 59 | - powerIncreasePerDay -- float, in R*+ -- % increase in storage capacity of network per day. Default: .025 (2.5%) 60 | - wPunishFactor -- float, in [0, 1] -- see [weight fn observable](https://observablehq.com/d/3812cd65c054082d) 61 | - wStartPunish -- integer, in N+ -- see [weight fn observable](https://observablehq.com/d/3812cd65c054082d) 62 | - wBlocksFactor -- float, in [0, inf] -- see [weight fn observable](https://observablehq.com/d/3812cd65c054082d) 63 | 64 | Typically static: 65 | 66 | - Miners -- integer, > 100 -- all miners have equal power so p = expected_number_of_blocks_per_round/miners. Have found that for m > 100, sim results mostly converge. Default: 10k 67 | - sim_rounds -- integer, in N+ -- This number is how long each simulation lasts (in rounds). Needs to be long enough so that longest selfish run is representative of adv power. Default: 5k 68 | 69 | #### Example uses 70 | 71 | - Understanding tradeoffs of TipSets in EC (comparing EC, NOHS, NOTS) 72 | - Used to determine how costly TipSets are to consensus. See [early-sims](https://github.com/filecoin-project/consensus/issues/67) 73 | - Important factors to modulate: 74 | - sim_to_run 75 | - Understanding impact of different expected blocks per round 76 | - Important factors to modulate: 77 | - e_blocks_per_round 78 | - Understanding impact of lookback 79 | - Important factors to modulate: 80 | - lookahead 81 | -------------------------------------------------------------------------------- /code/other-sims/ec_markov.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | from scipy.stats import binom 4 | import numpy as np 5 | import pandas as pd 6 | import pdb 7 | import matplotlib.pyplot as plt 8 | 9 | # system wide params 10 | miners = 1000 11 | eBlocksPerRound = 1.0 12 | p = eBlocksPerRound/float(miners) 13 | 14 | # can be used to estimate with a headstart 15 | headstart = True 16 | 17 | if headstart: 18 | # random heuristic: let's pick best headstart with likelihood > 1/10k to happen for a 49% attacker, 19 | # or -x st binom.pmf(x, 490, 1./1000) > 10**-4 20 | start = -5 21 | else: 22 | start = 0 23 | 24 | # meta params 25 | errorBound = 10**-10 26 | stdDevPrecision = .99 27 | 28 | def checkDistrib(inProb): 29 | assert abs(1 - inProb) <= errorBound 30 | 31 | def chebLimits(mean, stdDev): 32 | # find number of std devs we want to get right precision 33 | k = 0 34 | i = 0. 35 | while k == 0: 36 | i += 1 37 | if i ** 2 >= 1./(1-stdDevPrecision): 38 | k = i 39 | return (int(math.floor(mean - k*stdDev)), int(math.ceil(mean + k*stdDev))) 40 | 41 | class MarkovChain: 42 | def __init__(self, alpha): 43 | self.alpha = alpha 44 | self.honestMiners = int(round((1-self.alpha)*miners)) 45 | self.advMiners = int(round(self.alpha*miners)) 46 | self.setupChain() 47 | 48 | def setupChain(self): 49 | self.mean = (self.honestMiners - self.advMiners)*p 50 | self.stdDev = miners*p*(1-p) 51 | self.transProbs = self.getTransProbs() 52 | 53 | def calculateTrans(self, trans): 54 | _sum = 0. 55 | if trans < 0: 56 | trans = abs(trans) 57 | for i in range(self.advMiners - trans + 1): 58 | # H[i] A[i+trans] 59 | _sum += binom.pmf(i, self.honestMiners, p) * binom.pmf(i + trans, self.advMiners, p) 60 | else: 61 | for i in range(self.advMiners + 1): 62 | # H[i+trans] A[i] 63 | _sum += binom.pmf(i + trans, self.honestMiners, p) * binom.pmf(i, self.advMiners, p) 64 | return _sum 65 | 66 | def calculateTransProbs(self, probs): 67 | # in any given round, we can move up to honestMiners up, or advMiners down, for a total 68 | # of miners unique possible state transitions 69 | for trans in range(-1*self.advMiners, self.honestMiners + 1): 70 | prob = self.calculateTrans(trans) 71 | probs[trans] = prob 72 | return probs 73 | 74 | def getTransProbs(self): 75 | transDoc = "./markov/transitions_{alpha}_{miners}.json".format(alpha=alpha, miners=miners) 76 | transProbs = dict() 77 | try: 78 | with open(transDoc) as f: 79 | tmp = json.load(f) 80 | # ergh, json int keys 81 | for key in tmp.keys(): 82 | transProbs[int(key)] = tmp[key] 83 | except: 84 | transProbs = self.calculateTransProbs(transProbs) 85 | assert sum(transProbs.values()) >= stdDevPrecision 86 | with open(transDoc, 'w') as f: 87 | json.dump(transProbs, f, sort_keys=True, indent=4) 88 | return transProbs 89 | 90 | def generateTransMatrix(probs, states): 91 | # transitionMatrix is represents the proba from state(row) to state(col) 92 | # for each slot: [ row -> col ]: what distance between col and row: col - row 93 | # also works with negs 94 | transMatrix = np.zeros(shape = (len(states), len(states))) 95 | for row in range(len(states)): 96 | for col in range(len(states)): 97 | key = states[col]-states[row] 98 | if key in probs: 99 | transMatrix[row][col] = probs[key] 100 | else: 101 | transMatrix[row][col] = 0.0 102 | return transMatrix 103 | 104 | def getBoundedStates(markovChain, rounds): 105 | bounds = chebLimits(markovChain.mean, markovChain.stdDev) 106 | return range(bounds[0]*rounds, bounds[1]*rounds + 1) 107 | 108 | def calcEndStateProbs(states, transMatrix, rounds): 109 | startVector = np.zeros(len(states)) 110 | startVector[states.index(start)] = 1 111 | expMatrix = np.linalg.matrix_power(transMatrix, rounds) 112 | endStates = np.matmul(startVector, expMatrix) 113 | checkDistrib(sum(endStates)) 114 | return endStates 115 | 116 | def atkSuccess(states, endStates): 117 | # all states for which gap <= 0 118 | _sum = 0 119 | for i in range(states.index(0) + 1): 120 | _sum += endStates[i] 121 | return _sum 122 | 123 | def getEndStates(markovChain, states, rounds): 124 | transMatrix = generateTransMatrix(markovChain.transProbs, states) 125 | return calcEndStateProbs(states, transMatrix, rounds) 126 | 127 | def plot(endStates, states, alpha, rounds): 128 | plt.plot(states, endStates) 129 | plt.xlabel("gap between H and A") 130 | plt.ylabel("likelihood") 131 | plt.title("{m} miners, alpha = {al}, over {rd} rounds".format(m = miners, al=alpha, rd = rounds)) 132 | plt.show() 133 | 134 | alphas = [k/100.0 for k in range(2, 54, 2)] 135 | roundsBack = range(5, 105, 10) 136 | 137 | successRates = [] 138 | print "{min} miners\n{eb} blocks per round on expectation\nheadstart: {start}".format(min=miners, eb=eBlocksPerRound, start=start) 139 | for alpha in alphas: 140 | print "\nFor alpha = {alpha}".format(alpha=alpha) 141 | AtkrSuccess = [] 142 | mc = MarkovChain(alpha) 143 | for rounds in roundsBack: 144 | states = getBoundedStates(mc, rounds) 145 | endStates = getEndStates(mc, states, rounds) 146 | # plot(endStates, states, alpha, rounds) 147 | prob = atkSuccess(states, endStates) 148 | print "{rounds} rounds back, atkr wins: {win}".format(rounds = rounds, win = prob) 149 | AtkrSuccess.append(prob) 150 | successRates.append(AtkrSuccess) 151 | 152 | df = pd.DataFrame(successRates, columns=roundsBack, index=alphas) 153 | print df 154 | -------------------------------------------------------------------------------- /code/other-sims/grinding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import numpy as np 3 | import multiprocessing as mp 4 | import collections as c 5 | 6 | poisson = np.random.poisson 7 | 8 | sim=1000 #number of simulations 9 | attacker=1/3 # fraction of attacker power 10 | honest=1-attacker 11 | e=5 12 | null_blocks=6#how many "grinds" we allow 13 | 14 | Node = c.namedtuple("Node",['weight','won','slot']) 15 | 16 | def nogrinding(e,power,sim,kmax): 17 | def runsim(): 18 | # + 1 to account for round where attack is based off ? 19 | return sum([poisson(e*power) for slot in range(kmax+1)]) 20 | return [runsim() for i in range(sim)] 21 | 22 | # grind_branch grinds on a branch of possiblities in the whole grinding tree 23 | def grind_branch(node,info): 24 | # constants but allows for parallelism 25 | power,e,null,kmax = info['power'],info['e'],info['null'],info['kmax'] 26 | if node.slot >= kmax: 27 | return [] 28 | 29 | # contains list of new nodes that future grinding attempts will try 30 | branches = [] 31 | # grinding attempts here means null blocks 32 | for null_block in range(null+1): 33 | new_slot = node.slot + null_block + 1 # one round after i null blocks 34 | if new_slot >= kmax: 35 | return branches 36 | 37 | # for the blocks won previously, I can try to grind on each of them 38 | for trial in range(node.won): 39 | won = poisson(e*power) 40 | if won == 0: 41 | continue 42 | 43 | new_node = Node(weight=node.weight + won - null_block, 44 | won=won, 45 | slot=new_slot) 46 | 47 | branches.append(new_node) 48 | 49 | return branches 50 | 51 | def grinding_runsim(info): 52 | e = info['e'] 53 | expected_blocks = int(info['power'] * e) + 1 54 | ## assumes he starts from his own blocks 55 | node = Node(weight=0, won=expected_blocks,slot=0) 56 | if info['headstart'] == True: 57 | # for headstart, he starts grinding on all the blocks presents in the 58 | # tipset 59 | node = Node(weight=0, won=info['e'],slot=0) 60 | 61 | branches = [node] 62 | max_weight = 0 63 | # as long as there are possiblities 64 | while len(branches) > 0: 65 | # take maximum weight seen so far 66 | max_local = max(n.weight for n in branches) 67 | if max_local > max_weight: 68 | max_weight = max_local 69 | 70 | res = [] 71 | # grind on all branches 72 | for branch in branches: 73 | newb = grind_branch(branch,info) 74 | if len(newb) > 0: 75 | res = res + newb 76 | 77 | branches = res 78 | 79 | return max_weight 80 | 81 | def grinding(e,power,sim,kmax,null_blocks,headstart=False,cpus=mp.cpu_count()): 82 | with mp.Pool(processes=cpus,maxtasksperchild=1) as pool: 83 | info = { 'e':e, 'power':power, 'null':null_blocks, 'kmax': kmax, 84 | 'headstart': headstart, 85 | } 86 | return pool.map(grinding_runsim, [info]*sim) 87 | 88 | def quality_chain(attacker, honest): 89 | return [1 if attacker[i]>=honest[i] else 0 for i in range(sim)] 90 | 91 | def run(kmax,null,e,attacker,headstart=False,log=False,cpus=mp.cpu_count()): 92 | honest = 1 - attacker 93 | honest_chain = nogrinding(e,honest,sim,kmax) 94 | avg_honest = np.average(honest_chain) 95 | 96 | attacker_nogrind = nogrinding(e,attacker,sim,kmax) 97 | avg_attacker_ng = np.average(attacker_nogrind) 98 | attacker_grind = grinding(e,attacker,sim,kmax,null_blocks,headstart,cpus=cpus) 99 | avg_attacker_grind = np.average(attacker_grind) 100 | 101 | nogrind_quality = quality_chain(attacker_nogrind,honest_chain) 102 | nogrind_prob = np.average(nogrind_quality) 103 | grinding_quality = quality_chain(attacker_grind,honest_chain) 104 | grinding_prob = np.average(grinding_quality) 105 | if log == True: 106 | print("Simulation starting with e={}, kmax={} and null blocks={}".format(e,kmax,null_blocks)) 107 | print("-> headstart mode: {}".format(headstart)) 108 | cpus = mp.cpu_count() 109 | print("-> number of CPUs: {}".format(cpus)) 110 | print("-> honest chain weight average: {:.3f}".format(avg_honest)) 111 | print("-> attacker chain weight average no grinding: {:.3f}".format(avg_attacker_ng)) 112 | print("-> attacker chain average with grinding: {:.3f}".format(avg_attacker_grind)) 113 | print("-> probability of success when not grinding: {:.3f}".format(nogrind_prob)) 114 | print("-> probability of success when grinding: {:.3f}".format(grinding_prob)) 115 | return avg_honest,avg_attacker_grind,grinding_prob 116 | 117 | def run_multiple(kmaxes,nulls,es,attackers,cpus=mp.cpu_count()): 118 | def f(v): 119 | if isinstance(v, float): 120 | return "{:.3f}".format(v) 121 | return "{}".format(v) 122 | 123 | print("e,attacker,kmax,null,headstart,weight_honest,weight_grinding,prob_success") 124 | for kmax in kmaxes: 125 | for null in nulls: 126 | # failsafe to try low value of kmax 127 | if null >= kmax: 128 | if kmax > 2: 129 | null=1 130 | else: 131 | null=0 132 | for e in es: 133 | for att in attackers: 134 | (wh,wa,noheadstart) = run(kmax,null,e,att,cpus=cpus) 135 | str1 = map(f,[e,att,kmax,null] + [False,wh,wa,noheadstart]) 136 | print("{}".format(",".join(str1))) 137 | (wh,wa,headstart) = run(kmax,null,e,att,headstart=True,cpus=cpus) 138 | str2 = map(f,[e,att,kmax,null] + [True,wh,wa,headstart]) 139 | print("{}".format(",".join(str2))) 140 | 141 | 142 | 143 | kmaxes = [2,5,6,7,8,9,10,11] 144 | run_multiple(kmaxes,[5],[e],[attacker]) 145 | -------------------------------------------------------------------------------- /specs/ec/ec.tla: -------------------------------------------------------------------------------- 1 | /* Expected Consensus 2 | --------------------------------- MODULE ec --------------------------------- 3 | EXTENDS Integers, TLC, FiniteSets, Sequences 4 | CONSTANT miners, NumMiners, USE_RANDOM 5 | 6 | MaxRounds == 20 7 | EarlyKill == 20 8 | MaxLeaders == 5 9 | rounds == 1..MaxRounds 10 | 11 | 12 | Min(set) == 13 | CHOOSE x \in set: \A y \in set: x <= y 14 | 15 | Range(f) == 16 | {f[x]: x \in DOMAIN f} 17 | 18 | (* --algorithm ExpectedConsensus 19 | variables 20 | miner_rounds = [miner \in miners |-> 2]; 21 | leaders = <<{<>}, {}>>; \* Let someone win the first round. 22 | 23 | define 24 | FairMining == 25 | \A miner \in miners: miner_rounds[miner] - Min(Range(miner_rounds)) <= 1 26 | 27 | BoundedLeaders == 28 | ~\E leader_group \in Range(leaders): Cardinality(leader_group) > MaxLeaders 29 | ModelBoundedLeaders == 30 | ~\E leader_group \in Range(leaders): Cardinality(leader_group) > MaxLeaders - 1 31 | 32 | \* This invariant is used to force an error trace after MaxRounds. 33 | NoProgress == 34 | /\ Len(leaders) < MaxRounds 35 | /\ miner_rounds \in [miners -> 0..MaxRounds] 36 | 37 | Scratch(miner, tipset) == 38 | \* all have equal chance of winning 39 | RandomElement(miners) = miner 40 | 41 | end define; 42 | 43 | fair process miner \in miners 44 | begin 45 | Start: 46 | await /\ miner_rounds[self] = Min(Range(miner_rounds)) 47 | /\ Len(leaders) >= miner_rounds[self] - 1; 48 | if miner_rounds[self] >= MaxRounds then goto Done end if; 49 | Elect: 50 | with current_leaders = leaders[miner_rounds[self] - 1] do 51 | if current_leaders = {} then 52 | leaders[Len(leaders)] := leaders[Len(leaders)] \union {<>}; 53 | 54 | skip; \* TODO: Handle null mining. 55 | else 56 | with tipset \in SUBSET {x[1] : x \in current_leaders } do 57 | if USE_RANDOM then 58 | if Scratch(self, tipset) then 59 | leaders[Len(leaders)] := leaders[Len(leaders)] \union {<>}; 60 | else 61 | skip; 62 | end if; 63 | else 64 | with scratched \in BOOLEAN do 65 | if scratched then 66 | leaders[Len(leaders)] := leaders[Len(leaders)] \union {<>}; 67 | else 68 | skip; 69 | end if; 70 | end with; 71 | end if; 72 | end with; 73 | end if; 74 | end with; 75 | 76 | miner_rounds[self] := miner_rounds[self] + 1; 77 | goto Start; 78 | 79 | end process; 80 | 81 | fair process ticker = "ticker" 82 | begin 83 | Tick: 84 | await \A m \in miners: miner_rounds[m] = Min(Range(miner_rounds)); 85 | leaders := Append(leaders, {}); 86 | goto Tick; 87 | end process; 88 | 89 | end algorithm; *) 90 | 91 | \* BEGIN TRANSLATION 92 | VARIABLES miner_rounds, leaders, pc 93 | 94 | (* define statement *) 95 | FairMining == 96 | \A miner \in miners: miner_rounds[miner] - Min(Range(miner_rounds)) <= 1 97 | 98 | BoundedLeaders == 99 | ~\E leader_group \in Range(leaders): Cardinality(leader_group) > MaxLeaders 100 | ModelBoundedLeaders == 101 | ~\E leader_group \in Range(leaders): Cardinality(leader_group) > MaxLeaders - 1 102 | 103 | 104 | NoProgress == 105 | /\ Len(leaders) < MaxRounds 106 | /\ miner_rounds \in [miners -> 0..MaxRounds] 107 | 108 | Scratch(miner, tipset) == 109 | 110 | RandomElement(miners) = miner 111 | 112 | 113 | vars == << miner_rounds, leaders, pc >> 114 | 115 | ProcSet == (miners) \cup {"ticker"} 116 | 117 | Init == (* Global variables *) 118 | /\ miner_rounds = [miner \in miners |-> 2] 119 | /\ leaders = <<{<>}, {}>> 120 | /\ pc = [self \in ProcSet |-> CASE self \in miners -> "Start" 121 | [] self = "ticker" -> "Tick"] 122 | 123 | Start(self) == /\ pc[self] = "Start" 124 | /\ /\ miner_rounds[self] = Min(Range(miner_rounds)) 125 | /\ Len(leaders) >= miner_rounds[self] - 1 126 | /\ IF miner_rounds[self] >= MaxRounds 127 | THEN /\ pc' = [pc EXCEPT ![self] = "Done"] 128 | ELSE /\ pc' = [pc EXCEPT ![self] = "Elect"] 129 | /\ UNCHANGED << miner_rounds, leaders >> 130 | 131 | Elect(self) == /\ pc[self] = "Elect" 132 | /\ LET current_leaders == leaders[miner_rounds[self] - 1] IN 133 | IF current_leaders = {} 134 | THEN /\ leaders' = [leaders EXCEPT ![Len(leaders)] = leaders[Len(leaders)] \union {<>}] 135 | /\ TRUE 136 | ELSE /\ \E tipset \in SUBSET {x[1] : x \in current_leaders }: 137 | IF USE_RANDOM 138 | THEN /\ IF Scratch(self, tipset) 139 | THEN /\ leaders' = [leaders EXCEPT ![Len(leaders)] = leaders[Len(leaders)] \union {<>}] 140 | ELSE /\ TRUE 141 | /\ UNCHANGED leaders 142 | ELSE /\ \E scratched \in BOOLEAN: 143 | IF scratched 144 | THEN /\ leaders' = [leaders EXCEPT ![Len(leaders)] = leaders[Len(leaders)] \union {<>}] 145 | ELSE /\ TRUE 146 | /\ UNCHANGED leaders 147 | /\ miner_rounds' = [miner_rounds EXCEPT ![self] = miner_rounds[self] + 1] 148 | /\ pc' = [pc EXCEPT ![self] = "Start"] 149 | 150 | miner(self) == Start(self) \/ Elect(self) 151 | 152 | Tick == /\ pc["ticker"] = "Tick" 153 | /\ \A m \in miners: miner_rounds[m] = Min(Range(miner_rounds)) 154 | /\ leaders' = Append(leaders, {}) 155 | /\ pc' = [pc EXCEPT !["ticker"] = "Tick"] 156 | /\ UNCHANGED miner_rounds 157 | 158 | ticker == Tick 159 | 160 | Next == ticker 161 | \/ (\E self \in miners: miner(self)) 162 | \/ (* Disjunct to prevent deadlock on termination *) 163 | ((\A self \in ProcSet: pc[self] = "Done") /\ UNCHANGED vars) 164 | 165 | Spec == /\ Init /\ [][Next]_vars 166 | /\ \A self \in miners : WF_vars(miner(self)) 167 | /\ WF_vars(ticker) 168 | 169 | Termination == <>(\A self \in ProcSet: pc[self] = "Done") 170 | 171 | \* END TRANSLATION 172 | 173 | \* Liveness == []<>(Len(leaders) < 5) 174 | ============================================================================= 175 | -------------------------------------------------------------------------------- /code/other-sims/grinding.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | crypto_rand "crypto/rand" 5 | "encoding/binary" 6 | "fmt" 7 | "runtime" 8 | 9 | xrand "golang.org/x/exp/rand" 10 | 11 | "gonum.org/v1/gonum/stat/distuv" 12 | ) 13 | 14 | type Node struct { 15 | Weight int 16 | Won int 17 | Slot int 18 | } 19 | 20 | type Info struct { 21 | Kmax int 22 | Null int 23 | Sims int 24 | Power float64 25 | E int 26 | } 27 | 28 | type Distribution interface { 29 | Rand() float64 30 | } 31 | 32 | func (i *Info) HonestDistribution() Distribution { 33 | return &distuv.Poisson{ 34 | Lambda: float64(i.E) * (1 - i.Power), 35 | Src: newSeed(), 36 | } 37 | } 38 | 39 | func (i *Info) AttackerDistribution() Distribution { 40 | return &distuv.Poisson{ 41 | Lambda: float64(i.E) * (i.Power), 42 | Src: newSeed(), 43 | } 44 | } 45 | func NewInfo(e int, power float64, kmax, null, sims int) *Info { 46 | return &Info{ 47 | E: e, 48 | Power: power, 49 | Kmax: kmax, 50 | Null: null, 51 | Sims: sims, 52 | } 53 | } 54 | 55 | func (i *Info) Rate() float64 { 56 | return i.Power * float64(i.E) 57 | } 58 | 59 | func run_sim(info *Info, sim func(Distribution) int, newRand func() Distribution) []int { 60 | results := make([]int, info.Sims) 61 | numCPUs := runtime.NumCPU() 62 | work := make(chan bool, info.Sims) 63 | resultsCh := make(chan int, info.Sims) 64 | 65 | worker := func() { 66 | for _ = range work { 67 | resultsCh <- sim(newRand()) 68 | } 69 | } 70 | for i := 0; i < numCPUs; i++ { 71 | go worker() 72 | } 73 | 74 | for i := 0; i < info.Sims; i++ { 75 | work <- true 76 | } 77 | close(work) 78 | for i := 0; i < info.Sims; i++ { 79 | results[i] = <-resultsCh 80 | } 81 | return results 82 | } 83 | 84 | func nogrinding(info *Info, newRand func() Distribution) []int { 85 | onesim := func(d Distribution) int { 86 | sum := 0 87 | for i := 0; i < info.Kmax; i++ { 88 | sum += int(d.Rand()) 89 | } 90 | return int(sum) 91 | } 92 | return run_sim(info, onesim, newRand) 93 | } 94 | 95 | func grind_node(node *Node, info *Info, d Distribution) []Node { 96 | if node.Slot >= info.Kmax { 97 | return []Node{} 98 | } 99 | var results = []Node{} 100 | for null := 0; null < info.Null; null++ { 101 | newslot := node.Slot + null + 1 102 | if newslot >= info.Kmax { 103 | return results 104 | } 105 | 106 | for trial := 0; trial < node.Won; trial++ { 107 | won := int(d.Rand()) 108 | if won == 0 { 109 | continue 110 | } 111 | 112 | results = append(results, Node{ 113 | Won: won, 114 | Weight: node.Weight + won - trial, 115 | Slot: newslot, 116 | }) 117 | } 118 | } 119 | return results 120 | } 121 | 122 | func grind_once(info *Info, d Distribution) int { 123 | firstnode := Node{ 124 | Won: 1, 125 | Weight: 0, 126 | Slot: -1, 127 | } 128 | nodes := []Node{firstnode} 129 | max_weight := 0 130 | for len(nodes) > 0 { 131 | // find max 132 | for _, n := range nodes { 133 | if n.Weight > max_weight { 134 | max_weight = n.Weight 135 | } 136 | } 137 | // run grinding 138 | res := []Node{} 139 | for _, n := range nodes { 140 | res = append(res, grind_node(&n, info, d)...) 141 | } 142 | nodes = res 143 | } 144 | return max_weight 145 | } 146 | 147 | type GrindingResult struct { 148 | Weights []int 149 | Success []bool 150 | TotalTrial int 151 | } 152 | 153 | func grind(info *Info) *GrindingResult { 154 | weights := run_sim(info, func(d Distribution) int { return grind_once(info, d) }, info.AttackerDistribution) 155 | exp_honest := float64(info.E) * float64((1 - info.Power)) 156 | total_exp_honest := int(exp_honest * float64(info.Kmax)) 157 | success := make([]bool, info.Sims) 158 | total := 0 159 | for i, w := range weights { 160 | if w >= total_exp_honest { 161 | success[i] = true 162 | total++ 163 | } 164 | } 165 | return &GrindingResult{ 166 | Weights: weights, 167 | Success: success, 168 | TotalTrial: total, 169 | } 170 | } 171 | 172 | func weight(simuls []int, nruns int) float64 { 173 | sum := 0 174 | for _, n := range simuls { 175 | sum += n 176 | } 177 | return float64(sum) / float64(nruns) 178 | } 179 | 180 | func prob_success(attacker, honest []int) float64 { 181 | better := 0 182 | for i := 0; i < len(attacker); i++ { 183 | if attacker[i] >= honest[i] { 184 | better++ 185 | } 186 | } 187 | return float64(better) / float64(len(attacker)) 188 | } 189 | 190 | func prob_success_smart(att *GrindingResult, honest []int) float64 { 191 | better := 0 192 | for i := 0; i < len(att.Weights); i++ { 193 | if !att.Success[i] { 194 | continue 195 | } 196 | if att.Weights[i] >= honest[i] { 197 | better++ 198 | } 199 | } 200 | return float64(better) / float64(att.TotalTrial) 201 | } 202 | 203 | type SimulResult struct { 204 | HonestWeight float64 205 | NoGrindWeight float64 206 | GrindWeight float64 207 | GrindSuccess float64 208 | ExpectedAdditionalBlock float64 209 | ExpectedReward float64 210 | SmartGrinding float64 211 | SmartPercTrial float64 212 | } 213 | 214 | func (s *SimulResult) ComputeExpectedReward() { 215 | extraBlock := s.GrindWeight - s.NoGrindWeight 216 | exp_extra := s.GrindSuccess * extraBlock 217 | s.ExpectedAdditionalBlock = exp_extra 218 | ratio := (s.NoGrindWeight + exp_extra) / s.NoGrindWeight 219 | s.ExpectedReward = ratio 220 | } 221 | 222 | func run(info *Info) *SimulResult { 223 | honest := nogrinding(info, info.HonestDistribution) 224 | attacker_nogrind := nogrinding(info, info.AttackerDistribution) 225 | attacker_grind := grind(info) 226 | succ_grinding := prob_success(attacker_grind.Weights, honest) 227 | smart_grinding := prob_success_smart(attacker_grind, honest) 228 | s := &SimulResult{ 229 | HonestWeight: weight(honest, info.Sims), 230 | NoGrindWeight: weight(attacker_nogrind, info.Sims), 231 | GrindWeight: weight(attacker_grind.Weights, info.Sims), 232 | GrindSuccess: succ_grinding, 233 | SmartGrinding: smart_grinding, 234 | SmartPercTrial: float64(attacker_grind.TotalTrial) / float64(info.Sims), 235 | } 236 | s.ComputeExpectedReward() 237 | return s 238 | } 239 | 240 | func run_multiple(infos ...*Info) { 241 | fmt.Printf("e,attacker,kmax,null,honestw,nogrindw,grindingw,prob_success,exp_additional_block,reward_advantage,smart_prob_success,smart_perc_trial\n") 242 | for _, info := range infos { 243 | res := run(info) 244 | fmt.Printf("%d,%.3f,%d,%d,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f\n", info.E, info.Power, info.Kmax, info.Null, res.HonestWeight, res.NoGrindWeight, res.GrindWeight, res.GrindSuccess, res.ExpectedAdditionalBlock, res.ExpectedReward, res.SmartGrinding, res.SmartPercTrial) 245 | } 246 | } 247 | 248 | func main() { 249 | infos := []*Info{} 250 | power := 1.0 / 3.0 251 | for _, kmax := range []int{2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} { 252 | if kmax <= 5 { 253 | infos = append(infos, NewInfo(5, power, kmax, 1, 1000)) 254 | } else { 255 | infos = append(infos, NewInfo(5, power, kmax, 5, 1000)) 256 | } 257 | } 258 | run_multiple(infos...) 259 | } 260 | 261 | func newSeed() xrand.Source { 262 | var b [8]byte 263 | _, err := crypto_rand.Read(b[:]) 264 | if err != nil { 265 | panic("cannot seed math/rand package with cryptographically secure random number generator") 266 | } 267 | return xrand.NewSource(uint64(binary.LittleEndian.Uint64(b[:]))) 268 | } 269 | -------------------------------------------------------------------------------- /code/other-sims/epochboundary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import math 3 | import numpy as np 4 | import utils as u 5 | 6 | ## Epoch Boundary attack mitigation: 7 | ## This is the analysis of the effectiveness of the randomization mitigiation 8 | ## for the epoch boundary boundary attack: an honest player chooses a random 9 | ## cutoff at each round after which he doesn't accept new blocks for the current 10 | ## round. 11 | 12 | # Time divided in T slots 13 | # H_j : random variable denoting the slot honest player j chooses 14 | # Pr[ H_j = t ] = 1 / T 15 | # A_j: random variable denoting the slot attacker guesses for honest player j 16 | # Pr[ A_j = t ] = 1 / T = Pr[ H_j = t ] 17 | # Then want to know the probability of sucess for targeting a specific player: 18 | # S_j: random variable being 1 if attacker guessed a smaller t than the one 19 | # honest player j picked. See later for a better model. 20 | # Pr[ S_j = 1 ] = SUM(t: 0->T) Pr[ A_j = t | H_j >= t] 21 | # = SUM(..) 1 / T * (1 - Pr[ H_j < t]) 22 | # = SUM(..) Pr[ A_j = t] * (1 - SUM(ti:0 -> t) Pr[ H_j = ti ] 23 | # = SUM(..) 1 / T * (1 - SUM(ti:0 -> t) 1 / T) 24 | # 25 | # A more precise model introduces the notion of a maximum delay d such that the 26 | # attacker that transmits a block to a node do it "just" before the node's 27 | # deadline. Just before means between [t-d; t] 28 | # If the adversary broadcasts before t-d, the honest player have the chance to 29 | # rebroadcast the block to other peers hence defeating the attack. 30 | # P_j: random variable = 1 if attacker guessed a time "close" to the time t of 31 | # player j 32 | # Pr[ P_j = 1 ] = SUM(t: d->T) Pr[ t-d <= A_j <= t | H_j = t ] 33 | # = SUM(t: d->T) (Pr[Aj <= t] - Pr[ A_j < t-d])* Pr[ H_j = t ] 34 | # 35 | # Then want to know probability of attacker to reach success on half of the 36 | # honest nodes in the network (of size n*h with h = # honest players) 37 | # A: random variable denoting how many nodes did attacker reached before their 38 | # timeout 39 | # Pr[ A = n*h/2 ] = Binomial(n*h/2, n, Pr[ S_j = 1]) 40 | # Or using the more precise model 41 | # Pr[ A = n*h/2 ] = Binomial(n*h/2, n, Pr[ P_j = 1]) 42 | # Probability that attacker reaches approximately a range 43 | # Pr[ low < A < high ] = BinomialCDF(low -> high) = SUM(t: low -> high) Pr[ A = t ] 44 | 45 | ## Approximation taken: 46 | ## * n is size of the network but we want to target in terms of power. We assume 47 | ## there are enough small miners to have a reliable mapping 48 | ## - this actually gives more prob. of success for attacker, since it 49 | ## doesn't differentiate between miners while it should. 50 | ## * There are less honest nodes than n: 51 | ## -in this case the prob. of success of ## the attacker is a lower bound so 52 | ## it's OK (attacker has to reach lesss target ## if we remove this approx. so 53 | ## chances of success are higher). 54 | ## - we can consider n the number of honest players 55 | ## * Pr[ S_j = 1 ] is too relaxed: in practice if he sends it one sec before the 56 | ## random deadline, the block is gonna get passed around via gossip. However 57 | ## analysis shows it already has a low chance of probability. 58 | 59 | def pr_hj(nslot): 60 | return 1 / nslot 61 | 62 | def pr_hj_cdf(upto,nslot): 63 | return sum([pr_hj(nslot) for t in range(upto)]) 64 | 65 | ## Pr[ A_j = t ] 66 | def pr_aj(nslot): 67 | return pr_hj(nslot) 68 | 69 | def pr_aj_cdf(upto,nslot,start=0): 70 | return sum([pr_aj(nslot) for t in range(start,upto)]) 71 | 72 | ## Pr[ S_j = 1] 73 | def pr_sj(info): 74 | nslot = info['nslot'] 75 | res = 0 76 | for t in range(nslot): 77 | res += pr_aj(nslot) * (1 - pr_hj_cdf(t,nslot)) 78 | return res 79 | 80 | def pr_pj(info): 81 | nslot = info['nslot'] 82 | delay = info['delay'] 83 | res = 0 84 | for t in range(delay,nslot): 85 | ## even though it's a constant because of uniform dist. let's keep 86 | ## according to formulas 87 | left = pr_aj_cdf(t,nslot,t-delay) 88 | right = pr_hj(nslot) 89 | res += left*right 90 | # print("{} - {} -> {}".format(left,right,res)) 91 | return res 92 | 93 | def pr_a(info,target): 94 | mean_prob = np.mean([info['prob'](info) for t in range(10)]) 95 | return u.binomial(target,info['honests'],mean_prob) 96 | 97 | def pr_a_sums(info): 98 | lown = int(info['low'] * info['target']) 99 | highn = int(info['high'] * info['target']) 100 | targets = range(lown,highn) 101 | return sum([pr_a(info,target) for target in targets]) 102 | 103 | ## pr to run this continuously 104 | def continuous(info): 105 | return np.prod([pr_a_sums(info) for i in range(info['rounds'])]) 106 | 107 | def print_info(info): 108 | print("Computation using {}".format(info['prob'].__name__)) 109 | print("Total number of nodes: {}".format(info['nodes'])) 110 | print("Honest nodes: {} - Attacker nodes: {}".format(info['honests'],info['attacker'])) 111 | print("Target: t={} - range target [{}t,{}t] - for {} rounds".format(info['target'],info['low'],info['high'],info['rounds'])) 112 | print("Number of time slots: {} - Maximum delay: {}".format(info['nslot'],info['delay'])) 113 | 114 | def default(prob=pr_sj): 115 | info={} 116 | ## let's imagine 100ms discrete time slots: 117 | ## 1s contains 10 of those 118 | ## if we spread randomized cutoff for a period of 4s we get 40 timeslots 119 | ## [ T - 2s; T + 2s] 120 | info['nslot'] = 40 121 | info['delay'] = 5 122 | # number of nodes in total 123 | n = 50 124 | info['nodes']=n 125 | info['attacker']=1/3 * n 126 | info['honests']= n - info['attacker'] 127 | # there are two viable strategies: 128 | # 1. send to only the minimum number of nodes such that it reaches 50% so 129 | # attacker will mine next round on its own block 130 | # 2. send to half of the honest nodes and attack can mine on any half 131 | # 2 gives higher chances of prob. 132 | # target=int(n/2 - n/3) 133 | info['target']=int(info['honests']/2) 134 | # Attacker tries to attack a certain percentage of nodes, not necessarily 135 | # exactly 50%. We give here the factor for the lower bounds and a factor for 136 | # the highest bounds. It translates to the attacker trying to attack a 137 | # portion of honest nodes between [low * target; high * target] 138 | # Here I set low = 1 because with low < 1, low * target < 50% so attacker 139 | # risks losing its block. 140 | info['low']=1 141 | info['high']=1.9 142 | info['rounds']=10 143 | info['prob'] = prob 144 | return info 145 | 146 | def run_computations(info): 147 | print("-----------------------------------------") 148 | print("\n+++ INFO +++") 149 | print_info(info) 150 | print("\n+++ RESULTS +++") 151 | target = info['target'] 152 | prob = info['prob'] 153 | print("Probability of successfully targeting one node: {}".format(prob(info))) 154 | print("Probability of reaching exactly the # of targets: {}".format(pr_a(info,target))) 155 | sums=pr_a_sums(info) 156 | print("Probability of targeting the range of nodes: {}".format(sums)) 157 | rounds=continuous(info) 158 | print("Probability of running attack for 10 rounds (for target [..]): {}".format(rounds)) 159 | print("-----------------------------------------") 160 | 161 | info=default() 162 | run_computations(info) 163 | info2=default(prob=pr_pj) 164 | run_computations(info2) 165 | -------------------------------------------------------------------------------- /code/ec-sim-zs/utils/blocktree.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pdb 3 | from collections import defaultdict 4 | 5 | # Parse a block tree from json 6 | class BlockTree: 7 | def __init__(self, filename): 8 | self.readAndParse(filename) 9 | 10 | def readAndParse(self, filename): 11 | # Setup output datastructures. 12 | self.BlocksByNonce = dict() 13 | self.BlocksByHeight = dict() 14 | self.BlocksByParentWeight = dict() 15 | self.MaxWeight = 0 16 | self.Miners = [] 17 | self.TotalNonNull = 0 18 | 19 | with open(filename, 'r') as jfile: 20 | data = json.load(jfile) 21 | print "loaded {file}".format(file=filename) 22 | 23 | # Get all blocks. Add all into indexes. 24 | blocks = data["blocks"] 25 | for block in blocks: 26 | self.BlocksByNonce[block["nonce"]] = block 27 | h = block["height"] 28 | if h not in self.BlocksByHeight: 29 | self.BlocksByHeight[h] = [] 30 | self.BlocksByHeight[h].append(block) 31 | w = block["parentWeight"] 32 | if w not in self.BlocksByParentWeight: 33 | self.BlocksByParentWeight[w] = [] 34 | self.BlocksByParentWeight[w].append(block) 35 | if w > self.MaxWeight: 36 | self.MaxWeight = w 37 | if not block["null"]: 38 | self.TotalNonNull += 1 39 | print "parsed blocks" 40 | 41 | # Get all miners 42 | self.Miners = data["miners"] 43 | print "parsed miners" 44 | 45 | ### Extract various metrics from the block tree. 46 | 47 | # HeaviestChains returns an array of sets of nonces of blocks that form the heaviest 48 | # chain in the block tree. 49 | def HeaviestChains(self): 50 | # get the heaviest live TipSets 51 | heaviestBlocks = [] 52 | maxWeight = self.MaxWeight 53 | while not heaviestBlocks: 54 | heaviestBlocks = filter(lambda x: not x["null"], self.BlocksByParentWeight[maxWeight]) 55 | maxWeight -= 1 56 | 57 | heaviestTipsets = dict() 58 | for block in heaviestBlocks: 59 | parentsName = block["tipset"]["name"] 60 | if not parentsName in heaviestTipsets: 61 | heaviestTipsets[parentsName] = [] 62 | heaviestTipsets[parentsName].append(block) 63 | 64 | chains = [] 65 | for tipset in heaviestTipsets.keys(): 66 | chain = set() 67 | heaviestTS = heaviestTipsets[tipset] 68 | for blk in heaviestTS: 69 | chain.add(blk["nonce"]) 70 | # all will have the same parents and height so only need to take first 71 | cur = heaviestTS[0] 72 | curHeight = cur["height"] 73 | while curHeight > 0: 74 | next = parentNonces(cur["tipset"]["name"]) 75 | for nonce in next: 76 | chain.add(nonce) 77 | cur = self.BlocksByNonce[next[0]] 78 | curHeight = cur["height"] 79 | chains += sorted(chain) 80 | return chains 81 | # TODO: potentially do something if there are multiple chains of same weight (unlikely) 82 | 83 | # RatioUsefulBlocks returns the ratio of blocks making it into the 84 | # heaviest chain to the total blocks mined. 85 | def RatioUsefulBlocks(self): 86 | mainChain = self.HeaviestChains()[0] 87 | return float(self.countNonNullBlocks(mainChain)) / float(self.TotalNonNull) 88 | 89 | # AvgHeadsPerRound returns the mean number of possible mining heads per 90 | # round. 91 | def AvgHeadsPerRound(self): 92 | acc = 0.0 93 | numTrials = len(self.BlocksByHeight) 94 | for round in range(0, numTrials): 95 | if round in self.BlocksByHeight: 96 | acc += len(self.BlocksByHeight[round]) 97 | 98 | return acc / float(numTrials) 99 | 100 | # NumReorgs returns the number of times that the heaviest tipset in round 101 | # n was on a different parent than the heaviest tipset at round n - 1. 102 | def NumReorgs(self): 103 | # maps length of head to number of times it led to a reorg 104 | # reorgs[curHeadLen]-># of reorgs 105 | reorgs = defaultdict(int) 106 | # length of current head 107 | curHeadLen = 1 108 | # start with genesis 109 | prevHead = self.headAtHeight(0) 110 | curHeight = 0 111 | print "curH: {curH}, chainH: {chainH}".format(curH=curHeight, chainH=len(self.BlocksByHeight)) 112 | # traverse chain 113 | while curHeight < len(self.BlocksByHeight): 114 | curHeight += 1 115 | curHead = self.headAtHeight(curHeight) 116 | # This round the head is a null block 117 | if curHead == []: 118 | curHeadLen += 1 119 | continue 120 | # Found a non-null head 121 | else: 122 | # check if this head traverses back to the previous one through 123 | # null blocks. If not we have a reorg. 124 | parentNonces = self.nonNullParentNonces(curHead) 125 | prevNonces = [ block["nonce"] for block in prevHead] 126 | if set(parentNonces) != set(prevNonces): 127 | reorgs[findBucket(curHeadLen)] += 1 128 | # reset curHeadLen 129 | curHeadLen = 1 130 | else: 131 | curHeadLen += 1 132 | prevHead = curHead 133 | return reorgs 134 | 135 | # Return the first non null parent nonces of input tipset child 136 | def nonNullParentNonces(self, child): 137 | childBlock = child[0] # all tipset blocks have the same parent so use one 138 | while True: 139 | nonces = parentNonces(childBlock["tipset"]["name"]) 140 | # can immediately return if len > 1 because can't build a TS out of multiple null blocks 141 | if len(nonces) > 1: 142 | return nonces 143 | else: 144 | childBlock = self.BlocksByNonce[nonces[0]] 145 | # if null: then loop and go back one more 146 | if not childBlock["null"]: 147 | return nonces 148 | 149 | 150 | # return list of blocks representing head tipset at a round if one exists. 151 | def headAtHeight(self, h): 152 | blocks = [] 153 | if h not in self.BlocksByHeight: 154 | return blocks 155 | for block in self.BlocksByHeight[h]: 156 | if block["inHead"]: 157 | blocks.append(block) 158 | 159 | return blocks 160 | 161 | 162 | # return blocks of heaviest tipset at height 163 | def heaviestAtHeight(self, h): 164 | if h not in self.BlocksByHeight: 165 | raise "no BlocksByHeight entry at height " 166 | 167 | # make all possible heaviest tipsets 168 | # tipsets repr with triple: (name, blockset, weightint) 169 | tipsets = [] 170 | for block in self.BlocksByHeight[h]: 171 | placed = False 172 | for ts in tipsets: 173 | if ts[0]["tipset"]["name"] == block["tipset"]["name"]: 174 | ts.append(block) 175 | placed = True 176 | break 177 | if not placed: 178 | tipsets.append([block]) 179 | 180 | maxTs = tipsets[0] 181 | for ts in tipsets: 182 | if tipsetWeight(ts) > tipsetWeight(maxTs): 183 | maxTs = ts 184 | 185 | return maxTs 186 | 187 | def countNonNullBlocks(self, blockSet): 188 | count = 0 189 | for block in blockSet: 190 | if not self.BlocksByNonce[block]["null"]: 191 | count += 1 192 | 193 | return count 194 | 195 | def tipsetWeight(blocks): 196 | return blocks[0]["parentWeight"] + len(blocks) 197 | 198 | def parentNonces(name): 199 | nonces = name.split('-') 200 | return [int(nonce) for nonce in nonces] 201 | 202 | 203 | # 5000 is a max more or less 204 | LENGTHS = [1, 2, 5, 10, 25, 50, 100, 250, 5000] 205 | def findBucket(forkLen): 206 | for idx, elem in enumerate(LENGTHS): 207 | if elem > forkLen: 208 | return elem 209 | 210 | -------------------------------------------------------------------------------- /code/ec-sim-zs/utils/driver.py: -------------------------------------------------------------------------------- 1 | # system utils 2 | import argparse 3 | import subprocess 4 | import datetime 5 | import time 6 | import os 7 | from os import listdir 8 | from os.path import isfile, join 9 | import sys 10 | import numpy as np 11 | import pdb 12 | from collections import defaultdict 13 | 14 | # simulation output parser 15 | import blocktree 16 | 17 | # plotting 18 | import matplotlib 19 | # use different backend ahead of importing pyplot for running remotely 20 | matplotlib.use('Agg') 21 | import matplotlib.pyplot as plt 22 | 23 | COLORS = ['blue', 'orange', 'green', 'red', 'purple', 'brown', 'pink', 'gray', 'olive', 'cyan'] 24 | 25 | PROGRAM = "./ec-sim-zs" 26 | 27 | def buildArgs(miners=-1, lbp=-1, trials=-1, rounds=-1, output="../output"): 28 | params = "" 29 | if rounds > 0: 30 | params += " -rounds={}".format(rounds) 31 | if miners > 0: 32 | params += " -miners={}".format(miners) 33 | if lbp > 0: 34 | params += " -lbp={}".format(lbp) 35 | if trials > 0: 36 | params += " -trials={}".format(trials) 37 | params += " -quiet" 38 | params += " -output={}".format(output) 39 | 40 | return params 41 | 42 | def sweepByMinersAndLBP(miners, lbps, trials, rounds, sweepDir): 43 | data = dict() 44 | for lbp in lbps: 45 | lbpDir = sweepDir + "/lbp-" + str(lbp) 46 | data[lbp] = dict() 47 | for m in miners: 48 | data[lbp][m] = [] 49 | outputDir = lbpDir + "/miner-" + str(m) 50 | command = "{command}{params}".format(command=PROGRAM, params=buildArgs(m,lbp,trials,rounds, outputDir)) 51 | print "\ntime is {time}".format(time=datetime.datetime.now()) 52 | print command 53 | p = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) 54 | out, err = p.communicate() 55 | print "output was {output}\n-*-*-*\nerr was {error}".format(output=out, error=err) 56 | 57 | # readSweepData traverses the directories of a sweep output, runs metrics on 58 | # the outputs and returns a map data[lbp][minerNum][metric] to data series. 59 | def readSweepData(miners, lbps, metrics, sweepDir): 60 | data = dict() # data[lbp][minerNum][metric] is a list of metrics 61 | for lbp in lbps: 62 | lbpDir = sweepDir + "/lbp-" + str(lbp) 63 | data[lbp] = dict() 64 | for m in miners: 65 | outputDir = lbpDir + "/miner-" + str(m) 66 | data[lbp][m] = dict() 67 | for metric in metrics: 68 | data[lbp][m][metric] = [] 69 | # traverse files 70 | onlyfiles = [join(outputDir, f) for f in listdir(outputDir) if isfile(join(outputDir, f))] 71 | for f in onlyfiles: 72 | print "Currently on {cFile}".format(cFile=f) 73 | tree = blocktree.BlockTree(f) 74 | print "Done building blocktree" 75 | print "\ntime is {time}".format(time=datetime.datetime.now()) 76 | for metric in metrics: 77 | if metric == "AvgHeadsPerRound": 78 | avgHeads = tree.AvgHeadsPerRound() 79 | data[lbp][m][metric].append(avgHeads) 80 | print "average heads per round was {avg}".format(avg=avgHeads) 81 | if metric == "NumReorgs": 82 | reorgs = tree.NumReorgs() 83 | data[lbp][m][metric].append(reorgs) 84 | print "num reorgs was {reorgs}".format(reorgs=sum(reorgs.values())) 85 | return data 86 | 87 | def pickTrial(trials): 88 | # return the run that spawned the median amount of reorgs 89 | num = [sum(dic.values()) for dic in trials] 90 | return trials[np.argsort(num)[len(num)//2]] 91 | 92 | # Creates a histogram 93 | def plotLenReorgs(data, metric, rounds): 94 | # overloading that one data name 95 | metric = "NumReorgs" 96 | lbps = sorted([lbp for lbp in data]) 97 | for lbp in lbps: 98 | colorIndex = 0 99 | filteredTrials = defaultdict(int) 100 | keys = [] 101 | minerNums = sorted([m for m in data[lbp]]) 102 | trials = defaultdict(int) 103 | for m in minerNums: 104 | # let's only take one trial, and get all the keys we need first 105 | trial = pickTrial(data[lbp][m][metric]) 106 | filteredTrials[m] = trial 107 | keys += trial.keys() 108 | # now we can set up our data 109 | keys = sorted(list(set(keys))) 110 | for k in keys: 111 | trials[k] = [filteredTrials[m][k] for m in minerNums] 112 | # let's do it 113 | ind = np.arange(len(minerNums)) 114 | width = .3 115 | bottoms = [0] * len(minerNums) 116 | for i, k in enumerate(keys): 117 | plt.bar(ind, tuple(trials[k]), width, bottom=bottoms, color=COLORS[colorIndex]) 118 | colorIndex += 1 119 | colorIndex %= len(COLORS) 120 | # increment y index across the bars. 121 | bottoms = [sum(tup) for tup in zip(bottoms, trials[k])] 122 | plt.xlabel("Number of miners") 123 | plt.ylabel("Number of reorgs") 124 | plt.xticks(ind, minerNums) 125 | plt.legend(["lenReorg="+str(k) for k in keys], loc='best') 126 | plt.title("reorgs split by chain size, for lbp={k}".format(k=lbp)) 127 | fig1 = plt.gcf() 128 | fig1.savefig("{metric}-k{k}-{rounds}rds-{time}.png".format(metric=metric, rounds=rounds, k=lbp,time=milliTS()), format="png") 129 | plt.clf() 130 | 131 | # plotSeries plots the mean value of a metric varying over the number of miners 132 | # mining on the chain. It plots multiple series, one for each lbp value in the 133 | # sweep. 134 | def plotSeries(data, metric, rounds): 135 | if metric == "AvgHeadsPerRound" or metric == "NumReorgs": 136 | lbps = sorted([lbp for lbp in data]) 137 | for lbp in lbps: 138 | series = [] 139 | minerNums = sorted([m for m in data[lbp]]) 140 | for m in minerNums: 141 | trialValues = data[lbp][m][metric] 142 | if metric == "AvgHeadsPerRound": 143 | series.append(sum(trialValues) / len(trialValues)) 144 | if metric == "NumReorgs": 145 | num = [sum(dic.values()) for dic in trialValues] 146 | series.append(sum(num) / len(num)) 147 | plt.plot(minerNums, series, 'x-') 148 | plt.xlabel("Number of miners") 149 | plt.ylabel(metric) 150 | plt.legend(["k="+str(lbp) for lbp in sorted(lbps)] , loc='upper right') 151 | plt.title(metric + " varied over miner number and lookback parameter k") 152 | fig1 = plt.gcf() 153 | fig1.savefig("{metric}-{rounds}rds-{time}.png".format(metric=metric,rounds=rounds,time=milliTS()), format="png") 154 | plt.clf() 155 | 156 | # plot value of chain metrics through a sweep of miner number and lookback 157 | # parameters. metrics is a list of strings, each corresponding to a metric 158 | # identifier. 159 | def plotSweep(miners, lbps, metrics, sweepDir, rounds): 160 | # Gather data from sweep output artifacts. 161 | data = readSweepData(miners, lbps, metrics, sweepDir) 162 | 163 | # Plot data for each metric 164 | for metric in metrics: 165 | if metric == "AvgHeadsPerRound" or metric == "NumReorgs": 166 | plotSeries(data, metric, rounds) 167 | if metric == "LenReorgs": 168 | plotLenReorgs(data, metric, rounds) 169 | 170 | def milliTS(): 171 | return int(round(time.time() * 1000)) 172 | 173 | if __name__ == "__main__": 174 | # TODO -- should use argparse to set values of these slices or read from config file 175 | miners = [10, 20]#50, 100, 200, 400] 176 | lbps = [1, 10]#, 20, 50, 100] 177 | trials = 3 178 | rounds = 200 179 | # sweepDir = "/home/snarky/space/ec-sim/output/sweep-f" 180 | sweepDir = "./output/sweep-f" 181 | 182 | # TODO -- shoulduse argparse to express which operations should be done: 183 | # run simulation and output (sweepByMinersAndLBP), plot existing data 184 | # (plotMetricSweep), or load and print data (printing of data doesn't 185 | # exist yet but is easy to do alongside readSweepData) 186 | 187 | # right now I simply comment things out and rewrite vals in this function, 188 | # which might be good enough for a while. 189 | 190 | 191 | print("""runs can take a while and scale quadratically in number of rounds and exponentially in number of miners. E.g. 192 | 100 rounds, 50 miners ===> 2.5s 193 | 100 rounds, 200 miners ===> 45s 194 | 100 rounds, 400 miners ===> 5m13s 195 | 196 | 200 rounds, 50 miners ===> 4.8s 197 | 200 rounds, 200 miners ===> 1m52s 198 | 200 rounds, 400 miners ===> 12m44s 199 | 200 | 400 rounds, 50 miners ===> 13s 201 | 400 rounds, 200 miners ===> 6m51s 202 | 400 rounds, 400 miners ===> 40m""" 203 | ) 204 | 205 | # sweepByMinersAndLBP(miners, lbps, trials, rounds, sweepDir) 206 | plotSweep(miners, lbps, ["NumReorgs", "LenReorgs"], sweepDir, rounds) 207 | -------------------------------------------------------------------------------- /code/other-sims/general_grinding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import utils as u 3 | import numpy as np 4 | poisson = np.random.poisson 5 | 6 | e=5 7 | att=1/3 8 | h=1-att 9 | max_block=20 10 | 11 | def grindWithHS(): 12 | numSims = 100000 13 | blocksNoGrind=[] 14 | blocksGrind=[] 15 | biggerThanExp=0 16 | HSBlocksLost=[] 17 | failedAttempts=0 18 | noAttackBlocks=[] 19 | for i in range(numSims): 20 | # honest do not grind 21 | ch1, ch2 = poisson(h*e),poisson(h*e) 22 | # attacker is fixed only for first round 23 | ca = poisson(att*e) 24 | # only for comparison: attacker that doesn't grind will draw this 25 | ca2 = poisson(att*e) 26 | 27 | honWins = ch1 + ch2 28 | tipset = ch1 + ca 29 | 30 | # grindingRound: adversary tries the second round again 31 | # first trial is on the full tipset, then removing one block at a time 32 | maxGrindedBlock = ca2 33 | attackSuccess=False 34 | triedAttack=False 35 | # from 1 since I already include the first trial to reusing none of the tipset 36 | for trial in range(1,tipset+1): 37 | grindRound = poisson(att*e) 38 | advWin = tipset - trial + grindRound 39 | 40 | # simple heuristic where attacker doesn't attack unless 41 | # they can pull off the attack based on the honest chain's expectation 42 | # otherwise risk losing their withheld blocks 43 | if advWin < ch1 + h*e: 44 | continue 45 | 46 | triedAttack=True 47 | # attack success -- we assume better network connectivity from attacker 48 | if advWin >= honWins: 49 | attackSuccess = True 50 | # keep the best - that's how much you can win more 51 | if grindRound > maxGrindedBlock: 52 | maxGrindedBlock = grindRound 53 | # attacker tried attack but it failed 54 | 55 | # adv successfully ran the attack 56 | if attackSuccess: 57 | # how many blocks adv won 58 | blocksGrind.append(ca + maxGrindedBlock) 59 | # alternatively would have won ca2 60 | blocksNoGrind.append(ca+ca2) 61 | # he tried but failed the attack 62 | elif not attackSuccess and triedAttack: 63 | # how many blocks he lost = round1 + round2 64 | HSBlocksLost.append(ca + ca2) 65 | failedAttempts+=1 66 | # he did not try the attack 67 | else: 68 | noAttackBlocks.append(ca+ca2) 69 | 70 | # all blocks that the adversary gets (or could have had) without grinding 71 | honest_behavior=sum(blocksNoGrind)+sum(HSBlocksLost)+sum(noAttackBlocks) 72 | # all blocks that adv. gets by grinding 73 | adv_behavior=sum(blocksGrind) + sum(noAttackBlocks) 74 | adv_attacking = adv_behavior / honest_behavior 75 | avg_grind=np.average(blocksGrind) 76 | avg_nogrind=np.average(blocksNoGrind) 77 | avg_block_losts=np.average(HSBlocksLost) 78 | prop_failed_attempts=failedAttempts/numSims 79 | print("Average # of blocks when not grinding: {:.2f}".format(avg_nogrind)) 80 | print("Average # of blocks when grinding: {:.2f}".format(avg_grind)) 81 | print("-> # of blocks losts (grinding): {}".format(sum(HSBlocksLost))) 82 | print("-> # of blocks he won (grinding): {}".format(sum(blocksGrind))) 83 | print("-> # of blocks normally won (no grinding): {}".format(sum(blocksNoGrind))) 84 | print("-> proportion of failed attemps: {}".format(prop_failed_attempts)) 85 | print("-> adv. attacking: {:.3f}".format(adv_attacking)) 86 | # adv can only attack every second epoch: 87 | # epoch 0, honest blocks are put out, adv decides whether to withhold or not 88 | # epoch 1, adv attacks (or not): puts out withheld blocks on grinded tipset 89 | # repeat 90 | # we account for blocks from both epochs here (preparation and attack) 91 | 92 | # in the case without HS the adv can no longer grind on their own blocks 93 | # lest they get slashed (they've already put them out). 94 | def grindWithoutHS(): 95 | numSims = 100000 96 | blocksNoGrind=[] 97 | blocksGrind=[] 98 | biggerThanExp=0 99 | HSBlocksLost=[] 100 | failedAttempts=0 101 | noAttackBlocks=[] 102 | for i in range(numSims): 103 | # honest do not grind 104 | ch1 = poisson(h*e) 105 | ca = poisson(att*e) 106 | 107 | tipset = ch1 + ca 108 | 109 | # second round on full tipset 110 | ca2 = poisson(att*e) 111 | honWins = tipset + poisson(h*e) 112 | 113 | # grindingRound: adversary tries the second round on subsets of tipset 114 | 115 | # first trial is on the full tipset, then removing one block at a time 116 | # until only left with the adversary's (can't drop those) 117 | maxGrindedBlock = ca2 118 | # from 1 since I already include the first trial to reusing none of the tipset 119 | for trial in range(1,ch1 + 1): 120 | grindRound = poisson(att*e) 121 | advWin = tipset - trial + grindRound 122 | 123 | # attack success -- we assume better network connectivity from attacker 124 | if advWin > honWins: 125 | # keep the best - that's how much you can win more 126 | if grindRound >= maxGrindedBlock: 127 | maxGrindedBlock = grindRound 128 | # attacker tried attack but it failed 129 | 130 | # adv ran the attack -- if didn't work, still gets ca2 blocks by mining honestly. 131 | blocksGrind.append(ca + maxGrindedBlock) 132 | # alternatively would have won ca2 regardless 133 | blocksNoGrind.append(ca+ca2) 134 | 135 | # all blocks that the adversary gets (or could have had) without grinding 136 | honest_behavior=sum(blocksNoGrind) 137 | # all blocks that adv. gets by grinding 138 | adv_behavior=sum(blocksGrind) 139 | adv_attacking = adv_behavior / honest_behavior 140 | avg_grind=np.average(blocksGrind) 141 | avg_nogrind=np.average(blocksNoGrind) 142 | print("Average # of blocks when not grinding: {:.2f}".format(avg_nogrind)) 143 | print("Average # of blocks when grinding: {:.2f}".format(avg_grind)) 144 | print("-> # of blocks he won (grinding): {}".format(sum(blocksGrind))) 145 | print("-> # of blocks normally won (no grinding): {}".format(sum(blocksNoGrind))) 146 | print("-> adv. attacking: {:.3f}".format(adv_attacking)) 147 | # adv can attack every epoch here 148 | 149 | def nico(): 150 | maxGrindPr = 0 151 | maxNoGrindPr = 0 152 | for hb1 in range(max_block): 153 | for ab1 in range(max_block): 154 | for ab2 in range(max_block): 155 | totalA= ab1 + ab2 156 | # - 1 157 | totalGrindPr = 0 158 | totalNoGrindPr = 0 159 | for grinded in range(totalA): 160 | # HonestToFind 161 | # honest must find this value at second round to have 162 | # a heavier chain 163 | htf = totalA - grinded 164 | if ab1 + ab2 < int(h*e): 165 | continue 166 | # probability that honest finds less than htf 167 | phtf = u.poisson_cdf(htf, h*e) 168 | # probability of winning for adversary means: 169 | # 1 honest finds hb1 blocks in first round 170 | # 2 attacker finds ab1 blocks in first round 171 | # 3 honest finds less than htf blocks in second round 172 | # 4 attacker finds ab2 blocks in second round 173 | p1 = u.poisson(hb1,h*e) 174 | p2 = u.poisson(ab1,att*e) 175 | p3 = u.poisson_cdf(htf,h*e) 176 | p4 = u.poisson(ab2,att*e) 177 | pwinA = p1 * p2 * p3 * p4 178 | totalGrindPr += pwinA 179 | 180 | # probability of winning for adversary that don't grind is 181 | # computed as the same way as for no grinding but there is only one 182 | # tentative for the attack, no "for grinded" loop 183 | # 1. honest finds hb1 blocks in first round 184 | # 2. attacker finds ab1 blocks in first round 185 | # 3. honest finds less than totalA at second round 186 | # 4. attacker finds ab2 blocks in second round 187 | p1 = u.poisson(hb1,h*e) 188 | p2 = u.poisson(ab1,att*e) 189 | p3 = u.poisson_cdf(totalA,h*e) 190 | p4 = u.poisson(ab2,att*e) 191 | pwinANoGrind = p1*p2*p3*p4 192 | totalNoGrindPr += pwinANoGrind 193 | 194 | if totalGrindPr > maxGrindPr: 195 | maxGrindPr = totalGrindPr 196 | 197 | if totalNoGrindPr > maxNoGrindPr: 198 | maxNoGrindPr = totalNoGrindPr 199 | 200 | print("max probability when grinding {:.4f}".format(maxGrindPr)) 201 | print("max probability when no grinding {:.4f}".format(maxNoGrindPr)) 202 | print("ratio: {:.4f}".format(maxGrindPr/maxNoGrindPr)) 203 | 204 | print("\nWith HS:") 205 | grindWithHS() 206 | print("\nWithout HS:") 207 | grindWithoutHS() 208 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Filecoin Consensus 2 | --- 3 | 4 | One of Filecoin's main goals is to create a useful Proof-of-Work based on storage, building upon past work in both Proof-of-Work and Proof-of-Stake protocols. 5 | 6 | **Disclaimer:** While we work hard to document our work as it progresses, research progress may not be fully reflected here for some time, or may be worked out out-of-band. 7 | 8 | This repository houses a lot of our work on this area of research: Filecoin consensus. While it is by no means exhaustive, it should provide a good place from which to start engaging on Filecoin consensus research. You may also want to read up on [Filecoin](https://github.com/filecoin-project/specs) and [Filecoin Research](https://github.com/filecoin-project/research). 9 | 10 | Broadly, our goals are to: 11 | - Finalize design aspects of consensus starting with EC to make it secure and workable for wanted Filecoin design, 12 | - Formalize parameters and other implementation requirements in a clear Filecoin Ccnsensus spec implementable by a dev team, 13 | - Define and prove Filecoin consensus security properties. 14 | 15 | **Note 1**: Content here may be out-of-sync. 16 | 17 | **Note 2**: We may sometimes link to inaccessible content here, alas some of these research endeavours require some gestation or privacy on part of our endeavors. 18 | 19 | **Note 3**: Throughout this repo, *miners* will most often refer to Filecoin Storage miners (unless otherwise specified). While we refer to both storage and retrieval miners as miners, strictu sensu, only participation in EC (from storage miners) is mining. 20 | 21 | ## Table of Contents 22 | 23 | - [What is consensus in Filecoin?](#what-is-consensus-in-filecoin?) 24 | - [Consensus Research](#consensus-research) 25 | - [FAQ](#faq) 26 | - [Communication](#communication) 27 | - [License](#license) 28 | 29 | ## What is consensus in Filecoin? 30 | 31 | The state of the Filecoin network is maintained in a blockchain. It is updated as new blocks are mined by a chosen network participant at regular intervals. This "leader" earns FIL for this and is chosen from the set of participants in the Filecoin network with a probability proportional to how much storage this participant is verifiably providing the network. In proceeding thus, block creation 32 | - Provably updates the state of the Filecoin network to reflect network activity (i.e. token transfer and storage proofs); 33 | - Mints new FIL tokens that can be used to buy or sell storage; 34 | - Incentivizes actors to put storage on the network. 35 | 36 | From the perspective of consensus research this can be decomposed into three distinct protocols: 37 | 38 | 1. **The Filecoin protocol** -- this allows miners to make storage deals with clients, submit and process messages to the state machine and create cryptographically verifiable proofs of storage over data. 39 | 1. **Storage Power Consensus (SPC)** -- this generates a power table provably reflecting how much storage participants are providing to the network. 40 | 1. **Expected Consensus (EC)** -- invokes leader election to select a miner from a weighted set of participants and ensures chain growth and convergence. 41 | 42 | These protocols are meant to be modular and have [interfaces](./research-notes/interfaces.md) which enable us to swap them out or improve them without affecting the rest of the Filecoin stack, for instance updating proofs without changing how SPC functions, or leader election without changing the Filecoin protocol. 43 | 44 | ## Consensus Research 45 | 46 | #### Current state of Filecoin consensus work 47 | 48 | To gain familiarity with this subject matter, we refer the reader to: 49 | - [Filecoin Spec](https://github.com/filecoin-project/specs/) -- The spec is the output of these research efforts and reflects our must up-to-date working version of the protocols. The reader may specifically want to look at: 50 | - [Expected Consensus](https://github.com/filecoin-project/specs/blob/master/expected-consensus.md) 51 | - [Mining](https://github.com/filecoin-project/specs/blob/master/mining.md) -- the routine that invokes expected consensus. 52 | - [Filecoin Whitepaper](https://filecoin.io/filecoin.pdf) -- We urge readers to pay special attention to Section 6: `Useful Work Consensus`. 53 | - [Power Fault Tolerance Technical Report](https://filecoin.io/power-fault-tolerance.pdf) (PFT) -- Outlining some of the motivations and implications of this work, reframing Byzantine Fault Tolerance as a function of power committed to the network (in our case storage) rather than number of nodes. 54 | - Expected Consensus Overview -- This is a quick talk going over the basics of EC from ConsensusDay held in February 2019 at Stanford. [Talk](https://www.youtube.com/watch?v=pUIVMG4ZS2E&list=PLhuBigpl7lqtG6LgQ0FiiR4Pbrph9nocn&index=4&t=1s)/[Slides](https://drive.google.com/open?id=1eXLTSPmXTdtNoPk58VVgcwxdlrn8dUVr). 55 | 56 | #### Current avenues of research 57 | 58 | Most of our work on consensus to date focuses on these major endeavours. You can read more about general open problems on the [Filecoin Research repo](https://github.com/filecoin-project/research). 59 | 60 | By design, these are meant to be extremely large, open-ended endeavours each of which breaks out into multiple open or completed problems. The endeavours themselves are evergreen sources of enquiry for and beyond Filecoin. 61 | 62 | | **Project** | **Description** | **Status** | **Notes** | 63 | | ---- | ---- | ---- | ---- | 64 | | **Formal Treatment of Expected Consensus** | Formal analysis of Expected Consensus' security guarantees | Working On/Collaboration | -[issue](https://github.com/filecoin-project/consensus/issues/19) | 65 | | **EC incentive compatibility** | This broadly refers to EC incentive compatibility and initial parameter setting for the Filecoin blockchain ensuring EC incentive compatibility using simulations or probabilistic proofs. | Working On/Collaboratoin | - Chain convergence
- [Weighting](https://github.com/filecoin-project/consensus/issues/27)
- [LBP](https://github.com/filecoin-project/consensus/issues/11)
- [Slashing](https://github.com/filecoin-project/consensus/issues/32)
- [VDF use](https://github.com/filecoin-project/consensus/issues/25)
- [Block time ](https://github.com/filecoin-project/consensus/issues/28)
- [Finality](https://github.com/filecoin-project/consensus/issues/29) | 66 | | **Simulate EC Attacks** | Bottoms-up analysis of EC security simulating likely attacks under various proportions of honest/rational/adversarial miners to iterate on protocol design | Working On | -[issue](https://github.com/filecoin-project/consensus/issues/26) | 67 | | **Secret Single Leader Election** | Working out a full construction for SSLE. In spirit similar to cryptographic sortition but guaranteeing a single leader at every round | Collaboration/RFP | - [issue](https://github.com/filecoin-project/research-private/issues/8)
- [SSLE RFP](https://github.com/protocol/research-RFPs/blob/master/RFPs/rfp-6-SSLE.md)
- [SSLE Overview](https://www.youtube.com/watch?v=_ha6abiM0Uw&list=PLhuBigpl7lqtG6LgQ0FiiR4Pbrph9nocn&index=5&t=0s) from ConsensusDay 2019 | 68 | | **Formalizing Power Fault Tolerance (PFT)** | BFT is abstracted in terms of influence over the protocol rather than machines | Working On/Collaboration | - [issue](https://github.com/filecoin-project/consensus/issues/38) | 69 | | **Random beacons and the Filecoin blockchain** | Looking at and beyond the chain for trusted randomness in Filecoin | In Progress (70%) | -[issue](https://github.com/filecoin-project/consensus/issues/24)| 70 | 71 | ## FAQ 72 | 73 | **Why build EC?** 74 | 75 | **Q**: Alright, so Filecoin wants a semi-permissionless (or optimally fully-permissionless), robustly reconfigurable consensus protocol that SPC can invoke to do leader election. There are a number of existing proof-of-stake protocol that may be adapted for this purpose. Why roll out our own? 76 | 77 | **A**: The answer comes down to a [number of factors](https://github.com/filecoin-project/consensus/issues/13) that boil down to what we have found often happens when trying to adapt theoretical work to real-world security models, including: 78 | 79 | - Wanting a secret leader election process (otherwise known as unpredictibility i.e. accounting for DOSing and adaptive attackers) 80 | - Wanting certain liveness guarantees that make MPCs unattractive (use of VDFs) 81 | - Ensuring chain safety under eventual synchrony 82 | - The complexity or partial omissions that we found in other candidate proposals 83 | - Accounting for "rational" miner behaviors rather than simply "honest" or "byzantine" (e.g. [rushing the protocol](research-notes/waiting.md)) 84 | 85 | With all of that said, it remains important to specify that our work builds upon existing work, notably Snow White and Algorand, and we believe our security analysis will be based on that of Shi and Pass. 86 | 87 | **Common misconceptions** 88 | 89 | **Q**: Why does EC use tickets for randomness? 90 | 91 | **A**: We use tickets for two reasons in the spec as currently laid out: 92 | 93 | - Preventing PoST precomputation - we use winning tickets from the previous block as our challenge for a per-slot delay function. 94 | - Wanted property: "verifiable recency" 95 | - Leader Election - we use the tickets from a past block as a means of secretly and provably checking whether someone has been elected to post the block 96 | - Wanted property: “verifiable” input on the chain common to all miners 97 | 98 | Ultimately, in leader election, we are using tickets in order to approximate a [random beacon](./research-notes/randomness.md), but a promising area of research is to swap this source of randomness out for another on or off-chain source of verifiable randomness. 99 | 100 | **Q**: Is block generation (reward and transaction fees) the only way miners will earn FIL? 101 | 102 | **A**: No. Miners will also earn FIL through the orders they manage on the network (dealing with clients). 103 | It is interesting to note that a miner must commit storage to the network (and thus appear in the power table) in order to participate in leader election and earn a block reward. This is in fact key to Filecoin's design of a `useful Proof of Work`. 104 | Further, it is worth noting that only storage miners participate in Filecoin consensus. Retrieval miners only earn FIL through deals. 105 | 106 | **Q**: Where does collateral come into this? 107 | 108 | **A**: This is a direct follow-up to the above question. Because miners earn FIL in two ways (through participation in leader election and in deals), collateral is used in Filecoin to ensure good behavior in both cases. Specifically: 109 | 110 | - The Filecoin protocol slashes miners who break a contract (i.e. do not prove they are storing client data during the agreed upon period). 111 | 112 | - Expected Consensus slashes miners that sign two distinct blocks in the same slot as a means of speeding up convergence/disincentivizing forks. 113 | - Expected Consensus slashes miners that provably ignore smaller tickets from the tipset, as a means of grinding the chain (and unfairly winning leader election). 114 | 115 | The collateral needs for both actions are distinct (in fact EC may not strictly require collateral). 116 | 117 | **EC vs SSLE** 118 | 119 | **Q**: What is the distinction between EC and SSLE? 120 | 121 | **A**: SSLE is an aspirational protocol for finding a single block proposer in leader election. EC is a consensus protocol that includes a block proposer and a way to achieve agreement (PoS Nakamoto consensus) on a particular block. 122 | EC's block proposer function is secret but it is not single. 0 or many blocks could be proposed in a given time slot. 123 | The expected value of proposed leaders per time slot on any chain is 1. 124 | SSLE is an open-problem on which we are actively working as it should greatly simplify the Filecoin consensus construction and lead to faster convergence on the blockchain. 125 | 126 | ## Communication 127 | 128 | - Slack channel: #filecoin-research 129 | - Issues in this repo 130 | 131 | ## License 132 | 133 | The Filecoin Project is dual-licensed under Apache 2.0 and MIT terms: 134 | 135 | - Apache License, Version 2.0, ([LICENSE-APACHE](https://github.com/filecoin-project/research/blob/master/LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 136 | - MIT license ([LICENSE-MIT](https://github.com/filecoin-project/research/blob/master/LICENSE-MIT) or http://opensource.org/licenses/MIT/) 137 | -------------------------------------------------------------------------------- /code/ec-sim-w/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/json" 6 | "fmt" 7 | "math/rand" 8 | "os" 9 | "os/signal" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | type Block struct { 15 | Parents [][32]byte 16 | Owner int 17 | Height uint64 18 | Nonce int 19 | Challenge int 20 | NullBlocks uint64 21 | PWeight int 22 | Timestamp int64 23 | 24 | // cached, for sp33d 25 | hash [32]byte 26 | } 27 | 28 | // IncrWeight returns the incremental weight added by this block according to 29 | // the consensus rules 30 | func (b *Block) IncrWeight(c *Consensus) int { 31 | return 10 + ((c.Power[b.Owner] * 100) / c.TotalPower) 32 | } 33 | 34 | // Hash returns the hash of this block 35 | func (b *Block) Hash() [32]byte { 36 | if b.hash == [32]byte{} { 37 | d, _ := json.Marshal(b) 38 | h := sha256.Sum256(d) 39 | b.hash = h 40 | } 41 | 42 | return b.hash 43 | } 44 | 45 | func (b *Block) ShortName() string { 46 | h := b.Hash() 47 | return fmt.Sprintf("%x", h[:8]) 48 | } 49 | 50 | type Consensus struct { 51 | Power map[int]int 52 | TotalPower int 53 | Miners []*Miner 54 | Blockstore *Blockstore 55 | } 56 | 57 | func (c *Consensus) isWinningTicket(m *Miner, t int) bool { 58 | return t < consensus.Power[m.id] 59 | } 60 | 61 | // writeGraph outputs a dot graph of the entire blockchain generated by the simulation 62 | // (as observed by one miner) 63 | func writeGraph(height uint64, ct *chainTracker) { 64 | fmt.Println("writing graph: ", height) 65 | fi, err := os.Create("chain.dot") 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | defer fi.Close() 71 | 72 | fmt.Fprintln(fi, "digraph G {") 73 | fmt.Fprintln(fi, "\t{\n\t\tnode [shape=plaintext];") 74 | fmt.Fprint(fi, "\t\t0") 75 | for cur := uint64(1); cur <= height; cur++ { 76 | fmt.Fprintf(fi, " -> %d", cur) 77 | } 78 | fmt.Fprintln(fi, ";") 79 | fmt.Fprintln(fi, "\t}") 80 | 81 | fmt.Fprintln(fi, "\tnode [shape=box];") 82 | for cur := int(height); cur >= 0; cur-- { 83 | cps, ok := ct.blks[uint64(cur)] 84 | if !ok { 85 | continue 86 | } 87 | 88 | for _, blks := range cps.s { 89 | fmt.Fprintf(fi, "\t{ rank = same; %d;", cur) 90 | for _, b := range blks { 91 | fmt.Fprintf(fi, " \"b%s\";", b.ShortName()) 92 | } 93 | fmt.Fprintln(fi, " }") 94 | for _, b := range blks { 95 | for _, parent := range b.Parents { 96 | fmt.Fprintf(fi, "\tb%s -> b%x;\n", b.ShortName(), parent[:8]) 97 | } 98 | } 99 | } 100 | } 101 | 102 | fmt.Fprintln(fi, "}\n") 103 | } 104 | 105 | // simStats computes some statistics about the performance of the simulation 106 | // using the given parameters 107 | func (c *Consensus) simStats() { 108 | ct := c.Miners[0].chain 109 | height := c.Miners[0].curHeight 110 | 111 | ignoredBlocks := make(map[[32]byte]struct{}) 112 | for i := uint64(0); i <= height; i++ { 113 | cps, ok := ct.blks[i] 114 | if !ok { 115 | continue 116 | } 117 | for _, bs := range cps.s { 118 | for _, blk := range bs { 119 | ignoredBlocks[blk.Hash()] = struct{}{} 120 | } 121 | } 122 | } 123 | 124 | cur := ct.heaviestTipset.parents 125 | var avtimestamps []int64 126 | 127 | for len(cur) > 0 { 128 | var tipsettime int64 129 | for _, p := range cur { 130 | pb := ct.getBlock(cur[0]) 131 | tipsettime += pb.Timestamp 132 | delete(ignoredBlocks, p) 133 | } 134 | tipsettime /= int64(len(cur)) 135 | avtimestamps = append(avtimestamps, tipsettime) 136 | 137 | next := ct.getBlock(cur[0]) 138 | cur = next.Parents 139 | } 140 | 141 | var blocktimes []int64 142 | for i := 1; i < len(avtimestamps); i++ { 143 | blocktimes = append(blocktimes, avtimestamps[i-1]-avtimestamps[i]) 144 | } 145 | 146 | fmt.Println("orphaned blocks: ", len(ignoredBlocks)) 147 | 148 | for i, m := range c.Miners { 149 | bts := m.chain.getHeaviestTipset() 150 | blk := &Block{ 151 | Parents: bts.parents, 152 | } 153 | fmt.Printf("[%2d] %s (height=%d)\n", i, blk.ShortName(), bts.height+1) 154 | } 155 | 156 | var blocktimesum int64 157 | for _, bt := range blocktimes { 158 | blocktimesum += bt 159 | } 160 | avblktime := time.Duration(blocktimesum/int64(len(blocktimes))) * time.Nanosecond 161 | fmt.Printf("average block time: %s\n", avblktime) 162 | } 163 | 164 | type Miner struct { 165 | id int 166 | inblocks chan *Block 167 | netDelay func(int) 168 | curHeight uint64 169 | 170 | chain *chainTracker 171 | wait *sync.WaitGroup 172 | } 173 | 174 | func (m *Miner) getMiningDelay() time.Duration { 175 | return time.Second * 2 176 | } 177 | 178 | // gracePeriod is the span of time in which a miner will change their mining base 179 | // if a new better block at the current height is found 180 | func (m *Miner) gracePeriod() time.Duration { 181 | return time.Millisecond * 200 182 | } 183 | 184 | // nullBlockDelay is the period of time a miner will wait after checking their ticket, 185 | // without seeing a new block, before they start attempting to check for a null block 186 | func (m *Miner) nullBlockDelay() time.Duration { 187 | return time.Millisecond * 500 188 | } 189 | 190 | func (m *Miner) broadcast(b *Block) { 191 | consensus.Blockstore.Put(b) 192 | for _, om := range consensus.Miners { 193 | go func(om *Miner) { 194 | m.netDelay(om.id) 195 | om.inblocks <- b 196 | }(om) 197 | } 198 | } 199 | 200 | func (m *Miner) maybeGenerateBlock(ts *tipSet) { 201 | // NOTE: in the real implementation, this randomness will be secure 202 | // randomness from the chain 203 | challenge := rand.Intn(consensus.TotalPower) 204 | if consensus.isWinningTicket(m, challenge) { 205 | // winner winner chicken dinner 206 | myblock := &Block{ 207 | Height: m.curHeight, 208 | Nonce: rand.Intn(100), 209 | Owner: m.id, 210 | Parents: ts.parents, 211 | NullBlocks: m.curHeight - (1 + ts.height), 212 | PWeight: ts.weight, 213 | Challenge: challenge, 214 | Timestamp: time.Now().UnixNano(), 215 | } 216 | h := myblock.Hash() 217 | 218 | pref := colors[int(m.curHeight)%len(colors)].Sprintf("[h:%d m:%d w:%d i:%d]", m.curHeight, m.id, ts.weight, myblock.IncrWeight(consensus)) 219 | fmt.Printf("%s mined block %x with parents: %x\n", pref, h[:4], hashPrefs(ts.parents)) 220 | m.broadcast(myblock) 221 | } 222 | } 223 | 224 | // mine is where the main consensus logic is implemented 225 | func (m *Miner) mine(done <-chan struct{}, genesis *Block) { 226 | m.chain.addBlock(genesis) 227 | m.curHeight = 1 228 | 229 | workCompleted := time.NewTimer(m.getMiningDelay()) 230 | 231 | epochStart := time.Now() 232 | 233 | // will select genesis block, but the variables get reused 234 | curtipset := m.chain.getParentsForHeight(m.curHeight) 235 | 236 | nullBlockTimer := time.NewTimer(time.Hour) 237 | nullBlockTimer.Stop() 238 | 239 | for { 240 | select { 241 | case <-workCompleted.C: 242 | m.maybeGenerateBlock(curtipset) 243 | nullBlockTimer.Reset(m.nullBlockDelay()) 244 | case nblk := <-m.inblocks: 245 | if err := verifyBlock(nblk); err != nil { 246 | fmt.Println("VERIFICATION FAILED:", err) 247 | break 248 | } 249 | if nblk.Height == m.curHeight-1 || nblk.Height == m.curHeight { 250 | m.chain.addBlock(nblk) 251 | } else { 252 | fmt.Printf("got unexpected block of height %d when we are mining block %d\n", nblk.Height, m.curHeight) 253 | } 254 | 255 | bts := m.chain.getHeaviestTipset() 256 | if bts.weight > curtipset.weight { 257 | if bts.height == curtipset.height { 258 | // we're currently mining a block for this height 259 | // are we within the grace period? if so, restart mining on top of this one 260 | if time.Now().Sub(epochStart) < m.gracePeriod() { 261 | curtipset = bts 262 | workCompleted.Reset(m.getMiningDelay()) 263 | nullBlockTimer.Stop() 264 | // note: not resetting our epoch here 265 | } else { 266 | //fmt.Println("not within grace period, dropping it...") 267 | } 268 | } else if bts.height >= curtipset.height { 269 | m.curHeight = bts.height + 1 270 | curtipset = bts 271 | workCompleted.Reset(m.getMiningDelay()) 272 | nullBlockTimer.Stop() 273 | 274 | // start a new epoch when we receive the first block of a greater height 275 | epochStart = time.Now() 276 | } else { 277 | fmt.Println("new height was less than our current height:", bts.height, m.curHeight, curtipset.height) 278 | } 279 | } else { 280 | if bts.height > curtipset.height { 281 | fmt.Println("Got a new block, it didnt weigh as much, but its got a longer chain height") 282 | } 283 | 284 | } 285 | case <-nullBlockTimer.C: 286 | m.curHeight++ 287 | workCompleted.Reset(m.getMiningDelay()) 288 | case <-done: 289 | m.wait.Done() 290 | return 291 | } 292 | } 293 | 294 | } 295 | 296 | func verifyBlock(blk *Block) error { 297 | if blk.Challenge >= consensus.Power[blk.Owner] { 298 | return fmt.Errorf("block was not a winner") 299 | } 300 | 301 | var parents []*Block 302 | for _, p := range blk.Parents { 303 | parents = append(parents, consensus.Blockstore.Get(p)) 304 | } 305 | 306 | w := parents[0].PWeight 307 | for _, p := range parents { 308 | w += p.IncrWeight(consensus) 309 | } 310 | if w != blk.PWeight { 311 | fmt.Println("PWeight mismatch!!") 312 | return fmt.Errorf("block had incorrect PWeight") 313 | } 314 | return nil 315 | } 316 | 317 | type candidateParentSet struct { 318 | s map[string][]*Block 319 | height uint64 320 | opts []string 321 | lessFunc func(a, b []*Block) bool 322 | } 323 | 324 | func newCandidateParentSet(h uint64) *candidateParentSet { 325 | return &candidateParentSet{ 326 | s: make(map[string][]*Block), 327 | height: h, 328 | lessFunc: func(a, b []*Block) bool { 329 | return weighParentSet(a) < weighParentSet(b) 330 | }, 331 | } 332 | } 333 | 334 | func (cps *candidateParentSet) addNewBlock(b *Block) { 335 | if b.Height != cps.height { 336 | panic("ni") 337 | } 338 | k := keyForParentSet(b.Parents) 339 | if cps.s[k] == nil { 340 | cps.opts = append(cps.opts, k) 341 | } 342 | cps.s[k] = append(cps.s[k], b) 343 | } 344 | 345 | func weighParentSet(blks []*Block) int { 346 | if len(blks) == 0 { 347 | return -1 348 | } 349 | 350 | var addWeight, pw int 351 | 352 | for i, b := range blks { 353 | if i == 0 { 354 | pw = b.PWeight 355 | } else if b.PWeight != pw { 356 | panic("blocks in same sibling set had different pweights") 357 | } 358 | addWeight += b.IncrWeight(consensus) 359 | } 360 | return addWeight + blks[0].PWeight 361 | } 362 | 363 | func getParentSetHashes(blks []*Block) [][32]byte { 364 | var out [][32]byte 365 | for _, b := range blks { 366 | out = append(out, b.Hash()) 367 | } 368 | return out 369 | } 370 | 371 | func (cps *candidateParentSet) getBestCandidates() ([][32]byte, int) { 372 | if len(cps.s) == 0 { 373 | panic("nope") 374 | } 375 | if len(cps.s) == 1 { 376 | selblks := cps.s[cps.opts[0]] 377 | return getParentSetHashes(selblks), weighParentSet(selblks) 378 | } 379 | 380 | var bestParents []*Block 381 | 382 | for _, blks := range cps.s { 383 | if cps.lessFunc(bestParents, blks) { 384 | bestParents = blks 385 | } 386 | } 387 | 388 | fmt.Println("Chain fork detected!") 389 | 390 | return getParentSetHashes(bestParents), weighParentSet(bestParents) 391 | } 392 | 393 | type chainTracker struct { 394 | blks map[uint64]*candidateParentSet 395 | allBlocks map[[32]byte]*Block 396 | maxheight uint64 397 | heaviestTipset *tipSet 398 | } 399 | 400 | type tipSet struct { 401 | parents [][32]byte 402 | height uint64 403 | weight int 404 | } 405 | 406 | func newChainTracker() *chainTracker { 407 | return &chainTracker{ 408 | blks: make(map[uint64]*candidateParentSet), 409 | allBlocks: make(map[[32]byte]*Block), 410 | } 411 | } 412 | 413 | func (ct *chainTracker) addBlock(b *Block) { 414 | ct.allBlocks[b.Hash()] = b 415 | cps, ok := ct.blks[b.Height] 416 | if !ok { 417 | cps = newCandidateParentSet(b.Height) 418 | ct.blks[b.Height] = cps 419 | } 420 | 421 | cps.addNewBlock(b) 422 | 423 | ts := ct.getParentsForHeight(b.Height + 1) 424 | if ct.heaviestTipset == nil { 425 | ct.heaviestTipset = ts 426 | return 427 | } 428 | 429 | if ts.weight > ct.heaviestTipset.weight { 430 | ct.heaviestTipset = ts 431 | } 432 | } 433 | 434 | func (ct *chainTracker) getParentsForHeight(h uint64) *tipSet { 435 | for v := h - 1; ; v-- { 436 | cps := ct.blks[v] 437 | if cps != nil { 438 | p, weight := cps.getBestCandidates() 439 | return &tipSet{ 440 | parents: p, 441 | height: v, 442 | weight: weight, 443 | } 444 | } 445 | 446 | if v == 0 { 447 | panic("shouldnt happen") 448 | } 449 | } 450 | } 451 | 452 | func (ct *chainTracker) getHeaviestTipset() *tipSet { 453 | return ct.heaviestTipset 454 | } 455 | 456 | func (ct *chainTracker) getBlock(h [32]byte) *Block { 457 | b, ok := ct.allBlocks[h] 458 | if !ok { 459 | panic("no such block") 460 | } 461 | 462 | return b 463 | } 464 | 465 | var consensus *Consensus 466 | 467 | func main() { 468 | rand.Seed(time.Now().UnixNano()) 469 | genesis := &Block{ 470 | Nonce: 42, 471 | PWeight: 1, 472 | Timestamp: time.Now().UnixNano(), 473 | } 474 | 475 | numMiners := 10 476 | 477 | var waitWg sync.WaitGroup 478 | 479 | consensus = &Consensus{ 480 | Power: make(map[int]int), 481 | Blockstore: newBlockstore(), 482 | } 483 | consensus.Blockstore.Put(genesis) 484 | for i := 0; i < numMiners; i++ { 485 | pow := 10 + rand.Intn(10) 486 | consensus.TotalPower += pow 487 | consensus.Power[i] = pow 488 | consensus.Miners = append(consensus.Miners, &Miner{ 489 | id: i, 490 | inblocks: make(chan *Block, 32), 491 | chain: newChainTracker(), 492 | netDelay: func(int) { 493 | time.Sleep(time.Duration(rand.Intn(200)+1) * time.Millisecond) 494 | }, 495 | wait: &waitWg, 496 | }) 497 | } 498 | 499 | doneCh := make(chan struct{}) 500 | 501 | for _, m := range consensus.Miners { 502 | waitWg.Add(1) 503 | go m.mine(doneCh, genesis) 504 | } 505 | 506 | c := make(chan os.Signal) 507 | signal.Notify(c, os.Interrupt) 508 | select { 509 | case <-c: 510 | close(doneCh) 511 | waitWg.Wait() 512 | fmt.Println("done") 513 | writeGraph(consensus.Miners[0].curHeight, consensus.Miners[0].chain) 514 | consensus.simStats() 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /code/other-sims/ec_withhold.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import pandas as pd 4 | import pdb 5 | import math 6 | import json 7 | import calendar 8 | import time 9 | from scipy.stats import binom 10 | import operator as op 11 | from functools import reduce 12 | 13 | 14 | print "Takes around 25 mins..." 15 | # only set to True if running for a single expected_blocks_per_round 16 | store_output = True 17 | ##### 18 | ## System level params 19 | ##### 20 | lookbacks = [0] # [k for k in range(0, 11)] + [k for k in range(15, 105, 5)] 21 | alphas = [.1, .33] 22 | # alphas = [k/100.0 for k in range(2, 52, 2)] 23 | rounds_back = [] 24 | # rounds_back = range(5, 105, 10) 25 | total_qual_ec = [] 26 | total_qual_nohs = [] 27 | total_qual_nots = [] 28 | miners = 1000 29 | sim_rounds = 5000 30 | e_blocks_per_round = [1, 5] 31 | num_sims = 10000 32 | # conf denom needs to be no bigger than number of sims (otherwise can't get that precision) 33 | target_conf = [.0001] 34 | 35 | ## Model complex weighting fn? Based on observable wt fn params 36 | wt_fn = False 37 | # wt_fn = True 38 | powerAtStart = 5000 # in PBs 39 | powerIncreasePerDay = .025 # 2.5% per day for double power in 28 days (1,250 TB a day) 40 | # assuming a 30 sec block time and uniform increase 41 | RDS_PER_DAY = 86400./30 42 | powerIncreasePerRound = powerIncreasePerDay/RDS_PER_DAY 43 | wBlocksFactorTransitionConst = 350 44 | wStartPunish = 5 45 | wBlocksFactor = "wBlocksFactorTransitionCost*(log2((1-alpha)*networkSize(r))/(1-alpha))" 46 | wForkFactor = "1 for k > E[X] - stddev[X]; CDF(X, k) otherwise" 47 | 48 | ##### 49 | ## Helper fns 50 | ##### 51 | 52 | def cdf(k, n, p): 53 | _sum = 0 54 | for i in range(k + 1): 55 | _sum += ncr(n, i)*(p**i)*((1-p)**(n-i)) 56 | return _sum 57 | 58 | def ncr(n, r): 59 | r = min(r, n-r) 60 | numer = reduce(op.mul, range(n, n-r, -1), 1) 61 | denom = reduce(op.mul, range(1, r+1), 1) 62 | return numer / denom 63 | 64 | def enum(*sequential, **named): 65 | enums = dict(zip(sequential, range(len(sequential))), **named) 66 | reverse = dict((value, key) for key, value in enums.iteritems()) 67 | # hack to enumerate the elements 68 | enums['ks'] = range(len(sequential)) 69 | enums['rev'] = reverse 70 | return type('Enum', (), enums) 71 | 72 | Sim = enum('EC', 'NOHS', 'NOTS') 73 | ## What to run 74 | sim_to_run = [Sim.EC] 75 | # sim_to_run = [Sim.EC, Sim.NOHS, Sim.NOTS] 76 | 77 | def confidence_of_k(target, array): 78 | _sum = 0.0 79 | for idx, i in enumerate(array): 80 | # attack will fail 81 | if i < target: 82 | _sum += 1 83 | # attack succeeds 84 | return float(len(array) - _sum) / len(array) 85 | 86 | # because of EC's lookback param, adversary can effectively look ahead that many blocks to decide whether to release 87 | # when honest party catches up: is this a real "catch up" or a temporary one? 88 | def should_end_attack(honest_weight, adversarial_weight, honest_chain, adversarial_chain, lookahead): 89 | assert(len(honest_chain) == len(adversarial_chain)) 90 | # no need to lookahead then, let's just compare the weights. 91 | if lookahead == 0: 92 | return adversarial_weight <= honest_weight 93 | 94 | if adversarial_weight > honest_weight: 95 | return False 96 | 97 | # if we're getting to end of sim, let's not look beyond the end. 98 | if len(honest_chain) < lookahead: 99 | lookahead = len(honest_chain) 100 | 101 | # If we are not, let's look ahead to make a choice 102 | for i in range(lookahead): 103 | honest_weight += honest_chain[i] 104 | adversarial_weight += adversarial_chain[i] 105 | # if at some point in the next lookahead blocks adversary takes over again, cancel the release and wait it out 106 | if adversarial_weight > honest_weight: 107 | return False 108 | # adversary can't take the risk of never getting back on top. Stop the attack. 109 | return True 110 | 111 | def should_launch_attack(_type, start, advCount, honCount): 112 | if _type == Sim.NOHS: 113 | return advCount > 0 and advCount >= honCount 114 | else: 115 | # for EC, makes sense 116 | # for NOTS, no need to check either: you'll be tied at worst, just launch attack anyways 117 | return advCount > 0 118 | 119 | 120 | 121 | def get_settings(): 122 | params = {} 123 | params["lookbacks"] = lookbacks 124 | params["alphas"] = alphas 125 | params["rounds_back"] = rounds_back 126 | params["miners"] = miners 127 | params["sim_rounds"] = sim_rounds 128 | params["e_blocks_per_round"] = e_blocks_per_round 129 | params["num_sims"] = num_sims 130 | params["wt_fn"] = { 131 | "enabled": wt_fn, 132 | "powerAtStart": powerAtStart, 133 | "powerIncreasePerRound": powerIncreasePerRound, 134 | "wForkFactor": wForkFactor, 135 | "wStartPunish": wStartPunish, 136 | "wBlocksFactor": wBlocksFactor 137 | } 138 | return params 139 | 140 | def store_output(succ_atk, succ_targ, total_qual, e, lb): 141 | params = get_settings() 142 | params["current_e"] = e 143 | params["current_lb"] = lb 144 | output = {} 145 | for el in sim_to_run: 146 | output[Sim.rev[el]] = { 147 | "conv": [{"alpha": alpha, "result": [{"rounds_back": k, "prob": prob} for k, prob in zip(rounds_back, succ_atk[el][idx])]} for idx, alpha in enumerate(alphas)], 148 | "target": [{"alpha": alpha, "result": [{"target": targ, "rounds": res} for targ, res in zip(target_conf, succ_targ[el][idx])]} for idx, alpha in enumerate(alphas)], 149 | "qual": [{"alpha": alpha, "qual": qual} for alpha, qual in zip(alphas, total_qual[el])] 150 | } 151 | 152 | outputDoc = "./monte/sim_results_{ts}.json".format(ts=calendar.timegm(time.gmtime())) 153 | _json = {"params": params, "output": output} 154 | with open(outputDoc, 'w') as f: 155 | json.dump(_json, f, indent=4) 156 | 157 | ##### 158 | ## Sim runner 159 | ##### 160 | 161 | class MonteCarlo: 162 | def __init__(self): 163 | self.reset_top_level() 164 | 165 | def reset_top_level(self): 166 | # What portion of block will adversary publish? 167 | self.total_qual = {k: [] for k in sim_to_run} 168 | # type -> alpha -> blocksback -> int 169 | # How often will adversarial attack succeed across rounds_back? 170 | self.succ_atk = {k: [] for k in sim_to_run} 171 | # and for specific target 172 | self.succ_targ = {k: [] for k in sim_to_run} 173 | 174 | def reset_sim(self): 175 | # type -> int 176 | self.nostart = {k: 0 for k in sim_to_run} 177 | self.noend = {k: 0 for k in sim_to_run} 178 | # type -> array -> int 179 | self.lengths = {k: [] for k in sim_to_run} 180 | self.launched = {k: [] for k in sim_to_run} 181 | self.quality = {k: [] for k in sim_to_run} 182 | blocksStdDev = math.sqrt(self.e*(1-self.p)) 183 | self.forkFactorCutoff = self.e - blocksStdDev 184 | self.CDFMemoization = {} 185 | 186 | def wForkFactor(self, blocksR): 187 | if blocksR > self.forkFactorCutoff: 188 | return 1 189 | # super inefficient 190 | # return binom.cdf(blocksR, self.e*miners, self.p) 191 | if blocksR not in self.CDFMemoization.keys(): 192 | self.CDFMemoization[blocksR] = cdf(blocksR, self.e*miners, self.p) 193 | return self.CDFMemoization[blocksR] 194 | 195 | def wBlocksFactor(self, power): 196 | return wBlocksFactorTransitionConst*(math.log((1.0-self.alpha)*power, 2)/(1.0-self.alpha)) 197 | 198 | def wPowerFactor(self, power): 199 | return math.log(power, 2) 200 | 201 | def new_wt(self, old_wt, numBlocks, power, nulls, supp=0): 202 | # supp will be added weight for eg headstart 203 | if wt_fn: 204 | return old_wt + self.wForkFactor(numBlocks)*(self.wPowerFactor(power) + self.wBlocksFactor(power)*(numBlocks + supp)) 205 | else: 206 | return old_wt + numBlocks + supp 207 | 208 | 209 | def run(self): 210 | # state gets too funky if doing both. Only test one set of top level vars at a time 211 | assert(len(e_blocks_per_round) == 1 or len(lookbacks) == 1) 212 | for e in e_blocks_per_round: 213 | self.reset_top_level() 214 | 215 | # equal sized miners: worst case 216 | self.p = e/float(1*miners) 217 | self.e = e 218 | 219 | for lb in lookbacks: 220 | # below reset would break if both e and lh are multiple values 221 | self.reset_top_level() 222 | self.lb = lb 223 | 224 | for alpha in alphas: 225 | self.alpha = alpha 226 | self.reset_sim() 227 | for i in range(num_sims): 228 | for sim in sim_to_run: 229 | self.run_sim(sim) 230 | 231 | self.aggr_alpha_stats() 232 | self.output_full_stats() 233 | 234 | # print params to make results reproducible 235 | params = get_settings() 236 | print json.dumps(params, indent=4) 237 | 238 | def output_full_stats(self): 239 | ### Prettify for output 240 | print "\nConvergence" 241 | for el in sim_to_run: 242 | assert(len(self.succ_atk[el]) == len(alphas)) 243 | assert(len(self.succ_targ[el]) == len(alphas)) 244 | 245 | print Sim.rev[el] 246 | df = pd.DataFrame(self.succ_atk[el], columns=rounds_back, index=alphas) 247 | print df 248 | 249 | print "\nQuality" 250 | for el in sim_to_run: 251 | assert(len(self.total_qual[el]) == len(alphas)) 252 | 253 | print Sim.rev[el] 254 | df = pd.DataFrame(self.total_qual[el], index=alphas) 255 | print df 256 | 257 | if store_output: 258 | store_output(self.succ_atk, self.succ_targ, self.total_qual, self.e, self.lb) 259 | 260 | def aggr_alpha_stats(self): 261 | 262 | print "\nAttacker power alpha: {alpha}%, num of rounds: {sim_rounds}, num of sims: {sims}, lookahead: {la}, expected blocks per round: {e}".format(alpha=self.alpha*100, sims=num_sims, sim_rounds=sim_rounds, la=self.lb, e=self.e) 263 | 264 | # statement: the median is the distance such that an attacker creates a fork 50% of the time. 265 | # Q1: how often can the attacker create a fork from the average? 266 | 267 | succ_atk_alpha = {k: [] for k in sim_to_run} 268 | succ_targ_alpha = {k: [] for k in sim_to_run} 269 | for el in sim_to_run: 270 | _type = Sim.rev[el] 271 | avg = np.average(self.lengths[el]) 272 | med = np.median(self.lengths[el]) 273 | num = len(self.lengths[el]) 274 | nostart = self.nostart[el] 275 | noend = self.noend[el] 276 | launch = np.average(self.launched[el]) 277 | avg_qual = np.average(self.quality[el]) 278 | med_qual = np.median(self.quality[el]) 279 | print "{_type}: num of attacks {num}; num didn't start {nostart}, didn't end {noend}, launched on avg: {launch}\n\tconv: avg {avg}; med {med}; \n\tqual: avg {avg_qual}; med {med_qual}".format(_type=_type, avg=avg, med = med, num = num, nostart = nostart, noend = noend, launch = launch, avg_qual = avg_qual, med_qual = med_qual) 280 | 281 | # how far back in order for us to reach target confidence? 282 | for conf in target_conf: 283 | rounds = np.percentile(self.lengths[el], (1-conf)*100) 284 | print "{_type}: {rounds_back} rounds back, atk success is {targ}".format(_type=_type, rounds_back=rounds, targ=conf) 285 | succ_targ_alpha[el].append(rounds) 286 | # what is confidence at various ranges? 287 | for lookback in rounds_back: 288 | likelihood = confidence_of_k(lookback, self.lengths[el]) 289 | print "{_type}: k {avg} will succeed {num}".format(_type=_type, avg=lookback, num=likelihood) 290 | succ_atk_alpha[el].append(likelihood) 291 | 292 | # store across alphas 293 | for el in sim_to_run: 294 | assert(len(succ_atk_alpha[el]) == len(rounds_back)) 295 | self.succ_atk[el].append(succ_atk_alpha[el]) 296 | assert(len(succ_targ_alpha[el]) == len(target_conf)) 297 | self.succ_targ[el].append(succ_targ_alpha[el]) 298 | # Quality is not dependent on lookback, any successful attack will do it so we take avg 299 | self.total_qual[el].append(np.average(self.quality[el])) 300 | # plot for a given alpha 301 | # plt.plot(rounds_back, succ_atk_alpha[el]) 302 | # plt.xlabel("blocks behind") 303 | # plt.ylabel("chance of success") 304 | # plt.title("Confirmation time for alpha={alpha}".format(alpha=self.alpha)) 305 | # fig1 = plt.gcf() 306 | # fig1.savefig("{alpha}-confirmationTime.png".format(alpha=self.alpha), format="png") 307 | # plt.clf() 308 | 309 | def run_sim(self, _type): 310 | # result of flipping a coin honest_miners/adv_miners times, tested 1000 times. 311 | # is this sim of praos overly optimistic: it glosses over potential fork when multiple honest wins 312 | # put another way, it represents choice between longest honest and longest adv, and not between longest honests 313 | hon_miners = round((1-self.alpha)*miners) 314 | adv_miners = round(self.alpha*miners) 315 | ch = np.random.binomial(hon_miners, self.p, sim_rounds) 316 | ca = np.random.binomial(adv_miners, self.p, sim_rounds) 317 | if _type == Sim.NOTS: 318 | chain_hon = [1 if ch[i]>0 else 0 for i in range(len(ch))] 319 | chain_adv = [1 if ca[i]>0 else 0 for i in range(len(ca))] 320 | else: 321 | chain_hon = ch 322 | chain_adv = ca 323 | 324 | #### 325 | ## set Params 326 | #### 327 | weight_adv = 0 328 | weight_hon = 0 329 | # for quality, we are not looking at chains together but rather which has its blocks counted 330 | tot_blocks_adv = 0 331 | tot_blocks_hon = 0 332 | pot_blocks_adv = 0 333 | pot_blocks_hon = 0 334 | # flags to detect whether attack is running 335 | start = -2 336 | end = -1 337 | weight_at_start = -1 338 | # stats 339 | max_len = -1 340 | num_atks = 0 341 | # wt_fn 342 | power = powerAtStart 343 | adv_nulls = 0 344 | hon_nulls = 0 345 | 346 | #### 347 | ## Actual sim 348 | #### 349 | for idx, j in enumerate(chain_adv): 350 | 351 | # track nulls across chains 352 | if wt_fn: 353 | hon_nulls = hon_nulls + 1 if chain_hon[idx] == 0 else 0 354 | adv_nulls = adv_nulls + 1 if j == 0 else 0 355 | power *= (1 + powerIncreasePerRound) 356 | 357 | # start attack 358 | if start < 0 and should_launch_attack(_type, start, j, chain_hon[idx]): 359 | # reset since atkr will compare to this to time end 360 | weight_adv = self.new_wt(weight_hon, j, power, adv_nulls, chain_hon[idx]) 361 | weight_hon = self.new_wt(weight_hon, chain_hon[idx], power, hon_nulls) 362 | pot_blocks_adv = tot_blocks_adv + j 363 | pot_blocks_hon = tot_blocks_hon + chain_hon[idx] 364 | 365 | start = idx 366 | num_atks += 1 367 | 368 | # ongoing attack 369 | elif start >= 0: 370 | 371 | # update weights 372 | weight_hon = self.new_wt(weight_hon, chain_hon[idx], power, hon_nulls) 373 | weight_adv = self.new_wt(weight_adv, j, power, adv_nulls) 374 | pot_blocks_adv = pot_blocks_adv + j 375 | pot_blocks_hon = pot_blocks_hon + chain_hon[idx] 376 | 377 | # should it be ended? 378 | if should_end_attack(weight_hon, weight_adv, chain_hon[idx+1:], chain_adv[idx+1:], self.lb): 379 | end = idx 380 | 381 | # check to see who won 382 | # in case not equal, then attacker took too much risk and failed 383 | if weight_hon != weight_adv: 384 | assert(weight_hon > weight_adv) 385 | tot_blocks_hon = pot_blocks_hon 386 | else: 387 | # else, atk pays off (we assume atker has better connectivity and wins ties) 388 | tot_blocks_adv = pot_blocks_adv 389 | # successful fork 390 | weight_hon = weight_adv 391 | if _type == Sim.EC: 392 | # in case of loss, EC still gets first block will be counted 393 | tot_blocks_hon += chain_hon[start] 394 | 395 | # compare to current longest successful attack in this sim. 396 | if end - start > max_len: 397 | max_len = end - start 398 | # reset to run attack again 399 | start = -1 400 | end = -1 401 | 402 | # attack didn't end and sim ends 403 | elif end < 0 and idx == sim_rounds - 1: 404 | self.noend[_type] += 1 405 | # stop attack here successfully (conservative). account for max (could have gone on) 406 | tot_blocks_adv = pot_blocks_adv 407 | weight_hon = weight_adv 408 | if _type == Sim.EC: 409 | # in case of loss, EC still gets first block will be counted 410 | tot_blocks_hon += chain_hon[start] 411 | if sim_rounds - start > max_len: 412 | max_len = sim_rounds - start 413 | 414 | else: 415 | # this is the case we end while not in attack 416 | assert(start < 0) 417 | 418 | # attack never started and sim ends 419 | if start == -2 and idx == sim_rounds - 1: 420 | self.nostart[_type] += 1 421 | 422 | # update all counts 423 | tot_blocks_adv += j 424 | tot_blocks_hon += chain_hon[idx] 425 | weight_hon = self.new_wt(weight_hon, chain_hon[idx], power, hon_nulls) 426 | # adv weight doesn't matter here since it kicks off as weight_hon 427 | 428 | # at end of sim, retain stats 429 | # longest atk, num launched, adv earnings (qual) 430 | self.lengths[_type].append(max_len) 431 | self.launched[_type].append(num_atks) 432 | self.quality[_type].append(float(tot_blocks_adv)/(tot_blocks_hon + tot_blocks_adv)) 433 | 434 | mc = MonteCarlo() 435 | mc.run() 436 | -------------------------------------------------------------------------------- /code/ec-sim-zs/main.go: -------------------------------------------------------------------------------- 1 | // example run: ./long_sim -lbp=100 -rounds=10 -miners=10 -trials=100 -output="output" 2 | 3 | package main 4 | 5 | import ( 6 | crand "crypto/rand" 7 | "encoding/json" 8 | "flag" 9 | "fmt" 10 | "hash/fnv" 11 | "math" 12 | "math/big" 13 | "math/rand" 14 | "os" 15 | "runtime/pprof" 16 | "sort" 17 | "strconv" 18 | "strings" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") 24 | var suite bool 25 | 26 | var uniqueID int 27 | var _IDLock sync.Mutex 28 | 29 | const bigOlNum = math.MaxUint32 30 | 31 | //**** Utils 32 | 33 | func printSingle(content string) { 34 | if !suite { 35 | fmt.Printf(content) 36 | } 37 | } 38 | 39 | func getUniqueID() int { 40 | _IDLock.Lock() 41 | defer _IDLock.Unlock() 42 | uniqueID += 1 43 | return uniqueID - 1 44 | } 45 | 46 | func randInt(limit int64) int64 { 47 | limitBig := big.NewInt(limit) 48 | n, err := crand.Int(crand.Reader, limitBig) 49 | if err != nil { 50 | panic(err) 51 | } 52 | return n.Int64() 53 | } 54 | 55 | //**** Helpers 56 | // TODO: fix this hack with a special case 57 | // makeGen makes the genesis block. In the case the lbp is more than 1 it also 58 | // makes lbp -1 genesis ancestors for sampling the first lbp - 1 blocks after genesis 59 | func makeGen(lbp int, totalMiners int) *Block { 60 | var gen *Tipset 61 | for i := 0; i < lbp; i++ { 62 | gen = NewTipset([]*Block{&Block{ 63 | InHead: true, 64 | Nonce: getUniqueID(), 65 | Parents: gen, 66 | Owner: -1, 67 | Height: 0, 68 | Null: false, 69 | ParentWeight: 0, 70 | Seed: uint64(randInt(int64(bigOlNum * totalMiners))), 71 | }}) 72 | } 73 | return gen.Blocks[0] 74 | } 75 | 76 | // Input a set of newly mined blocks, return a map grouping these blocks 77 | // into tipsets that obey the tipset invariants. 78 | func allTipsets(blks []*Block) []*Tipset { 79 | tipsets := make([]*Tipset, 0, len(blks)) 80 | for i, blk1 := range blks { 81 | tipset := []*Block{blk1} 82 | for _, blk2 := range blks[i+1:] { 83 | if blk1.Height == blk2.Height && blk1.Parents.Name == blk2.Parents.Name { 84 | tipset = append(tipset, blk2) 85 | } 86 | } 87 | tipsets = append(tipsets, NewTipset(tipset)) 88 | } 89 | return tipsets 90 | } 91 | 92 | // forksFromTipset returns the n subsets of a tipset of length n: for every ticket 93 | // it returns a tipset containing the block containing that ticket and all blocks 94 | // containing a ticket larger than it. This is a rational miner trying to mine 95 | // all possible non-slashable forks off of a tipset. 96 | func forksFromTipset(ts *Tipset) []*Tipset { 97 | var forks []*Tipset 98 | // works because blocks are kept ordered in Tipsets 99 | for i := range ts.Blocks { 100 | currentFork := []*Block{ts.Blocks[i]} 101 | for j := i + 1; j < len(ts.Blocks); j++ { 102 | currentFork = append(currentFork, ts.Blocks[j]) 103 | } 104 | forks = append(forks, NewTipset(currentFork)) 105 | } 106 | return forks 107 | } 108 | 109 | func sortBlocks(blocks []*Block) { 110 | sort.Slice(blocks, func(i, j int) bool { return blocks[i].Seed < blocks[j].Seed }) 111 | } 112 | 113 | func stringifyBlocks(blocks []*Block) string { 114 | // blocks are already sorted... just do the easy thing 115 | b := new(strings.Builder) 116 | for i, blk := range blocks { 117 | b.WriteString(strconv.Itoa(blk.Nonce)) 118 | if i != len(blocks)-1 { 119 | b.WriteByte('-') 120 | } 121 | } 122 | return b.String() 123 | } 124 | 125 | //**** Structs 126 | 127 | // Block 128 | type Block struct { 129 | // Nonce is unique for each block 130 | Nonce int `json:"nonce"` 131 | Parents *Tipset `json:"tipset"` 132 | Owner int `json:"owner"` 133 | Height int `json:"height"` 134 | Null bool `json:"null"` 135 | ParentWeight int `json:"parentWeight"` 136 | Seed uint64 `json:"seed"` 137 | InHead bool `json:"inHead"` 138 | } 139 | 140 | // Tipset 141 | // bringing in from json would need to manually link blocks into Tipset using name 142 | type Tipset struct { 143 | // Blocks are sorted 144 | Blocks []*Block `json:"-"` 145 | Name string `json:"name"` 146 | MinTicket uint64 `json:"minTicket"` 147 | WasHead bool `json:"wasHead"` 148 | Weight int `json:"weight"` 149 | } 150 | 151 | // Chain tracker 152 | type chainTracker struct { 153 | // index tipsets per height 154 | liveBlocksByHeight map[int][]*Block `json:"liveBlocksByHeight"` 155 | allBlocks map[int]*Block `json:"allBlocks"` 156 | maxHeight int `json:"maxHeight"` 157 | head *Tipset `json:"head"` 158 | miners []*RationalMiner `json:"miner"` 159 | } 160 | 161 | // Rational Miner 162 | type RationalMiner struct { 163 | Power float64 `json:"power"` 164 | PrivateForks map[string]*Tipset `json:"-"` 165 | ID int `json:"id"` 166 | TotalMiners int `json:"-"` 167 | Rand *rand.Rand `json:"-"` 168 | } 169 | 170 | //**** Block helpers 171 | 172 | // Walk back until we find a tipset with a live parent 173 | func (bl *Block) liveParents() *Tipset { 174 | // Tipsets with null blocks only contain one block (since null blocks are mined privately) 175 | // All blocks in a tipset share parents 176 | parents := bl.Parents 177 | for parents.Blocks[0].Null { 178 | parents = parents.Blocks[0].Parents 179 | } 180 | return parents 181 | } 182 | 183 | //**** Tipset helpers 184 | 185 | func NewTipset(blocks []*Block) *Tipset { 186 | 187 | if len(blocks) == 0 { 188 | panic("Don't call weight on no parents") 189 | } 190 | 191 | sortBlocks(blocks) 192 | minTicket := blocks[0].Seed 193 | for _, block := range blocks { 194 | if block.Seed < minTicket { 195 | minTicket = block.Seed 196 | } 197 | } 198 | 199 | // Setting weight works because all blocks in a tipset have the same parent (see allTipsets) 200 | // block weight is equal to parent tipset weight, so we simply add the number of non-null 201 | // blocks here. 202 | tsWeight := blocks[0].ParentWeight 203 | if !blocks[0].Null { 204 | tsWeight += len(blocks) 205 | } 206 | 207 | return &Tipset{ 208 | Blocks: blocks, 209 | Name: stringifyBlocks(blocks), 210 | MinTicket: minTicket, 211 | WasHead: false, 212 | Weight: tsWeight, 213 | } 214 | } 215 | 216 | func (ts *Tipset) getHeight() int { 217 | if len(ts.Blocks) == 0 { 218 | panic("Don't call height on no parents") 219 | } 220 | // Works because all blocks in a tipset have same height (see allTipsets) 221 | return ts.Blocks[0].Height 222 | } 223 | 224 | func (ts *Tipset) getParents() *Tipset { 225 | if len(ts.Blocks) == 0 { 226 | panic("Don't call parents on nil blocks") 227 | } 228 | return ts.Blocks[0].Parents 229 | } 230 | 231 | //**** CT Helpers 232 | 233 | func NewChainTracker(miners []*RationalMiner) *chainTracker { 234 | return &chainTracker{ 235 | liveBlocksByHeight: make(map[int][]*Block), 236 | allBlocks: make(map[int]*Block), 237 | maxHeight: -1, 238 | miners: miners, 239 | } 240 | } 241 | 242 | // setHead updates the heaviest tipset seen by the network. 243 | func (ct *chainTracker) setHead(blocks []*Block) { 244 | candidateHead := ct.head 245 | for _, ts := range allTipsets(blocks) { 246 | if ts.Weight > candidateHead.Weight { 247 | candidateHead = ts 248 | } else if ts.Weight == candidateHead.Weight { 249 | // if of equal weight, pick min ticket 250 | if ts.MinTicket < candidateHead.MinTicket { 251 | candidateHead = ts 252 | } 253 | } 254 | } 255 | 256 | if candidateHead != ct.head { 257 | printSingle(fmt.Sprintf("setting head to %s\n", ct.head.Name)) 258 | ct.head = candidateHead 259 | ct.head.WasHead = true 260 | for _, blk := range ct.head.Blocks { 261 | blk.InHead = true 262 | } 263 | } 264 | } 265 | 266 | //**** Miner Helpers 267 | 268 | func NewRationalMiner(id int, power float64, totalMiners int, rng *rand.Rand) *RationalMiner { 269 | return &RationalMiner{ 270 | Power: power, 271 | PrivateForks: make(map[string]*Tipset, 0), 272 | ID: id, 273 | TotalMiners: totalMiners, 274 | Rand: rng, 275 | } 276 | } 277 | 278 | // generateBlock makes a new block with the given parents 279 | // note that while it uses a "null block abstraction" rather than ticket arrays as in 280 | // the spec, the result is the same for consensus. 281 | // To that end, we use separate tickets for new ticket generation and election proof generation 282 | // in case there is randomness skew (though can't think of what it would be rn) 283 | func (m *RationalMiner) generateBlock(parents *Tipset, lbp int) *Block { 284 | // Given parents and id we have a unique source for new ticket 285 | lotteryTicket := lookbackTipset(parents, lbp).MinTicket 286 | lastTicket := lookbackTipset(parents, 1).MinTicket 287 | 288 | // Also need live parents off of which to calculate new weight 289 | liveParents := parents 290 | if parents.Blocks[0].Null { 291 | // null blocks will only ever be in single-block tipsets so this works 292 | liveParents = parents.Blocks[0].liveParents() 293 | } 294 | 295 | // generate a new ticket from parent tipset 296 | t := m.generateTicket(lastTicket) 297 | // include in new block 298 | nextBlock := &Block{ 299 | Nonce: getUniqueID(), 300 | Parents: parents, 301 | Owner: m.ID, 302 | Height: parents.getHeight() + 1, 303 | ParentWeight: liveParents.Weight, 304 | Seed: t, 305 | InHead: false, 306 | } 307 | 308 | // check lotteryTicket to see if the block can be published 309 | electionProof := m.generateTicket(lotteryTicket) 310 | if isWinningTicket(electionProof, m.Power) { 311 | nextBlock.Null = false 312 | } else { 313 | nextBlock.Null = true 314 | } 315 | 316 | return nextBlock 317 | } 318 | 319 | // generateTicket, simulates a VRF 320 | func (m *RationalMiner) generateTicket(minTicket uint64) uint64 { 321 | // old way 322 | // seed := minTicket + uint64(m.ID) 323 | // m.Rand.Seed(int64(seed)) 324 | // return uint64(m.Rand.Int63n(int64(bigOlNum))) 325 | 326 | // return fnv hash of ticket + miner id 327 | // Hacks R Us: reversing minTicket and m.ID somehow makes this very non pseudo-random 328 | hash := fnv.New64() 329 | hash.Write([]byte(fmt.Sprintf("%d%d", m.ID, minTicket))) 330 | newTix := hash.Sum64() % bigOlNum 331 | return newTix 332 | } 333 | 334 | func (m *RationalMiner) ConsiderAllForks(atsforks [][]*Tipset) { 335 | // rational miner strategy look for all potential minblocks there 336 | for _, forks := range atsforks { 337 | for _, ts := range forks { 338 | m.PrivateForks[ts.Name] = ts 339 | } 340 | } 341 | } 342 | 343 | // Input the base tipset for mining lookbackTipset will return the ancestor 344 | // tipset that should be used for sampling the leader election seed. 345 | // On LBP == 1, returns itself (as in no farther than direct parents) 346 | func lookbackTipset(tipset *Tipset, lbp int) *Tipset { 347 | for i := 0; i < lbp-1; i++ { 348 | tipset = tipset.getParents() 349 | } 350 | return tipset 351 | } 352 | 353 | func isWinningTicket(ticket uint64, power float64) bool { 354 | // this is a simulation of ticket checking: the ticket is drawn uniformly from 0 to bigOlNum 355 | // If it is smaller than that * the miner's power (between 0 and 1), it wins. 356 | return float64(ticket) < float64(bigOlNum)*power 357 | } 358 | 359 | //**** Main logic 360 | 361 | // Mine outputs the block that a miner mines in a round where the leaves of 362 | // the block tree are given by newBlocks. A miner will only ever mine one 363 | // block in a round because if it mines two or more it gets slashed. 364 | func (m *RationalMiner) Mine(ct *chainTracker, atsforks [][]*Tipset, lbp int) *Block { 365 | // Start by combining existing pforks and new blocks available to mine atop of 366 | m.ConsiderAllForks(atsforks) 367 | 368 | var nullBlocks []*Block 369 | maxWeight := 0 370 | var bestBlock *Block 371 | printSingle(fmt.Sprintf("miner %d. number of priv forks: %d\n", m.ID, len(m.PrivateForks))) 372 | for k := range m.PrivateForks { 373 | // generateBlock takes in a block's parent tipset, as in current head of PrivateForks 374 | blk := m.generateBlock(m.PrivateForks[k], lbp) 375 | if !blk.Null && blk.ParentWeight > maxWeight { 376 | bestBlock = blk 377 | maxWeight = blk.ParentWeight 378 | } else if blk.Null && bestBlock == nil { 379 | // if blk is null and we haven't found a winning block yet 380 | // we will want to extend private forks with it 381 | // no need to do it if blk is not null since the pforks will get deleted anyways 382 | nullBlocks = append(nullBlocks, blk) 383 | 384 | // we will also want to add this null block to the set of allBlocks we track 385 | // this will allow us to reform full history in case a winning block is 386 | // mined off of the null block 387 | ct.allBlocks[blk.Nonce] = blk 388 | } 389 | } 390 | 391 | // if bestBlock is not null 392 | if bestBlock != nil { 393 | // kill all pforks 394 | m.PrivateForks = make(map[string]*Tipset) 395 | } else { 396 | // extend null block chain 397 | for _, nblk := range nullBlocks { 398 | delete(m.PrivateForks, nblk.Parents.Name) 399 | // add the new null block to our private forks 400 | nullTipset := NewTipset([]*Block{nblk}) 401 | m.PrivateForks[nullTipset.Name] = nullTipset 402 | } 403 | } 404 | return bestBlock 405 | } 406 | 407 | func runSim(totalMiners int, roundNum int, lbp int, c chan *chainTracker) { 408 | seed := randInt(1 << 62) // this is ok because crypto library should return new set each time (vs having to use timestamp to seed) 409 | r := rand.New(rand.NewSource(seed)) 410 | 411 | miners := make([]*RationalMiner, totalMiners) 412 | chainTracker := NewChainTracker(miners) 413 | gen := makeGen(lbp, totalMiners) 414 | chainTracker.head = NewTipset([]*Block{gen}) 415 | 416 | for m := 0; m < totalMiners; m++ { 417 | miners[m] = NewRationalMiner(m, 1.0/float64(totalMiners), totalMiners, r) 418 | } 419 | 420 | blocks := []*Block{gen} 421 | // Throughout we represent chains (or forks) as arrays of arrays of Tipsets. 422 | // Tipsets are possible sets of blocks to mine of off in a given round. 423 | // Arrays of tipsets represent the multiple choices a miner has in a given 424 | // round for a given chain. 425 | // Arrays of arrays of tipsets represent each chain/fork. 426 | atsforks := make([][]*Tipset, 0, 50) 427 | var currentHeight int 428 | for round := 0; round < roundNum; round++ { 429 | // Update heaviest chain 430 | chainTracker.setHead(blocks) 431 | 432 | // Cache live blocks for future stats 433 | for _, blk := range blocks { 434 | chainTracker.allBlocks[blk.Nonce] = blk 435 | } 436 | 437 | // checking an assumption 438 | if len(blocks) > 0 { 439 | currentHeight = blocks[0].Height 440 | // add new blocks if we have any! 441 | chainTracker.liveBlocksByHeight[currentHeight] = blocks 442 | } 443 | for _, blk := range blocks { 444 | if currentHeight != blk.Height { 445 | panic("Check your assumptions: all block heights from a round are not equal") 446 | } 447 | } 448 | 449 | printSingle(fmt.Sprintf("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n")) 450 | printSingle(fmt.Sprintf("Round %d -- %d new blocks\n", round, len(blocks))) 451 | for _, blk := range blocks { 452 | printSingle(fmt.Sprintf("b%d (m%d)\t", blk.Nonce, blk.Owner)) 453 | } 454 | printSingle(fmt.Sprintf("\n")) 455 | printSingle(fmt.Sprintf("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n")) 456 | var newBlocks = []*Block{} 457 | 458 | ats := allTipsets(blocks) 459 | // declaring atsforks outside of loop and reusing it for better mem mgmt 460 | atsforks = atsforks[:0] 461 | // map to array 462 | for _, v := range ats { 463 | atsforks = append(atsforks, forksFromTipset(v)) 464 | } 465 | 466 | for _, m := range miners { 467 | // Each miner mines 468 | blk := m.Mine(chainTracker, atsforks, lbp) 469 | if blk != nil { 470 | newBlocks = append(newBlocks, blk) 471 | } 472 | } 473 | // NewBlocks added to network 474 | printSingle(fmt.Sprintf("\n")) 475 | blocks = newBlocks 476 | } 477 | // height is 0 indexed 478 | chainTracker.maxHeight = roundNum - 1 479 | c <- chainTracker 480 | } 481 | 482 | //**** IO 483 | 484 | // writeChain output a json from which you can rebuild your chain tracker 485 | func writeChain(ct *chainTracker, name string, outputDir string) { 486 | fmt.Printf(fmt.Sprintf("Writing Out %s\n", name)) 487 | 488 | if _, err := os.Stat(outputDir); os.IsNotExist(err) { 489 | err2 := os.MkdirAll(outputDir, 0755) 490 | if err2 != nil { 491 | panic(err2) 492 | } 493 | } 494 | 495 | fil, err := os.Create(fmt.Sprintf("%s/%s.json", outputDir, name)) 496 | if err != nil { 497 | panic(err) 498 | } 499 | defer fil.Close() 500 | 501 | // What do we need? 502 | // 1. Nodes: All blocks, including their details. 503 | // 2. Edges: Included in Node pointer to Tipset 504 | 505 | // open JSON block 506 | fmt.Fprintln(fil, "{") 507 | 508 | blocks := make([]*Block, 0, len(ct.allBlocks)) 509 | for _, value := range ct.allBlocks { 510 | blocks = append(blocks, value) 511 | } 512 | 513 | marshalledBlocks, err := json.MarshalIndent(blocks, "", "\t") 514 | if err != nil { 515 | panic(err) 516 | } 517 | 518 | fmt.Fprintln(fil, "\"blocks\":") 519 | fmt.Fprintln(fil, string(marshalledBlocks)) 520 | fmt.Fprintln(fil, ",") 521 | 522 | // 3. Miners: All minersV 523 | // This should appropriately capture tipsets as well as full tree. 524 | // TODO: some form of checksumming for this data (e.g. some stats about tispets or heads over time) 525 | marshalledMiners, err := json.MarshalIndent(ct.miners, "", "\t") 526 | if err != nil { 527 | panic(err) 528 | } 529 | 530 | fmt.Fprintln(fil, "\"miners\":") 531 | fmt.Fprintln(fil, string(marshalledMiners)) 532 | 533 | // close JSON block 534 | fmt.Fprintln(fil, "}") 535 | } 536 | 537 | // drawChain output a dot graph of the entire blockchain generated by the simulation 538 | func drawChain(ct *chainTracker, name string, outputDir string) { 539 | fmt.Printf(fmt.Sprintf("Drawing Graph %s\n", name)) 540 | 541 | fil, err := os.Create(fmt.Sprintf("%s/dot/%s.dot", outputDir, name)) 542 | if err != nil { 543 | panic(err) 544 | } 545 | defer fil.Close() 546 | 547 | fmt.Fprintln(fil, "digraph G {") 548 | fmt.Fprintln(fil, "\t{\n\t\tnode [shape=plaintext];") 549 | 550 | // Write out height index alongside the block graph 551 | fmt.Fprintf(fil, "\t\t0") 552 | // Start at 1 because we already wrote out the 0 for the .dot file 553 | for cur := int(1); cur <= ct.maxHeight+1; cur++ { 554 | fmt.Fprintf(fil, " -> %d", cur) 555 | } 556 | fmt.Fprintln(fil, ";") 557 | fmt.Fprintln(fil, "\t}") 558 | 559 | fmt.Fprintln(fil, "\tnode [shape=box];") 560 | // Write out the actual blocks 561 | for cur := ct.maxHeight; cur >= 0; cur-- { 562 | // get blocks per height 563 | blocks, ok := ct.liveBlocksByHeight[cur] 564 | 565 | if cur == 0 { 566 | fmt.Printf(fmt.Sprintf("at height 0, blocks: %d", len(blocks))) 567 | } 568 | 569 | // if no blocks at height, skip 570 | if !ok { 571 | continue 572 | } 573 | 574 | // for every block at this height 575 | fmt.Fprintf(fil, "\t{ rank = same; %d;", cur) 576 | 577 | for _, block := range blocks { 578 | // print block 579 | if block.InHead { 580 | fmt.Fprintf(fil, " \"b%d (m%d)\" [color=\"red\", style=\"bold\"];", block.Nonce, block.Owner) 581 | } else { 582 | fmt.Fprintf(fil, " \"b%d (m%d)\";", block.Nonce, block.Owner) 583 | } 584 | } 585 | fmt.Fprintln(fil, " }") 586 | 587 | // link to parents 588 | for _, block := range blocks { 589 | // genesis has no parents 590 | if block.Owner == -1 { 591 | continue 592 | } 593 | for _, parent := range block.liveParents().Blocks { 594 | fmt.Fprintf(fil, "\t\"b%d (m%d)\" -> \"b%d (m%d)\";\n", block.Nonce, block.Owner, parent.Nonce, parent.Owner) 595 | } 596 | } 597 | } 598 | 599 | fmt.Fprintln(fil, "}\n") 600 | } 601 | 602 | func main() { 603 | fQuiet := flag.Bool("quiet", false, "will prevent .dot file creation") 604 | fLbp := flag.Int("lbp", 1, "sim lookback") 605 | fRoundNum := flag.Int("rounds", 100, "number of rounds to sim") 606 | fTotalMiners := flag.Int("miners", 10, "number of miners to sim") 607 | fNumTrials := flag.Int("trials", 1, "number of trials to run") 608 | fOutput := flag.String("output", ".", "output folder") 609 | 610 | flag.Parse() 611 | quiet := *fQuiet 612 | lbp := *fLbp 613 | roundNum := *fRoundNum 614 | totalMiners := *fTotalMiners 615 | trials := *fNumTrials 616 | outputDir := *fOutput 617 | 618 | if trials <= 0 { 619 | panic("None of your assumptions have been proven wrong") 620 | } 621 | 622 | if *cpuprofile != "" { 623 | f, err := os.Create(*cpuprofile) 624 | if err != nil { 625 | panic(err) 626 | } 627 | pprof.StartCPUProfile(f) 628 | defer pprof.StopCPUProfile() 629 | } 630 | 631 | suite = trials > 1 || quiet 632 | 633 | var cts []*chainTracker 634 | c := make(chan *chainTracker, trials) 635 | for n := 0; n < trials; n++ { 636 | fmt.Printf("Trial %d\n", n) 637 | fmt.Printf("-*-*-*-*-*-*-*-*-*-*-\n") 638 | go runSim(totalMiners, roundNum, lbp, c) 639 | } 640 | for result := range c { 641 | cts = append(cts, result) 642 | if len(cts) == trials { 643 | close(c) 644 | } 645 | chainName := fmt.Sprintf("rds=%d-lbp=%d-mins=%d-ts=%d-%d", roundNum, lbp, totalMiners, time.Now().Unix(), len(cts)) 646 | 647 | // create output folder if it doesn't exist 648 | if _, err := os.Stat(outputDir); os.IsNotExist(err) { 649 | os.Mkdir(outputDir, 0700) 650 | } 651 | 652 | // capture chain for future use 653 | writeChain(result, chainName, outputDir) 654 | 655 | // if single trial, draw output 656 | if !suite { 657 | drawChain(result, chainName, outputDir) 658 | } 659 | } 660 | } 661 | --------------------------------------------------------------------------------