lottery.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import matplotlib.pyplot as plt
  2. from tqdm import tqdm
  3. import time
  4. from datetime import timedelta
  5. from core.darkie import *
  6. from pid.cascade import *
  7. from tqdm import tqdm
  8. import random
  9. class DarkfiTable:
  10. def __init__(self, airdrop, running_time, controller_type=CONTROLLER_TYPE_DISCRETE, kp=0, ki=0, kd=0, dt=1, kc=0, ti=0, td=0, ts=0, debug=False, r_kp=0, r_ki=0, r_kd=0, fee_kp=0, fee_ki=0, fee_kd=0):
  11. self.Sigma=airdrop
  12. self.darkies = []
  13. self.running_time=running_time
  14. self.start_time=None
  15. self.end_time=None
  16. self.secondary_pid = SecondaryDiscretePID(kp=kp, ki=ki, kd=kd) if controller_type==CONTROLLER_TYPE_DISCRETE else SecondaryTakahashiPID(kc=kc, ti=ti, td=td, ts=ts)
  17. print('secondary min/max : {}/{}'.format(self.secondary_pid.clip_min, self.secondary_pid.clip_max))
  18. self.primary_pid = PrimaryDiscretePID(kp=r_kp, ki=r_ki, kd=r_kd) if controller_type==CONTROLLER_TYPE_DISCRETE else PrimaryTakahashiPID(kc=kc, ti=ti, td=td, ts=ts)
  19. print('primary min/max : {}/{}'.format(self.primary_pid.clip_min, self.primary_pid.clip_max))
  20. self.basefee_pid = FeePID(kp=fee_kp, ki=fee_ki, kd=fee_kd)
  21. self.debug=debug
  22. self.rewards = []
  23. self.winners = [1]
  24. self.computational_cost = [0]
  25. self.base_fee = []
  26. self.tips_avg = []
  27. self.cc_diff = []
  28. def add_darkie(self, darkie):
  29. self.darkies+=[darkie]
  30. """
  31. for every slot under given running time, set f based off prior on-chain public \
  32. values, set sigmas, f, update vesting, stake for every stakeholder, resolve \
  33. forks.
  34. @param rand_running_time: randomization running time state
  35. @param debug: debug option
  36. @param hp: high precision option
  37. @returns: acc, avg_apy, avg_reward, stake_ratio, avg_apr
  38. """
  39. def background(self, rand_running_time=True, debug=False, hp=True):
  40. self.debug=debug
  41. self.start_time=time.time()
  42. # random running time
  43. rand_running_time = random.randint(1,self.running_time) if rand_running_time else self.running_time
  44. self.running_time = rand_running_time
  45. rt_range = tqdm(np.arange(0,self.running_time, 1))
  46. # loop through slots
  47. for slot in rt_range:
  48. # calculate probability of winning owning 100% of stake
  49. f = self.secondary_pid.pid_clipped(float(self.winners[-1]), debug)
  50. # calculate reward value every epoch
  51. if slot%EPOCH_LENGTH == 0:
  52. acc = self.secondary_pid.acc()
  53. reward = self.primary_pid.pid_clipped(acc, debug)
  54. self.rewards += [reward]
  55. #note! thread overhead is 10X slower than sequential node execution!
  56. total_stake = 0
  57. Ys = []
  58. Ts = []
  59. for i in range(len(self.darkies)):
  60. self.darkies[i].set_sigma_feedback(self.Sigma, self.winners[-1], f, slot, hp)
  61. diff = self.darkies[i].update_vesting()
  62. self.Sigma += diff
  63. y, T = self.darkies[i].run(hp)
  64. Ys+=[y]
  65. Ts+=[T]
  66. total_stake += self.darkies[i].stake
  67. # slot secondary controller feedback
  68. self.winners += [sum([self.darkies[i].won_hist[-1] for i in range(len(self.darkies))])]
  69. if self.winners[-1]==1:
  70. is_slashed = self.reward_slash_lead(debug)
  71. if is_slashed==False:
  72. self.resolve_fork(slot, debug)
  73. avg_y = sum(Ys)/len(Ys)
  74. avg_t = sum(Ts)/len(Ts)
  75. avg_tip = self.tips_avg[-1] if len(self.tips_avg)>0 else 0
  76. base_fee = self.base_fee[-1] if len(self.base_fee)>0 else 0
  77. cc_diff = self.cc_diff[-1] if len(self.cc_diff)>0 else 0
  78. rt_range.set_description('epoch: {}, fork: {}, winners: {}, issuance {} DRK, f: {}, acc: {}%, stake: {}%, sr: {}%, reward:{}, apr: {}%, basefee: {}, avg(fee): {}, cc_diff: {}, avg(y): {}, avg(T): {}'.format(int(slot/EPOCH_LENGTH), self.merge_length(), self.winners[-1], round(self.Sigma,2), round(f, 5), round(self.secondary_pid.acc()*100, 2), round(total_stake/self.Sigma*100 if self.Sigma>0 else 0,2), round(self.avg_stake_ratio()*100,2) , round(self.rewards[-1],2), round(self.avg_apr()*100,2), round(base_fee, 5), round(avg_tip, 2), round(cc_diff, 5), round(float(avg_y), 2), round(float(avg_t), 2)))
  79. #assert round(total_stake,1) <= round(self.Sigma,1), 'stake: {}, sigma: {}'.format(total_stake, self.Sigma)
  80. slot+=1
  81. self.end_time=time.time()
  82. avg_reward = sum(self.rewards)/len(self.rewards)
  83. stake_ratio = self.avg_stake_ratio()
  84. avg_apy = self.avg_apy()
  85. avg_apr = self.avg_apr()
  86. cc_diff_avg = sum([0 if math.fabs(i)<CC_DIFF_EPSILON else 1 for i in self.cc_diff])/len(self.cc_diff) if len(self.cc_diff)>0 else 0
  87. return self.secondary_pid.acc_percentage(), cc_diff_avg, avg_apy, avg_reward, stake_ratio, avg_apr
  88. """
  89. reward single lead, or slash lead with probability len(self.darkies)**-1
  90. @returns: True if slashed False otherwise
  91. """
  92. def reward_slash_lead(self, debug=False):
  93. # reward the single lead
  94. for i in range(len(self.darkies)):
  95. if self.darkies[i].won_hist[-1]:
  96. if random.random() < len(self.darkies)**-1:
  97. self.darkies.remove(self.darkies[i])
  98. print('stakeholder {} slashed'.format(i))
  99. return True
  100. else:
  101. self.darkies[i].update_stake(self.rewards[-1])
  102. self.Sigma += self.rewards[-1]
  103. self.tx_fees(i, debug)
  104. break
  105. return False
  106. """
  107. resolve fork, for slots with multiple leads, shuffle nodes, and reward first winner.
  108. """
  109. def resolve_fork(self, slot, debug=False):
  110. # resolve fork
  111. for i in range(self.merge_length()):
  112. resync_slot_id = slot-(i+1)
  113. resync_reward_id = int((resync_slot_id)/EPOCH_LENGTH)
  114. resync_reward = self.rewards[resync_reward_id]
  115. # resyncing depends on the random branch chosen,
  116. # it's simulated by choosing first wining node
  117. darkie_winning_idx = -1
  118. random.shuffle(self.darkies)
  119. for darkie_idx in range(len(self.darkies)):
  120. if self.darkies[darkie_idx].won_hist[resync_slot_id]:
  121. self.darkies[darkie_idx].resync_stake(resync_reward)
  122. self.Sigma += resync_reward
  123. def merge_length(self):
  124. merge_length = 0
  125. for i in reversed(self.winners[:-1]):
  126. if i !=1:
  127. merge_length+=1
  128. else:
  129. break
  130. return merge_length
  131. """
  132. simulate general purpose transactions made by stakeholders,
  133. deduct basefee, tip from senders pay miners tipss.
  134. """
  135. def tx_fees(self, darkie_lead_idx, debug=False):
  136. txs = []
  137. for darkie in self.darkies:
  138. # make sure tip is covered by darkie stake
  139. txs += [darkie.tx(self.rewards[-1])]
  140. ret, actual_cc = DarkfiTable.auction(txs)
  141. self.computational_cost += [actual_cc]
  142. self.cc_diff += [MAX_BLOCK_CC - actual_cc]
  143. tips = ret[0]
  144. idxs = ret[1]
  145. self.tips_avg += [tips/len(idxs) if len(idxs)>0 else 0]
  146. basefee = self.basefee_pid.pid_clipped(self.computational_cost[-1], debug)
  147. self.base_fee+=[basefee]
  148. assert tips == sum(txs[idx].tip for idx in idxs), 'tips: {}, sum(tips): {}'.format(tips, sum(tx.tip for tx in txs))
  149. for idx in idxs:
  150. fee = txs[idx].tip+basefee
  151. self.darkies[idx].pay_fee(fee)
  152. #print("charging darkie[{}]: {} DRK per tx of length: {}, burning: {}".format(idx, fee, len(txs[idx]), basefee))
  153. self.darkies[darkie_lead_idx].pay_fee(-1*tips)
  154. # subtract base fee from total stake
  155. self.Sigma -= basefee*len(idxs)
  156. """
  157. average APY (with compound interest added every epoch) ,
  158. scaled to running time for all nodes
  159. @returns: average APY for all nodes
  160. """
  161. def avg_apy(self):
  162. return Num(sum([darkie.apy_scaled_to_runningtime(self.rewards) for darkie in self.darkies])/len(self.darkies))
  163. """
  164. average APR scaled to running time for all nodes
  165. @returns: average APR for all nodes
  166. """
  167. def avg_apr(self):
  168. return Num(sum([darkie.apr_scaled_to_runningtime() for darkie in self.darkies])/len(self.darkies))
  169. """
  170. returns: average stake ratio for all nodes
  171. """
  172. def avg_stake_ratio(self):
  173. return sum([darkie.staked_tokens_ratio() for darkie in self.darkies]) / len(self.darkies)
  174. """
  175. write lottery reward log
  176. """
  177. def write(self):
  178. elapsed=self.end_time-self.start_time
  179. for id, darkie in enumerate(self.darkies):
  180. darkie.write(id)
  181. if self.debug:
  182. print("total time: {}, slot time: {}".format(str(timedelta(seconds=elapsed)), str(timedelta(seconds=elapsed/self.running_time))))
  183. self.secondary_pid.write()
  184. with open('log/rewards.log', 'w+') as f:
  185. buff = ','.join([str(i) for i in self.rewards])
  186. f.write(buff)
  187. """
  188. tip auction
  189. @return total tip for miner, and list of indices of darkies included.
  190. """
  191. def auction(txs):
  192. W = MAX_BLOCK_CC
  193. n = len(txs)
  194. K = [[[0,[]] for x in range(W + 1)] for x in range(n + 1)]
  195. for i in range(n + 1):
  196. for w in range(W + 1):
  197. if i == 0 or w == 0:
  198. K[i][w] = [0,[]]
  199. elif txs[i-1].cc() <= w:
  200. if txs[i-1].tip + K[i-1][w-txs[i-1].cc()][0] > K[i-1][w][0]:
  201. K[i][w] = [txs[i-1].tip + K[i-1][w-txs[i-1].cc()][0], K[i-1][w-txs[i-1].cc()][1] + [i-1]]
  202. else:
  203. K[i][w] = K[i-1][w]
  204. else:
  205. K[i][w] = K[i-1][w]
  206. tip = K[n][W][0]
  207. actual_cc = W
  208. for w in reversed(range(W+1)):
  209. if K[n][w][0] == tip:
  210. actual_cc = w
  211. else:
  212. break
  213. return K[n][W], actual_cc