648 lines
122 KiB
Plaintext
Raw Normal View History

2024-11-25 19:28:53 +04:00
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "3f485372-2531-4a49-8d15-5b26e9018a6a",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from dataclasses import dataclass\n",
"from pyvis.network import Network\n",
"from pyvis.options import Layout\n",
"import networkx as nx"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "8ea18f7d-34a8-4de8-b18f-e93329825840",
"metadata": {},
"outputs": [],
"source": [
"@dataclass\n",
"class Block:\n",
" id: int\n",
" t: float\n",
" height: int\n",
" parent: int\n",
" leader: int"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "cabf7946-8382-4102-b730-d74ed42ceb38",
"metadata": {},
"outputs": [],
"source": [
"@dataclass\n",
"class NetworkParams:\n",
" mixnet_delay_mean: int # seconds\n",
" mixnet_delay_var: int\n",
" broadcast_delay_mean: int # second\n",
" pol_proof_time: int # seconds\n",
"\n",
" def sample_mixnet_delay(self):\n",
" scale = self.mixnet_delay_var / self.mixnet_delay_mean\n",
" shape = self.mixnet_delay_mean / scale\n",
" return np.random.gamma(shape=shape, scale=scale)\n",
" \n",
" def sample_broadcast_delay(self, blocks):\n",
" return np.random.exponential(self.broadcast_delay_mean, size=blocks.shape)\n",
"\n",
" def block_arrival_time(self, block_time):\n",
" return self.pol_proof_time + self.sample_mixnet_delay() + self.sample_broadcast_delay(block_time) + block_time"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "4e9df29f-fb4a-4dfb-a7b4-b8b4b0a6e6b7",
"metadata": {},
"outputs": [],
"source": [
"@dataclass\n",
"class Params:\n",
" TIME: int\n",
" MEAN_BLOCK_TIME: int\n",
" honest_hash_power: np.array\n",
" adversary_control: float\n",
"\n",
" @property\n",
" def N(self):\n",
" return len(self.hash_power)\n",
"\n",
" @property\n",
" def hash_power(self):\n",
" return np.append(self.honest_hash_power, self.honest_hash_power.sum() / (1/self.adversary_control - 1))\n",
" \n",
" @property\n",
" def relative_hash_power(self):\n",
" return self.hash_power / self.hash_power.sum()\n",
"\n",
" def next_block(self):\n",
" return np.random.exponential(self.MEAN_BLOCK_TIME / self.relative_hash_power)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "5616a037-ef12-44a0-915f-ca64e774c549",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 640x480 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"np.random.seed(0)\n",
"\n",
"params = Params(\n",
" TIME=60 * 60, # seconds\n",
" MEAN_BLOCK_TIME=10*60,\n",
" honest_hash_power = np.random.pareto(10, size=1000),\n",
" adversary_control=0.001,\n",
")\n",
"ax = plt.subplot(121)\n",
"ax.hist(params.relative_hash_power)\n",
"ax = plt.subplot(122)\n",
"next_block_times = params.next_block()\n",
"ax.hist(next_block_times / 60, bins=1000)\n",
"ax.set_xscale(\"log\")\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "48c54c25-c7b4-47f9-a00a-5f10997cf185",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABjUAAAMWCAYAAAC5gwQ2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAADKsklEQVR4nOzdf3zP9f7/8fvbftnY3mzsV82mYn5sJIrhhDC/RlGtWg0ROgoLiTqFc2TooHM4JDlUSKdTzlFqmWIlPxsrJKnjZ20mzXuIbbbX948+Xt/e9sPGtvfe2+16ubwuF6/n6/F6vR/P1969ez3fj/fz9bIYhmEIAAAAAAAAAACgiqvl6AQAAAAAAAAAAABKg6IGAAAAAAAAAABwChQ1AAAAAAAAAACAU6CoAQAAAAAAAAAAnAJFDQAAAAAAAAAA4BQoagAAAAAAAAAAAKdAUQMAAAAAAAAAADgFihoAAAAAAAAAAMApUNQAAAAAAAAAAABOgaIGAKCQI0eOyGKxaMWKFY5ORZK0aNGicsnFYrFo2rRpZd6vqp0PAAAA1BzTpk2TxWLRzz//fNXYrl27qmvXrhWWS1hYmGJiYip938q2YsUKWSwWHTlyxGE5rF69Wi+//HK5HvN63h9hYWEaOnRoueYDANeKogYAoJCgoCBt27ZN/fr1c3QqksqvqAEAAAAAzqAiihoAUF24OjoBAEDV4+HhoQ4dOjg6DQAAAABQXl6eLBaLXF35GgsAwEwNAKiWLk9R//rrr3X//ffLarXK19dX48eP16VLl3Tw4EH17t1b3t7eCgsL05w5c+z2v/J2SxcvXlSbNm10yy23yGazmXEZGRkKDAxU165dlZ+fL0kaOnSo6tatq++//159+/ZV3bp1FRISogkTJignJ8fudXJzczVjxgw1a9ZMHh4eatiwoR599FGdOnXKjAkLC9P+/fuVkpIii8Uii8WisLCwEvufnZ2tESNGyM/PT3Xr1lXv3r313XffFRl76NAhxcXFyd/fXx4eHmrevLn+8Y9/XPUcf//993r00UfVpEkTeXl56YYbblD//v21d+9eM+bcuXOqV6+eRo0aVWj/I0eOyMXFRS+99NJVXwsAAAA4fvy4Bg0aJB8fH1mtVj3yyCN2183F+eWXXzR69GjdcMMNcnd310033aTnnnuu0LV5QUGBFixYoFtvvVWenp6qV6+eOnTooHXr1pV4/EWLFsnV1VVTp04tVT/Wrl2rVq1aqXbt2rrpppv097//3W775s2bZbFY9Oabb2rChAm64YYb5OHhoe+//16S9M9//lOtW7dW7dq15evrq4EDB+rAgQN2x/jyyy/14IMPKiwsTJ6engoLC9NDDz2ko0ePFspn+/bt6tSpk2rXrq3g4GBNmTJFeXl5Rea+evVqRUVFqW7duqpbt65uvfVWLVu2zNyenJysu+++WzfeeKNq166tW265RaNGjSp067BTp05p5MiRCgkJMcdBnTp10saNGyX9dpuo9evX6+jRo+YYyGKxlOr8SpJhGJozZ45CQ0NVu3Zt3Xbbbfroo4+KjM3OztbEiRPVuHFjubu764YbblBCQoLOnz9f4mtcvHhREyZM0K233mqON6OiovTf//7XLq579+5q1qyZDMMolOMtt9xSZe4OAMC5UOIGgGosNjZWjzzyiEaNGqXk5GTNmTNHeXl52rhxo0aPHq2JEydq9erVeuaZZ3TLLbdo0KBBRR6ndu3a+te//qW2bdtq2LBhevfdd1VQUKCHH35YhmHorbfekouLixmfl5enAQMGaPjw4ZowYYI+++wz/eUvf5HVatULL7wg6bdB0913363PP/9ckyZNUseOHXX06FFNnTpVXbt21ZdffilPT0+tXbtW9913n6xWqxYtWiTpt5kkxTEMQ/fcc4+2bt2qF154Qbfffru++OIL9enTp1DsN998o44dO6pRo0aaO3euAgMD9fHHH2vs2LH6+eefSxyY/fTTT/Lz89OsWbPUsGFD/fLLL3r99dfVvn177dmzR+Hh4apbt66GDRumV199VXPmzJHVajX3X7Rokdzd3TVs2LCS/4gAAACApIEDByo2NlaPP/649u/fr+eff17ffPONduzYITc3tyL3uXjxorp166YffvhB06dPV6tWrfT5558rMTFRaWlpWr9+vRk7dOhQrVy5UsOHD9ef//xnubu7a/fu3cU+V8IwDD399NP6+9//rtdee61Uz1tIS0tTQkKCpk2bpsDAQK1atUrjxo1Tbm6uJk6caBc7ZcoURUVF6ZVXXlGtWrXk7++vxMREPfvss3rooYeUmJio06dPa9q0aYqKitKuXbvUpEkTSb/9gCg8PFwPPvigfH19lZ6ersWLF+v222/XN998owYNGkj6bTzQvXt3hYWFacWKFfLy8tKiRYu0evXqQrm/8MIL+stf/qJBgwZpwoQJslqt2rdvn12h5IcfflBUVJQee+wxWa1WHTlyRPPmzVPnzp21d+9e8+8UHx+v3bt368UXX1TTpk115swZ7d69W6dPn5b021hh5MiR+uGHH7R27dqrntcrTZ8+XdOnT9fw4cN133336fjx4xoxYoTy8/MVHh5uxv3666/q0qWLTpw4oWeffVatWrXS/v379cILL2jv3r3auHFjscWUnJwc/fLLL5o4caJuuOEG5ebmauPGjRo0aJCWL1+uwYMHS5LGjRunu+++W5988ol69Ohh7v/RRx/phx9+KFTUAoBSMQAA1c7UqVMNScbcuXPt2m+99VZDkvHee++ZbXl5eUbDhg2NQYMGmW2HDx82JBnLly+32//tt982JBkvv/yy8cILLxi1atUyNmzYYBczZMgQQ5Lxr3/9y669b9++Rnh4uLn+1ltvGZKMd9991y5u165dhiRj0aJFZlvLli2NLl26lKrvH330kSHJ+Nvf/mbX/uKLLxqSjKlTp5ptvXr1Mm688UbDZrPZxT755JNG7dq1jV9++aXE8/F7ly5dMnJzc40mTZoYTz31lNn+ww8/GLVq1TLmz59vtl24cMHw8/MzHn300VL1CQAAADXX5Wv7319jGoZhrFq1ypBkrFy50mzr0qWL3XXzK6+8UuS1+ezZsw1J5rX8Z599ZkgynnvuuRJzCQ0NNfr162f8+uuvxr333mtYrVZj48aNpepHaGioYbFYjLS0NLv2nj17Gj4+Psb58+cNwzCMTZs2GZKMO++80y4uKyvL8PT0NPr27WvXfuzYMcPDw8OIi4sr9rUvXbpknDt3zqhTp47dOOGBBx4wPD09jYyMDLvYZs2aGZKMw4cPG4ZhGP/73/8MFxcX4+GHHy5VXw3DMAoKCoy8vDzj6NGjhiTjv//9r7mtbt26RkJCQon79+vXzwgNDS31612WlZVl1K5d2xg4cKBd+xdffGFIsnt/JCYmGrVq1TJ27dplF/vvf//bkGR8+OGHZltoaKgxZMiQYl/30qVLRl5enjF8+HCjTZs2Znt+fr5x0003GXfffbddfJ8+fYybb77ZKCgoKHMfAYDbTwFANRYTE2O33rx5c1ksFrtZC66urrrllluKnIp9pdjYWP3xj3/U008/rRkzZujZZ59Vz549C8VZLBb179/frq1Vq1Z2r/HBBx+oXr166t+/vy5dumQut956qwIDA7V58+Yy9vY3mzZtkiQ9/PDDdu1xcXF26xcvXtQnn3yigQMHysvLyy6Hvn376uLFi9q+fXuxr3Pp0iXNnDlTLVq0kLu7u1xdXeXu7q5Dhw7ZTX+/6aabFBMTo0WLFplTrlevXq3Tp0/rySefvKY+AgAAoOa58vo2NjZWrq6u5vVvUT799FPVqVNH9913n1375VkVn3zyiSSZtyZ64oknrprH6dOnddddd2nnzp3asmWLunfvXuo+tGzZUq1bt7Zri4uLU3Z2tnbv3m3Xfu+999qtb9u2TRcuXCg0IyQkJER33XWX2Rfpt9vAXp6N7urqKldXV9WtW1fnz5+3u1bftGmTunfvroCAALPNxcVFDzzwgN1rJCcnKz8//6rnJzMzU48//rhCQkLk6uoqNzc3hYaGSpLd695xxx1asWKFZsyYoe3
"text/plain": [
"<Figure size 1600x800 with 3 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"network = NetworkParams(\n",
" mixnet_delay_mean=10, # seconds\n",
" mixnet_delay_var=4,\n",
" broadcast_delay_mean=1, # second\n",
" pol_proof_time=2, # seconds\n",
")\n",
"\n",
"\n",
"mixnet_delay_data = np.array([network.sample_mixnet_delay() for _ in range(100000)])\n",
"\n",
"plt.figure(figsize=(16,8))\n",
"ax = plt.subplot(221)\n",
"_ = ax.hist(mixnet_delay_data, bins=100)\n",
"ax.set_title(f\"mixnet delay\")\n",
"_ = ax.set_ylabel(\"frequency\")\n",
"_ = ax.set_xlabel(\"delay (seconds)\")\n",
"\n",
"broadcast_delay_data = network.sample_broadcast_delay(np.zeros(100000))\n",
"ax = plt.subplot(222)\n",
"_ = ax.hist(broadcast_delay_data, bins=100)\n",
"ax.set_title(f\"block broadcast_delay\")\n",
"ax.set_ylabel(\"frequency\")\n",
"ax.set_xlabel(\"delay (seconds)\")\n",
"\n",
"BLOCK_TIME = 0\n",
"block_arrival_slots = np.array([network.block_arrival_time(np.array([BLOCK_TIME])) for _ in range(10000)])\n",
"\n",
"ax = plt.subplot(212)\n",
"_ = ax.hist(block_arrival_slots, bins=100)\n",
"ax.set_title(f\"block arrival slot when sent in slot {BLOCK_TIME}\")\n",
"ax.set_ylabel(\"frequency\")\n",
"ax.set_xlabel(\"arrival time\")\n",
"\n",
"plt.tight_layout()"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "a8eff21e-bbd0-432b-84e4-10da319764b5",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"821"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"params.next_block().argmax()"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "94cc80de-2c60-495f-a73a-d126717f1007",
"metadata": {},
"outputs": [],
"source": [
"class Sim:\n",
" def __init__(self, params: Params, network: NetworkParams):\n",
" self.params = params\n",
" self.network = network\n",
" self.events = {}\n",
" self.blocks = []\n",
" self.block_heights = np.array([], dtype=np.int64)\n",
" self.block_arrivals = np.zeros(shape=(params.N, 0), dtype=np.int64) # arrival time to each leader for each block\n",
"\n",
" def emit_block(self, t, leader, height, parent):\n",
" assert type(t) in [float, np.float64], type(t)\n",
" assert type(leader) in [int, np.int64]\n",
" assert type(height) in [int, np.int64]\n",
" assert type(parent) in [int, np.int64]\n",
" \n",
" block = Block(\n",
" id=len(self.blocks),\n",
" t=t,\n",
" height=height,\n",
" parent=parent,\n",
" leader=leader\n",
" )\n",
" self.blocks.append(block)\n",
" self.block_heights = np.append(self.block_heights, block.height)\n",
" \n",
" # decide when this block will arrive at each node\n",
" self.block_arrivals = np.append(self.block_arrivals, self.network.block_arrival_time(np.repeat(t, self.params.N).reshape((self.params.N, 1))), axis=1)\n",
" return block.id\n",
"\n",
" def emit_leader_block(self, leader, slot, parent):\n",
" assert type(leader) in [int, np.int64], type(leader)\n",
" assert isinstance(slot, int)\n",
" assert type(parent) in [int, np.int64], type(parent)\n",
" \n",
" refs = self.select_refs(leader, parent, slot)\n",
" return self.emit_block(\n",
" leader,\n",
" slot,\n",
" weight=self.blocks[parent].weight + len(refs) + 1,\n",
" height=self.blocks[parent].height + 1,\n",
" parent=parent,\n",
" refs=refs\n",
" )\n",
"\n",
" def select_refs(self, node: int, parent: int, slot: int) -> list[id]:\n",
" assert type(node) in [int, np.int64], node\n",
" assert type(parent) in [int, np.int64], parent\n",
" assert type(slot) in [int, np.int64], slot\n",
" \n",
" if self.blocks[parent].parent >= 0:\n",
" parents_siblings = self.block_siblings(node, self.blocks[parent].parent, slot)\n",
" # we are uniformly sampling from power_set(forks)\n",
" return np.array(parents_siblings)[np.random.uniform(size=len(parents_siblings)) < 0.5]\n",
" else:\n",
" return []\n",
" \n",
" def block_siblings(self, node, block, slot):\n",
" blocks_seen_by_node = self.block_arrivals[node,:] <= slot\n",
" parent = self.blocks[block].parent\n",
" if parent == -1:\n",
" return [block] if blocks_seen_by_node[block] else []\n",
" successor_blocks = self.block_slots > self.blocks[parent].slot\n",
" candidate_siblings = np.nonzero(blocks_seen_by_node & successor_blocks)[0]\n",
" return [b for b in candidate_siblings if self.blocks[b].parent == parent]\n",
"\n",
" def run(self, seed=None):\n",
" if seed is not None:\n",
" np.random.seed(seed)\n",
" \n",
" t = 0.0\n",
" \n",
" # emit the genesis block\n",
" self.emit_block(\n",
" t,\n",
" leader=0,\n",
" height=1,\n",
" parent=-1,\n",
" )\n",
" self.block_arrivals[:,:] = 0 # all nodes see the genesis block immediately\n",
"\n",
" while t < self.params.TIME:\n",
" next_block_times = self.params.next_block()\n",
" leader = next_block_times.argmin()\n",
" t += next_block_times[leader]\n",
"\n",
" seen_blocks = self.block_arrivals[leader] <= t\n",
" seen_heights = self.block_heights * seen_blocks\n",
" fork_heads = (seen_heights == seen_heights.max()) * (seen_heights > 0)\n",
" block_ids = np.nonzero(fork_heads)[0]\n",
" parent = np.random.choice(block_ids)\n",
" \n",
" self.emit_block(\n",
" t,\n",
" leader=leader,\n",
" height=self.blocks[parent].height + 1,\n",
" parent=parent\n",
" )\n",
"\n",
" def plot_spacetime_diagram(self, MAX_T=1 * 60 * 60):\n",
" alpha_index = sorted(range(self.params.N), key=lambda n: self.params.relative_hash_power[n])\n",
" nodes = [f\"$N_{n}$($\\\\alpha$={self.params.relative_hash_power[n]:.2f})\" for n in alpha_index]\n",
" messages = [(nodes[alpha_index.index(self.blocks[b].leader)], nodes[alpha_index.index(node)], self.blocks[b].t, arrival_t, f\"$B_{{{b}}}$\") for b, arrival_ts in enumerate(self.block_arrivals.T) for node, arrival_t in enumerate(arrival_ts) if arrival_t < MAX_T]\n",
" \n",
" fig, ax = plt.subplots(figsize=(8,8))\n",
" \n",
" # Plot vertical lines for each node\n",
" max_slot = max(s for _,_,start_t, end_t,_ in messages for s in [start_t, end_t])\n",
" for i, node in enumerate(nodes):\n",
" ax.plot([i, i], [0, max_slot], 'k-', linewidth=0.1)\n",
" ax.text(i, max_slot + 30 * (0 if i % 2 == 0 else 1), node, ha='center', va='bottom')\n",
" \n",
" # Plot messages\n",
" colors = plt.cm.rainbow(np.linspace(0, 1, len(messages)))\n",
" for (start, end, start_time, end_time, label), color in zip(messages, colors):\n",
" start_idx = nodes.index(start)\n",
" end_idx = nodes.index(end)\n",
" ax.annotate('', xy=(end_idx, end_time), xytext=(start_idx, start_time),\n",
" arrowprops=dict(arrowstyle='->', color=\"black\", lw=0.5))\n",
" placement = 0\n",
" mid_x = start_idx * (1 - placement) + end_idx * placement\n",
" mid_y = start_time * (1 - placement) + end_time * placement\n",
" ax.text(mid_x, mid_y, label, ha='center', va='center', \n",
" bbox=dict(facecolor='white', edgecolor='none', alpha=0.7))\n",
" \n",
" ax.set_xlim(-1, len(nodes))\n",
" ax.set_ylim(0, max_slot + 70)\n",
" ax.set_xticks(range(len(nodes)))\n",
" ax.set_xticklabels([])\n",
" # ax.set_yticks([])\n",
" ax.set_title('Space-Time Diagram')\n",
" ax.set_ylabel('Time')\n",
" \n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
" def honest_chain(self):\n",
" chain_head = max(self.blocks, key=lambda b: b.height)\n",
" honest_chain = {chain_head.id}\n",
" \n",
" curr_block = chain_head\n",
" while curr_block.parent >= 0:\n",
" honest_chain.add(curr_block.parent)\n",
" curr_block = self.blocks[curr_block.parent]\n",
" return sorted(honest_chain, key=lambda b: self.blocks[b].height)\n",
"\n",
" def visualize_chain(self):\n",
" honest_chain = self.honest_chain()\n",
" print(\"Honest chain length\", len(honest_chain))\n",
" honest_chain_set = set(honest_chain)\n",
" \n",
" layout = Layout()\n",
" layout.hierachical = True\n",
" \n",
" G = Network(width=1600, height=800, notebook=True, directed=True, layout=layout, cdn_resources='in_line')\n",
"\n",
" for block in self.blocks:\n",
" # level = slot\n",
" level = block.height\n",
" color = \"lightgrey\"\n",
" if block.id in honest_chain_set:\n",
" color = \"orange\"\n",
"\n",
" G.add_node(int(block.id), level=level, color=color, label=f\"{block.t}\")\n",
" if block.parent >= 0:\n",
" G.add_edge(int(block.id), int(block.parent), width=2, color=color)\n",
" \n",
" return G.show(\"chain.html\")\n",
"\n",
" def adverserial_analysis(self):\n",
" np.random.seed(0)\n",
" adversary = self.params.N - 1\n",
" \n",
" reorg_depths = []\n",
" honest_chain = self.honest_chain()\n",
" print(\"honest_chain length\", len(honest_chain))\n",
" \n",
" for block in self.blocks:\n",
" nearest_honest_block = block\n",
" while nearest_honest_block.height >= len(honest_chain) or honest_chain[nearest_honest_block.height-1] != nearest_honest_block.id:\n",
" nearest_honest_block = self.blocks[nearest_honest_block.parent]\n",
" \n",
" \n",
" adversary_blocks = []\n",
" already_reorged = set()\n",
" t = block.t\n",
" while t < self.params.TIME:\n",
" adversary_block_t = int(self.params.next_block()[adversary])\n",
" t += adversary_block_t\n",
" adversary_blocks.append(t)\n",
" adverserial_height = block.height + len(adversary_blocks)\n",
" honest_chain_up_to_t = [\n",
" b for b in honest_chain\n",
" if self.blocks[b].t <= t\n",
" ]\n",
" last_honest_block = self.blocks[honest_chain_up_to_t[-1]]\n",
" assert last_honest_block.height >= nearest_honest_block.height, (t, last_honest_block, nearest_honest_block)\n",
" if last_honest_block.height < adverserial_height:\n",
" reorg_depths += [last_honest_block.height - nearest_honest_block.height]\n",
" # reorged_blocks = [\n",
" # b for b in honest_chain\n",
" # if b not in already_reorged\n",
" # and self.blocks[b].height > nearest_honest_block.height\n",
" # and self.blocks[b].height < adverserial_height\n",
" # ]\n",
" # already_reorged |= set(reorged_blocks)\n",
" # reorg_depths += [self.blocks[b].height - nearest_honest_block.height for b in reorged_blocks]\n",
" \n",
" \n",
" plt.hist(reorg_depths, bins=max(reorg_depths, default=1))\n",
" plt.xticks(minor=True)\n",
" plt.title(f\"reorg depths with {self.params.adversary_control * 100:.0f}% adversary\")\n",
" plt.show()"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "641d8a34-7549-42fd-ad7c-823c1693ec61",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"blocks 184\n",
"blocks time 0.33m\n",
"honest_chain length 120\n"
]
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAigAAAGxCAYAAABIjE2TAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAzdklEQVR4nO3deXRV1f3+8ecSkpuBEEgoCQlTYqPIIFJAKlAChQQ1WpTiAKio/CwUsKRSGcSWoJAAtRQrgsVWQBFBKyoKVcJg1IIVGRRDi7hkrjGWIQHBBMjn9wffXL0kTOWG7MD7tdZZi7PPPufss3PDfbLP5DEzEwAAgENqVHUDAAAATkZAAQAAziGgAAAA5xBQAACAcwgoAADAOQQUAADgHAIKAABwDgEFAAA4h4ACAACcQ0ABAuSdd96Rx+PRO++8U6n7mTFjhubMmXPK/f/tb3+r1P3/LzwejzIzM33zmzdvVmZmprZv316ubteuXdWyZcv/eV9du3aVx+MpN1133XXl6h49elTjx49X06ZN5fV61axZMz355JPl6r377rtq06aNIiMj1aVLF23evLlcnaFDhyolJUUX6uHcmZmZ8ng8F2RfQFWoWdUNAHBuZsyYoXr16umee+6p6qactTVr1qhhw4a++c2bN2v8+PHq2rWrmjZtGvD9JSUl6YUXXvArq1OnTrl6Q4YM0fPPP6/HHntM7du319tvv63hw4fr4MGDevjhhyVJBw4c0C233KKf//znmjJliv70pz+pd+/eysvLU1BQkCTpgw8+0LPPPqsNGzYQGoAAIaCgyh0+fFjh4eEX3b7wnR//+McXdH9hYWFn3GdeXp7++te/auLEiXrooYcknRh92bt3ryZMmKDBgwcrOjpaa9asUXFxsZ566ikFBwerRYsWSkhI0NatW9WsWTMdPXpUv/jFLzRq1Cg1a9bsQhxelTpy5IjCwsIqdR9mpm+//bbS9wO3cYoHF1TZsPT69evVp08f1a1bV5dddpmkE/8pzZgxQ1dffbXCwsJUt25d9enTR1988UW57Tz77LNq3bq1QkNDFR0drVtuuUX/+te//Orcc889qlWrljZt2qS0tDRFRkaqe/fukk78VTxw4EBFR0erVq1aSk9P1xdffFHuVMSp/Pvf/9Z1112n8PBw1atXT4MHD9bBgwcrrLt8+XJ1795dtWvXVnh4uDp16qQVK1ZU2C8bNmxQ7969Vbt2bUVFRenOO+/U119/7avXtGlT5eXlKTc313fq4uQRiKNHj2rs2LGKj49X7dq11aNHD23ZssWvzoYNG3TjjTeqfv368nq9io+PV3p6unbv3n3KY37qqadUo0YNFRQU+Mr+8Ic/yOPxaOjQob6y0tJS1a1bVyNGjPCVfb9f58yZo1tvvVWS1K1bN99xnHzaau3atfrJT36i8PBwJSUladKkSSotLT1l+87Va6+9JjPTvffe61d+77336siRI3rrrbckSd9++628Xq+Cg4MlSbVq1fKVS9Ljjz+ukpISjRkz5pz2n5OTo169eqlhw4YKDQ3VD3/4Qw0aNEj//e9/y9VdsmSJrr76anm9XiUmJurxxx8vV6dNmzb6yU9+Uq78+PHjSkhIUO/evX1lJSUlmjBhgpo1ayav16sf/OAHuvfee/0+a9KJz9uNN96oRYsWqU2bNgoNDdX48eMlSS+//LI6dOigqKgo38/ovvvu86377bffasSIEbr66qsVFRWl6OhoXXvttXr99dfLtdHj8WjYsGF6+umndeWVV8rr9WrOnDlKTk5Wz549y9U/dOiQoqKi/D53uAgZcAGNGzfOJFmTJk1s1KhRlpOTY6+99pqZmd1///0WHBxsI0aMsLfeesvmz59vzZo1s9jYWMvPz/dtIysryyRZ3759bcmSJfbcc89ZUlKSRUVF2WeffearN2DAAAsODramTZtadna2rVixwt5++207fvy4de7c2UJDQ23SpEm2bNkyGz9+vCUnJ5skGzdu3GmPIT8/3+rXr28JCQk2e/ZsW7p0qfXv398aN25skmzVqlW+us8//7x5PB67+eabbdGiRfbGG2/YjTfeaEFBQbZ8+fIK++Whhx6yt99+26ZOnWoRERHWpk0bKykpMTOz9evXW1JSkrVp08bWrFlja9assfXr15uZ2apVq0ySNW3a1Pr3729LliyxF1980Ro3bmzJycl27NgxMzM7dOiQxcTEWLt27eyll16y3NxcW7hwoQ0ePNg2b958yuP+97//bZJs/vz5vrLrrrvOwsLCLDk52Vf2z3/+0yTZ0qVLfWXf79eCggLfz/Cpp57yHUdBQYGZmaWkpFhMTIwlJyfb008/bTk5OTZkyBCTZHPnzj3tz6Zs/dDQUKtbt64FBQVZUlKSPfzww3b48GG/enfccYf94Ac/KLf+oUOHTJKNGTPGzMx27txpwcHBNmPGDNu/f7+NGjXKYmJi7PDhw/b5559beHi45ebmnrFdJ5s5c6ZlZ2fb4sWLLTc31+bOnWutW7e2K664wvfzNjNbvny5BQUFWefOnW3RokX28ssvW/v27X2ftzJPPPGESfL7HTAzW7p0qUmyxYsXm5nZ8ePH7brrrrOIiAgbP3685eTk2F/+8hdLSEiw5s2b+/VTkyZNrEGDBpaUlGTPPvusrVq1yj788ENbvXq1eTweu+OOO2zp0qW2cuVKmz17tt11112+dQ8cOGD33HOPPf/887Zy5Up766237De/+Y3VqFGj3M9RkiUkJNhVV11l8+fPt5UrV9qnn35qTzzxhHk8nnLH9NRTT5kky8vLO+d+R/VBQMEFVfZF/Lvf/c6vfM2aNSbJ/vCHP/iV79q1y8LCwmzkyJFmZrZ//34LCwuzG264wa/ezp07zev1Wr9+/XxlAwYMMEn27LPP+tVdsmSJSbKZM2f6lWdnZ59VQBk1apR5PB7buHGjX3lqaqpfQPnmm28sOjrabrrpJr96x48ft9atW9s111xTrl9+/etf+9V94YUXTJLNmzfPV9aiRQtLSUkp166ygHJy37z00ksmydasWWNmZh999JFJ8gXDc9GwYUO77777zMysuLjYIiIibNSoUSbJduzYYWZmEydOtODgYDt06JBvvZP79eWXXy4X5sqkpKSYJPvnP//pV968eXPr2bPnGds4duxYmzFjhq1cudKWLFliw4YNs5o1a1qXLl3s+PHjvnqpqal2xRVXVLiNkJAQ+8UvfuGbnzFjhoWEhJgki4qKstdff93MzHr06GEDBw48Y5vOpLS01I4ePWo7duwwSb7tm5l16NDB4uPj7ciRI76yoqIii46O9gso//3vfy0kJMQefvhhv23fdtttFhsba0ePHjUzsxdffNEk2SuvvOJXb+3atSbJZsyY4Str0qSJBQUF2ZYtW/zqPv744ybJDhw4cNbHeOzYMTt69KgNHDjQ2rRp47esrF/37dvnV15UVGSRkZE2fPhwv/LmzZtbt27dznrfqJ4IKLigyr6IP/74Y7/ysWPHmsfjsa+++sqOHj3qN/34xz/2fZmX/TX40ksvldv29ddfb7Gxsb75soBSWFjoV2/kyJEmyfbu3etXvn379rMKKNdcc421bNmyXPns2bP9vnRzcnJMkv3tb38rd0xlIafsS7ysXz766CO/bR49etRq1qzp9yV4poDy9NNP+5WXjXwsWLDAzE78ZVu3bl274oorbObMmef0V+iAAQOscePGvv2V/czq1atnf/nLX8zMrFu3btalSxe/9c41oMTFxZUrv+OOO6xZs2Zn3dbvK/tCXbRoka8sNTX1lNsLCQmxQYMG+ZUdOnTI/vWvf9m3335rZmbPPfec1a9f3/bt22d79+61fv36Wb169SwpKalc+K3IV199ZYMGDbKGDRtajRo1TJJvmjRpkm+fNWrUsGHDhpVbv+zz/X0///nPLSEhwRfE9u3bZ16v1x566CFfnf79+1udOnWspKSk3OcyLi7ObrvtNl/dJk2alAsTZma5ubkmydLS0mzhwoW2e/fuCo/xpZdeso4dO1pERITf8YWGhvrVk2S33HJLhdv41a9+ZVFRUb7flRUrVlQYsHD
"text/plain": [
"<Figure size 640x480 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"np.random.seed(0)\n",
"sim = Sim(\n",
" params=Params(\n",
" TIME=1 * 60 * 60, # seconds\n",
" MEAN_BLOCK_TIME=20,\n",
" honest_hash_power = np.random.pareto(10, size=10),\n",
" adversary_control=0.5,\n",
" ),\n",
" network=NetworkParams(\n",
" mixnet_delay_mean=10, # seconds\n",
" mixnet_delay_var=4,\n",
" broadcast_delay_mean=1, # second\n",
" pol_proof_time=2, # seconds\n",
" )\n",
")\n",
"\n",
"sim.run(seed=1)\n",
"print(\"blocks\", len(sim.blocks))\n",
"print(f\"blocks time {sim.params.TIME / len(sim.blocks) / 60:.2f}m\")\n",
"np.random.seed(0)\n",
"sim.adverserial_analysis()"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "e7c9c405-155e-4101-ada6-25b29ca854c7",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Honest chain length 120\n",
"chain.html\n"
]
},
{
"data": {
"text/html": [
"\n",
" <iframe\n",
" width=\"1600\"\n",
" height=\"800\"\n",
" src=\"chain.html\"\n",
" frameborder=\"0\"\n",
" allowfullscreen\n",
" \n",
" ></iframe>\n",
" "
],
"text/plain": [
"<IPython.lib.display.IFrame at 0x11a835910>"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sim.visualize_chain()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "37a10d0a-b847-434d-b1b3-69dc52c996cc",
"metadata": {},
"outputs": [],
"source": [
"sim = Sim(\n",
" params=Params(\n",
" TIME=25 * 60 * 60, # seconds\n",
" MEAN_BLOCK_TIME=20,\n",
" honest_hash_power = np.random.pareto(10, size=10),\n",
" adversary_control=0.2,\n",
" ),\n",
" network=NetworkParams(\n",
" mixnet_delay_mean=10, # seconds\n",
" mixnet_delay_var=4,\n",
" broadcast_delay_mean=1, # second\n",
" pol_proof_time=2, # seconds\n",
" )\n",
")\n",
"\n",
"sim.run()\n",
"print(\"blocks\", len(sim.blocks))\n",
"print(f\"blocks time {sim.params.TIME / len(sim.blocks) / 60:.2f}m\")\n",
"sim.adverserial_analysis()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e4acf7a9-105d-4dc9-8a42-bcec57d5ff6e",
"metadata": {},
"outputs": [],
"source": [
"sim = Sim(\n",
" params=Params(\n",
" TIME=25 * 60 * 60, # seconds\n",
" MEAN_BLOCK_TIME=20,\n",
" honest_hash_power = np.random.pareto(10, size=10),\n",
" adversary_control=0.1,\n",
" ),\n",
" network=NetworkParams(\n",
" mixnet_delay_mean=10, # seconds\n",
" mixnet_delay_var=4,\n",
" broadcast_delay_mean=1, # second\n",
" pol_proof_time=2, # seconds\n",
" )\n",
")\n",
"\n",
"sim.run()\n",
"print(\"blocks\", len(sim.blocks))\n",
"print(f\"blocks time {sim.params.TIME / len(sim.blocks) / 60:.2f}m\")\n",
"sim.adverserial_analysis()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "89bf9a0f-7f47-4216-80e8-5e6e24998f3b",
"metadata": {},
"outputs": [],
"source": [
"N = 100\n",
"net_params = [NetworkParams(\n",
" mixnet_delay_mean=0.1, # seconds\n",
" mixnet_delay_var=0.1,\n",
" broadcast_delay_mean=0.1, # second\n",
" pol_proof_time=i/N * 5, # seconds\n",
" ) for i in range(N)]\n",
"\n",
"sims = [Sim(\n",
" params=Params(\n",
" TIME=5 * 60 * 60, # seconds\n",
" MEAN_BLOCK_TIME=20,\n",
" honest_hash_power = np.random.pareto(10, size=10),\n",
" adversary_control=0.1,\n",
" ),\n",
" network=net\n",
") for net in net_params]\n",
"\n",
"[sim.run() for sim in sims]\n",
"\n",
"\n",
"plt.scatter([sim.network.pol_proof_time / sim.params.MEAN_BLOCK_TIME for sim in sims], [100 - 100 * len(sim.honest_chain()) / len(sim.blocks) for sim in sims])\n",
"plt.ylabel(\"wasted blocks %\")\n",
"plt.xlabel(\"PoL time as fraction of mean block time\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1c21cfba-68b3-487b-a273-76776defddca",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "776cc7df-7308-45e8-a8ec-6130824623a7",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "eb22240b-a5fb-4470-801d-1598922080b2",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
}
},
"nbformat": 4,
"nbformat_minor": 5
}