167 lines
6.6 KiB
Python
167 lines
6.6 KiB
Python
|
# The purpose of this script is to create an evolutionary
|
||
|
# model to study the equilibrium effects of Bitcoin Unlimited-style
|
||
|
# "emergent consensus". Note that the model is not yet quite
|
||
|
# complete as it does not take into account the benefits of
|
||
|
# mining "sister blocks" that steal transaction fees, though it
|
||
|
# does give a rough idea of what equilibrium behavior
|
||
|
# among the various miner policy dimensions (block accept size,
|
||
|
# override depth, block creation size) looks like
|
||
|
import random
|
||
|
|
||
|
# Block reward
|
||
|
REWARD = 1000
|
||
|
# Call this function to get a tx with the right fee
|
||
|
TX_FEE_DISTRIBUTION = lambda: (10000 // random.randrange(5, 250)) * 0.01
|
||
|
# TX_FEE_DISTRIBUTION = lambda: 20
|
||
|
# Propagation time
|
||
|
PROPTIME_FACTOR = 1
|
||
|
|
||
|
# List of tuples:
|
||
|
# (default limit, n-block limit, acceptance depth, creation limit)
|
||
|
strategies = []
|
||
|
for i in range(4):
|
||
|
for j in range(4):
|
||
|
strategies.append([2 + i * 2, 100, 3, 10 + j * 4])
|
||
|
|
||
|
class Block():
|
||
|
def __init__(self, parent, size, fees, miner):
|
||
|
self.hash = random.randrange(10**20)
|
||
|
self.parent = parent
|
||
|
self.score = 1 if self.parent is None else parent.score + 1
|
||
|
self.miner = miner
|
||
|
self.size = size
|
||
|
self.fees = fees
|
||
|
|
||
|
class Miner():
|
||
|
def __init__(self, strategy, id):
|
||
|
self.limit, self.big_limit, self.accept_depth, self.creation_limit = strategy
|
||
|
self.chain = {}
|
||
|
self.big_chain = {}
|
||
|
self.head = None
|
||
|
self.big_head = None
|
||
|
self.id = id
|
||
|
self.future = {}
|
||
|
self.children = {}
|
||
|
self.created = 0
|
||
|
|
||
|
def process_history(self, time):
|
||
|
deletes = []
|
||
|
for t in self.future:
|
||
|
if t <= time:
|
||
|
for b in self.future[t]:
|
||
|
self.process_block(b)
|
||
|
deletes.append(t)
|
||
|
for t in deletes:
|
||
|
del self.future[t]
|
||
|
|
||
|
def add_block(self, block, time):
|
||
|
self.process_history(time)
|
||
|
if time + int(block.size * PROPTIME_FACTOR) not in self.future:
|
||
|
self.future[time + int(block.size * PROPTIME_FACTOR)] = [block]
|
||
|
else:
|
||
|
self.future[time + int(block.size * PROPTIME_FACTOR)].append(block)
|
||
|
|
||
|
def process_block(self, block):
|
||
|
if block.size <= self.limit and (block.parent is None or block.parent.hash in self.chain):
|
||
|
self.chain[block.hash] = block
|
||
|
if block.score > (self.head.score if self.head else 0):
|
||
|
self.head = block
|
||
|
if block.size <= self.big_limit and (block.parent is None or block.parent.hash in self.big_chain):
|
||
|
self.big_chain[block.hash] = block
|
||
|
if block.score > (self.big_head.score if self.big_head else 0):
|
||
|
self.big_head = block
|
||
|
if block.score > (self.head.score if self.head else 0) + self.accept_depth:
|
||
|
self.head = block
|
||
|
self.chain[block.hash] = block
|
||
|
if block.parent and block.parent.hash not in self.chain and block.parent.hash not in self.big_chain:
|
||
|
if block.parent.hash not in self.children:
|
||
|
self.children[block.parent.hash] = [block]
|
||
|
else:
|
||
|
self.children[block.parent.hash].append(block)
|
||
|
if block.hash in self.children:
|
||
|
for c in self.children[block.hash]:
|
||
|
self.process_block(c)
|
||
|
del self.children[block.hash]
|
||
|
|
||
|
def create_block(self, backlog, time):
|
||
|
self.process_history(time)
|
||
|
fees = sum(backlog[:self.creation_limit])
|
||
|
# print 'Creating block of size %d (fees %d, seq %d)' % (self.creation_limit, fees, self.head.score + 1 if self.head else 1)
|
||
|
self.created += 1
|
||
|
return Block(self.head, self.creation_limit, fees, self.id)
|
||
|
|
||
|
|
||
|
def simulate(strats):
|
||
|
miners = [Miner(strat, i) for i, strat in enumerate(strats)]
|
||
|
backlog = []
|
||
|
for i in range(100000):
|
||
|
if i % 10000 == 0:
|
||
|
print 'Progress %d' % i
|
||
|
backlog.append(TX_FEE_DISTRIBUTION())
|
||
|
if random.random() < 0.01:
|
||
|
backlog = sorted(backlog)[::-1]
|
||
|
miner = random.choice(miners)
|
||
|
b = miner.create_block(backlog, i)
|
||
|
backlog = backlog[b.size:]
|
||
|
for m in miners:
|
||
|
m.add_block(b, i)
|
||
|
rewards = [0] * len(miners)
|
||
|
blocks = [0] * len(miners)
|
||
|
h = miners[0].head
|
||
|
sz = 0
|
||
|
while h is not None:
|
||
|
rewards[h.miner] += REWARD + h.fees
|
||
|
blocks[h.miner] += 1
|
||
|
h = h.parent
|
||
|
sz += 1
|
||
|
return rewards, blocks, [m.created for m in miners]
|
||
|
|
||
|
for r in range(200):
|
||
|
tests = []
|
||
|
for s in strategies:
|
||
|
tests.append(s)
|
||
|
tests.append((s[0] - 2, s[1], s[2], s[3]))
|
||
|
tests.append((s[0] + 2, s[1], s[2], s[3]))
|
||
|
tests.append((s[0], s[1], s[2] - 1, s[3]))
|
||
|
tests.append((s[0], s[1], s[2] + 1, s[3]))
|
||
|
tests.append((s[0], s[1], s[2], s[3] - 2))
|
||
|
tests.append((s[0], s[1], s[2], s[3] + 2))
|
||
|
NUM_TESTS = 7
|
||
|
print 'Starting simulation'
|
||
|
results, blks, created = simulate(tests)
|
||
|
for i, s in enumerate(strategies):
|
||
|
base = results[i * NUM_TESTS]
|
||
|
if results[i * NUM_TESTS + 1] < base < results[i * NUM_TESTS + 2]:
|
||
|
print 'Increasing base accept size beneficial at %r' % s
|
||
|
s[0] += 2
|
||
|
if results[i * NUM_TESTS + 1] > base > results[i * NUM_TESTS + 2] and s[0] > 2:
|
||
|
print 'Decreasing base accept size beneficial at %r' % s
|
||
|
s[0] -= 2
|
||
|
if results[i * NUM_TESTS + 3] < base < results[i * NUM_TESTS + 4]:
|
||
|
print 'Increasing override depth beneficial at %r' % s
|
||
|
s[2] += 1
|
||
|
if results[i * NUM_TESTS + 3] > base > results[i * NUM_TESTS + 4] and s[2] > 1:
|
||
|
print 'Decreasing override depth beneficial at %r' % s
|
||
|
s[2] -= 1
|
||
|
if results[i * NUM_TESTS + 5] < base < results[i * NUM_TESTS + 6]:
|
||
|
print 'Increasing creation size beneficial at %r' % s
|
||
|
s[3] += 2
|
||
|
if results[i * NUM_TESTS + 5] > base > results[i * NUM_TESTS + 6] and s[3] > 2:
|
||
|
print 'Decreasing creation size beneficial at %r' % s
|
||
|
s[3] -= 2
|
||
|
for s in strategies:
|
||
|
print s
|
||
|
print 'Chain quality (per miner):', [(b * 100 / c) if c else 0 for b, c in zip(blks, created)]
|
||
|
print 'Chain quality (total, non-perturbed miners only):', sum(blks[::NUM_TESTS]) * 1.0 / sum(created[::NUM_TESTS])
|
||
|
if r % 20 == 0:
|
||
|
print 'Control round'
|
||
|
results, blks, created = simulate(strategies)
|
||
|
print 'Chain quality (per miner):', [(b * 100 / c) if c else 0 for b, c in zip(blks, created)]
|
||
|
print 'Chain quality (total):', sum(blks) * 1.0 / sum(created)
|
||
|
|
||
|
|
||
|
|
||
|
# results = simulate(strategies)
|
||
|
# for s, r in zip(strategies, results):
|
||
|
# print s[0], s[3], r
|