├── requirements.txt ├── README.md ├── 04_wallet.ipynb ├── 01_hash.ipynb ├── 06_mine.ipynb ├── 08a_test.ipynb ├── 05_block.ipynb ├── 02_transaction.ipynb ├── 03_merkle.ipynb ├── 99_contract.ipynb └── 07_blockchain.ipynb /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask-Cors 2 | web3 3 | eth-account 4 | objsize 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blockchain from Scratch 2 | > Create a blockchain in pure python and basic cryptographic primitives. 3 | 4 | ### Video Series: 5 | I'm currently in the process of creating a video series out of these notebooks that you can find [here](https://www.youtube.com/watch?v=aA8O3A3btCc&t=21s.). 6 | 7 | ### Content: 8 | 9 | * [01_hash.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/01_hash.ipynb) 10 | - hash function 11 | - hash to emoji 12 | * [02_transaction.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/02_transaction.ipynb) 13 | - create transaction 14 | * [03_merkle.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/03_merkle.ipynb) 15 | - create merkle tree 16 | - merkle proof with merkle branch 17 | * [04_wallet.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/04_wallet.ipynb) 18 | - create public/private key pair 19 | - create account 20 | - sign transaction 21 | - validate signature 22 | * [05_block.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/05_block.ipynb) 23 | - create block header 24 | - create block 25 | * [06_mine.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/06_mine.ipynb) 26 | - create miner 27 | - create genesis block 28 | - add coinbase transaction to block 29 | - mine new block using pow 30 | - validate pow 31 | * [07_blockchain.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/07_blockchain.ipynb) 32 | - create blockchain 33 | - apply state transition 34 | - update account state 35 | * [08_api.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/08_api.ipynb) 36 | - create new blockchain 37 | - post, get, get all tx 38 | - post, get, get all block 39 | - get account info 40 | * [08a_test.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/08a_test.ipynb) 41 | - test api 42 | 43 | **Under Heavy Dev:** 44 | * [99_contract.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/99_contract.ipynb) 45 | - Create/Run Smart Contracts 46 | 47 | ### Visualization 48 | 49 | I wrote a small block-explorer that you can use to look at the blockchain [here](https://github.com/SharifElfouly/block-explorer). 50 | -------------------------------------------------------------------------------- /04_wallet.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "impossible-drive", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "%%capture\n", 11 | "%run 02_transaction.ipynb" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "id": "floating-friendly", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from eth_account.messages import encode_defunct as encode_msg\n", 22 | "import web3 as w3; w3 = w3.Account" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "id": "manufactured-champagne", 28 | "metadata": {}, 29 | "source": [ 30 | "Create a public key `pub_key`, private key `priv_key` pair. The public key is used as an address and the private key is used for signing txs. The `priv_key` is basically your password." 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "id": "closing-quarterly", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "def keys():\n", 41 | " acc = w3.create()\n", 42 | " return acc.privateKey.hex(), acc.address\n", 43 | "\n", 44 | "priv_key,pub_key = keys()\n", 45 | "ph(priv_key), ph(pub_key)" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "id": "1c493d87", 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "priv_key" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "id": "intensive-trinidad", 61 | "metadata": {}, 62 | "source": [ 63 | "## Wallet\n", 64 | "\n", 65 | "Holds a key pair and is used for signing txs. The `nonce` keeps track of the number of txs made with this account." 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "id": "above-think", 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "class Wallet:\n", 76 | " def __init__(self): \n", 77 | " self.priv, self.pub = keys()\n", 78 | " self.nonce = 0\n", 79 | " \n", 80 | " def sign(self, tx):\n", 81 | " self.nonce += 1\n", 82 | " m = encode_msg(bytes(tx))\n", 83 | " sig = w3.sign_message(m, self.priv)\n", 84 | " tx.sig = sig.signature.hex()\n", 85 | " return tx\n", 86 | " \n", 87 | " def signed_tx(self, to, value, fee):\n", 88 | " tx = TX(self.pub, to.pub, value, fee, self.nonce)\n", 89 | " return self.sign(tx)\n", 90 | " \n", 91 | " def __str__(self): return ph(self.pub)" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": null, 97 | "id": "sustainable-arbitration", 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "acc1 = Wallet(); print(acc1)\n", 102 | "acc2 = Wallet(); print(acc2)" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "id": "rental-coaching", 108 | "metadata": {}, 109 | "source": [ 110 | "### Transaction Signing\n", 111 | "Signing a tx generates a signature that only the private key can produce. This can be later validated very efficiently." 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "id": "hollow-dover", 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "tx = TX(acc1.pub, acc2.pub, 12, 0.2, acc1.nonce)\n", 122 | "tx_signed = acc1.sign(tx)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "id": "organized-librarian", 128 | "metadata": {}, 129 | "source": [ 130 | "The signature is exactly what Ethereum uses as well. There are some extra informations that the signature creates that we don't need. For more infos: https://medium.com/@angellopozo/ethereum-signing-and-validating-13a2d7cb0ee3" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "id": "chicken-omega", 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "tx_signed.sig" 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "id": "graduate-lafayette", 146 | "metadata": {}, 147 | "source": [ 148 | "Accounts can generate a signed tx quickly with `signed_tx`." 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": null, 154 | "id": "understanding-explorer", 155 | "metadata": {}, 156 | "outputs": [], 157 | "source": [ 158 | "tx = acc1.signed_tx(acc2, 7, 0.3); print(tx)" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "id": "recorded-links", 164 | "metadata": {}, 165 | "source": [ 166 | "### Transaction Validation" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "id": "serial-diana", 172 | "metadata": {}, 173 | "source": [ 174 | "Validates if the tx was signed by the sender." 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": null, 180 | "id": "massive-robertson", 181 | "metadata": {}, 182 | "outputs": [], 183 | "source": [ 184 | "def val_sig(tx):\n", 185 | " if not hasattr(tx, 'sig'): return False\n", 186 | " m = encode_msg(bytes(tx))\n", 187 | " return w3.recover_message(m, signature=tx.sig) == tx.fr" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "id": "improved-harrison", 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "assert val_sig(tx_signed)" 198 | ] 199 | }, 200 | { 201 | "cell_type": "markdown", 202 | "id": "square-holiday", 203 | "metadata": {}, 204 | "source": [ 205 | "If anything in the tx is changed, like increasing the value to send, the signature should become invalid." 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": null, 211 | "id": "plain-israeli", 212 | "metadata": {}, 213 | "outputs": [], 214 | "source": [ 215 | "tx_signed.value = 30\n", 216 | "assert not val_sig(tx_signed)" 217 | ] 218 | } 219 | ], 220 | "metadata": { 221 | "kernelspec": { 222 | "display_name": "Python 3", 223 | "language": "python", 224 | "name": "python3" 225 | }, 226 | "language_info": { 227 | "codemirror_mode": { 228 | "name": "ipython", 229 | "version": 3 230 | }, 231 | "file_extension": ".py", 232 | "mimetype": "text/x-python", 233 | "name": "python", 234 | "nbconvert_exporter": "python", 235 | "pygments_lexer": "ipython3", 236 | "version": "3.9.2" 237 | } 238 | }, 239 | "nbformat": 4, 240 | "nbformat_minor": 5 241 | } 242 | -------------------------------------------------------------------------------- /01_hash.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 119, 6 | "id": "several-segment", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import time, json\n", 11 | "from hashlib import sha256" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "id": "molecular-dragon", 17 | "metadata": {}, 18 | "source": [ 19 | "## Hash\n", 20 | "\n", 21 | "A hash function is one of the most important cryptographic primitives that we will use. It has the following characteristics:\n", 22 | "\n", 23 | "1. **fixed sized**: every input (also called message) creates a hash value of fixed size.\n", 24 | "2. **deterministic**: the same input will produce the same output every time.\n", 25 | "3. **one-way**: its practically infeasible to invert.\n", 26 | "4. **chaotic**: if only one bit changes the whole hash changes in a toatlly chaotic and random way.\n", 27 | "\n", 28 | "There are many differnt hash functions. Here we will use sha256 (used by Bitcoin). It returns 256 bits or 32 bytes. [This](https://emn178.github.io/online-tools/sha256.html) is a great site to look at other hash functions." 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "id": "hindu-medicaid", 34 | "metadata": {}, 35 | "source": [ 36 | "#### Create hash digest from string, int or float." 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "id": "innocent-india", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "sha = lambda x: '0x'+sha256(str(x).encode()).hexdigest()" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "id": "special-moldova", 52 | "metadata": {}, 53 | "source": [ 54 | "The same input will always produce the same output." 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "id": "blank-buying", 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "sha('satoshi')" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "id": "nonprofit-reliance", 70 | "metadata": {}, 71 | "source": [ 72 | "Changing the input just a little bit changes the whole hash chaotically." 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "id": "cosmetic-morrison", 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "sha('satochi2')" 83 | ] 84 | }, 85 | { 86 | "cell_type": "markdown", 87 | "id": "modern-negotiation", 88 | "metadata": {}, 89 | "source": [ 90 | "#### Create random hash for testing." 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "id": "steady-supplement", 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [ 100 | "rh = lambda: sha(time.time())\n", 101 | "h = rh(); h" 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "id": "mexican-florence", 107 | "metadata": {}, 108 | "source": [ 109 | "#### Turn hash into one of 256 possible emojis. " 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": null, 115 | "id": "involved-blanket", 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "def hash2emoji(h):\n", 120 | " if h[:2]!='0x': h='0x'+h\n", 121 | " offset = int(h[0:4], 0)\n", 122 | " unicode = b'\\U' + b'000' + str(hex(0x1F466+offset))[2:].encode()\n", 123 | " return unicode.decode('unicode_escape')\n", 124 | "\n", 125 | "hash2emoji(h)" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "id": "acting-communist", 131 | "metadata": {}, 132 | "source": [ 133 | "#### Print hash in a nice way." 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "id": "sixth-currency", 140 | "metadata": {}, 141 | "outputs": [], 142 | "source": [ 143 | "def ph(h):\n", 144 | " if len(h)<12: return h\n", 145 | " else : return hash2emoji(h)+' '+h[:12] + '...' + h[-3:]\n", 146 | "\n", 147 | "ph(h)" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "id": "flexible-hindu", 153 | "metadata": {}, 154 | "source": [ 155 | "Print mini hash." 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "id": "pretty-internet", 162 | "metadata": {}, 163 | "outputs": [], 164 | "source": [ 165 | "def pmh(h):\n", 166 | " if len(h)<6: return h\n", 167 | " else : return hash2emoji(h)+' '+h[:6]\n", 168 | "\n", 169 | "pmh(h)" 170 | ] 171 | }, 172 | { 173 | "cell_type": "markdown", 174 | "id": "surface-vienna", 175 | "metadata": {}, 176 | "source": [ 177 | "## Hashable\n", 178 | "\n", 179 | "Objects that are hashable have some common properties." 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": null, 185 | "id": "secure-independence", 186 | "metadata": {}, 187 | "outputs": [], 188 | "source": [ 189 | "class Hashable:\n", 190 | " def __eq__(self, other): return self.hash == other.hash\n", 191 | " def __bytes__(self): return self.hash.encode()\n", 192 | " def json(self): return json.dumps({**self.__dict__}, indent=4)\n", 193 | " \n", 194 | " def __setattr__(self, prop, val):\n", 195 | " super().__setattr__(prop, val)\n", 196 | " if prop not in ['sig','signed','hash','nonce']: \n", 197 | " super().__setattr__('hash', sha(self.__dict__))\n", 198 | " \n", 199 | " def __str__(self):\n", 200 | " s = []\n", 201 | " for k,v in self.__dict__.items():\n", 202 | " p = f'{k}:'.ljust(15)\n", 203 | " if hasattr(v,'messageHash'): s.append(p+ph(v.messageHash.hex()))\n", 204 | " elif str(v)[:2]=='0x' : s.append(p+ph(v))\n", 205 | " elif type(v) ==float : s.append(p+str(round(v,8))+' eth')\n", 206 | " else : s.append(p+str(v))\n", 207 | " return '\\n'.join(s)" 208 | ] 209 | } 210 | ], 211 | "metadata": { 212 | "kernelspec": { 213 | "display_name": "Python 3", 214 | "language": "python", 215 | "name": "python3" 216 | }, 217 | "language_info": { 218 | "codemirror_mode": { 219 | "name": "ipython", 220 | "version": 3 221 | }, 222 | "file_extension": ".py", 223 | "mimetype": "text/x-python", 224 | "name": "python", 225 | "nbconvert_exporter": "python", 226 | "pygments_lexer": "ipython3", 227 | "version": "3.9.2" 228 | } 229 | }, 230 | "nbformat": 4, 231 | "nbformat_minor": 5 232 | } 233 | -------------------------------------------------------------------------------- /06_mine.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 7, 6 | "id": "careful-suite", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "%%capture\n", 11 | "%run 04_account.ipynb\n", 12 | "%run 05_block.ipynb" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "photographic-madrid", 18 | "metadata": {}, 19 | "source": [ 20 | "## Miner\n", 21 | "\n", 22 | "Mining is a crucial component on any blockchain relying on proof of work (abbr. pow). It is used to reach consensus in a network of anonymous decentralized nodes. Anyone with an account `acc` can become a miner. \n", 23 | "\n", 24 | "The miner takes a new block and tries to solve a specific computational puzzle, a specific hash. This can only be done by brute force. \n", 25 | "\n", 26 | "`coinbase` creates a tx to the miner as a reward for providing the pow (also called coinbase tx). " 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 8, 32 | "id": "mexican-gilbert", 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "class Miner:\n", 37 | " def __init__(self, acc): \n", 38 | " self.acc = acc\n", 39 | " self.pub = acc.pub\n", 40 | " \n", 41 | " def mine(self, txs, prev_header, diff, reward, attempts=1000):\n", 42 | " txs.insert(0, self.coinbase(reward))\n", 43 | " mt = MerkleTree(txs)\n", 44 | " bh = Header(mt.root, prev_header.hash, prev_header.number+1, len(txs))\n", 45 | " bh.diff = diff\n", 46 | " bh.reward = reward\n", 47 | " bh.miner = self.pub\n", 48 | " bh.mined = True\n", 49 | " bh_b = bytes(bh)\n", 50 | " nonce = 0\n", 51 | " for i in range(attempts):\n", 52 | " candidate = bh_b + str(nonce).encode()\n", 53 | " candidate_h = sha(candidate)\n", 54 | " if candidate_h[2:2+diff] == '0'*diff: break\n", 55 | " nonce += 1\n", 56 | " assert nonce != attempts, 'no nonce could be found'\n", 57 | " bh.nonce = nonce\n", 58 | " return Block(bh, txs)\n", 59 | " \n", 60 | " def coinbase(self, reward): \n", 61 | " return self.acc.sign(TX(self.pub, self.pub, reward, 0, self.acc.nonce))" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "id": "remarkable-bibliography", 67 | "metadata": {}, 68 | "source": [ 69 | "### Genesis Block\n", 70 | "\n", 71 | "Is the first block in the blockchain. This is how it all begins. " 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 9, 77 | "id": "experienced-disaster", 78 | "metadata": {}, 79 | "outputs": [ 80 | { 81 | "name": "stdout", 82 | "output_type": "stream", 83 | "text": [ 84 | "root: 💀 0x1aceb8e9b2...6d2\n", 85 | "hash: 🕄 0xde2d3d5b47...da6\n", 86 | "prev_hash: 0x0\n", 87 | "number: 0\n", 88 | "n_txs: 2\n", 89 | "mined: False\n", 90 | "time: 1617485067.9508538 eth\n", 91 | "volume: 10.0 eth\n", 92 | "fees: 0.05 eth\n", 93 | "\n", 94 | "txs:\n", 95 | "📿 0x99fF -> 💝 0x37FF 8.0 eth\n", 96 | "📿 0x99fF -> 💝 0x37FF 2.0 eth\n" 97 | ] 98 | } 99 | ], 100 | "source": [ 101 | "def mine_genesis(txs):\n", 102 | " mt = MerkleTree(txs)\n", 103 | " bh = Header(mt.root, '0x0', 0, len(txs))\n", 104 | " return Block(bh, txs)\n", 105 | "\n", 106 | "gb = mine_genesis(r_stxs(2)); print(gb)" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "id": "global-logic", 112 | "metadata": {}, 113 | "source": [ 114 | "### Mining\n", 115 | "\n", 116 | "Mine a block on top of the genesis block" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 10, 122 | "id": "friendly-boating", 123 | "metadata": {}, 124 | "outputs": [ 125 | { 126 | "name": "stdout", 127 | "output_type": "stream", 128 | "text": [ 129 | "root: 📵 0x8fa74ce558...fac\n", 130 | "hash: 💬 0x4626f8a850...103\n", 131 | "prev_hash: 🕄 0xde2d3d5b47...da6\n", 132 | "number: 1\n", 133 | "n_txs: 11\n", 134 | "mined: True\n", 135 | "time: 1617485069.1429892 eth\n", 136 | "diff: 1\n", 137 | "reward: 100\n", 138 | "miner: 📿 0x99fF32a9F0...011\n", 139 | "nonce: 5\n", 140 | "volume: 166.0 eth\n", 141 | "fees: 0.49 eth\n", 142 | "\n", 143 | "txs:\n", 144 | "📿 0x99fF -> 📿 0x99fF 100.0 eth\n", 145 | "📿 0x99fF -> 💝 0x37FF 7.0 eth\n", 146 | "📿 0x99fF -> 💝 0x37FF 9.0 eth\n", 147 | "📿 0x99fF -> 💝 0x37FF 5.0 eth\n", 148 | "📿 0x99fF -> 💝 0x37FF 7.0 eth\n", 149 | "📿 0x99fF -> 💝 0x37FF 8.0 eth\n", 150 | "📿 0x99fF -> 💝 0x37FF 9.0 eth\n", 151 | "📿 0x99fF -> 💝 0x37FF 8.0 eth\n", 152 | "📿 0x99fF -> 💝 0x37FF 6.0 eth\n", 153 | "📿 0x99fF -> 💝 0x37FF 2.0 eth\n", 154 | "📿 0x99fF -> 💝 0x37FF 5.0 eth\n" 155 | ] 156 | } 157 | ], 158 | "source": [ 159 | "miner = Miner(acc1)\n", 160 | "mb = miner.mine(r_stxs(10), gb.header, 1, 100); print(mb)" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "id": "accredited-architect", 166 | "metadata": {}, 167 | "source": [ 168 | "### PoW validation" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "id": "willing-provider", 174 | "metadata": {}, 175 | "source": [ 176 | "We can validate a mined blocks pow very easily." 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 5, 182 | "id": "swedish-victor", 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [ 186 | "def val_pow(mb):\n", 187 | " bh = mb.header\n", 188 | " mb_b = bytes(bh)\n", 189 | " candidate = mb_b + str(bh.nonce).encode()\n", 190 | " candidate_h = sha(candidate)\n", 191 | " if candidate_h[2:2+bh.diff] == '0'*bh.diff: return True\n", 192 | " else : return False\n", 193 | " \n", 194 | "assert val_pow(mb)" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "id": "better-context", 200 | "metadata": {}, 201 | "source": [ 202 | "If we change something in the block the pow should break." 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 6, 208 | "id": "found-dublin", 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "mb.header.prev_hash = rh()\n", 213 | "assert not val_pow(mb)" 214 | ] 215 | } 216 | ], 217 | "metadata": { 218 | "kernelspec": { 219 | "display_name": "Python 3", 220 | "language": "python", 221 | "name": "python3" 222 | }, 223 | "language_info": { 224 | "codemirror_mode": { 225 | "name": "ipython", 226 | "version": 3 227 | }, 228 | "file_extension": ".py", 229 | "mimetype": "text/x-python", 230 | "name": "python", 231 | "nbconvert_exporter": "python", 232 | "pygments_lexer": "ipython3", 233 | "version": "3.8.2" 234 | } 235 | }, 236 | "nbformat": 4, 237 | "nbformat_minor": 5 238 | } 239 | -------------------------------------------------------------------------------- /08a_test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "affiliated-kingdom", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "%%capture\n", 11 | "%run 02_transaction.ipynb\n", 12 | "%run 06_mine.ipynb\n", 13 | "%run 04_account.ipynb" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 2, 19 | "id": "verified-tourism", 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import requests " 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "id": "interesting-eugene", 29 | "metadata": {}, 30 | "source": [ 31 | "# API Test\n", 32 | "\n", 33 | "Some sanity checks for testing the APIs in `08_api.ipynb`." 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 3, 39 | "id": "intense-entrepreneur", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "PORT = 5000\n", 44 | "URL = f'http://localhost:{PORT}/' " 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "id": "limited-parallel", 50 | "metadata": {}, 51 | "source": [ 52 | "### Accounts" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 4, 58 | "id": "healthy-breath", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "satochi,finney,szabo = Account(),Account(),Account()" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "id": "familiar-mortality", 68 | "metadata": {}, 69 | "source": [ 70 | "# Block" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "id": "written-event", 76 | "metadata": {}, 77 | "source": [ 78 | "#### Create Genesis Block" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 5, 84 | "id": "discrete-funeral", 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "tx1 = satochi.signed_tx(finney, 10, 0.0)\n", 89 | "tx2 = satochi.signed_tx(szabo, 10, 0.0)\n", 90 | "miner = Miner(satochi)\n", 91 | "\n", 92 | "gb = mine_genesis([tx1,tx2])" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "id": "smoking-clear", 98 | "metadata": {}, 99 | "source": [ 100 | "#### Post Genesis Block" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 6, 106 | "id": "sharp-assistant", 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "r = requests.post(URL+'gb', json={'gb': gb.json()})\n", 111 | "assert r.status_code == 200" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "id": "dynamic-sense", 117 | "metadata": {}, 118 | "source": [ 119 | "#### Get Genesis Block" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": 7, 125 | "id": "elect-mexico", 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "r = requests.get(URL+'block/'+str(0))\n", 130 | "assert r.status_code == 200" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "id": "adult-instruction", 136 | "metadata": {}, 137 | "source": [ 138 | "#### Mine new Block" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": 8, 144 | "id": "medieval-advantage", 145 | "metadata": {}, 146 | "outputs": [], 147 | "source": [ 148 | "tx3 = finney.signed_tx(szabo, 3, 0.2)\n", 149 | "mb = miner.mine([tx3], gb.header, 2, 10)" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 9, 155 | "id": "pregnant-importance", 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "r = requests.post(URL+'block', json={'block': mb.json()})\n", 160 | "assert r.status_code == 200" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "id": "constant-variance", 166 | "metadata": {}, 167 | "source": [ 168 | "#### Get all Blocks" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": 10, 174 | "id": "coral-kernel", 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "r = requests.get(URL+'blocks')\n", 179 | "#json.loads(json.loads(r.json()['blocks'])[0])\n", 180 | "assert r.status_code == 200" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "id": "aware-certification", 186 | "metadata": {}, 187 | "source": [ 188 | "# TX" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "id": "arctic-angle", 194 | "metadata": {}, 195 | "source": [ 196 | "#### Get TX" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 11, 202 | "id": "normal-pound", 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "tx_hash = list(mb.txs.keys())[0]\n", 207 | "\n", 208 | "r = requests.get(URL+'tx/'+str(mb.header.number)+'/'+tx_hash)\n", 209 | "assert r.status_code == 200" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": 12, 215 | "id": "cb411c7b", 216 | "metadata": {}, 217 | "outputs": [], 218 | "source": [ 219 | "r = requests.get(URL+'tx/'+str(1)+'/'+tx_hash)\n", 220 | "assert r.status_code == 200" 221 | ] 222 | }, 223 | { 224 | "cell_type": "markdown", 225 | "id": "divided-empty", 226 | "metadata": {}, 227 | "source": [ 228 | "#### Get all TXs from Blockchain" 229 | ] 230 | }, 231 | { 232 | "cell_type": "code", 233 | "execution_count": 13, 234 | "id": "speaking-province", 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [ 238 | "r = requests.get(URL+'txs')\n", 239 | "assert r.status_code == 200" 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "id": "b227af96", 245 | "metadata": {}, 246 | "source": [ 247 | "# TX Pool" 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "id": "55a7397c", 253 | "metadata": {}, 254 | "source": [ 255 | "#### Post TX to Pool" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": 14, 261 | "id": "82257de2", 262 | "metadata": {}, 263 | "outputs": [], 264 | "source": [ 265 | "r = requests.post(URL+'tx', json={'tx': tx.json()})\n", 266 | "assert r.status_code == 200" 267 | ] 268 | }, 269 | { 270 | "cell_type": "markdown", 271 | "id": "0ca26c02", 272 | "metadata": {}, 273 | "source": [ 274 | "#### Get all TXs from Pool" 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": 15, 280 | "id": "a9d80457", 281 | "metadata": {}, 282 | "outputs": [], 283 | "source": [ 284 | "r = requests.get(URL+'txs_pool')\n", 285 | "assert r.status_code == 200" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 19, 291 | "id": "6bf0eb6c", 292 | "metadata": {}, 293 | "outputs": [], 294 | "source": [ 295 | "r = requests.get(URL+'tx_pool/'+tx.hash)\n", 296 | "assert r.status_code == 200" 297 | ] 298 | }, 299 | { 300 | "cell_type": "markdown", 301 | "id": "f5f5ab32", 302 | "metadata": {}, 303 | "source": [ 304 | "## Get Account" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": 18, 310 | "id": "968bf032", 311 | "metadata": {}, 312 | "outputs": [], 313 | "source": [ 314 | "r = requests.get(URL+'acc/'+szabo.pub)\n", 315 | "assert r.status_code == 200" 316 | ] 317 | } 318 | ], 319 | "metadata": { 320 | "kernelspec": { 321 | "display_name": "Python 3", 322 | "language": "python", 323 | "name": "python3" 324 | }, 325 | "language_info": { 326 | "codemirror_mode": { 327 | "name": "ipython", 328 | "version": 3 329 | }, 330 | "file_extension": ".py", 331 | "mimetype": "text/x-python", 332 | "name": "python", 333 | "nbconvert_exporter": "python", 334 | "pygments_lexer": "ipython3", 335 | "version": "3.9.2" 336 | } 337 | }, 338 | "nbformat": 4, 339 | "nbformat_minor": 5 340 | } 341 | -------------------------------------------------------------------------------- /05_block.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 4, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%%capture\n", 10 | "%run 02_transaction.ipynb\n", 11 | "%run 03_merkle.ipynb\n", 12 | "%run 04_wallet.ipynb" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "metadata": {}, 18 | "source": [ 19 | "## Block Header\n", 20 | "\n", 21 | "The `Header` is a summary of the most important facts about a block. It contains the following:\n", 22 | "- `root`: Root of the Merkle Tree of TXs\n", 23 | "- `prev_hash`: Hash of the previous block\n", 24 | "- `number`: Block number\n", 25 | "- `n_txs`: Number of txs included in the block\n", 26 | "- `mined`: Does the header belong to a block that is mined\n", 27 | "- `time`: Time of creation\n", 28 | "\n", 29 | "The actual mining happens on the block header." 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 5, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "class Header(Hashable):\n", 39 | " def __init__(self, root, prev_hash, number, n_txs):\n", 40 | " self.root = root\n", 41 | " self.prev_hash = prev_hash\n", 42 | " self.number = number\n", 43 | " self.n_txs = n_txs\n", 44 | " self.mined = False\n", 45 | " self.time = time.ctime()" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 6, 51 | "metadata": {}, 52 | "outputs": [ 53 | { 54 | "name": "stdout", 55 | "output_type": "stream", 56 | "text": [ 57 | "root: 💳 0x4d94eb5b9b...3d3\n", 58 | "hash: 🔞 0xb8b11b9251...5c4\n", 59 | "prev_hash: 🔸 0xd2663b66a8...f00\n", 60 | "number: 2\n", 61 | "n_txs: 10\n", 62 | "mined: False\n", 63 | "time: Sun Apr 11 20:51:50 2021\n" 64 | ] 65 | } 66 | ], 67 | "source": [ 68 | "bh = Header(rh(),rh(),2,10); print(bh)" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "If anything in the block header changes its `hash` changes automatically as well." 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 8, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "bh_changed = deepcopy(bh)\n", 85 | "bh_changed.number = 120\n", 86 | "assert bh != bh_changed" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": {}, 92 | "source": [ 93 | "### Info\n", 94 | "\n", 95 | "Includes all extra informations that are irrelevant for mining and therefore are not in the block header. These are the total txs `volume` and `fees` included in a block." 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 9, 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [ 104 | "class Info(Hashable):\n", 105 | " def __init__(self, txs): \n", 106 | " self.volume = sum([tx.value for tx in txs])\n", 107 | " self.fees = sum([tx.fee for tx in txs])" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": {}, 113 | "source": [ 114 | "## Block\n", 115 | "\n", 116 | "Consists of the block header `bh` and a list of txs. To validate that the txs are correct, the merkle tree can be rebuild and checked against the root hash stored in the `bh`." 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 10, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "class Block(Hashable): \n", 126 | " def __init__(self, bh, txs):\n", 127 | " self.info = Info(txs)\n", 128 | " self.header = bh\n", 129 | " self.mt = MerkleTree(txs)\n", 130 | " self.txs = self.val_txs(txs)\n", 131 | " self.hash = bh.hash\n", 132 | " \n", 133 | " def val_txs(self, txs):\n", 134 | " for tx in txs: assert val_sig(tx), 'tx signature invalid'\n", 135 | " assert self.are_unique(txs), 'txs include duplicates'\n", 136 | " assert self.mt.root == self.header.root, 'txs root hash do not match'\n", 137 | " return {tx.hash: tx for tx in txs}\n", 138 | " \n", 139 | " def json(self): \n", 140 | " info = self.info.__dict__\n", 141 | " h = self.header.__dict__\n", 142 | " txs = [tx.json() for tx in self.txs.values()]\n", 143 | " d = {**info, **h, 'txs': txs}\n", 144 | " return json.dumps(d, indent=4)\n", 145 | " \n", 146 | " def are_unique(self, txs): return len(set([tx.hash for tx in txs])) == len(txs)\n", 147 | " def __getitem__(self, key):return self.txs[key] \n", 148 | " def __str__(self): return (str(self.header)\n", 149 | " +'\\n'+str(self.info)\n", 150 | " +'\\n\\ntxs:\\n' \n", 151 | " +'\\n'.join(tx.smry() for tx in self.txs.values()))" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "Create random list of signed txs." 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": 14, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "w1,w2 = Wallet(),Wallet()\n", 168 | "def r_stxs(n): return [w1.signed_tx(w2,ri(1,9),ri(1,9)/100) for _ in range(n)]" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "Create block from a block header containing its txs merkle tree." 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": 15, 181 | "metadata": {}, 182 | "outputs": [ 183 | { 184 | "name": "stdout", 185 | "output_type": "stream", 186 | "text": [ 187 | "root: 🔶 0xd0d28a0c53...53f\n", 188 | "hash: 👻 0x1520fd5335...2fb\n", 189 | "prev_hash: 🔌 0xa622851359...2d2\n", 190 | "number: 2\n", 191 | "n_txs: 4\n", 192 | "mined: False\n", 193 | "time: Sun Apr 11 20:53:49 2021\n", 194 | "volume: 28.0 eth\n", 195 | "hash: 📤 0x7e39fb4082...477\n", 196 | "fees: 0.21 eth\n", 197 | "\n", 198 | "txs:\n", 199 | "📮 0x88a6 -> 📽 0x97a7 7.0 eth\n", 200 | "📮 0x88a6 -> 📽 0x97a7 9.0 eth\n", 201 | "📮 0x88a6 -> 📽 0x97a7 8.0 eth\n", 202 | "📮 0x88a6 -> 📽 0x97a7 4.0 eth\n" 203 | ] 204 | } 205 | ], 206 | "source": [ 207 | "txs = r_stxs(4)\n", 208 | "mt = MerkleTree(txs)\n", 209 | "bh = Header(mt.root, rh(), 2, len(txs))\n", 210 | "b = Block(bh, txs); print(b)" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": {}, 216 | "source": [ 217 | "### Block JSON\n", 218 | "\n", 219 | "Every block has a json representation that we will use for our API later." 220 | ] 221 | }, 222 | { 223 | "cell_type": "code", 224 | "execution_count": 16, 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "block_json = b.json()" 229 | ] 230 | }, 231 | { 232 | "cell_type": "markdown", 233 | "metadata": {}, 234 | "source": [ 235 | "We can load this JSON string `d` and create a block object again." 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": 17, 241 | "metadata": {}, 242 | "outputs": [], 243 | "source": [ 244 | "def load_block(d):\n", 245 | " d = json.loads(d)\n", 246 | " txs = []\n", 247 | " for tx in d['txs']: txs.append(load_tx(tx))\n", 248 | " bh = Header(d['root'],d['prev_hash'],d['number'],d['n_txs']) \n", 249 | " for k,v in d.items(): setattr(bh, k, v)\n", 250 | " bh.hash = d['hash']\n", 251 | " b = Block(bh, txs)\n", 252 | " return b\n", 253 | " \n", 254 | "b_from_json = load_block(b.json())" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": 18, 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "assert b == b_from_json" 264 | ] 265 | }, 266 | { 267 | "cell_type": "markdown", 268 | "metadata": {}, 269 | "source": [ 270 | "#### Access specific tx from block with its hash." 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": 19, 276 | "metadata": {}, 277 | "outputs": [ 278 | { 279 | "data": { 280 | "text/plain": [ 281 | "'0x28de3ee962255c13968ef8497966194f272085d4ea2c91a35239c13115608f92'" 282 | ] 283 | }, 284 | "execution_count": 19, 285 | "metadata": {}, 286 | "output_type": "execute_result" 287 | } 288 | ], 289 | "source": [ 290 | "tx_hash = next(iter(b.txs.keys())); tx_hash" 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": 20, 296 | "metadata": {}, 297 | "outputs": [ 298 | { 299 | "name": "stdout", 300 | "output_type": "stream", 301 | "text": [ 302 | "fr: 📮 0x88a629B7F4...b55\n", 303 | "hash: 💎 0x28de3ee962...f92\n", 304 | "to: 📽 0x97a78D775b...4a1\n", 305 | "value: 7.0 eth\n", 306 | "fee: 0.08 eth\n", 307 | "nonce: 0\n", 308 | "time: Sun Apr 11 20:53:49 2021\n", 309 | "signed: True\n", 310 | "sig: 🕒 0xeca9040241...f1c\n" 311 | ] 312 | } 313 | ], 314 | "source": [ 315 | "tx = b[tx_hash]; print(tx)" 316 | ] 317 | }, 318 | { 319 | "cell_type": "markdown", 320 | "metadata": {}, 321 | "source": [ 322 | "Blocks can only contain unique txs. " 323 | ] 324 | }, 325 | { 326 | "cell_type": "code", 327 | "execution_count": 21, 328 | "metadata": {}, 329 | "outputs": [], 330 | "source": [ 331 | "txs[0] = txs[1]\n", 332 | "\n", 333 | "mt = MerkleTree(txs)\n", 334 | "bh = Header(mt.root, rh(), 2, len(txs))" 335 | ] 336 | }, 337 | { 338 | "cell_type": "code", 339 | "execution_count": 22, 340 | "metadata": {}, 341 | "outputs": [], 342 | "source": [ 343 | "dup_detected = False\n", 344 | "try: Block(bh, txs)\n", 345 | "except: dup_detected = True\n", 346 | "assert dup_detected" 347 | ] 348 | } 349 | ], 350 | "metadata": { 351 | "kernelspec": { 352 | "display_name": "Python 3", 353 | "language": "python", 354 | "name": "python3" 355 | }, 356 | "language_info": { 357 | "codemirror_mode": { 358 | "name": "ipython", 359 | "version": 3 360 | }, 361 | "file_extension": ".py", 362 | "mimetype": "text/x-python", 363 | "name": "python", 364 | "nbconvert_exporter": "python", 365 | "pygments_lexer": "ipython3", 366 | "version": "3.8.3" 367 | } 368 | }, 369 | "nbformat": 4, 370 | "nbformat_minor": 5 371 | } 372 | -------------------------------------------------------------------------------- /02_transaction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 23, 6 | "id": "insured-schedule", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "%run 01_hash.ipynb" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 24, 16 | "id": "uniform-tattoo", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from copy import deepcopy" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "id": "resistant-dylan", 26 | "metadata": {}, 27 | "source": [ 28 | "## Transaction\n", 29 | "\n", 30 | "A transaction (abbr. tx) is used for transfering `value` from one address `fr` to another `to`.\n", 31 | "\n", 32 | "The `fee` goes to the miner who included the tx in a block. More on mining in [05_block.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/04_mine.ipynb). The higher it is, the more likely it is included in a block.\n", 33 | "\n", 34 | "The `nonce` is the number of transactions sent from a given address. It's used to avoid replay attacks. For a more detailed explanation, see [here](https://kb.myetherwallet.com/en/transactions/what-is-nonce/). \n", 35 | "\n", 36 | "It's `hash` is a unique fingerprint. Every time something in the tx changes, it gets recalculated." 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 25, 42 | "id": "identified-proposition", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "class TX(Hashable): \n", 47 | " def __init__(self, fr, to, value, fee, nonce): \n", 48 | " self.fr, self.to = fr, to\n", 49 | " self.value = float(value)\n", 50 | " self.fee = fee\n", 51 | " self.nonce = nonce\n", 52 | " self.time = time.ctime()\n", 53 | " self.signed = False\n", 54 | " \n", 55 | " def __setattr__(self, prop, val):\n", 56 | " super().__setattr__(prop, val)\n", 57 | " if prop == 'sig': self.signed = True\n", 58 | " \n", 59 | " def smry(self): return f'{pmh(self.fr)} -> {pmh(self.to)} {self.value} eth'" 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "id": "cognitive-deputy", 65 | "metadata": {}, 66 | "source": [ 67 | "Create random txs." 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": 26, 73 | "id": "starting-horizon", 74 | "metadata": {}, 75 | "outputs": [ 76 | { 77 | "name": "stdout", 78 | "output_type": "stream", 79 | "text": [ 80 | "fr: 📾 0x98cffe36dc...69c\n", 81 | "hash: 📦 0x80c95f6ada...9b7\n", 82 | "to: 📀 0x5aaab7d340...357\n", 83 | "value: 12.0 eth\n", 84 | "fee: 0.2 eth\n", 85 | "nonce: 0\n", 86 | "time: Tue Apr 6 15:15:32 2021\n", 87 | "signed: False\n" 88 | ] 89 | } 90 | ], 91 | "source": [ 92 | "tx1 = TX(rh(), rh(), 12.0, 0.2, 0)\n", 93 | "tx2 = TX(rh(), rh(), 6.0, 0.15, 0); print(tx1)" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "id": "06f223d1", 99 | "metadata": {}, 100 | "source": [ 101 | "#### TX Summary" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 27, 107 | "id": "4a785a39", 108 | "metadata": {}, 109 | "outputs": [ 110 | { 111 | "data": { 112 | "text/plain": [ 113 | "'📾 0x98cf -> 📀 0x5aaa 12.0 eth'" 114 | ] 115 | }, 116 | "execution_count": 27, 117 | "metadata": {}, 118 | "output_type": "execute_result" 119 | } 120 | ], 121 | "source": [ 122 | "tx1.smry()" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "id": "failing-bradford", 128 | "metadata": {}, 129 | "source": [ 130 | "### TX JSON\n", 131 | "\n", 132 | "Every tx has a json representation that we will use for our API later." 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 28, 138 | "id": "artistic-basic", 139 | "metadata": {}, 140 | "outputs": [ 141 | { 142 | "name": "stdout", 143 | "output_type": "stream", 144 | "text": [ 145 | "{\n", 146 | " \"fr\": \"0x98cffe36dc311f41674d7b40fab407fcb2a3b16cdb5596f8a6e2f86034c8169c\",\n", 147 | " \"hash\": \"0x80c95f6ada198ed080df3ec72310fd43eb19203a9740bf59db6a68c4b9fe99b7\",\n", 148 | " \"to\": \"0x5aaab7d340e4205c0756fb14abcae67508685220b15e34a0e59e5824c6a2e357\",\n", 149 | " \"value\": 12.0,\n", 150 | " \"fee\": 0.2,\n", 151 | " \"nonce\": 0,\n", 152 | " \"time\": \"Tue Apr 6 15:15:32 2021\",\n", 153 | " \"signed\": false\n", 154 | "}\n" 155 | ] 156 | } 157 | ], 158 | "source": [ 159 | "tx1_json = tx1.json(); print(tx1_json)" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "id": "proved-thomas", 165 | "metadata": {}, 166 | "source": [ 167 | "We can load this JSON string `d` and create a tx object." 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 29, 173 | "id": "temporal-funds", 174 | "metadata": {}, 175 | "outputs": [], 176 | "source": [ 177 | "def load_tx(d):\n", 178 | " d = json.loads(d)\n", 179 | " tx = TX(d['fr'],d['to'],d['value'],d['fee'],d['nonce'])\n", 180 | " for k,v in d.items(): setattr(tx, k, v)\n", 181 | " tx.hash = d['hash']\n", 182 | " return tx" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 30, 188 | "id": "finite-platinum", 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "tx1_from_json = load_tx(tx1_json)\n", 193 | "assert tx1 == tx1_from_json" 194 | ] 195 | }, 196 | { 197 | "cell_type": "markdown", 198 | "id": "governing-oliver", 199 | "metadata": {}, 200 | "source": [ 201 | "#### Print several txs in a nice way." 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": 31, 207 | "id": "close-turtle", 208 | "metadata": {}, 209 | "outputs": [ 210 | { 211 | "name": "stdout", 212 | "output_type": "stream", 213 | "text": [ 214 | "fr: 📾 0x98cffe36dc...69c\n", 215 | "hash: 📦 0x80c95f6ada...9b7\n", 216 | "to: 📀 0x5aaab7d340...357\n", 217 | "value: 12.0 eth\n", 218 | "fee: 0.2 eth\n", 219 | "nonce: 0\n", 220 | "time: Tue Apr 6 15:15:32 2021\n", 221 | "signed: False\n", 222 | "\n", 223 | "fr: 💹 0x532666a841...ab2\n", 224 | "hash: 📓 0x6dd5970374...686\n", 225 | "to: 📢 0x7c4d6916e9...dfc\n", 226 | "value: 6.0 eth\n", 227 | "fee: 0.15 eth\n", 228 | "nonce: 0\n", 229 | "time: Tue Apr 6 15:15:32 2021\n", 230 | "signed: False\n", 231 | "\n" 232 | ] 233 | } 234 | ], 235 | "source": [ 236 | "def txs2str(txs): return '\\n'.join([str(tx)+'\\n' for tx in txs])\n", 237 | "print(txs2str([tx1,tx2]))" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "id": "amino-advocacy", 243 | "metadata": {}, 244 | "source": [ 245 | "#### Changing Tx\n", 246 | "Every change in the object is reflected by its hash. Compare the tx hash below with the hash of unchanged tx above. The tx hash has changed. This is how we can make sure that nobody changes the tx without us knowing." 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 33, 252 | "id": "natural-straight", 253 | "metadata": {}, 254 | "outputs": [ 255 | { 256 | "name": "stdout", 257 | "output_type": "stream", 258 | "text": [ 259 | "fr: 💹 0x532666a841...ab2\n", 260 | "hash: 🔧 0xc1a96432c9...ff6\n", 261 | "to: 📢 0x7c4d6916e9...dfc\n", 262 | "value: 120\n", 263 | "fee: 0.15 eth\n", 264 | "nonce: 0\n", 265 | "time: Tue Apr 6 15:15:32 2021\n", 266 | "signed: False\n" 267 | ] 268 | } 269 | ], 270 | "source": [ 271 | "tx2_false_value = deepcopy(tx2)\n", 272 | "tx2_false_value.value = 120\n", 273 | "print(tx2_false_value)" 274 | ] 275 | }, 276 | { 277 | "cell_type": "markdown", 278 | "id": "heated-hotel", 279 | "metadata": {}, 280 | "source": [ 281 | "Transactions can be determined unequal by simply comparing the hashes as implemented in `__eq__`." 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": 34, 287 | "id": "daily-premises", 288 | "metadata": {}, 289 | "outputs": [], 290 | "source": [ 291 | "assert tx2 != tx2_false_value" 292 | ] 293 | }, 294 | { 295 | "cell_type": "markdown", 296 | "id": "chief-stage", 297 | "metadata": {}, 298 | "source": [ 299 | "#### Signing Tx\n", 300 | "An account (implemented in [02_account.ipynb](https://github.com/SharifElfouly/blockchain-from-scratch/blob/main/02_account.ipynb)) has the ability to sign a tx. Here we simply mock a signature with a random hash. The signature `sig` is saved as an attribute." 301 | ] 302 | }, 303 | { 304 | "cell_type": "code", 305 | "execution_count": 35, 306 | "id": "danish-french", 307 | "metadata": {}, 308 | "outputs": [ 309 | { 310 | "name": "stdout", 311 | "output_type": "stream", 312 | "text": [ 313 | "fr: 📾 0x98cffe36dc...69c\n", 314 | "hash: 📦 0x80c95f6ada...9b7\n", 315 | "to: 📀 0x5aaab7d340...357\n", 316 | "value: 12.0 eth\n", 317 | "fee: 0.2 eth\n", 318 | "nonce: 0\n", 319 | "time: Tue Apr 6 15:15:32 2021\n", 320 | "signed: True\n", 321 | "sig: 📍 0x6730e5e62d...a37\n" 322 | ] 323 | } 324 | ], 325 | "source": [ 326 | "tx1_signed = deepcopy(tx1)\n", 327 | "tx1.sig = rh(); print(tx1)" 328 | ] 329 | }, 330 | { 331 | "cell_type": "markdown", 332 | "id": "educated-entry", 333 | "metadata": {}, 334 | "source": [ 335 | "Signing the tx should not change its hash. Therefore it is equal to the unsigned tx." 336 | ] 337 | }, 338 | { 339 | "cell_type": "code", 340 | "execution_count": 36, 341 | "id": "numerous-accessory", 342 | "metadata": {}, 343 | "outputs": [], 344 | "source": [ 345 | "assert tx1 == tx1_signed" 346 | ] 347 | } 348 | ], 349 | "metadata": { 350 | "kernelspec": { 351 | "display_name": "Python 3", 352 | "language": "python", 353 | "name": "python3" 354 | }, 355 | "language_info": { 356 | "codemirror_mode": { 357 | "name": "ipython", 358 | "version": 3 359 | }, 360 | "file_extension": ".py", 361 | "mimetype": "text/x-python", 362 | "name": "python", 363 | "nbconvert_exporter": "python", 364 | "pygments_lexer": "ipython3", 365 | "version": "3.9.2" 366 | } 367 | }, 368 | "nbformat": 4, 369 | "nbformat_minor": 5 370 | } 371 | -------------------------------------------------------------------------------- /03_merkle.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "oriented-weather", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "%%capture\n", 11 | "%run 02_transaction.ipynb" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "complicated-nickname", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from IPython.display import Image\n", 22 | "from random import randint as ri" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "id": "moral-integer", 28 | "metadata": {}, 29 | "source": [ 30 | "Create random txs." 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 3, 36 | "id": "postal-french", 37 | "metadata": {}, 38 | "outputs": [ 39 | { 40 | "name": "stdout", 41 | "output_type": "stream", 42 | "text": [ 43 | "fr: 📗 0x713df0f942...508\n", 44 | "hash: 📑 0x6b42b9970c...f18\n", 45 | "to: 💟 0x398140370a...ea6\n", 46 | "value: 8.0 eth\n", 47 | "fee: 0.4 eth\n", 48 | "nonce: 0\n", 49 | "time: Sat Apr 3 18:08:46 2021\n", 50 | "signed: False\n", 51 | "\n", 52 | "fr: 🕈 0xe23cff01e1...0c5\n", 53 | "hash: 📄 0x5e4813241b...4e6\n", 54 | "to: 🔵 0xcf7939c7c3...fb3\n", 55 | "value: 2.0 eth\n", 56 | "fee: 0.9 eth\n", 57 | "nonce: 0\n", 58 | "time: Sat Apr 3 18:08:46 2021\n", 59 | "signed: False\n", 60 | "\n" 61 | ] 62 | } 63 | ], 64 | "source": [ 65 | "def rtxs(n): return [TX(rh(),rh(),ri(1,9),ri(1,9)/10,0) for _ in range(n)] \n", 66 | "print(txs2str(rtxs(2)))" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "id": "departmental-combat", 72 | "metadata": {}, 73 | "source": [ 74 | "## Merkle Tree\n", 75 | "\n", 76 | "A binary tree in which every leaf node is a hash of a specific piece of data, in our case a tx. \n", 77 | "\n", 78 | "In a system like Bitcoin or Ethereum the merkle root is included in the block header, not the list of txs itself. This makes it very efficient to test if a specific tx is inclued in the tree.\n", 79 | "\n", 80 | "For more in depth [here](https://www.youtube.com/watch?v=3giNelTfeAk).\n", 81 | "\n", 82 | "
\n", 83 | "\n", 84 | "
\n", 85 | "\n", 86 | "\n", 87 | "`MerkleTree` is a wrapper around the `mt` dictionary which stores the hashes for each height in the tree." 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 4, 93 | "id": "transsexual-inquiry", 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "class MerkleTree:\n", 98 | " def __init__(self, txs):\n", 99 | " self.mt = {}\n", 100 | " leafs = [tx.hash for tx in txs]\n", 101 | " if len(leafs)%2!=0: leafs.append(sha('0x0'))\n", 102 | " self.mt['1'] = leafs\n", 103 | " self.merkle(leafs)\n", 104 | " \n", 105 | " def merkle(self, leafs):\n", 106 | " parents = []\n", 107 | " while len(leafs) > 1:\n", 108 | " for i in range(0, len(leafs), 2):\n", 109 | " l = leafs[i]\n", 110 | " if i+1>=len(leafs): r = '0x0'\n", 111 | " else : r = leafs[i+1]\n", 112 | " parents.append(sha(l+r))\n", 113 | " leafs = parents\n", 114 | " self.mt[f'{len(self)+1}'] = parents\n", 115 | " parents = []\n", 116 | " \n", 117 | " @property\n", 118 | " def root(self) : return self.mt[str(len(self))][0]\n", 119 | " def __eq__ (self, o): return self.root == o.root\n", 120 | " def __len__(self) : return len(self.mt)\n", 121 | " def __getitem__(self, k): return self.mt[str(k)]\n", 122 | " def __str__(self):\n", 123 | " s = ''\n", 124 | " for k,v in self.mt.items(): \n", 125 | " s += f'height {k}'\n", 126 | " for h in v: s += f'\\n {ph(h)}'\n", 127 | " s += '\\n'\n", 128 | " return s" 129 | ] 130 | }, 131 | { 132 | "cell_type": "markdown", 133 | "id": "parliamentary-resistance", 134 | "metadata": {}, 135 | "source": [ 136 | "Create Merkle Tree from `N` random txs." 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 5, 142 | "id": "lined-spending", 143 | "metadata": {}, 144 | "outputs": [ 145 | { 146 | "name": "stdout", 147 | "output_type": "stream", 148 | "text": [ 149 | "height 1\n", 150 | " 💩 0x4396e89fb2...c55\n", 151 | " 📌 0x66ff039daf...b97\n", 152 | " 💍 0x279efbfe85...818\n", 153 | " 🔪 0xc483749b21...163\n", 154 | " 🕎 0xe85a8febae...d20\n", 155 | " 💢 0x3c081c2e2b...523\n", 156 | " 📳 0x8dd0ff431f...3a4\n", 157 | " 💩 0x43e73a1f62...4d2\n", 158 | "height 2\n", 159 | " 📈 0x62dfc45282...3ef\n", 160 | " 🕏 0xe9b65e0b40...6ec\n", 161 | " 👸 0x12feff919f...6d1\n", 162 | " 📅 0x5ff42917f4...ec7\n", 163 | "height 3\n", 164 | " 🔛 0xb5bbdce4ee...ba7\n", 165 | " 🔄 0x9e078582f1...d7c\n", 166 | "height 4\n", 167 | " 💰 0x4ad4619814...69e\n", 168 | "\n" 169 | ] 170 | } 171 | ], 172 | "source": [ 173 | "N = 8\n", 174 | "txs = rtxs(N)\n", 175 | "\n", 176 | "mt = MerkleTree(txs); print(mt)" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 6, 182 | "id": "direct-prefix", 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [ 186 | "assert len(mt) == 4" 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "id": "passive-attempt", 192 | "metadata": {}, 193 | "source": [ 194 | "The merkle tree root hash is the top most node. If anything in the tree changes the root changes as well." 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 7, 200 | "id": "organic-lease", 201 | "metadata": {}, 202 | "outputs": [ 203 | { 204 | "data": { 205 | "text/plain": [ 206 | "'💰 0x4ad4619814...69e'" 207 | ] 208 | }, 209 | "execution_count": 7, 210 | "metadata": {}, 211 | "output_type": "execute_result" 212 | } 213 | ], 214 | "source": [ 215 | "ph(mt.root)" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "id": "secure-magic", 221 | "metadata": {}, 222 | "source": [ 223 | "If we change something in any tx the whole Merkle Tree changes. Here we change the `value` of the third tx. Compare the hashes below with the ones above." 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 8, 229 | "id": "fourth-rebate", 230 | "metadata": {}, 231 | "outputs": [ 232 | { 233 | "name": "stdout", 234 | "output_type": "stream", 235 | "text": [ 236 | "height 1\n", 237 | " 💩 0x4396e89fb2...c55\n", 238 | " 📌 0x66ff039daf...b97\n", 239 | " 🕟 0xf9d575acd1...86a\n", 240 | " 🔪 0xc483749b21...163\n", 241 | " 🕎 0xe85a8febae...d20\n", 242 | " 💢 0x3c081c2e2b...523\n", 243 | " 📳 0x8dd0ff431f...3a4\n", 244 | " 💩 0x43e73a1f62...4d2\n", 245 | "height 2\n", 246 | " 📈 0x62dfc45282...3ef\n", 247 | " 🕋 0xe5550fd67e...e35\n", 248 | " 👸 0x12feff919f...6d1\n", 249 | " 📅 0x5ff42917f4...ec7\n", 250 | "height 3\n", 251 | " 💮 0x487d9b2721...89a\n", 252 | " 🔄 0x9e078582f1...d7c\n", 253 | "height 4\n", 254 | " 💳 0x4dfde676ee...f07\n", 255 | "\n" 256 | ] 257 | } 258 | ], 259 | "source": [ 260 | "txs[2].value = 500\n", 261 | "\n", 262 | "changed_mt = MerkleTree(txs); print(changed_mt)" 263 | ] 264 | }, 265 | { 266 | "cell_type": "code", 267 | "execution_count": 9, 268 | "id": "civilian-compound", 269 | "metadata": {}, 270 | "outputs": [], 271 | "source": [ 272 | "assert mt != changed_mt" 273 | ] 274 | }, 275 | { 276 | "cell_type": "markdown", 277 | "id": "continental-carolina", 278 | "metadata": {}, 279 | "source": [ 280 | "## Merkle Proof" 281 | ] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "id": "electoral-controversy", 286 | "metadata": {}, 287 | "source": [ 288 | "Proof that `tx` at position `pos` in the merkle tree `mt` is part of it by using a corresponding merkle branch `mb`.\n", 289 | "\n", 290 | "A merkle branch consists of those nodes that are needed to build up the root hash of the merkle tree. For example:\n", 291 | "\n", 292 | "Merkle branch `[5,3,0, ..., x]` stands for the following: \n", 293 | "- node *5* from height *0*\n", 294 | "- node *3* from height *1*\n", 295 | "- node *0* from height *2*\n", 296 | "- ...\n", 297 | "- node *x* from height `len(mt)-1`" 298 | ] 299 | }, 300 | { 301 | "cell_type": "code", 302 | "execution_count": 10, 303 | "id": "raised-france", 304 | "metadata": {}, 305 | "outputs": [], 306 | "source": [ 307 | "def proof(mt, mb, tx):\n", 308 | " if mb[0]%2 != 0: cn = sha(tx.hash +mt[1][mb[0]])\n", 309 | " else : cn = sha(mt[1][mb[0]]+tx.hash)\n", 310 | " for i in range(2, len(mt)):\n", 311 | " if mb[i-1]%2 != 0: cn = sha(cn +mt[i][mb[i-1]])\n", 312 | " else : cn = sha(mt[i][mb[i-1]]+cn)\n", 313 | " return cn == mt.root" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "id": "soviet-underground", 319 | "metadata": {}, 320 | "source": [ 321 | "Examples of some txs with their corresponding merkle branch." 322 | ] 323 | }, 324 | { 325 | "cell_type": "code", 326 | "execution_count": 11, 327 | "id": "pursuant-architect", 328 | "metadata": {}, 329 | "outputs": [], 330 | "source": [ 331 | "tx = txs[0]\n", 332 | "mb = [1,1,1]\n", 333 | "\n", 334 | "assert proof(mt, mb, tx)" 335 | ] 336 | }, 337 | { 338 | "cell_type": "code", 339 | "execution_count": 12, 340 | "id": "covered-longer", 341 | "metadata": {}, 342 | "outputs": [], 343 | "source": [ 344 | "tx = txs[1]\n", 345 | "mb = [0,1,1]\n", 346 | "\n", 347 | "assert proof(mt, mb, tx)" 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": 13, 353 | "id": "impaired-banks", 354 | "metadata": {}, 355 | "outputs": [], 356 | "source": [ 357 | "tx = txs[4]\n", 358 | "mb = [5,3,0]\n", 359 | "\n", 360 | "assert proof(mt, mb, tx)" 361 | ] 362 | } 363 | ], 364 | "metadata": { 365 | "kernelspec": { 366 | "display_name": "Python 3", 367 | "language": "python", 368 | "name": "python3" 369 | }, 370 | "language_info": { 371 | "codemirror_mode": { 372 | "name": "ipython", 373 | "version": 3 374 | }, 375 | "file_extension": ".py", 376 | "mimetype": "text/x-python", 377 | "name": "python", 378 | "nbconvert_exporter": "python", 379 | "pygments_lexer": "ipython3", 380 | "version": "3.8.2" 381 | } 382 | }, 383 | "nbformat": 4, 384 | "nbformat_minor": 5 385 | } 386 | -------------------------------------------------------------------------------- /99_contract.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 224, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%%capture\n", 10 | "%run 01_hash.ipynb\n", 11 | "%run 02_transaction.ipynb\n", 12 | "%run 05_block.ipynb" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 225, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import subprocess as sub" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "## 99_contract\n", 29 | "\n", 30 | "Our goal is to be able to run a small piece of code in a tx, just like Ethereum. This piece of code is also called a smart contract (sc). The sc is like another user, but in addition to the balance it also has storage for variables and the sc itself. \n", 31 | "\n", 32 | "The smart contract should be able to access the current tx and block information. These will get injected as `_tx` and `_b`." 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "metadata": {}, 38 | "source": [ 39 | "The smart contract that we are going to run will contain the following logic:" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 226, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "cc = \"\"\"\n", 49 | "def init():\n", 50 | " storage r = 0\n", 51 | " s = 2\n", 52 | " h = 'hello how are you?'\n", 53 | " storage l = ['this', 'is', 'a', 'list']\n", 54 | " b = _b['number'] + 100\n", 55 | "\n", 56 | "r += 5\n", 57 | "storage time = _tx['time'] + 'XXXXX'\n", 58 | "l.append('XXXXX')\n", 59 | "\"\"\"" 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "metadata": {}, 65 | "source": [ 66 | "Temporary file name where code for execution will be stored." 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 227, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "F = 'temp.py'" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "This is injected into every execution to get all the variables used in the contract." 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 228, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "PRINT_VARS = \"\"\"\n", 92 | "\\n\n", 93 | "import json\n", 94 | "_types = [str, float, int, list] \n", 95 | "out = {} \n", 96 | "keys = list(locals().keys()) \n", 97 | "for k in keys: \n", 98 | " if not k.startswith('_'): \n", 99 | " v = locals()[k] \n", 100 | " for t in _types: \n", 101 | " if isinstance(v, t): out[k] = [v, str(t)] \n", 102 | "print(json.dumps(out)) \n", 103 | "\"\"\"\n", 104 | "PRINT_VARS = PRINT_VARS.rstrip()" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "All `storage` vars used will be stored in the `STORAGE` dict." 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 229, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "STORAGE = {}" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "Inject transaction `tx` and `block` into the execution. These can be accessed in the sc." 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 230, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "def inject(tx, code, block=None): \n", 137 | " tx = tx.json().replace('true', 'True').replace('false', 'False')\n", 138 | " b = block.json().replace('true', 'True').replace('false', 'False')\n", 139 | " return '_tx = '+tx+'\\n'+'_b = '+b+'\\n'+code" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 231, 145 | "metadata": {}, 146 | "outputs": [], 147 | "source": [ 148 | "def save(code):\n", 149 | " with open(F, 'w+') as f: f.write(code)" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 232, 155 | "metadata": {}, 156 | "outputs": [], 157 | "source": [ 158 | "def run():\n", 159 | " r = sub.check_output(f'python3 {F}', shell=True)\n", 160 | " return json.loads(r)" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "metadata": {}, 166 | "source": [ 167 | "The deployment will run all the code in `init()`. The rest will run in `execution`." 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 233, 173 | "metadata": {}, 174 | "outputs": [], 175 | "source": [ 176 | "def deployable(code):\n", 177 | " init = []\n", 178 | " lines = code.split('\\n')\n", 179 | " for i,l in enumerate(lines):\n", 180 | " if 'init' in l: \n", 181 | " for j in range(i+1, len(lines)):\n", 182 | " if lines[j][:1] == ' ': init.append(lines[j].strip())\n", 183 | " else : break\n", 184 | " init.append(PRINT_VARS)\n", 185 | " return '\\n'.join(l for l in init)" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "metadata": {}, 191 | "source": [ 192 | "Execution will run everything in the sc except what is inside `init()`." 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 234, 198 | "metadata": {}, 199 | "outputs": [], 200 | "source": [ 201 | "def executable(code):\n", 202 | " exe = []\n", 203 | " for k,v in STORAGE.items():\n", 204 | " if 'str' in v[1]: exe.append(f'{k} = \"{v[0]}\"')\n", 205 | " else : exe.append(f'{k} = {v[0]}')\n", 206 | " lines = code.split('\\n')\n", 207 | " in_init = False\n", 208 | " for l in lines:\n", 209 | " if 'init' in l: in_init = True; continue\n", 210 | " if in_init:\n", 211 | " if l[:1] == ' ': continue\n", 212 | " else : in_init = False\n", 213 | " else: exe.append(l)\n", 214 | " exe.append(PRINT_VARS)\n", 215 | " return '\\n'.join(l for l in exe)" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "metadata": {}, 221 | "source": [ 222 | "## Smart Contract\n", 223 | "\n", 224 | "Stores all the necesary information for deploying and running smart contract `code`. \n", 225 | "\n", 226 | "The `tx` and `b` should be injected when the contract is run on the blockchain. This way the contract has always access to its tx and current block." 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "metadata": {}, 232 | "source": [ 233 | "Stores all variables in `STORAGE`." 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": 235, 239 | "metadata": {}, 240 | "outputs": [], 241 | "source": [ 242 | "def parse(c): return code.replace('storage ', '')" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": 236, 248 | "metadata": {}, 249 | "outputs": [], 250 | "source": [ 251 | "class SmartContract(Hashable):\n", 252 | " def __init__(self, code):\n", 253 | " self.st_vars = storage_vars(code)\n", 254 | " self.code = parse(code)\n", 255 | " self.deployed = False\n", 256 | " self.time = time.ctime()\n", 257 | " self.runs = 0\n", 258 | " self.address = sha(self.__dict__)\n", 259 | " \n", 260 | " def store(self, r):\n", 261 | " for k,v in r.items():\n", 262 | " if k in self.st_vars: STORAGE[k] = [v[0],v[1]]\n", 263 | "\n", 264 | " def exe(self, code, tx, b):\n", 265 | " ic = inject(tx, code, b)\n", 266 | " save(ic)\n", 267 | " r = run()\n", 268 | " self.store(r)\n", 269 | " \n", 270 | " def deploy(self, tx, b): \n", 271 | " dc = deployable(self.code)\n", 272 | " self.exe(dc, tx, b)\n", 273 | " self.deployed = True\n", 274 | " \n", 275 | " def run(self, tx, b): \n", 276 | " assert self.deployed, 'deploy contract first'\n", 277 | " ic = executable(self.code)\n", 278 | " self.exe(ic, tx, b)\n", 279 | " self.runs += 1" 280 | ] 281 | }, 282 | { 283 | "cell_type": "markdown", 284 | "metadata": {}, 285 | "source": [ 286 | "Notice that only the storage vars will be stored in `STORAGE`" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "Each smart contract has a unique address." 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": 237, 299 | "metadata": {}, 300 | "outputs": [ 301 | { 302 | "data": { 303 | "text/plain": [ 304 | "'0xfe9aaf47a2dea123f920743e24f103c26e6e4cbcc2a3ff6be84ed42f55aa6bfc'" 305 | ] 306 | }, 307 | "execution_count": 237, 308 | "metadata": {}, 309 | "output_type": "execute_result" 310 | } 311 | ], 312 | "source": [ 313 | "sc = SmartContract(code)\n", 314 | "sc.address" 315 | ] 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": 238, 320 | "metadata": {}, 321 | "outputs": [ 322 | { 323 | "data": { 324 | "text/plain": [ 325 | "{'r': [0, \"\"],\n", 326 | " 'l': [['this', 'is', 'a', 'list'], \"\"]}" 327 | ] 328 | }, 329 | "execution_count": 238, 330 | "metadata": {}, 331 | "output_type": "execute_result" 332 | } 333 | ], 334 | "source": [ 335 | "sc.deploy(tx1, b)\n", 336 | "STORAGE" 337 | ] 338 | }, 339 | { 340 | "cell_type": "code", 341 | "execution_count": 239, 342 | "metadata": {}, 343 | "outputs": [ 344 | { 345 | "data": { 346 | "text/plain": [ 347 | "{'r': [5, \"\"],\n", 348 | " 'l': [['this', 'is', 'a', 'list', 'XXXXX'], \"\"],\n", 349 | " 'time': ['Mon Apr 12 22:12:23 2021XXXXX', \"\"]}" 350 | ] 351 | }, 352 | "execution_count": 239, 353 | "metadata": {}, 354 | "output_type": "execute_result" 355 | } 356 | ], 357 | "source": [ 358 | "sc.run(tx1, b)\n", 359 | "STORAGE" 360 | ] 361 | }, 362 | { 363 | "cell_type": "markdown", 364 | "metadata": {}, 365 | "source": [ 366 | "## Gas\n", 367 | "To avoid smart contracts that run forever, we are going to meter gas usage. Every computational step uses gas. To make things easier we will just track the time the execution takes." 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": 312, 373 | "metadata": {}, 374 | "outputs": [ 375 | { 376 | "data": { 377 | "text/plain": [ 378 | "54" 379 | ] 380 | }, 381 | "execution_count": 312, 382 | "metadata": {}, 383 | "output_type": "execute_result" 384 | } 385 | ], 386 | "source": [ 387 | "def gas():\n", 388 | " s = time.time()\n", 389 | " run()\n", 390 | " e = time.time()\n", 391 | " return int((e-s)*1000)\n", 392 | "\n", 393 | "gas()" 394 | ] 395 | } 396 | ], 397 | "metadata": { 398 | "kernelspec": { 399 | "display_name": "Python 3", 400 | "language": "python", 401 | "name": "python3" 402 | }, 403 | "language_info": { 404 | "codemirror_mode": { 405 | "name": "ipython", 406 | "version": 3 407 | }, 408 | "file_extension": ".py", 409 | "mimetype": "text/x-python", 410 | "name": "python", 411 | "nbconvert_exporter": "python", 412 | "pygments_lexer": "ipython3", 413 | "version": "3.8.3" 414 | } 415 | }, 416 | "nbformat": 4, 417 | "nbformat_minor": 4 418 | } 419 | -------------------------------------------------------------------------------- /07_blockchain.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "special-somewhere", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "%%capture\n", 11 | "%run 05_block.ipynb\n", 12 | "%run 06_mine.ipynb" 13 | ] 14 | }, 15 | { 16 | "cell_type": "markdown", 17 | "id": "twenty-shore", 18 | "metadata": {}, 19 | "source": [ 20 | "Account state holds the current balance and nonce of an account." 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 21, 26 | "id": "tribal-network", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "class AccountState:\n", 31 | " def __init__(self): self.balance,self.nonce = 0,0\n", 32 | " def inc_nonce (self): self.nonce += 1\n", 33 | " def add_balance(self, value): self.balance += value\n", 34 | " def sub_balance(self, value): self.balance -= value\n", 35 | " def json(self): return json.dumps(self.__dict__)\n", 36 | " def __str__(self): return f'balance: {self.balance}\\nnonce: {self.nonce}'" 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "id": "derived-retro", 42 | "metadata": {}, 43 | "source": [ 44 | "The state stores the dictionary `state` which stores the `AccountState` for each address. A genesis block is used to initialize state the first time.\n", 45 | "\n", 46 | "The state is updated with txs. `apply` validates if the tx is correct and then updates each `AccountState` accordingly." 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 22, 52 | "id": "stretch-latex", 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "class State:\n", 57 | " def __init__(self): self.state={}\n", 58 | " def new (self, pub): self.state[pub] = AccountState()\n", 59 | " def is_new(self, pub): return pub not in self.state\n", 60 | " def add(self, pubs):\n", 61 | " for pub in pubs:\n", 62 | " if self.is_new(pub): self.new(pub)\n", 63 | " \n", 64 | " def init(self, gb):\n", 65 | " gh = gb.header\n", 66 | " assert gh.number == 0\n", 67 | " assert gh.prev_hash == '0x0'\n", 68 | " for tx in gb.txs.values():\n", 69 | " self.add([tx.fr, tx.to])\n", 70 | " assert val_sig(tx)\n", 71 | " self.state[tx.fr].inc_nonce()\n", 72 | " self.state[tx.to].add_balance(tx.value)\n", 73 | " \n", 74 | " def apply(self, tx, miner):\n", 75 | " self.add([tx.fr,tx.to,miner])\n", 76 | " if tx.fr != tx.to: \n", 77 | " assert val_sig(tx)\n", 78 | " assert self.state[tx.fr].nonce == tx.nonce\n", 79 | " assert self.state[tx.fr].balance - tx.value > 0\n", 80 | " else: \n", 81 | " assert tx.fr == tx.to == miner\n", 82 | " self.state[tx.fr].inc_nonce()\n", 83 | " self.state[tx.fr].sub_balance(tx.value)\n", 84 | " self.state[tx.to].add_balance(tx.value)\n", 85 | " self.state[miner].add_balance(tx.fee)\n", 86 | " return True\n", 87 | " \n", 88 | " def apply_reward(self, miner, reward):\n", 89 | " self.state[miner].add_balance(reward)\n", 90 | " \n", 91 | " def __getitem__(self, key): return self.state[key] \n", 92 | " def __str__(self):\n", 93 | " return '\\n'.join(f'{ph(k)}\\n{v}\\n' for k,v in self.state.items())" 94 | ] 95 | }, 96 | { 97 | "cell_type": "markdown", 98 | "id": "precious-parts", 99 | "metadata": {}, 100 | "source": [ 101 | "## Blockchain\n", 102 | "\n", 103 | "Consists of a number of blocks. Each block points to the block that came before it. Imagine a linked list of blocks linked by its `prev_hash`.\n", 104 | "\n", 105 | "A `candidate` block is one that could be used for mining." 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 23, 111 | "id": "stylish-sleeping", 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "class Blockchain:\n", 116 | " def __init__(self, gb):\n", 117 | " self.state = State()\n", 118 | " self.blocks = []\n", 119 | " self.state.init(gb)\n", 120 | " self.blocks.append(gb)\n", 121 | "\n", 122 | " def candidate(self, txs):\n", 123 | " mt = MerkleTree(txs)\n", 124 | " bh = Header(mt.root, self.blocks[-1].hash, len(self.blocks), len(txs))\n", 125 | " return Block(bh, txs)\n", 126 | " \n", 127 | " def add(self, mb):\n", 128 | " assert self.val(mb)\n", 129 | " for tx in mb.txs.values(): assert self.state.apply(tx, mb.header.miner)\n", 130 | " self.state.apply_reward(mb.header.miner, mb.header.reward)\n", 131 | " self.blocks.append(mb)\n", 132 | " return True\n", 133 | " \n", 134 | " def val(self, mb):\n", 135 | " bh = mb.header\n", 136 | " assert val_pow(mb)\n", 137 | " assert bh.number == len(self.blocks)\n", 138 | " if len(self.blocks) > 0: assert self.blocks[-1].hash == bh.prev_hash\n", 139 | " return True\n", 140 | " \n", 141 | " def __getitem__(self, key): return self.state.__getitem__(key)\n", 142 | " def __str__(self): \n", 143 | " return (('\\n'*2+'--'*20+'\\n'*2).join(f'{str(block)}' for block in self.blocks)\n", 144 | " +'\\n'*2+'-'*6+'\\nstate:\\n'+'-'*6\n", 145 | " +'\\n'+str(self.state))" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 24, 151 | "id": "western-definition", 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "acc1,acc2,acc3 = Account(),Account(),Account()\n", 156 | "miner = Miner(acc3)" 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": 25, 162 | "id": "potential-pound", 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "tx1 = acc1.signed_tx(acc2, 24, 0.1)\n", 167 | "tx2 = acc2.signed_tx(acc1, 18, 0.12)\n", 168 | "\n", 169 | "gb = mine_genesis([tx1, tx2])" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 27, 175 | "id": "worst-county", 176 | "metadata": {}, 177 | "outputs": [ 178 | { 179 | "name": "stdout", 180 | "output_type": "stream", 181 | "text": [ 182 | "root: 📤 0x7e04e96571...ee3\n", 183 | "hash: 🕟 0xf9a0ba7c86...223\n", 184 | "prev_hash: 0x0\n", 185 | "number: 0\n", 186 | "n_txs: 2\n", 187 | "mined: False\n", 188 | "time: 1617571974.650272 eth\n", 189 | "volume: 42.0 eth\n", 190 | "fees: 0.22 eth\n", 191 | "\n", 192 | "txs:\n", 193 | "💳 0x4D4d -> 🕗 0xf19d 24.0 eth\n", 194 | "🕗 0xf19d -> 💳 0x4D4d 18.0 eth\n", 195 | "\n", 196 | "------\n", 197 | "state:\n", 198 | "------\n", 199 | "💳 0x4D4d017Ea9...444\n", 200 | "balance: 18.0\n", 201 | "nonce: 1\n", 202 | "\n", 203 | "🕗 0xf19dB46598...b3C\n", 204 | "balance: 24.0\n", 205 | "nonce: 1\n", 206 | "\n" 207 | ] 208 | } 209 | ], 210 | "source": [ 211 | "bc = Blockchain(gb); print(bc)" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": 28, 217 | "id": "blessed-behavior", 218 | "metadata": {}, 219 | "outputs": [ 220 | { 221 | "name": "stdout", 222 | "output_type": "stream", 223 | "text": [ 224 | "root: 📤 0x7e04e96571...ee3\n", 225 | "hash: 🕟 0xf9a0ba7c86...223\n", 226 | "prev_hash: 0x0\n", 227 | "number: 0\n", 228 | "n_txs: 2\n", 229 | "mined: False\n", 230 | "time: 1617571974.650272 eth\n", 231 | "volume: 42.0 eth\n", 232 | "fees: 0.22 eth\n", 233 | "\n", 234 | "txs:\n", 235 | "💳 0x4D4d -> 🕗 0xf19d 24.0 eth\n", 236 | "🕗 0xf19d -> 💳 0x4D4d 18.0 eth\n", 237 | "\n", 238 | "----------------------------------------\n", 239 | "\n", 240 | "root: 🔛 0xb50e329471...f16\n", 241 | "hash: 🔁 0x9b76e4fd28...650\n", 242 | "prev_hash: 🕟 0xf9a0ba7c86...223\n", 243 | "number: 1\n", 244 | "n_txs: 3\n", 245 | "mined: True\n", 246 | "time: 1617571992.019669 eth\n", 247 | "diff: 2\n", 248 | "reward: 50\n", 249 | "miner: 👾 0x18a70Cd2EB...c68\n", 250 | "nonce: 12\n", 251 | "volume: 62.0 eth\n", 252 | "fees: 0.21 eth\n", 253 | "\n", 254 | "txs:\n", 255 | "👾 0x18a7 -> 👾 0x18a7 50.0 eth\n", 256 | "💳 0x4D4d -> 🕗 0xf19d 3.0 eth\n", 257 | "🕗 0xf19d -> 💳 0x4D4d 9.0 eth\n", 258 | "\n", 259 | "------\n", 260 | "state:\n", 261 | "------\n", 262 | "💳 0x4D4d017Ea9...444\n", 263 | "balance: 24.0\n", 264 | "nonce: 2\n", 265 | "\n", 266 | "🕗 0xf19dB46598...b3C\n", 267 | "balance: 18.0\n", 268 | "nonce: 2\n", 269 | "\n", 270 | "👾 0x18a70Cd2EB...c68\n", 271 | "balance: 50.21\n", 272 | "nonce: 1\n", 273 | "\n" 274 | ] 275 | } 276 | ], 277 | "source": [ 278 | "tx1 = acc1.signed_tx(acc2, 3, 0.05)\n", 279 | "tx2 = acc2.signed_tx(acc1, 9, 0.16)\n", 280 | "\n", 281 | "block = bc.candidate([tx1,tx2])\n", 282 | "mb = miner.mine([tx1,tx2], gb.header, 2, 50)\n", 283 | "bc.add(mb); print(bc)" 284 | ] 285 | }, 286 | { 287 | "cell_type": "code", 288 | "execution_count": 9, 289 | "id": "placed-situation", 290 | "metadata": { 291 | "scrolled": false 292 | }, 293 | "outputs": [ 294 | { 295 | "name": "stdout", 296 | "output_type": "stream", 297 | "text": [ 298 | "root: 🔅 0x9f49810139...5c3\n", 299 | "hash: 📹 0x93a42646e7...1da\n", 300 | "prev_hash: 0x0\n", 301 | "number: 0\n", 302 | "n_txs: 2\n", 303 | "mined: False\n", 304 | "time: Sat Apr 3 20:46:32 2021\n", 305 | "volume: 42.0 eth\n", 306 | "fees: 0.22 eth\n", 307 | "\n", 308 | "txs:\n", 309 | "📥 0x7f37 -> 🔎 0xa895 24.0 eth\n", 310 | "🔎 0xa895 -> 📥 0x7f37 18.0 eth\n", 311 | "\n", 312 | "----------------------------------------\n", 313 | "\n", 314 | "root: 📨 0x820597760a...24f\n", 315 | "hash: 💒 0x2c1c14f555...577\n", 316 | "prev_hash: 📹 0x93a42646e7...1da\n", 317 | "number: 1\n", 318 | "n_txs: 3\n", 319 | "mined: True\n", 320 | "time: Sat Apr 3 20:46:34 2021\n", 321 | "diff: 2\n", 322 | "reward: 50\n", 323 | "miner: 📽 0x979Ad81fa9...63D\n", 324 | "nonce: 237\n", 325 | "volume: 62.0 eth\n", 326 | "fees: 0.21 eth\n", 327 | "\n", 328 | "txs:\n", 329 | "📽 0x979A -> 📽 0x979A 50.0 eth\n", 330 | "📥 0x7f37 -> 🔎 0xa895 3.0 eth\n", 331 | "🔎 0xa895 -> 📥 0x7f37 9.0 eth\n", 332 | "\n", 333 | "----------------------------------------\n", 334 | "\n", 335 | "root: 💾 0x58760631ac...260\n", 336 | "hash: 🔿 0xd91c1d92d1...332\n", 337 | "prev_hash: 💒 0x2c1c14f555...577\n", 338 | "number: 2\n", 339 | "n_txs: 2\n", 340 | "mined: True\n", 341 | "time: Sat Apr 3 20:46:36 2021\n", 342 | "diff: 2\n", 343 | "reward: 50\n", 344 | "miner: 📽 0x979Ad81fa9...63D\n", 345 | "nonce: 426\n", 346 | "volume: 62.0 eth\n", 347 | "fees: 1.5 eth\n", 348 | "\n", 349 | "txs:\n", 350 | "📽 0x979A -> 📽 0x979A 50.0 eth\n", 351 | "🔎 0xa895 -> 🕛 0xf595 12.0 eth\n", 352 | "\n", 353 | "------\n", 354 | "state:\n", 355 | "------\n", 356 | "📥 0x7f3729B312...005\n", 357 | "balance: 24.0\n", 358 | "nonce: 2\n", 359 | "\n", 360 | "🔎 0xa89597dF3d...0e6\n", 361 | "balance: 6.0\n", 362 | "nonce: 3\n", 363 | "\n", 364 | "📽 0x979Ad81fa9...63D\n", 365 | "balance: 101.71000000000001\n", 366 | "nonce: 2\n", 367 | "\n", 368 | "🕛 0xf595222089...a3a\n", 369 | "balance: 12.0\n", 370 | "nonce: 0\n", 371 | "\n" 372 | ] 373 | } 374 | ], 375 | "source": [ 376 | "acc4 = Account()\n", 377 | "tx1 = acc2.signed_tx(acc4, 12, 1.5)\n", 378 | "\n", 379 | "block = bc.candidate([tx1])\n", 380 | "mb = miner.mine([tx1], mb.header, 2, 50)\n", 381 | "bc.add(mb); print(bc)" 382 | ] 383 | } 384 | ], 385 | "metadata": { 386 | "kernelspec": { 387 | "display_name": "Python 3", 388 | "language": "python", 389 | "name": "python3" 390 | }, 391 | "language_info": { 392 | "codemirror_mode": { 393 | "name": "ipython", 394 | "version": 3 395 | }, 396 | "file_extension": ".py", 397 | "mimetype": "text/x-python", 398 | "name": "python", 399 | "nbconvert_exporter": "python", 400 | "pygments_lexer": "ipython3", 401 | "version": "3.9.2" 402 | } 403 | }, 404 | "nbformat": 4, 405 | "nbformat_minor": 5 406 | } 407 | --------------------------------------------------------------------------------