diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57feb1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,152 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +# .env +.env/ +.venv/ +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# operating system-related files +*.DS_Store #file properties cache/storage on macOS +Thumbs.db #thumbnail cache on Windows + +# profiling data +.prof + + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/Event.pyc b/Event.pyc deleted file mode 100644 index 3e705a5..0000000 Binary files a/Event.pyc and /dev/null differ diff --git a/InputsConfig.py b/InputsConfig.py index f07205b..a5e224f 100644 --- a/InputsConfig.py +++ b/InputsConfig.py @@ -1,3 +1,6 @@ +import random +from Models.Bitcoin.Pool import Pool +from Models.Bitcoin.Node import Node class InputsConfig: @@ -5,9 +8,9 @@ class InputsConfig: 0 : The base model 1 : Bitcoin model 2 : Ethereum model - 3 : AppendableBlock model + 3 : AppendableBlock model """ - model = 3 + model = 1 ''' Input configurations for the base model ''' if model == 0: @@ -44,7 +47,9 @@ class InputsConfig: Binterval = 600 # Average time (in seconds)for creating a block in the blockchain Bsize = 1.0 # The block size in MB Bdelay = 0.42 # average block propogation delay in seconds, #Ref: https://bitslog.wordpress.com/2016/04/28/uncle-mining-an-ethereum-consensus-protocol-flaw/ - Breward = 12.5 # Reward for mining a block + Breward = 6.25 # Reward for mining a block + Bprice = 58000 + jump_threshold = 0.02 ''' Transaction Parameters ''' hasTrans = True # True/False to enable/disable transactions in the simulator @@ -52,20 +57,268 @@ class InputsConfig: Tn = 10 # The rate of the number of transactions to be created per second # The average transaction propagation delay in seconds (Only if Full technique is used) Tdelay = 5.1 - Tfee = 0.000062 # The average transaction fee + Tfee = 0.00029 # The average transaction fee Tsize = 0.000546 # The average transaction size in MB ''' Node Parameters ''' Nn = 3 # the total number of nodes in the network - NODES = [] - from Models.Bitcoin.Node import Node - # here as an example we define three nodes by assigning a unique id for each one + % of hash (computing) power - NODES = [Node(id=0, hashPower=50), Node( - id=1, hashPower=20), Node(id=2, hashPower=30)] + + pool_types = { + 'F2Pool': ('PPS+', 2), + 'Poolin': ('PPS+', 2), + 'BTC.com': ('FPPS', 2), + 'AntPool': ('PPLNS', 2), + 'Huobi': ('PPLNS', 1), + 'Binance Pool': ('PPS', 3), + 'ViaBTC': ('PPS', 4), + '1THash': ('FPPS', 4), + # 'OKExPool':, + # 'SlushPool': '', + # 'BTC Guild': 'PPLNS', + # 'GHash.IO':, + # 'BitFury':, + # 'BTCC': 'PPS' + } ''' Simulation Parameters ''' - simTime = 10000 # the simulation length (in seconds) - Runs = 2 # Number of simulation runs + simTime = 1 * 24 * 60 * 60 # the simulation length (in seconds) + Runs = 1 # Number of simulation runs + + # choose which sim to run + sim_type = 'honest' + # sim_type = 'hopping' + + i = 0 # counter to track pool objects + j = 0 # counter to track node objects + NODES = [] # list of node objects + POOLS = [] # list of pool objects + + # function to assign nodes with decreasing hash power to pools + def create_nodes(node_id, pool, hash_power, NODES=NODES): + n = random.randint(7, 10) + for _ in range(n): + hash_power /= 2 + NODES.append(Node(id=node_id, pool=pool, hashPower=hash_power)) + node_id += 1 + return node_id + + # function to create miner nodes for the selfish sim + def create_selfish_nodes(node_id, pool, hash_power, NODES=NODES): + n = random.randint(7, 10) + for _ in range(n): + hash_power /= 2 + r = random.random() + # each hopping strategy (honest, best, strategy-based, random) has a 25% chance of being adopted by a node + if r < 0.25: + NODES.append(Node(id=node_id, pool=pool, hashPower=hash_power, node_type='selfish', node_strategy='best')) + elif r < 0.5: + NODES.append(Node(id=node_id, pool=pool, hashPower=hash_power, node_type='selfish', node_strategy='strategy based')) + elif r < 0.75: + NODES.append(Node(id=node_id, pool=pool, hashPower=hash_power, node_type='selfish', node_strategy='random')) + else: + NODES.append(Node(id=node_id, pool=pool, hashPower=hash_power)) + node_id += 1 + return node_id + + # initialising base fee rates + base_rate = {'PPS': 2.5, 'FPPS': 2.5, 'PPS+': 2.5, 'PPLNS': 0} + + if sim_type == 'honest': + hopping = False + # for each kind of strategy instantiate node and pool objects + for pool_type in ['SOLO', 'PPS', 'FPPS', 'PPLNS', 'PPS+']: + + if pool_type == 'SOLO': + hp = 12 + # 10 solo miners with monotonically decreasing hash rates + for _ in range(10): + hp /= 2 + NODES.append(Node(id=j, hashPower=hp)) + j += 1 + else: + rate = base_rate[pool_type] + + # 4 pools for PPS and FPPS strategies each + if pool_type in ['PPS', 'FPPS']: + for f in range(4): + hp = 4 + pool = Pool(_id=i, strategy=pool_type, fee_rate=rate) + POOLS.append(pool) + j = create_nodes(j, pool, hp) + i += 1 + rate += 0.5 + + + else: + for w in range(4): + if rate in [1, 3]: + # for PPS+ and PPLNS pools we vary block window as well + for bw in [6, 8, 10, 12]: + hp = 4 + pool = Pool(_id=i, strategy=pool_type, fee_rate=rate, block_window=bw) + POOLS.append(pool) + j = create_nodes(j, pool, hp) + i += 1 + rate += 0.5 + + else: + hp = 4 + pool = Pool(_id=i, strategy=pool_type, fee_rate=rate, block_window=8) + POOLS.append(pool) + j = create_nodes(j, pool, hp) + i += 1 + rate += 0.5 + + else: + hopping = True + # hopping sim only considers PPS and PPLNS + for pool_type in ['PPS', 'PPLNS']: + + # using the same base fee rates as the honest sim + rate = base_rate[pool_type] + + # create PPS pools of varying fee rate + if pool_type in ['PPS']: + for f in range(4): + hp = 5 + pool = Pool(_id=i, strategy=pool_type, fee_rate=rate) + POOLS.append(pool) + j = create_selfish_nodes(j, pool, hp) + i += 1 + rate += 0.5 + + else: + # in case of PPLNS, we vary the block window as well + for w in range(4): + for bw in [6, 8, 10, 12]: + hp = 5 + pool = Pool(_id=i, strategy=pool_type, fee_rate=rate, block_window=bw) + POOLS.append(pool) + j = create_selfish_nodes(j, pool, hp) + i += 1 + rate += 0.5 + + + # sim_type = 'baseline' + # for pool, (strat, fee) in pool_types.items(): + # if strat in ['PPLNS', 'PPS+']: + # POOLS.append(Pool(_id=pool, strategy=strat, fee_rate=fee, block_window=8)) + # else: + # POOLS.append(Pool(_id=pool, strategy=strat, fee_rate=fee)) + + # for i, pool in enumerate(POOLS): + # NODES.append( + # Node(id=i, pool=pool, hashPower=12.5)) + + # sim_type = 'solo' + + # hp = 100 + # for i in range(8): + # hp /= 2 + # NODES.append(Node(id=i, hashPower=hp)) + + # sim_type = 'pps' + + # for i, name in enumerate(pool_types): + # POOLS.append(Pool(_id=name, strategy='PPS', fee_rate=i)) + + # hp = 100 + # for i, pool in enumerate(POOLS): + # hp /= 2 + # NODES.append(Node(id=i, pool=pool, hashPower=hp)) + + # sim_type = 'fpps' + + # for i, name in enumerate(pool_types): + # POOLS.append(Pool(_id=name, strategy='FPPS', fee_rate=i)) + + # hp = 100 + # for i, pool in enumerate(POOLS): + # hp /= 2 + # NODES.append(Node(id=i, pool=pool, hashPower=hp)) + + # sim_type = 'pplns' + + # for i, name in enumerate(pool_types): + # POOLS.append(Pool(_id=name, strategy='PPLNS', fee_rate=i, block_window=8)) + + # hp = 100 + # for i, pool in enumerate(POOLS): + # hp /= 2 + # NODES.append(Node(id=i, pool=pool, hashPower=hp)) + + # sim_type = 'pplns_windows' + + # for i, name in enumerate(pool_types): + # POOLS.append(Pool(_id=name, strategy='PPLNS', fee_rate=2, block_window=i+2)) + + # hp = 100 + # for i, pool in enumerate(POOLS): + # hp /= 2 + # NODES.append(Node(id=i, pool=pool, hashPower=hp)) + + # sim_type = 'pps+' + + # for i, name in enumerate(pool_types): + # POOLS.append(Pool(_id=name, strategy='PPS+', fee_rate=i, block_window=8)) + + # hp = 100 + # for i, pool in enumerate(POOLS): + # hp /= 2 + # NODES.append(Node(id=i, pool=pool, hashPower=hp)) + + # sim_type = 'pps+_windows' + + # for i, name in enumerate(pool_types): + # POOLS.append(Pool(_id=name, strategy='PPS+', fee_rate=3, block_window=i+2)) + + # hp = 100 + # for i, pool in enumerate(POOLS): + # hp /= 2 + # NODES.append(Node(id=i, pool=pool, hashPower=hp)) + + + # Creating the mining pools + # POOLS = [ + # Pool(_id=0, strategy='PPS', fee_rate=3), + # Pool(_id=1, strategy='FPPS', fee_rate=3), + # Pool(_id=2, strategy='PPS+', fee_rate=3, block_window=8), + # Pool(_id=3, strategy='PPLNS', fee_rate=1, block_window=8), + # Pool(_id=4, strategy='PPLNS', fee_rate=2, block_window=6), + # Pool(_id=5, strategy='PPS', fee_rate=3), + # Pool(_id=6, strategy='FPPS', fee_rate=4), + # Pool(_id=7, strategy='PPS+', fee_rate=1, block_window=10), + # Pool(_id=8, strategy='PPS', fee_rate=3), + # ] + + # # here as an example we define three nodes by assigning a unique id for each one + % of hash (computing) power + # NODES = [ + # Node(id=0, pool=POOLS[0], hashPower=7), + # Node(id=1, pool=POOLS[1], hashPower=5), + # Node(id=2, pool=POOLS[2], hashPower=5), + # Node(id=3, pool=POOLS[6], hashPower=8), + # Node(id=4, pool=POOLS[7], hashPower=5), + # Node(id=5, pool=POOLS[4], hashPower=8), + # Node(id=6, pool=POOLS[4], hashPower=5), + # Node(id=7, pool=POOLS[3], hashPower=5), + # Node(id=8, pool=POOLS[6], hashPower=5), + # Node(id=9, pool=POOLS[0], hashPower=7, node_type='selfish', node_strategy='strategy_based'), + # Node(id=10, pool=POOLS[3], hashPower=6, node_type='selfish', node_strategy='strategy_based'), + # Node(id=11, pool=POOLS[4], hashPower=8, node_type='selfish', node_strategy='strategy_based'), + # Node(id=12, pool=POOLS[5], hashPower=5, node_type='selfish', node_strategy='strategy_based'), + # Node(id=13, pool=POOLS[7], hashPower=2), + # Node(id=14, pool=POOLS[1], hashPower=3), + # Node(id=15, hashPower=1), + # Node(id=16, hashPower=1), + # Node(id=17, hashPower=1), + # Node(id=18, hashPower=2), + # Node(id=19, pool=POOLS[5], hashPower=2), + # Node(id=20, pool=POOLS[5], hashPower=2, node_type='selfish', node_strategy='strategy_based'), + # Node(id=21, hashPower=1), + # Node(id=22, pool=POOLS[8], hashPower=3, node_type='selfish', node_strategy='strategy_based'), + # Node(id=23, pool=POOLS[8], hashPower=2), + # Node(id=24, pool=POOLS[8], hashPower=1), + # ] ''' Input configurations for Ethereum model ''' if model == 2: @@ -107,7 +360,7 @@ class InputsConfig: simTime = 500 # the simulation length (in seconds) Runs = 2 # Number of simulation runs - ''' Input configurations for AppendableBlock model ''' + ''' Input configurations for AppendableBlock model ''' if model == 3: ''' Transaction Parameters ''' hasTrans = True # True/False to enable/disable transactions in the simulator diff --git a/InputsConfig.pyc b/InputsConfig.pyc deleted file mode 100644 index 69f2abf..0000000 Binary files a/InputsConfig.pyc and /dev/null differ diff --git a/Main.py b/Main.py index 221e9bb..da4d669 100644 --- a/Main.py +++ b/Main.py @@ -1,3 +1,4 @@ +from datetime import datetime from InputsConfig import InputsConfig as p from Event import Event, Queue from Scheduler import Scheduler @@ -24,6 +25,7 @@ from Models.Bitcoin.Consensus import Consensus from Models.Transaction import LightTransaction as LT, FullTransaction as FT from Models.Bitcoin.Node import Node + from Models.Bitcoin.Pool import Pool from Models.Incentives import Incentives elif p.model == 0: @@ -38,6 +40,27 @@ def main(): for i in range(p.Runs): + print('-'*10, f'Run: {i+1}', '-'*10) + print(p.sim_type) + print('No. of Miners:', len(p.NODES)) + + hash_power = 0 + # Giving every pool a reference to the nodes it contains. Also, update the total hashrate of a pool. + print('SOLO Nodes: ', end='') + for node in p.NODES: + hash_power += node.hashPower + if node.pool: + node.pool.nodes.append(node) + node.pool.hash_power += node.hashPower + else: + print(node.id, end=', ') + print() + + print('Pools:') + for pool in p.POOLS: + print(' -', pool.id, pool.strategy, 'Fee Rate:', pool.fee_rate, 'Nodes:', [node.id for node in pool.nodes], 'Hash power:', pool.hash_power) + print('Total hash power:', hash_power, '\n') + clock = 0 # set clock to 0 at the start of the simulation if p.hasTrans: if p.Ttechnique == "Light": @@ -67,7 +90,7 @@ def main(): # distribute the rewards between the particiapting nodes Incentives.distribute_rewards() # calculate the simulation results (e.g., block statstics and miners' rewards) - Statistics.calculate() + Statistics.calculate(i) if p.model == 3: Statistics.print_to_excel(i, True) @@ -76,17 +99,16 @@ def main(): ########## reset all global variable before the next run ############# Statistics.reset() # reset all variables used to calculate the results Node.resetState() # reset all the states (blockchains) for all nodes in the network - fname = "(Allverify)1day_{0}M_{1}K.xlsx".format( - p.Bsize/1000000, p.Tn/1000) - # print all the simulation results in an excel file - Statistics.print_to_excel(fname) - fname = "(Allverify)1day_{0}M_{1}K.xlsx".format( - p.Bsize/1000000, p.Tn/1000) - # print all the simulation results in an excel file - Statistics.print_to_excel(fname) - Statistics.reset2() # reset profit results + Pool.resetState() # reset all pools in the network + # set file name for results + fname = f"{p.sim_type}_{int(p.simTime/(24*60*60))}days_{datetime.now()}.xlsx".replace(':', '_') + # fname = f"(Allverify)1day_{p.Bsize/1000000}M_{p.Tn/1000}K-{i}-{datetime.now()}.xlsx".replace(':', '_') + # print all the simulation results in an excel file + Statistics.print_to_excel(fname) + # Statistics.reset2() # reset profit results ######################################################## Run Main method ##################################################################### + if __name__ == '__main__': main() diff --git a/Models/Bitcoin/BlockCommit.py b/Models/Bitcoin/BlockCommit.py index 6b4329f..bc76459 100644 --- a/Models/Bitcoin/BlockCommit.py +++ b/Models/Bitcoin/BlockCommit.py @@ -30,7 +30,9 @@ def generate_block (event): elif p.Ttechnique == "Full": blockTrans,blockSize = FT.execute_transactions(miner,eventTime) event.block.transactions = blockTrans - event.block.usedgas= blockSize + for tx in event.block.transactions: + event.block.fee += tx.fee + event.block.usedgas= blockSize # FIXME miner.blockchain.append(event.block) diff --git a/Models/Bitcoin/Node.py b/Models/Bitcoin/Node.py index 36a3d28..1ed324f 100644 --- a/Models/Bitcoin/Node.py +++ b/Models/Bitcoin/Node.py @@ -1,13 +1,41 @@ from Models.Block import Block from Models.Node import Node as BaseNode + class Node(BaseNode): - def __init__(self,id,hashPower): + def __init__(self, id, hashPower, pool=None, join_time=0, node_type='honest', node_strategy=None): '''Initialize a new miner named name with hashrate measured in hashes per second.''' - super().__init__(id)#,blockchain,transactionsPool,blocks,balance) + super().__init__(id) # blockchain, transactionsPool, blocks, balance self.hashPower = hashPower - self.blockchain= []# create an array for each miner to store chain state locally - self.transactionsPool= [] - self.blocks= 0# total number of blocks mined in the main chain - self.balance= 0# to count all reward that a miner made, including block rewards + uncle rewards + transactions fees - + self.pool = pool + self.node_type = node_type # honest or selfish + self.node_strategy = node_strategy # best, strategy_based, random + + # initialise attributes to track pool related information + if self.pool: + self.join_time = join_time + self.original_pool = self.pool + # the following lists are used to maintain a record of + # multiple pools when pool hopping occurs + self.pool_list = [pool.id] + self.blocks_list = [0] + self.reward_list = [0] + self.balance_list = [0] + + def resetState(): + from InputsConfig import InputsConfig as p + + for node in p.NODES: + node.blockchain = [] # reset array for each miner to store chain state locally + node.transactionsPool = [] # reset transaction pool + node.blocks = 0 # reset total number of blocks mined in the main chain + node.fee = 0 # reset total transaction fee recieved from mined block + node.balance = 0 # reset miner reward + + # reset all pool related information + if node.pool: + node.pool = node.original_pool + node.pool_list = [node.original_pool.id] + node.blocks_list = [0] + node.reward_list = [0] + node.balance_list = [0] diff --git a/Models/Bitcoin/Node.pyc b/Models/Bitcoin/Node.pyc deleted file mode 100644 index 99fced9..0000000 Binary files a/Models/Bitcoin/Node.pyc and /dev/null differ diff --git a/Models/Bitcoin/Pool.py b/Models/Bitcoin/Pool.py new file mode 100644 index 0000000..5562073 --- /dev/null +++ b/Models/Bitcoin/Pool.py @@ -0,0 +1,23 @@ +class Pool(): + + def __init__(self, _id, strategy, fee_rate, block_window=None): + # initialise pool parameters + self.id = _id + self.strategy = strategy # PPS, PPLNS, PPS+, FPPS + self.fee_rate = fee_rate + self.nodes = [] # nodes currently minnig in the pool + self.hash_power = 0 # % of hash power controlled by pool + self.blocks = 0 # number of blocks mined by miners in pool + self.block_fee = 0 # total transaction fee kept by pool + self.balance = 0 # pool balance + self.block_window = block_window # block window in case of PPLNS and PPS+ pools + + def resetState(): + from InputsConfig import InputsConfig as p + # reset pool attribures after each run + for pool in p.POOLS: + pool.blocks = 0 # total number of blocks mined in the main chain + pool.block_fee = 0 # total transaction fee recieved from mined block + pool.balance = 0 # to count all reward that a miner made + pool.nodes = [] + pool.hash_power = 0 diff --git a/Models/Block.py b/Models/Block.py index 84254eb..c62089a 100644 --- a/Models/Block.py +++ b/Models/Block.py @@ -1,5 +1,5 @@ class Block(object): - + """ Defines the base Block model. :param int depth: the index of the block in the local blockchain ledger (0 for genesis block) @@ -27,3 +27,4 @@ def __init__(self, self.miner = miner self.transactions = transactions or [] self.size = size + self.fee = 0 diff --git a/Models/Ethereum/Block.pyc b/Models/Ethereum/Block.pyc deleted file mode 100644 index 5d62487..0000000 Binary files a/Models/Ethereum/Block.pyc and /dev/null differ diff --git a/Models/Ethereum/Node.pyc b/Models/Ethereum/Node.pyc deleted file mode 100644 index 99fced9..0000000 Binary files a/Models/Ethereum/Node.pyc and /dev/null differ diff --git a/Models/Incentives.py b/Models/Incentives.py index 287eb09..1025ec5 100644 --- a/Models/Incentives.py +++ b/Models/Incentives.py @@ -1,23 +1,231 @@ +import random from InputsConfig import InputsConfig as p from Models.Consensus import Consensus as c + class Incentives: """ - Defines the rewarded elements (block + transactions), calculate and distribute the rewards among the participating nodes + Defines the rewarded elements (block + transactions), calculate and distribute the rewards among the participating nodes """ def distribute_rewards(): - for bc in c.global_chain: + + total_transactions = 0 + + for i, bc in enumerate(c.global_chain[1:]): + + current_time = bc.timestamp + + # save node miner object + miner = p.NODES[bc.miner] + # increment miner block count + miner.blocks += 1 + + # in case miner belongs to a pool, update pool attributes + if miner.pool: + miner_pool = miner.pool + miner_pool.blocks += 1 + miner.blocks_list[-1] += 1 + # pool gets block reward when a block is found + miner_pool.balance += p.Breward + + + windows = {} + cumulative_shares = {} + # calculating sum of shares for PPLNS and PPS+ pools since block window or the join time + for pool in p.POOLS: + if pool.strategy not in ['PPLNS', 'PPS+']: + continue + + N = pool.block_window + # extract timestamp of earliest block within window + if i > N: + window_timestamp = c.global_chain[i-N-1].timestamp + else: + window_timestamp = 0 + + shares = 0 + for pool_node in pool.nodes: + # select the latest timestamp from the pool join time and block windows + latest_window_time = max(pool_node.join_time, window_timestamp) + # calculate shares contributed since the latest time + shares += (current_time - latest_window_time) * pool_node.hashPower + + # save window timestamp and total shares for later use + windows[pool] = window_timestamp + cumulative_shares[pool] = shares + + for m in p.NODES: - if bc.miner == m.id: - m.blocks +=1 - m.balance += p.Breward # increase the miner balance by the block reward - tx_fee= Incentives.transactions_fee(bc) - m.balance += tx_fee # add transaction fees to balance - - - def transactions_fee(bc): - fee=0 - for tx in bc.transactions: - fee += tx.fee - return fee + + # for solo miners, pay miner directly in case block found + if not m.pool: + if miner == m: + m.balance += p.Breward # increase the miner balance by the block reward + m.fee += bc.fee + m.balance += bc.fee # add transaction fees to balance + + + elif m.pool.strategy == 'PPS': + + reward = m.hashPower/100 * p.Breward + # miners get a constant payout after deducting pool fee + reward = (100 - m.pool.fee_rate)/100 * reward + # decrease pool balance and increase miner balance + m.pool.balance -= reward + m.balance += reward + m.balance_list[-1] += reward + m.reward_list[-1] += reward + + if miner == m: + # pool keeps transaction fee + miner_pool.block_fee += bc.fee + miner_pool.balance += bc.fee + + + elif m.pool.strategy == 'FPPS': + + reward = m.hashPower/100 * p.Breward + # miners get a constant payout after deducting pool fee + reward = (100 - m.pool.fee_rate)/100 * reward + # decrease pool balance and increase miner balance + m.pool.balance -= reward + m.balance += reward + + # expected transaction fee also paid out at each block + fee = m.hashPower/100 * bc.fee + m.pool.balance -= fee + m.fee += fee + m.balance += fee + + if miner == m: + # pool gets transaction fee in case block found + miner_pool.block_fee += bc.fee + miner_pool.balance += bc.fee + + + elif m.pool.strategy == 'PPLNS': + + # payout only occurs in case pool finds the block + if miner == m: + # reward to be distributed after deducting pool fee + reward = (100 - miner_pool.fee_rate)/100 * p.Breward + miner_pool.balance -= reward + + for node in miner_pool.nodes: + # once again, calculate the latest time to be considered calculating miner shares + latest_window_time = max(node.join_time, windows[miner_pool]) + # calculate miner shares as a fraction of total shares + frac = (current_time - latest_window_time) * node.hashPower/cumulative_shares[miner_pool] + # transaction fee and reward distributed as per fraction of time and hashpower spent + node.balance += frac * reward + node.fee += frac * bc.fee + node.balance += frac * bc.fee + + node.reward_list[-1] + frac * reward + node.balance_list[-1] += frac * (bc.fee + reward) + + + # elif m.pool.strategy == 'Proportional': + + # if bc.miner == m.id: + # m.blocks += 1 + # m.pool.blocks += 1 + # # deducting pool fee + # reward = (100 - m.pool.fee_rate)/100 * p.Breward + # m.pool.balance += m.pool.fee_rate/100 * p.Breward + + # # all nodes share block reward and transaction fee + # for node in m.pool.nodes: + # node.balance += node.hashPower/m.pool.hash_power * reward + # node_fee = node.hashPower/m.pool.hash_power * bc.fee + # node.fee += node_fee + # node.balance += node_fee + + + elif m.pool.strategy == 'PPS+': + + reward = m.hashPower/100 * p.Breward + # miner gets a constant payout of block reward after deducting pool fee (PPS) + reward = (100 - m.pool.fee_rate)/100 * reward + m.pool.balance -= reward + m.balance += reward + + if miner == m: + + for node in miner_pool.nodes: + # calculate the latest time to be considered calculating miner shares + latest_window_time = max(node.join_time, windows[miner_pool]) + # calculate miner shares as a fraction of total shares + frac = (current_time - latest_window_time) * node.hashPower/cumulative_shares[miner_pool] + # transaction fee shared by PPLNS method as per fraction of time and hashpower + node.fee += frac * bc.fee + node.balance += frac * bc.fee + + + # increment total transactions and calculate average transactions per block, S + total_transactions += len(bc.transactions) + S = round(total_transactions/(i+1), 2) + + avg_payout = 0 + pool_payout = {} + # calculate expected payout for each pool + for pool in p.POOLS: + # print('pool', pool.id, [node.id for node in pool.nodes]) + if pool.nodes and pool.strategy in ['PPS', 'PPLNS']: + mu = ((100 - pool.fee_rate)/100) * ((p.Breward + p.Tfee * S)/len(pool.nodes)) * pool.hash_power/100 + pool_payout[pool] = mu + avg_payout += mu + + if len(pool_payout) == 0: + continue + + avg_payout /= len(pool_payout) + # print([(pool.id, pool.strategy, pay) for pool, pay in pool_payout.items()]) + + # implementation of pool hopping + for node in p.NODES: + if node.node_type == 'selfish' and node.pool.strategy in ['PPS', 'PPLNS']: + + mu = pool_payout[node.pool] + + # if current payout is lesser than average payout over all pools then the difference + # between average and current pool payouts decides probability of pool hopping + if mu < avg_payout and random.random() < (avg_payout - mu)/avg_payout: + # print(node.id, ':', node.pool.id, end=' ') + + # remove miner and hash power from current pool + node.pool.hash_power -= node.hashPower + node.pool.nodes.remove(node) + + # implement node hopping strategies + if node.node_strategy == 'best': + node.pool = max(pool_payout, key=pool_payout.get) + + # elif node.node_strategy == 'best by strategy': + # strategy_pools = [pool for pool in sorted(pool_payout, key=pool_payout.get) if pool.strategy == node.pool.strategy and pool != node.pool] + # node.pool = strategy_pools[0] + + elif node.node_strategy == "strategy based": + strategy_pools = [pool for pool in p.POOLS if pool.strategy == node.pool.strategy and pool != node.pool] + choosenPool = random.randint(0, len(strategy_pools) - 1) + node.pool = strategy_pools[choosenPool] + + elif node.node_strategy == "random": + strategy_pools = [pool for pool in p.POOLS if pool.strategy in ['PPS', 'PPLNS'] and pool != node.pool] + choosenPool = random.randint(0, len(strategy_pools) - 1) + node.pool = strategy_pools[choosenPool] + + # TODO improve time assignment + # c.global_chain[i-1].timestamp + 0.432 * random.expovariate(hashPower * 1/p.Binterval) + + # update node join time and add miner to new pool + node.join_time = current_time + node.pool.hash_power += node.hashPower + node.pool.nodes.append(node) + node.pool_list.append(node.pool.id) + # print('--->', node.pool.id, [n.id for n in node.pool.nodes]) + # add 0 to all pool tracker to begin tracking new pool counts + node.blocks_list.append(0) + node.balance_list.append(0) + node.reward_list.append(0) diff --git a/Models/Network.pyc b/Models/Network.pyc deleted file mode 100644 index 3e0572a..0000000 Binary files a/Models/Network.pyc and /dev/null differ diff --git a/Models/Node.py b/Models/Node.py index cba18af..23f0e5e 100644 --- a/Models/Node.py +++ b/Models/Node.py @@ -10,11 +10,12 @@ class Node(object): :param int blocks: the total number of blocks mined in the main chain :param int balance: the amount of cryptocurrencies a node has """ - def __init__(self,id): + def __init__(self, id): self.id= id self.blockchain= [] self.transactionsPool= [] self.blocks= 0# + self.fee = 0 self.balance= 0 # Generate the Genesis block and append it to the local blockchain for all nodes @@ -31,11 +32,12 @@ def last_block(self): def blockchain_length(self): return len(self.blockchain)-1 - # reset the state of blockchains for all nodes in the network (before starting the next run) + # reset the state of blockchains for all nodes in the network (before starting the next run) def resetState(): from InputsConfig import InputsConfig as p for node in p.NODES: node.blockchain= [] # create an array for each miner to store chain state locally node.transactionsPool= [] node.blocks=0 # total number of blocks mined in the main chain + node.fee = 0 # total transaction fee recieved from mined block node.balance= 0 # to count all reward that a miner made diff --git a/Models/Node.pyc b/Models/Node.pyc deleted file mode 100644 index 99fced9..0000000 Binary files a/Models/Node.pyc and /dev/null differ diff --git a/Scheduler.py b/Scheduler.py index 5d05db2..13823f0 100644 --- a/Scheduler.py +++ b/Scheduler.py @@ -1,8 +1,6 @@ from InputsConfig import InputsConfig as p import random -from Models.Block import Block from Event import Event, Queue - if p.model == 2: from Models.Ethereum.Block import Block elif p.model == 3: diff --git a/Statistics.py b/Statistics.py index 5a42fc0..1a51ca8 100644 --- a/Statistics.py +++ b/Statistics.py @@ -16,17 +16,18 @@ class Statistics: staleRate=0 blockData=[] blocksResults=[] - profits= [[0 for x in range(7)] for y in range(p.Runs * len(p.NODES))] # rows number of miners * number of runs, columns =7 - index=0 + profits = [[] for y in range(p.Runs * len(p.NODES))] # number of miners * number of runs + pool_profits = [] chain=[] - def calculate(): - Statistics.global_chain() # print the global chain - Statistics.blocks_results() # calcuate and print block statistics e.g., # of accepted blocks and stale rate etc - Statistics.profit_results() # calculate and distribute the revenue or reward for miners + def calculate(run_id): + Statistics.global_chain(run_id) # print the global chain + Statistics.blocks_results(run_id) # calcuate and print block statistics e.g., # of accepted blocks and stale rate etc + Statistics.profit_results(run_id) # calculate and distribute the revenue or reward for miners + Statistics.pool_results(run_id) ########################################################### Calculate block statistics Results ########################################################################################### - def blocks_results(): + def blocks_results(run_id): trans = 0 Statistics.mainBlocks= len(c.global_chain)-1 @@ -38,36 +39,57 @@ def blocks_results(): Statistics.staleRate= round(Statistics.staleBlocks/Statistics.totalBlocks * 100, 2) if p.model==2: Statistics.uncleRate= round(Statistics.uncleBlocks/Statistics.totalBlocks * 100, 2) else: Statistics.uncleRate==0 - Statistics.blockData = [ Statistics.totalBlocks, Statistics.mainBlocks, Statistics.uncleBlocks, Statistics.uncleRate, Statistics.staleBlocks, Statistics.staleRate, trans] + Statistics.blockData = [run_id, Statistics.totalBlocks, Statistics.mainBlocks, Statistics.uncleBlocks, Statistics.uncleRate, Statistics.staleBlocks, Statistics.staleRate, trans] Statistics.blocksResults+=[Statistics.blockData] ########################################################### Calculate and distibute rewards among the miners ########################################################################################### - def profit_results(): + def profit_results(run_id): for m in p.NODES: - i = Statistics.index + m.id * p.Runs - Statistics.profits[i][0]= m.id - if p.model== 0: Statistics.profits[i][1]= "NA" - else: Statistics.profits[i][1]= m.hashPower - Statistics.profits[i][2]= m.blocks - Statistics.profits[i][3]= round(m.blocks/Statistics.mainBlocks * 100,2) + i = run_id * len(p.NODES) + m.id + Statistics.profits[i] = [run_id, m.id, m.node_type] + if p.hopping: + if m.pool: + Statistics.profits[i] += [m.node_strategy, m.pool_list, m.blocks_list, m.reward_list, m.balance_list] + else: + Statistics.profits[i] += [m.node_strategy, None, None] + else: + if m.pool: + Statistics.profits[i] += [m.pool.id, m.pool.strategy, m.pool.fee_rate] + else: + Statistics.profits[i] += [None, 'SOLO', None] + if p.model== 0: + Statistics.profits[i].append("NA") + else: + Statistics.profits[i].append(m.hashPower) + Statistics.profits[i].append(m.blocks) + Statistics.profits[i].append(round(m.blocks/Statistics.mainBlocks * 100, 2)) if p.model==2: - Statistics.profits[i][4]= m.uncles - Statistics.profits[i][5]= round((m.blocks + m.uncles)/(Statistics.mainBlocks + Statistics.totalUncles) * 100,2) - else: Statistics.profits[i][4]=0; Statistics.profits[i][5]=0 - Statistics.profits[i][6]= m.balance + Statistics.profits[i].append(m.uncles) + Statistics.profits[i].append(round((m.blocks + m.uncles)/(Statistics.mainBlocks + Statistics.totalUncles) * 100,2)) + else: + Statistics.profits[i].append(0) + Statistics.profits[i].append(0) + Statistics.profits[i].append(m.fee) + Statistics.profits[i].append(m.balance) + Statistics.profits[i].append(m.balance * p.Bprice) + + + def pool_results(run_id): + for pool in p.POOLS: + Statistics.pool_profits.append([run_id, pool.id, pool.strategy, pool.fee_rate, pool.block_window, pool.hash_power, pool.blocks, + round(pool.blocks/Statistics.mainBlocks * 100, 2), pool.block_fee, pool.balance, pool.balance * p.Bprice]) - Statistics.index+=1 ########################################################### prepare the global chain ########################################################################################### - def global_chain(): + def global_chain(run_id): if p.model==0 or p.model==1: for i in c.global_chain: - block= [i.depth, i.id, i.previous, i.timestamp, i.miner, len(i.transactions), i.size] + block= [run_id, i.depth, i.id, i.previous, i.timestamp, i.miner, len(i.transactions), i.fee, i.size] Statistics.chain +=[block] elif p.model==2: for i in c.global_chain: - block= [i.depth, i.id, i.previous, i.timestamp, i.miner, len(i.transactions), i.usedgas, len(i.uncles)] + block= [run_id, i.depth, i.id, i.previous, i.timestamp, i.miner, len(i.transactions), i.fee, i.usedgas, len(i.uncles)] Statistics.chain +=[block] ########################################################### Print simulation results to Excel ########################################################################################### @@ -77,21 +99,32 @@ def print_to_excel(fname): #data = {'Stale Rate': Results.staleRate,'Uncle Rate': Results.uncleRate ,'# Stale Blocks': Results.staleBlocks,'# Total Blocks': Results.totalBlocks, '# Included Blocks': Results.mainBlocks, '# Uncle Blocks': Results.uncleBlocks} df2= pd.DataFrame(Statistics.blocksResults) - df2.columns= ['Total Blocks', 'Main Blocks', 'Uncle blocks', 'Uncle Rate', 'Stale Blocks', 'Stale Rate', '# transactions'] + df2.columns= ['Run ID', 'Total Blocks', 'Main Blocks', 'Uncle blocks', 'Uncle Rate', 'Stale Blocks', 'Stale Rate', '# transactions'] df3 = pd.DataFrame(Statistics.profits) - df3.columns = ['Miner ID', '% Hash Power','# Mined Blocks', '% of main blocks','# Uncle Blocks','% of uncles', 'Profit (in ETH)'] + if p.hopping: + df3.columns = ['Run ID', 'Miner ID', 'Miner Type', 'Hopping Strategy', 'Pool IDs', 'Blocks per pool', 'Reward per pool', 'Balance per pool', '% Hash Power','# Mined Blocks', '% of main blocks', '# Uncle Blocks','% of uncles', 'Transaction Fee', 'Profit (in crypto)', 'Profit in $'] + else: + df3.columns = ['Run ID', 'Miner ID', 'Miner Type', 'Pool Id', 'Pool Strategy', 'Pool Fee', '% Hash Power','# Mined Blocks', '% of main blocks', '# Uncle Blocks','% of uncles', 'Transaction Fee', 'Profit (in crypto)', 'Profit in $'] + + df4 = pd.DataFrame(Statistics.pool_profits) + if len(df4) > 0: + df4.columns = ['Run ID', 'Pool ID', 'Pool Strategy', '% Fee Rate', 'Block Window', '% Hash Power', '# Mined Blocks', '% of main blocks', 'Transaction Fee', 'Profit (in crypto)', 'Profit in $'] + + df5 = pd.DataFrame(Statistics.chain) + if p.model==2: + df5.columns= ['Run ID', 'Block Depth', 'Block ID', 'Previous Block', 'Block Timestamp', 'Miner ID', '# transactions', 'Transaction Fee', 'Block Limit', 'Uncle Blocks'] + else: + df5.columns= ['Run ID', 'Block Depth', 'Block ID', 'Previous Block', 'Block Timestamp', 'Miner ID', '# transactions', 'Transaction Fee', 'Block Size'] + - df4 = pd.DataFrame(Statistics.chain) - #df4.columns= ['Block Depth', 'Block ID', 'Previous Block', 'Block Timestamp', 'Miner ID', '# transactions','Block Size'] - if p.model==2: df4.columns= ['Block Depth', 'Block ID', 'Previous Block', 'Block Timestamp', 'Miner ID', '# transactions','Block Limit', 'Uncle Blocks'] - else: df4.columns= ['Block Depth', 'Block ID', 'Previous Block', 'Block Timestamp', 'Miner ID', '# transactions', 'Block Size'] writer = pd.ExcelWriter(fname, engine='xlsxwriter') - df1.to_excel(writer, sheet_name='InputConfig') - df2.to_excel(writer, sheet_name='SimOutput') - df3.to_excel(writer, sheet_name='Profit') - df4.to_excel(writer,sheet_name='Chain') + df1.to_excel(writer, sheet_name='InputConfig', startcol=-1) + df2.to_excel(writer, sheet_name='SimOutput', startcol=-1) + df3.to_excel(writer, sheet_name='Profit', startcol=-1) + df4.to_excel(writer, sheet_name='Pools', startcol=-1) + df5.to_excel(writer, sheet_name='Chain', startcol=-1) writer.save() @@ -106,8 +139,7 @@ def reset(): Statistics.staleRate=0 Statistics.blockData=[] - def reset2(): - Statistics.blocksResults=[] - Statistics.profits= [[0 for x in range(7)] for y in range(p.Runs * len(p.NODES))] # rows number of miners * number of runs, columns =7 - Statistics.index=0 - Statistics.chain=[] + # def reset2(): + # Statistics.blocksResults=[] + # Statistics.profits= [[] for y in range(p.Runs * len(p.NODES))] # rows number of miners * number of runs, columns =7 + # Statistics.chain=[]