Code:
import datetime
import random
import numpy as np
import matplotlib.pyplot as plt
# sudo apt-get install python-tk
# pip2 install numpy matplotlib
class RetargetTest():
# experiment with the number of work packages
def __init__(self):
self.current_time = datetime.datetime.now()
self.works_to_create = 3
self.generate_blocks = 100
self.current_height = 0
self.blockchain = []
self.work_packages = []
self.base_target = 0x000000ffffffffffffffffffffffffff
self.stretch_number_pows = True
self.do_not_randomize_block_times_but_do_always_60_sec = True
self.new_miner_every_xth_second = 10
self.how_many_miners_come_or_go = 70242
self.initial_miners = 50
self.miners_kick_in_at_block=50
self.miners_drop_at = 0
self.miners_drop_for=20
self.jitter_size = 0
def seeder(self, hasher):
random.seed(hasher)
def randomize_params(self):
self.generate_blocks = random.randint(80,500)
self.new_miner_every_xth_second = random.randint(5,50)
self.how_many_miners_come_or_go = random.randint(0,1000000)
self.initial_miners = random.randint(0,1000000)
self.miners_kick_in_at_block = random.randint(0,40)
self.jitter_size = random.randint(1,7)
self.miners_drop_at = random.randint(self.generate_blocks/2,self.generate_blocks)
pass
def create_block(self,timestamp, num_pow):
return {'time_stamp' : timestamp, 'num_pow' : num_pow, 'first_work_factor':0}
def create_work(self,idx, factor, target):
return {'id': idx, 'base_executions_per_second' : factor, 'target' : target}
def addSecs(self,tm, secs):
fulldate = tm + datetime.timedelta(seconds=secs)
return fulldate
def randomDuration(self):
if self.do_not_randomize_block_times_but_do_always_60_sec:
return 60
else:
return int(random.uniform(25, 120))
def currently_active_miners(self,current_height):
if self.current_height<self.miners_kick_in_at_block:
return 0
if self.current_height>=self.miners_drop_at and self.current_height<=self.miners_drop_at+self.miners_drop_for:
return 0
# get the current active number of miners in relation of blockchain height,
# but the number of miners increases by 1 every 10 blocks
increases = int(self.current_height/self.new_miner_every_xth_second) * self.how_many_miners_come_or_go
return self.initial_miners+increases
def miner_pows_based_on_target(self,work, height, dur):
current_target = work["target"]
factor = (current_target / self.base_target) * 1.0*dur/60.0
actual_pow_mined = work["base_executions_per_second"]
# random jitter
actual_pow_mined = abs((actual_pow_mined - self.jitter_size) + random.uniform(self.jitter_size,2*self.jitter_size)) * self.currently_active_miners(height)
if actual_pow_mined < 0:
actual_pow_mined = 0
actual_pow_mined = actual_pow_mined *factor
# rate limit to 20 pows per block
if actual_pow_mined > 20:
actual_pow_mined = 20
if actual_pow_mined < 0:
actual_pow_mined = 0
return actual_pow_mined
def kimoto(self,x):
return 1 + (0.7084 * pow(((x)/(144)), -1.228))
def retarget_work(self,block, x):
targetI = x["target"]
pastMass = 0
counter = 0
current_block = block
current_block_timestamp = self.blockchain[current_block]["time_stamp"]
adjustment = 0
max_block_reading = 44
min_block_reading = 2
lowerbound = 0.2
higherbound = 1.8
minval = 0x000000ffffffffffffffffffffffffff
maxval = 0
ultra_aggressive = False
modest_aggressive = False
was_negative = False
seconds_passed = 0
while True:
counter += 1
currpows = self.blockchain[current_block]["num_pow"][x["id"]]
pastMass += currpows
if currpows<minval:
minval=currpows
if currpows>maxval:
maxval=currpows
old_seconds_passed = seconds_passed
seconds_passed = (current_block_timestamp - self.blockchain[current_block-1]["time_stamp"]).seconds
if current_block_timestamp < self.blockchain[current_block-1]["time_stamp"]:
current_block_timestamp = self.blockchain[current_block-1]["time_stamp"]
current_block -= 1
if seconds_passed < 1:
seconds_passed = 1
seconds_target_passed = pastMass * 6 # 6 seconds per POW
adj = seconds_target_passed / seconds_passed
# break if fluctuations are too heavy
if counter*20 == pastMass and counter >= min_block_reading:
ultra_aggressive = True
break
if counter==1:
if adj<1:
was_negative = True
else:
if adj<1 and was_negative == False:
seconds_passed = old_seconds_passed
pastMass -= currpows
break
if adj>=1 and was_negative == True:
seconds_passed = old_seconds_passed
pastMass -= currpows
break
if minval > 0 and maxval/minval>1.5:
modest_aggressive = True
if minval > 0 and maxval/minval>1.5 and counter >= min_block_reading:
break
if current_block < 1 or counter == max_block_reading:
break
print "used",counter
lowerbound = min(0.75,lowerbound+counter*0.25)
higherbound = max(1.25,higherbound-counter*0.25)
if pastMass>=1 and ultra_aggressive==False:
seconds_target_passed = pastMass * 6 # 6 seconds per POW
adj = seconds_target_passed / seconds_passed
if adj<lowerbound:
adj=lowerbound
if adj>higherbound:
adj=higherbound
if modest_aggressive and adj>=1:
adj *= 1.2
if modest_aggressive and adj<1:
adj /= 1.2
targetI /= adj
elif pastMass>=1 and ultra_aggressive==True:
targetI = targetI/(3*counter)
else:
targetI *= 2
if targetI>self.base_target:
targetI = self.base_target
if targetI<1000:
targetI=1000
if x["id"] == 0:
self.blockchain[block]["first_work_factor"] = adjustment
x["target"] = targetI
#print "Retarget using",counter,"blocks","fullcnt",fullCnt,"emptyCnt",emptyCnt
def retarget_works(self,block):
for x in self.work_packages:
self.retarget_work(block,x)
def reset_chain(self):
self.blockchain = []
self.work_packages = []
self.current_height = 0
self.current_time = datetime.datetime.now()
# Here we create up to three different work objects
if self.works_to_create>=1:
self.work_packages.append(self.create_work(0, 20, self.base_target))
if self.works_to_create>=2:
self.work_packages.append(self.create_work(1, 60, self.base_target))
if self.works_to_create>=3:
self.work_packages.append(self.create_work(2, 35, self.base_target))
def generate_chain(self):
while self.current_height < self.generate_blocks:
if self.current_height%1000==0:
print " -> generated block",self.current_height
dur = self.randomDuration()
self.current_time = self.addSecs(self.current_time,dur) # random block generation time
block_pows = {}
for x in self.work_packages:
num_pow = self.miner_pows_based_on_target(x, self.current_height, dur) # mine some POW depending on the current difficulty
block_pows[x["id"]] = num_pow
self.blockchain.append(self.create_block(self.current_time, block_pows))
self.retarget_works(self.current_height) # This retargeting method is the "critical part here"
self.current_height = self.current_height + 1
def get_total_error(self):
values = []
ideal = []
for idx in range(len(self.blockchain)):
if idx == 0:
continue
x = self.blockchain[idx]
x_minus_one = self.blockchain[idx-1]
time_passed = (x["time_stamp"] - x_minus_one["time_stamp"]).seconds
strech_normalizer = time_passed / 60.0
sum_x = 0
for y in x["num_pow"]:
sum_x += x["num_pow"][y]
if sum_x == 0:
ideal.append(0)
else:
if self.stretch_number_pows == False:
ideal.append(self.works_to_create*10*strech_normalizer)
else:
ideal.append(self.works_to_create*10)
if self.stretch_number_pows == False:
values.append(sum_x)
else:
values.append(sum_x/strech_normalizer)
#print values
#print ideal
total_error = 0
for x in range(len(values)):
soll = ideal[x]
ist = values[x]
total_error += abs(soll-ist)
return total_error/len(values)
def plot(self,run):
values = []
target_factors = []
ideal = []
for idx in range(len(self.blockchain)):
if idx == 0:
continue
x = self.blockchain[idx]
x_minus_one = self.blockchain[idx-1]
time_passed = (x["time_stamp"] - x_minus_one["time_stamp"]).seconds
strech_normalizer = time_passed / 60.0
sum_x = 0
for y in x["num_pow"]:
sum_x += x["num_pow"][y]
if sum_x == 0:
ideal.append(0)
else:
if self.stretch_number_pows == False:
ideal.append(self.works_to_create*10*strech_normalizer)
else:
ideal.append(self.works_to_create*10)
if self.stretch_number_pows == False:
values.append(sum_x)
else:
values.append(sum_x/strech_normalizer)
x = range(self.generate_blocks)[1:]
y = values
#print "LEN: x",len(x),"y",len(y),"ideal",len(ideal)
#fig = plt.figure()
ax0 = plt.subplot(211)
if self.stretch_number_pows:
ax0.set_ylabel('POW rate per 60s', color='b')
else:
ax0.set_ylabel('POWs per Block', color='b')
ax0.set_xlabel('Block height')
ax0.plot(x,y,'-o',x,ideal,'r--')
values = []
ideal = []
target_factors = []
for idx in range(len(self.blockchain)):
if idx == 0:
continue
x = self.blockchain[idx]
x_minus_one = self.blockchain[idx-1]
time_passed = (x["time_stamp"] - x_minus_one["time_stamp"]).seconds
strech_normalizer = time_passed / 60.0
sum_x = 0
sum_x += x["num_pow"][0]
if sum_x == 0:
ideal.append(0)
else:
if self.stretch_number_pows == False:
ideal.append(10*strech_normalizer)
else:
ideal.append(10)
#print "sumx",sum_x
if self.stretch_number_pows == False:
values.append(sum_x)
else:
values.append(sum_x/strech_normalizer)
x = range(self.generate_blocks)[1:]
y = values
plt.title('All Works: Total POWs')
#print "LEN: x",len(x),"y",len(y),"ideal",len(ideal)
ax1 = plt.subplot(212)
ax1.plot(x,y,'-o',x,ideal,'r--')
ax1.set_xlabel('Block Height')
# Make the y-axis label and tick labels match the line color.
if self.stretch_number_pows:
ax1.set_ylabel('POW rate per 60s', color='b')
else:
ax1.set_ylabel('POWs per Block', color='b')
for tl in ax1.get_yticklabels():
tl.set_color('b')
ax2 = ax1.twinx()
ax2.set_ylim(0.4, 1.6)
ax2.bar(x,[x["first_work_factor"] for x in self.blockchain][1:],0.45,color='#deb0b0', alpha=0.2)
ax2.set_ylabel('Retargeting Factor', color='r')
for tl in ax2.get_yticklabels():
tl.set_color('r')
plt.title('First Work: POWs + Retargeting Factor')
plt.savefig('render-' + str(run) + '.png')
plt.close()