├── BuildYourOwnBlockchain.ipynb ├── PythonEthereum.ipynb ├── README.md └── WemoTutorial.ipynb /BuildYourOwnBlockchain.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Build Your Own Blockchain - The Basics\n", 8 | "\n", 9 | "This tutorial will walk you through the basics of how to build a blockchain from scratch. Focusing on the details of a concrete example will provide a deeper understanding of the strengths and limitations of blockchains. For a higher-level overview, I'd recommend [this excellent article from BitsOnBlocks](https://bitsonblocks.net/2015/09/09/a-gentle-introduction-to-blockchain-technology/)." 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "## Transactions, Validation, and updating system state\n", 17 | "At its core, a blockchain is a distributed database with a set of rules for verifying new additions to the database. We'll start off by tracking the accounts of two imaginary people: Alice and Bob, who will trade virtual money with each other.\n", 18 | "\n", 19 | "We'll need to create a transaction pool of incoming transactions, validate those transactions, and make them into a block." 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "We'll be using a [hash function](https://en.wikipedia.org/wiki/SHA-2) to create a 'fingerprint' for each of our transactions- this hash function links each of our blocks to each other. To make this easier to use, we'll define a helper function to wrap the [python hash function](https://docs.python.org/2/library/hashlib.html) that we're using. " 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 1, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "import hashlib, json, sys\n", 36 | "\n", 37 | "def hashMe(msg=\"\"):\n", 38 | " # For convenience, this is a helper function that wraps our hashing algorithm\n", 39 | " if type(msg)!=str:\n", 40 | " msg = json.dumps(msg,sort_keys=True) # If we don't sort keys, we can't guarantee repeatability!\n", 41 | " \n", 42 | " if sys.version_info.major == 2:\n", 43 | " return unicode(hashlib.sha256(msg).hexdigest(),'utf-8')\n", 44 | " else:\n", 45 | " return hashlib.sha256(str(msg).encode('utf-8')).hexdigest()" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "Next, we want to create a function to generate exchanges between Alice and Bob. We'll indicate withdrawals with negative numbers, and deposits with positive numbers. We'll construct our transactions to always be between the two users of our system, and make sure that the deposit is the same magnitude as the withdrawal- i.e. that we're neither creating nor destroying money." 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 2, 58 | "metadata": { 59 | "collapsed": true 60 | }, 61 | "outputs": [], 62 | "source": [ 63 | "import random\n", 64 | "random.seed(0)\n", 65 | "\n", 66 | "def makeTransaction(maxValue=3):\n", 67 | " # This will create valid transactions in the range of (1,maxValue)\n", 68 | " sign = int(random.getrandbits(1))*2 - 1 # This will randomly choose -1 or 1\n", 69 | " amount = random.randint(1,maxValue)\n", 70 | " alicePays = sign * amount\n", 71 | " bobPays = -1 * alicePays\n", 72 | " # By construction, this will always return transactions that respect the conservation of tokens.\n", 73 | " # However, note that we have not done anything to check whether these overdraft an account\n", 74 | " return {u'Alice':alicePays,u'Bob':bobPays}" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "Now let's create a large set of transactions, then chunk them into blocks. " 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 3, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "txnBuffer = [makeTransaction() for i in range(30)]" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "Next step: making our very own blocks! We'll take the first *k* transactions from the transaction buffer, and turn them into a block. Before we do that, we need to define a method for checking the valididty of the transactions we've pulled into the block. \n", 98 | "\n", 99 | "For bitcoin, the validation function checks that the input values are valid unspent transaction outputs (UTXOs), that the outputs of the transaction are no greater than the input, and that the keys used for the signatures are valid. In Ethereum, the validation function checks that the smart contracts were faithfully executed and respect gas limits.\n", 100 | "\n", 101 | "No worries, though- we don't have to build a system that complicated. We'll define our own, very simple set of rules which make sense for a basic token system:\n", 102 | "- The sum of deposits and withdrawals must be 0 (tokens are neither created nor destroyed)\n", 103 | "- A user's account must have sufficient funds to cover any withdrawals\n", 104 | "\n", 105 | "If either of these conditions are violated, we'll reject the transaction." 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 4, 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "def updateState(txn, state):\n", 115 | " # Inputs: txn, state: dictionaries keyed with account names, holding numeric values for transfer amount (txn) or account balance (state)\n", 116 | " # Returns: Updated state, with additional users added to state if necessary\n", 117 | " # NOTE: This does not not validate the transaction- just updates the state!\n", 118 | " \n", 119 | " # If the transaction is valid, then update the state\n", 120 | " state = state.copy() # As dictionaries are mutable, let's avoid any confusion by creating a working copy of the data.\n", 121 | " for key in txn:\n", 122 | " if key in state.keys():\n", 123 | " state[key] += txn[key]\n", 124 | " else:\n", 125 | " state[key] = txn[key]\n", 126 | " return state" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 5, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "def isValidTxn(txn,state):\n", 136 | " # Assume that the transaction is a dictionary keyed by account names\n", 137 | "\n", 138 | " # Check that the sum of the deposits and withdrawals is 0\n", 139 | " if sum(txn.values()) is not 0:\n", 140 | " return False\n", 141 | " \n", 142 | " # Check that the transaction does not cause an overdraft\n", 143 | " for key in txn.keys():\n", 144 | " if key in state.keys(): \n", 145 | " acctBalance = state[key]\n", 146 | " else:\n", 147 | " acctBalance = 0\n", 148 | " if (acctBalance + txn[key]) < 0:\n", 149 | " return False\n", 150 | " \n", 151 | " return True" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "Here are a set of sample transactions, some of which are fraudulent- but we can now check their validity!" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": 6, 164 | "metadata": {}, 165 | "outputs": [ 166 | { 167 | "name": "stdout", 168 | "output_type": "stream", 169 | "text": [ 170 | "True\n", 171 | "False\n", 172 | "False\n", 173 | "True\n", 174 | "False\n" 175 | ] 176 | } 177 | ], 178 | "source": [ 179 | "state = {u'Alice':5,u'Bob':5}\n", 180 | "\n", 181 | "print(isValidTxn({u'Alice': -3, u'Bob': 3},state)) # Basic transaction- this works great!\n", 182 | "print(isValidTxn({u'Alice': -4, u'Bob': 3},state)) # But we can't create or destroy tokens!\n", 183 | "print(isValidTxn({u'Alice': -6, u'Bob': 6},state)) # We also can't overdraft our account.\n", 184 | "print(isValidTxn({u'Alice': -4, u'Bob': 2,'Lisa':2},state)) # Creating new users is valid\n", 185 | "print(isValidTxn({u'Alice': -4, u'Bob': 3,'Lisa':2},state)) # But the same rules still apply!" 186 | ] 187 | }, 188 | { 189 | "cell_type": "markdown", 190 | "metadata": { 191 | "collapsed": true 192 | }, 193 | "source": [ 194 | "Each block contains a batch of transactions, a reference to the hash of the previous block (if block number is greater than 1), and a hash of its contents and the header" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "## Building the Blockchain: From Transactions to Blocks" 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "metadata": {}, 207 | "source": [ 208 | "We're ready to start making our blockchain! Right now, there's nothing on the blockchain, but we can get things started by defining the 'genesis block' (the first block in the system). Because the genesis block isn't linked to any prior block, it gets treated a bit differently, and we can arbitrarily set the system state. In our case, we'll create accounts for our two users (Alice and Bob) and give them 50 coins each." 209 | ] 210 | }, 211 | { 212 | "cell_type": "code", 213 | "execution_count": 7, 214 | "metadata": {}, 215 | "outputs": [], 216 | "source": [ 217 | "state = {u'Alice':50, u'Bob':50} # Define the initial state\n", 218 | "genesisBlockTxns = [state]\n", 219 | "genesisBlockContents = {u'blockNumber':0,u'parentHash':None,u'txnCount':1,u'txns':genesisBlockTxns}\n", 220 | "genesisHash = hashMe( genesisBlockContents )\n", 221 | "genesisBlock = {u'hash':genesisHash,u'contents':genesisBlockContents}\n", 222 | "genesisBlockStr = json.dumps(genesisBlock, sort_keys=True)" 223 | ] 224 | }, 225 | { 226 | "cell_type": "markdown", 227 | "metadata": {}, 228 | "source": [ 229 | "Great! This becomes the first element from which everything else will be linked." 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": 8, 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [ 238 | "chain = [genesisBlock]" 239 | ] 240 | }, 241 | { 242 | "cell_type": "markdown", 243 | "metadata": {}, 244 | "source": [ 245 | "For each block, we want to collect a set of transactions, create a header, hash it, and add it to the chain" 246 | ] 247 | }, 248 | { 249 | "cell_type": "code", 250 | "execution_count": 9, 251 | "metadata": { 252 | "collapsed": true 253 | }, 254 | "outputs": [], 255 | "source": [ 256 | "def makeBlock(txns,chain):\n", 257 | " parentBlock = chain[-1]\n", 258 | " parentHash = parentBlock[u'hash']\n", 259 | " blockNumber = parentBlock[u'contents'][u'blockNumber'] + 1\n", 260 | " txnCount = len(txns)\n", 261 | " blockContents = {u'blockNumber':blockNumber,u'parentHash':parentHash,\n", 262 | " u'txnCount':len(txns),'txns':txns}\n", 263 | " blockHash = hashMe( blockContents )\n", 264 | " block = {u'hash':blockHash,u'contents':blockContents}\n", 265 | " \n", 266 | " return block" 267 | ] 268 | }, 269 | { 270 | "cell_type": "markdown", 271 | "metadata": {}, 272 | "source": [ 273 | "Let's use this to process our transaction buffer into a set of blocks:" 274 | ] 275 | }, 276 | { 277 | "cell_type": "code", 278 | "execution_count": 10, 279 | "metadata": {}, 280 | "outputs": [], 281 | "source": [ 282 | "blockSizeLimit = 5 # Arbitrary number of transactions per block- \n", 283 | " # this is chosen by the block miner, and can vary between blocks!\n", 284 | "\n", 285 | "while len(txnBuffer) > 0:\n", 286 | " bufferStartSize = len(txnBuffer)\n", 287 | " \n", 288 | " ## Gather a set of valid transactions for inclusion\n", 289 | " txnList = []\n", 290 | " while (len(txnBuffer) > 0) & (len(txnList) < blockSizeLimit):\n", 291 | " newTxn = txnBuffer.pop()\n", 292 | " validTxn = isValidTxn(newTxn,state) # This will return False if txn is invalid\n", 293 | " \n", 294 | " if validTxn: # If we got a valid state, not 'False'\n", 295 | " txnList.append(newTxn)\n", 296 | " state = updateState(newTxn,state)\n", 297 | " else:\n", 298 | " print(\"ignored transaction\")\n", 299 | " sys.stdout.flush()\n", 300 | " continue # This was an invalid transaction; ignore it and move on\n", 301 | " \n", 302 | " ## Make a block\n", 303 | " myBlock = makeBlock(txnList,chain)\n", 304 | " chain.append(myBlock) " 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": 11, 310 | "metadata": {}, 311 | "outputs": [ 312 | { 313 | "data": { 314 | "text/plain": [ 315 | "{'contents': {'blockNumber': 0,\n", 316 | " 'parentHash': None,\n", 317 | " 'txnCount': 1,\n", 318 | " 'txns': [{'Alice': 50, 'Bob': 50}]},\n", 319 | " 'hash': '7c88a4312054f89a2b73b04989cd9b9e1ae437e1048f89fbb4e18a08479de507'}" 320 | ] 321 | }, 322 | "execution_count": 11, 323 | "metadata": {}, 324 | "output_type": "execute_result" 325 | } 326 | ], 327 | "source": [ 328 | "chain[0]" 329 | ] 330 | }, 331 | { 332 | "cell_type": "code", 333 | "execution_count": 12, 334 | "metadata": {}, 335 | "outputs": [ 336 | { 337 | "data": { 338 | "text/plain": [ 339 | "{'contents': {'blockNumber': 1,\n", 340 | " 'parentHash': '7c88a4312054f89a2b73b04989cd9b9e1ae437e1048f89fbb4e18a08479de507',\n", 341 | " 'txnCount': 5,\n", 342 | " 'txns': [{'Alice': 3, 'Bob': -3},\n", 343 | " {'Alice': -1, 'Bob': 1},\n", 344 | " {'Alice': 3, 'Bob': -3},\n", 345 | " {'Alice': -2, 'Bob': 2},\n", 346 | " {'Alice': 3, 'Bob': -3}]},\n", 347 | " 'hash': '7a91fc8206c5351293fd11200b33b7192e87fad6545504068a51aba868bc6f72'}" 348 | ] 349 | }, 350 | "execution_count": 12, 351 | "metadata": {}, 352 | "output_type": "execute_result" 353 | } 354 | ], 355 | "source": [ 356 | "chain[1]" 357 | ] 358 | }, 359 | { 360 | "cell_type": "markdown", 361 | "metadata": {}, 362 | "source": [ 363 | "As expected, the genesis block includes an invalid transaction which initiates account balances (creating tokens out of thin air). The hash of the parent block is referenced in the child block, which contains a set of new transactions which affect system state. We can now see the state of the system, updated to include the transactions:" 364 | ] 365 | }, 366 | { 367 | "cell_type": "code", 368 | "execution_count": 13, 369 | "metadata": {}, 370 | "outputs": [ 371 | { 372 | "data": { 373 | "text/plain": [ 374 | "{'Alice': 72, 'Bob': 28}" 375 | ] 376 | }, 377 | "execution_count": 13, 378 | "metadata": {}, 379 | "output_type": "execute_result" 380 | } 381 | ], 382 | "source": [ 383 | "state" 384 | ] 385 | }, 386 | { 387 | "cell_type": "markdown", 388 | "metadata": {}, 389 | "source": [ 390 | "## Checking Chain Validity\n", 391 | "\n", 392 | "Now that we know how to create new blocks and link them together into a chain, let's define functions to check that new blocks are valid- and that the whole chain is valid.\n", 393 | "\n", 394 | "On a blockchain network, this becomes important in two ways:\n", 395 | "- When we initially set up our node, we will download the full blockchain history. After downloading the chain, we would need to run through the blockchain to compute the state of the system. To protect against somebody inserting invalid transactions in the initial chain, we need to check the validity of the entire chain in this initial download.\n", 396 | "- Once our node is synced with the network (has an up-to-date copy of the blockchain and a representation of system state) it will need to check the validity of new blocks that are broadcast to the network. \n", 397 | "\n", 398 | "We will need three functions to facilitate in this:\n", 399 | "- **checkBlockHash**: A simple helper function that makes sure that the block contents match the hash\n", 400 | "- **checkBlockValidity**: Checks the validity of a block, given its parent and the current system state. We want this to return the updated state if the block is valid, and raise an error otherwise.\n", 401 | "- **checkChain**: Check the validity of the entire chain, and compute the system state beginning at the genesis block. This will return the system state if the chain is valid, and raise an error otherwise.\n" 402 | ] 403 | }, 404 | { 405 | "cell_type": "code", 406 | "execution_count": 14, 407 | "metadata": { 408 | "collapsed": true 409 | }, 410 | "outputs": [], 411 | "source": [ 412 | "def checkBlockHash(block):\n", 413 | " # Raise an exception if the hash does not match the block contents\n", 414 | " expectedHash = hashMe( block['contents'] )\n", 415 | " if block['hash']!=expectedHash:\n", 416 | " raise Exception('Hash does not match contents of block %s'%\n", 417 | " block['contents']['blockNumber'])\n", 418 | " return" 419 | ] 420 | }, 421 | { 422 | "cell_type": "code", 423 | "execution_count": 15, 424 | "metadata": {}, 425 | "outputs": [], 426 | "source": [ 427 | "def checkBlockValidity(block,parent,state): \n", 428 | " # We want to check the following conditions:\n", 429 | " # - Each of the transactions are valid updates to the system state\n", 430 | " # - Block hash is valid for the block contents\n", 431 | " # - Block number increments the parent block number by 1\n", 432 | " # - Accurately references the parent block's hash\n", 433 | " parentNumber = parent['contents']['blockNumber']\n", 434 | " parentHash = parent['hash']\n", 435 | " blockNumber = block['contents']['blockNumber']\n", 436 | " \n", 437 | " # Check transaction validity; throw an error if an invalid transaction was found.\n", 438 | " for txn in block['contents']['txns']:\n", 439 | " if isValidTxn(txn,state):\n", 440 | " state = updateState(txn,state)\n", 441 | " else:\n", 442 | " raise Exception('Invalid transaction in block %s: %s'%(blockNumber,txn))\n", 443 | "\n", 444 | " checkBlockHash(block) # Check hash integrity; raises error if inaccurate\n", 445 | "\n", 446 | " if blockNumber!=(parentNumber+1):\n", 447 | " raise Exception('Hash does not match contents of block %s'%blockNumber)\n", 448 | "\n", 449 | " if block['contents']['parentHash'] != parentHash:\n", 450 | " raise Exception('Parent hash not accurate at block %s'%blockNumber)\n", 451 | " \n", 452 | " return state" 453 | ] 454 | }, 455 | { 456 | "cell_type": "code", 457 | "execution_count": 16, 458 | "metadata": { 459 | "collapsed": true 460 | }, 461 | "outputs": [], 462 | "source": [ 463 | "def checkChain(chain):\n", 464 | " # Work through the chain from the genesis block (which gets special treatment), \n", 465 | " # checking that all transactions are internally valid,\n", 466 | " # that the transactions do not cause an overdraft,\n", 467 | " # and that the blocks are linked by their hashes.\n", 468 | " # This returns the state as a dictionary of accounts and balances,\n", 469 | " # or returns False if an error was detected\n", 470 | "\n", 471 | " \n", 472 | " ## Data input processing: Make sure that our chain is a list of dicts\n", 473 | " if type(chain)==str:\n", 474 | " try:\n", 475 | " chain = json.loads(chain)\n", 476 | " assert( type(chain)==list)\n", 477 | " except: # This is a catch-all, admittedly crude\n", 478 | " return False\n", 479 | " elif type(chain)!=list:\n", 480 | " return False\n", 481 | " \n", 482 | " state = {}\n", 483 | " ## Prime the pump by checking the genesis block\n", 484 | " # We want to check the following conditions:\n", 485 | " # - Each of the transactions are valid updates to the system state\n", 486 | " # - Block hash is valid for the block contents\n", 487 | "\n", 488 | " for txn in chain[0]['contents']['txns']:\n", 489 | " state = updateState(txn,state)\n", 490 | " checkBlockHash(chain[0])\n", 491 | " parent = chain[0]\n", 492 | " \n", 493 | " ## Checking subsequent blocks: These additionally need to check\n", 494 | " # - the reference to the parent block's hash\n", 495 | " # - the validity of the block number\n", 496 | " for block in chain[1:]:\n", 497 | " state = checkBlockValidity(block,parent,state)\n", 498 | " parent = block\n", 499 | " \n", 500 | " return state" 501 | ] 502 | }, 503 | { 504 | "cell_type": "markdown", 505 | "metadata": {}, 506 | "source": [ 507 | "We can now check the validity of the state:" 508 | ] 509 | }, 510 | { 511 | "cell_type": "code", 512 | "execution_count": 17, 513 | "metadata": {}, 514 | "outputs": [ 515 | { 516 | "data": { 517 | "text/plain": [ 518 | "{'Alice': 72, 'Bob': 28}" 519 | ] 520 | }, 521 | "execution_count": 17, 522 | "metadata": {}, 523 | "output_type": "execute_result" 524 | } 525 | ], 526 | "source": [ 527 | "checkChain(chain)" 528 | ] 529 | }, 530 | { 531 | "cell_type": "markdown", 532 | "metadata": {}, 533 | "source": [ 534 | "And even if we are loading the chain from a text file, e.g. from backup or loading it for the first time, we can check the integrity of the chain and create the current state:" 535 | ] 536 | }, 537 | { 538 | "cell_type": "code", 539 | "execution_count": 18, 540 | "metadata": {}, 541 | "outputs": [ 542 | { 543 | "data": { 544 | "text/plain": [ 545 | "{'Alice': 72, 'Bob': 28}" 546 | ] 547 | }, 548 | "execution_count": 18, 549 | "metadata": {}, 550 | "output_type": "execute_result" 551 | } 552 | ], 553 | "source": [ 554 | "chainAsText = json.dumps(chain,sort_keys=True)\n", 555 | "checkChain(chainAsText)" 556 | ] 557 | }, 558 | { 559 | "cell_type": "markdown", 560 | "metadata": {}, 561 | "source": [ 562 | "## Putting it together: The final Blockchain Architecture\n", 563 | "In an actual blockchain network, new nodes would download a copy of the blockchain and verify it (as we just did above), then announce their presence on the peer-to-peer network and start listening for transactions. Bundling transactions into a block, they then pass their proposed block on to other nodes.\n", 564 | "\n", 565 | "We've seen how to verify a copy of the blockchain, and how to bundle transactions into a block. If we recieve a block from somewhere else, verifying it and adding it to our blockchain is easy.\n", 566 | "\n", 567 | "Let's say that the following code runs on Node A, which mines the block:" 568 | ] 569 | }, 570 | { 571 | "cell_type": "code", 572 | "execution_count": 19, 573 | "metadata": {}, 574 | "outputs": [], 575 | "source": [ 576 | "import copy\n", 577 | "nodeBchain = copy.copy(chain)\n", 578 | "nodeBtxns = [makeTransaction() for i in range(5)]\n", 579 | "newBlock = makeBlock(nodeBtxns,nodeBchain)" 580 | ] 581 | }, 582 | { 583 | "cell_type": "markdown", 584 | "metadata": {}, 585 | "source": [ 586 | "Now assume that the `newBlock` is transmitted to our node, and we want to check it and update our state if it is a valid block:" 587 | ] 588 | }, 589 | { 590 | "cell_type": "code", 591 | "execution_count": 20, 592 | "metadata": {}, 593 | "outputs": [ 594 | { 595 | "name": "stdout", 596 | "output_type": "stream", 597 | "text": [ 598 | "Blockchain on Node A is currently 7 blocks long\n", 599 | "New Block Received; checking validity...\n", 600 | "Blockchain on Node A is now 8 blocks long\n" 601 | ] 602 | } 603 | ], 604 | "source": [ 605 | "print(\"Blockchain on Node A is currently %s blocks long\"%len(chain))\n", 606 | "\n", 607 | "try:\n", 608 | " print(\"New Block Received; checking validity...\")\n", 609 | " state = checkBlockValidity(newBlock,chain[-1],state) # Update the state- this will throw an error if the block is invalid!\n", 610 | " chain.append(newBlock)\n", 611 | "except:\n", 612 | " print(\"Invalid block; ignoring and waiting for the next block...\")\n", 613 | "\n", 614 | "print(\"Blockchain on Node A is now %s blocks long\"%len(chain))" 615 | ] 616 | }, 617 | { 618 | "cell_type": "markdown", 619 | "metadata": {}, 620 | "source": [ 621 | "# Conclusions and Extensions\n", 622 | "We've created all the basic architecture for a blockchain, from a set of state transition rules to a method for creating blocks, to mechanisms for checking the validity of transactions, blocks, and the full chain. We can derive the system state from a downloaded copy of the blockchain, validate new blocks that we recieve from the network, and create our own blocks.\n", 623 | "\n", 624 | "The system state that we've created is effectively a distributed ledger or database- the core of many blockchains. We could extend this to include special transaction types or full smart contracts.\n", 625 | "\n", 626 | "We haven't explored the network architecture, the proof-of-work or proof-of-state validation step, and the consensus mechanism which provides blockchains with security against attack. We also haven't discussed public key cryptography, privacy, and verification steps. More on that in the future!" 627 | ] 628 | } 629 | ], 630 | "metadata": { 631 | "kernelspec": { 632 | "display_name": "Python 3", 633 | "language": "python", 634 | "name": "python3" 635 | }, 636 | "language_info": { 637 | "codemirror_mode": { 638 | "name": "ipython", 639 | "version": 3 640 | }, 641 | "file_extension": ".py", 642 | "mimetype": "text/x-python", 643 | "name": "python", 644 | "nbconvert_exporter": "python", 645 | "pygments_lexer": "ipython3", 646 | "version": "3.5.2" 647 | } 648 | }, 649 | "nbformat": 4, 650 | "nbformat_minor": 1 651 | } 652 | -------------------------------------------------------------------------------- /PythonEthereum.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Ethereum makes it easy to run simple computations on blockchains while [keeping the value of decentralized networks](http://ecomunsing.com/what-are-blockchains-good-for)- but real applications will need access to off-blockchain resources, whether for heavy computations or pulling data from the internet. This tutorial walks through how to deploy a smart contract on a private test blockchain and use Python to read data from the blockchain or send data to the blockchain. We'll review the relevant components of the Ethereum network, walk through how to interact with the system using Python, and deploy example smart contracts. \n", 8 | "\n", 9 | "Note that this was written in July 2016 using the Ethereum Homestead release, Python 2.7.12, and geth 1.6.6. More resources are found on the official [Ethereum website](https://ethereum.org/) and the [Ethereum Homestead documentation](http://www.ethdocs.org/en/latest/). Because these projects are open-source, there is a lot of outdated information (probably including this guide, by the time you read it). This notebook is also [available on github here](https://github.com/emunsing/tutorials).\n", 10 | "\n", 11 | "\n", 12 | "## Background: Ethereum is not a computer\n", 13 | "\n", 14 | "For background, it's helpful to keep in mind that \"Ethereum\" is a communication protocol- not a specific network, computer, or application. Just as the main Internet exists alongside many private Intranets and local networks, there is a main community Ethereum network (which supports the Ether cryptocurrency) alongside a community test network (called \"Ropsten\") and many private networks. For this tutorial we'll be working on implementing a test server that just serves data on your local computer- just as you might run a `localhost://` server for developing a web page, but later deploy it to a wider network.\n", 15 | "\n", 16 | "For my research, I don't want to deploy on the full Ethereum network, where my code is subject to attack and operations cost real Ether- instead ther are a variety of different options for prototyping:\n", 17 | "- Private node in development mode: Just run an ethereum node on your own machine, ignoring all connections- the equivalent of running on localhost.\n", 18 | "- Private network: Networking a set of computers into a private network that isn't discoverable by others.\n", 19 | "- Public test network: Open for anyone to access, but configured to quickly generate Ether for testing software.\n", 20 | "- Full Ethereum network: This is the real deal, where Ether costs money and hackers may roam.\n", 21 | "\n", 22 | "So Ethereum is a communication protocol- but how do we interact with it? There are different open-source software packages which connect with Ethereum networks for mining or development purposes- these are equivalent to competing internet browsers. Common implementations are `eth` ([the C++ implementation](https://github.com/ethereum/cpp-ethereum), not to be confused with the cryptocurrency), and `geth` ([the GoLang implementation](https://github.com/ethereum/go-ethereum), which we'll be using here).\n", 23 | "\n", 24 | "Ultimately, we care about the scripts which are run on the Ethereum Virtual Machine (the EVM)- the decentralized, consensus-driven computer which distinguishes Ethereum from earlier blockchains. This Virtual Machine runs its own language of bytecode, which we'll generate with a compiler. We'll be writing scripts in [Solidity](http://solidity.readthedocs.io/), a Javascript-derived language, and compiling them into bytecode using the `sol-c` compiler. We'll then use our Ethereum client (i.e `geth`) to push this compiled bytecode to the Ethereum network, and to run the bytecode which others have written.\n", 25 | "\n", 26 | "From now on, we'll assume that we're working with the `geth` implementation and will work on creating a private test network on our own computer in order to develop and deploy smart contracts which we'll be interacting with using Python.\n", 27 | "\n", 28 | "## Big concepts: Navigating the software\n", 29 | "\n", 30 | "For development, there are 3 pieces of software that you'll interact with: the Ethereum node (the server), the console (where you can type code to interact directly with the server), and the Ethereum Wallet (a GUI which provides graphical interaction with the server. These are configured with two important directories: the datadir which stores the important configuration, and the location of your .ipc (inter-process communication) file which lets the pieces talk with each other.\n", 31 | "\n", 32 | "### Important software pieces:\n", 33 | "- Ethereum Node (e.g. Geth server): This runs your actual Ethereum node and maintains a connection with the rest of the Ethereum network. This can be created by running `geth` (starts server only), by running `geth console` (starts server and then console), or by opening the Ethereum Wallet. However you start it, the ethereum node is discoverable to related software through \n", 34 | "- Console: Allows Javascript command-line interface to the server. Can be used for changing settings or deploying contracts. Run by `geth console` (will attempt to spin up a node) or `geth attach` (will look for geth.ipc file)\n", 35 | "- Ethereum Wallet (aka Mist): GUI for interfacing with wallets and deploying contracts. Will look for /Users/emunsing/Library/Ethereum/geth.ipc and if it does not find geth.ipc the Wallet will start up a server for the node.\n", 36 | " \n", 37 | "**Configuration**\n", 38 | "The Ethereum environment is dictated by two data sources: the data directory (called 'datadir' here) and the `geth.ipc` file. The datadir stores the blockchain, account keys, and other personal settings. The `geth.ipc` file is created when a node starts up, and allows other services (geth console, ethereum wallet) to attach to that server. In addition, there are a number of command line options that you can use to direct how and where your Ethereum node should look for peers.\n", 39 | "- By default, the Ethereum Wallet or `geth attach` looks for the geth.ipc file in `~/Library/Ethereum/geth.ipc` (i.e. `/Users/username/Library/Ethereum/geth.ipc` on Mac OSX).\n", 40 | "- Specify the datadir location with the command line options `--datadir ~/data` and specify the ipc location with `--ipcpath ~/Ethereum/geth.ipc`\n", 41 | "- Generally avoid using `geth.ipc` locations other than the default- there is no easy way to configure the Wallet to connect to these\n", 42 | "If you're using a computer for developing new ethereum apps, you might want to have a number of different blockchains for different public and private implementations. I find it helpful to keep these separate by running one node per computer, using the default .ipc location, and using separate datadir locations for each project.\n", 43 | " \n", 44 | "### Back up the /keystore!\n", 45 | "Each account's private keys are encrypted and stored in the /keystore folder of the datadir. These should be backed up off of the hard drive in case of removal- whether accidental or malicious.\n", 46 | "- To restore an account after /keystore was removed, simply paste the backed-up files back to `/keystore` under the data directory\n", 47 | "- If trying to just clean up the blockchain, can do so with `geth upgradedb` or `geth removedb` - these will not affect the keystore\n", 48 | "\n", 49 | "\n", 50 | "## Installing Geth\n", 51 | "Geth, or go-ethereum, is the main software platform for running an ethereum node on your computer. You can use this to connect to the main ethereum network, the public test network, or to create your own private test network.\n", 52 | "\n", 53 | "We'll install Geth using Homebrew, following [the Geth installation instructions for Mac](https://github.com/ethereum/go-ethereum/wiki/Installation-Instructions-for-Mac). This tutorial was written using\n", 54 | "- Geth v1.6.6\n", 55 | "- Python v2.7.12\n", 56 | "- ethjsonrpc v0.3.0\n", 57 | "- py-solc v.1.2.0\n", 58 | "\n", 59 | "First, make sure that homebrew is installed and up-to-date by running `brew update` ...if it isn't installed, [follow these instructions](https://www.howtogeek.com/211541/homebrew-for-os-x-easily-installs-desktop-apps-and-terminal-utilities/). \n", 60 | "\n", 61 | " brew update\n", 62 | " brew tap ethereum/ethereum\n", 63 | " brew install ethereum\n", 64 | " \n", 65 | "Check that Geth is now installed by running `geth account new` which will prompt you to create a new account.\n", 66 | "\n", 67 | "To upgrade Geth, run `brew update` && `brew upgrade ethereum`. You can check your version with `geth version`\n", 68 | "\n", 69 | "\n", 70 | "## Installing Sol-c (compiler)\n", 71 | "\n", 72 | "- Install the solidity compiler: npm install -g solc or npm install solc --global then check that the installation succeeded: solcjs --help\n", 73 | "- Install solC using homebrew\n", 74 | " - Get the directory solc installed to: `$ which solc`\n", 75 | " - Copy this directory, and use it in the geth console like `> admin.setSolc(\"/usr/local/bin/solc\")`\n", 76 | " - Confirm that the compiler has been successfully installed by running `> eth.getCompilers()`\n", 77 | " \n", 78 | "## Installing Ethereum Wallet (optional)\n", 79 | "There are two common ways to interface with an Ethereum network: through the command line, and with the Wallet GUI app (also called 'Mist'). We'll be using the command line for coding, but you might find it helpful to use the Wallet app to explore what's going on in your Ethereum network and make sure that everything is running as expected. The Wallet is designed to provide easy usability out-of-the-box, and [installers can be found here](https://github.com/ethereum/mist/releases). Some notes:\n", 80 | "- The Wallet app will look for a `geth.ipc` file in the default directory, and if it doesn't find one will start the loooong process of syncing with the main Ethereum network. If you don't want this on the main network, be sure to run an Ethereum node from the command line ahead of time.\n", 81 | "- By default, an etherbase account is created. This is an externally owned account, not a wallet contract- i.e. it is not visible on the blockchain.\n", 82 | "- To get enough ether to create a transaction, start CPU mining (Menu> ) or use a *faucet* [like this one](http://faucet.ropsten.be:3001/) to request test Ether be sent to your account (this is \"play money\", as you're on the test network).\n", 83 | "\n", 84 | "## Install required Python packages:\n", 85 | "I'll be using a virtual environment running Python 2.7.12, and install the required packages for interfacing with Ethereum:\n", 86 | "- `pip install py-solc`\n", 87 | "- ~~`pip install ethjsonrpc`~~ **NOTE**: Due to changes in how Geth handles RPC calls, the pip version of ethjsonrpc (v0.3.0) does not currently work, and . I've made a [working fork and here](https://github.com/emunsing/ethjsonrpc)- for this demo, it is sufficient but necessary to be able to run the example in README.md. If my fork is the best version available, I've had success installing the current ethjsonrpc version with `$ pip install ethjsonrpc` then wiping the package with `$pip uninstall ethjsonrpc` -this may be necessary to avoid issues with locally building the dependencies. You can then clone my fork and set it up by running `$ python setup.py install`\n", 88 | "\n", 89 | "# Getting Up and Running\n", 90 | "\n", 91 | "From here on, we'll use the dollar sign to indicate Unix/bash command line entries like `$ geth attach` and use the angle bracket to indicate commands entered into the geth console like `> miner.start(1)`\n", 92 | "\n", 93 | "Setting up a private test network:\n", 94 | "1. Run a server node with `$ geth --dev --rpc --ipcpath ~/Library/Ethereum/geth.ipc --datadir ~/Library/Ethereum/pyEthTutorial`\n", 95 | "2. In a second terminal window, attach a geth console with `$ geth attach`\n", 96 | "3. If you haven't created an account yet, you'll need to create a new account with `> personal.newAccount()` and follow the prompt to create a password. If everything is running right, you should see your `geth` server output announce the creation of a new account, and should also see a new file appear in your /keystore directory. \n", 97 | "4. You can get a list of all the accounts associated with your keystore by running `> eth.accounts`. We'll assume that we want to work with the first of these, i.e. `> eth.accounts[0]`\n", 98 | "5. In the geth console, unlock the coinbase account with `> personal.unlockAccount( eth.accounts[0], 'passwd', 300)` where 300 is the number of seconds for which it should be unlocked (set to 0 for indefinite). The console should output **`true`**\n", 99 | "6. Start mining with `> miner.start(1)` which should trigger a stream of text on your first terminal window. You can now see your balance in Ether by checking `web3.fromWei( eth.getBalance(eth.coinbase), \"ether\")`" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 1, 105 | "metadata": {}, 106 | "outputs": [ 107 | { 108 | "name": "stdout", 109 | "output_type": "stream", 110 | "text": [ 111 | "Using environment in /Users/emunsing/Documents/BlogPosts/env/bin/..\n", 112 | "Python version 2.7.12 (default, Aug 23 2016, 08:54:23) \n", 113 | "[GCC 4.2.1 Compatible Apple LLVM 7.3.0 (clang-703.0.31)]\n" 114 | ] 115 | } 116 | ], 117 | "source": [ 118 | "import sys, os, time\n", 119 | "from solc import compile_source, compile_files, link_code\n", 120 | "from ethjsonrpc import EthJsonRpc\n", 121 | "\n", 122 | "print(\"Using environment in \"+sys.prefix)\n", 123 | "print(\"Python version \"+sys.version)" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 2, 129 | "metadata": {}, 130 | "outputs": [ 131 | { 132 | "data": { 133 | "text/plain": [ 134 | "u'Geth/v1.6.6-stable-10a45cb5/darwin-amd64/go1.8.3'" 135 | ] 136 | }, 137 | "execution_count": 2, 138 | "metadata": {}, 139 | "output_type": "execute_result" 140 | } 141 | ], 142 | "source": [ 143 | "# Initiate connection to ethereum node\n", 144 | "# Requires a node running with an RPC connection available at port 8545\n", 145 | "c = EthJsonRpc('127.0.0.1', 8545)\n", 146 | "c.web3_clientVersion()" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "We can also check the current block number with `c.eth_blockNumber()` which should match the most recent blocks mined in our miner window.\n", 154 | "\n", 155 | "## Solidity Source\n", 156 | "We can load the Solidity code from file or work with it as direct text. We'll take the Solidity code, compile it with Solc, and then send it to our private development network. The following is similar to the [Greeter example from the Ethereum docs](https://www.ethereum.org/greeter), but any Solidity code can be used. " 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": 3, 162 | "metadata": { 163 | "collapsed": true 164 | }, 165 | "outputs": [], 166 | "source": [ 167 | "source = \"\"\"\n", 168 | "pragma solidity ^0.4.2;\n", 169 | "\n", 170 | "contract Example {\n", 171 | "\n", 172 | " string s=\"Hello World!\";\n", 173 | "\n", 174 | " function set_s(string new_s) {\n", 175 | " s = new_s;\n", 176 | " }\n", 177 | "\n", 178 | " function get_s() returns (string) {\n", 179 | " return s;\n", 180 | " }\n", 181 | "}\"\"\"" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "### Compile the contract and send to network\n", 189 | "Once this code is compiled, it will be submitted to the network's transaction queue. In order for the code to become a valid smart contract, we need to mine it into the blockchain- othewise it's just stuck in the queue. Accordingly, make sure you're mining on your private network (or you're connected to a network where somebody is mining!" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 4, 195 | "metadata": {}, 196 | "outputs": [ 197 | { 198 | "name": "stdout", 199 | "output_type": "stream", 200 | "text": [ 201 | "Contract transaction id is 0xcb51f4a37526e65f1285d429f8fa50186c1b2290af83d48af5d7e7bafcf0765e\n", 202 | "Waiting for the contract to be mined into the blockchain...\n", 203 | "Contract address is 0xa67341158a341e8cb811b0d694cdce29786cb74b\n", 204 | "The message reads: 'Hello World!'\n" 205 | ] 206 | } 207 | ], 208 | "source": [ 209 | "# Basic contract compiling process.\n", 210 | "# Requires that the creating account be unlocked.\n", 211 | "# Note that by default, the account will only be unlocked for 5 minutes (300s). \n", 212 | "# Specify a different duration in the geth personal.unlockAccount('acct','passwd',300) call, or 0 for no limit\n", 213 | "\n", 214 | "compiled = compile_source(source)\n", 215 | "# compiled = compile_files(['Solidity/ethjsonrpc_tutorial.sol']) #Note: Use this to compile from a file\n", 216 | "compiledCode = compiled['Example']['bin']\n", 217 | "compiledCode = '0x'+compiledCode # This is a hack which makes the system work\n", 218 | "\n", 219 | "# Put the contract in the pool for mining, with a gas reward for processing\n", 220 | "contractTx = c.create_contract(c.eth_coinbase(), compiledCode, gas=300000)\n", 221 | "print(\"Contract transaction id is \"+contractTx)\n", 222 | "\n", 223 | "print(\"Waiting for the contract to be mined into the blockchain...\")\n", 224 | "while c.eth_getTransactionReceipt(contractTx) is None:\n", 225 | " time.sleep(1)\n", 226 | "\n", 227 | "contractAddr = c.get_contract_address(contractTx)\n", 228 | "print(\"Contract address is \"+contractAddr)\n", 229 | "\n", 230 | "# There is no cost or delay for reading the state of the blockchain, as this is held on our node\n", 231 | "results = c.call(contractAddr, 'get_s()', [], ['string'])\n", 232 | "print(\"The message reads: '\"+results[0]+\"'\")" 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": 5, 238 | "metadata": {}, 239 | "outputs": [ 240 | { 241 | "name": "stdout", 242 | "output_type": "stream", 243 | "text": [ 244 | "Sending these parameters to the function: ['Hello, fair parishioner!']\n", 245 | "Waiting for the state changes to be mined and take effect...\n", 246 | "The message now reads 'Hello, fair parishioner!'\n" 247 | ] 248 | } 249 | ], 250 | "source": [ 251 | "# Send a transaction to interact with the contract\n", 252 | "# We are targeting the set_s() function, which accepts 1 argument (a string)\n", 253 | "# In the contact definition, this was function set_s(string new_s){s = new_s;}\n", 254 | "params = ['Hello, fair parishioner!']\n", 255 | "tx = c.call_with_transaction(c.eth_coinbase(), contractAddr, 'set_s(string)', params)\n", 256 | "\n", 257 | "print(\"Sending these parameters to the function: \"+str(params))\n", 258 | "print(\"Waiting for the state changes to be mined and take effect...\")\n", 259 | "while c.eth_getTransactionReceipt(tx) is None:\n", 260 | " time.sleep(1)\n", 261 | "\n", 262 | "results = c.call(contractAddr, 'get_s()', [], ['string'])\n", 263 | "print(\"The message now reads '\"+results[0]+\"'\")" 264 | ] 265 | }, 266 | { 267 | "cell_type": "markdown", 268 | "metadata": {}, 269 | "source": [ 270 | "# That's it- start working on Dapps!\n", 271 | "We now have the ability to use all the power of Python to handle complex computations, and the slexibility of Ethereum for decentralized, trustless transactions. We've seen how to read the state of smart contracts on the blockchain, and how to change their state. This means that we could use Python to listen for a change on the blockchain (like the change in an account balance, or a message sent to a smart contract) and then use Python to run complex computations, control hardware, or interface with resources on the Internet and beyond." 272 | ] 273 | } 274 | ], 275 | "metadata": { 276 | "kernelspec": { 277 | "display_name": "Python 2", 278 | "language": "python", 279 | "name": "python2" 280 | }, 281 | "language_info": { 282 | "codemirror_mode": { 283 | "name": "ipython", 284 | "version": 2 285 | }, 286 | "file_extension": ".py", 287 | "mimetype": "text/x-python", 288 | "name": "python", 289 | "nbconvert_exporter": "python", 290 | "pygments_lexer": "ipython2", 291 | "version": "2.7.12" 292 | } 293 | }, 294 | "nbformat": 4, 295 | "nbformat_minor": 2 296 | } 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ecomunsing.com Tutorials 2 | 3 | This repository holds datasets and tutorials associated with [Ecomunsing.com](www.ecomunsing.com), my personal blog. Each of these should be self-contained and self-explanatory. I'm most likely to check the blog comments, so if you have any questions/issues please raise them in the comments section there! 4 | -------------------------------------------------------------------------------- /WemoTutorial.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Slick algorithms and whiz-bang math are nice to have, but sometimes nothing speaks as well as a hardware demo. And for energy nerds, this means flashing lights. Rather than wrangling an Arduino and working with LEDs, another option is to get a quick start using 'smart' WiFi-controllable lightbulbs and a Python package to control them.\n", 8 | "\n", 9 | "Based on their low price and Python support with the [Pywemo package](https://github.com/pavoni/pywemo/tree/master/pywemo), I picked up a set of Belkin Wemo bulbs and have been using them for demos. As I couldn't find a great demo of how to control them, I'm sharing my learnings in this tutorial.\n", 10 | "\n", 11 | "**Why you might want to use Wemo smart bulbs:**\n", 12 | "- Low price\n", 13 | "- Good open-source software controls\n", 14 | "- Popular smart switch\n", 15 | "\n", 16 | "**Why not to use Wemo smart bulbs:**\n", 17 | "- Limited support: see notes below\n", 18 | "- Designed for home applications; limited wifi configurability\n", 19 | "\n", 20 | "**Bulbs which work with the Wemo Link bridge:** \n", 21 | "- Belkin Wemo bulbs (no longer sold separately; only available with starter kit)\n", 22 | "- Sylvania Lightify A19 'tunable white' bulbs\n", 23 | "- Sylvania Lightify A19 'Tunable Color' bulbs (not tested).\n", 24 | "- Cree Connected Soft White dimmable LED Bulb (not tested).\n", 25 | "- Note: The standard non-tunable white Lightify bulb **does not support the Wemo Link**\n", 26 | "\n", 27 | "The smart device space is changing fast, and there is not good interoperability between manufacturers due to differences in their underlying zigbee protocols. Even Sylvania/Osram and Belkin, who had been collaborating on the WeMo bulbs, no longer have full compatability between their bulbs.\n", 28 | "\n", 29 | "Some of the other manufacturers with smart bulbs and Python support are:\n", 30 | "- Philips Hue and [Phue python package](https://github.com/studioimaginaire/phue)\n", 31 | "- LIFX and [Lifxlan python package](https://github.com/mclarkk/lifxlan)\n", 32 | "\n", 33 | "If you know of others, let me know! I haven't explored the [open-source Home Assistant app](https://home-assistant.io) and python hooks, but this may be another useful route.\n", 34 | "\n", 35 | "If I had to do this again, I would go with Phillips (more likely to have forward support), or a Samsung SmartThings base (broader support for a variety of devices).\n", 36 | "\n", 37 | "### Understanding Ouimeaux and Pywemo\n", 38 | "Ouimeaux is an underlying API interface that provids hooks for communicating with all of the WeMo devices (and others). Pywemo is an extension package which runs on top of ouimeaux and provides functionality for Wemo devices (especially smart bulbs). The raw ouimeaux package does not allow for controlled dimming or tuning light color. **Bottom line:** If you want to dim a set of smart bulbs, go with the pywemo package\n", 39 | "\n", 40 | "Note: I've [submitted a pull request](https://github.com/pavoni/pywemo/pull/71) to fix a bug that affected color tuning in the Sylvania Lightify Tunable White bulb. Work with the github version until this is released to Pip.\n", 41 | "\n", 42 | "### Setup\n", 43 | "If you're using these for demos (and not on a home network) you'll need to get used to resetting the WeMo Link and lightbulbs. Note that the WeMo app doesn't support token-based device authentication or authentication portals, so won't work on many commercial networks (hotels, universities). As a workaround, I've sometimes created an ad-hoc network using a cell phone, and connected the WeMo Link and my laptop to this cell phone network.\n", 44 | "\n", 45 | "**Wemo Link light codes:**\n", 46 | "- Alternating orange and green: Ready to setup; connect to ad-hoc wifi and set up wifi via the WeMo mobile app (ad-hoc wifi SSID is like 'WeMo.Link.A01'\n", 47 | "- Flashing green: Startup mode after being unplugged; working on connecting to WiFi and bulbs\n", 48 | "- Flashing orange: Disconnected from WiFi or unable to find network\n", 49 | "- No lights: Working\n", 50 | "\n", 51 | "**Changing WiFi networks:**\n", 52 | "- Reset the WeMo Link (remove from outlet, press down reset button, plug in outlet, wait ~10s, release reset button, wait until alternating orange and green)\n", 53 | "- Use mobile device to connect to WeMo link's ad-hoc WiFi network\n", 54 | "- Launch WeMo app and set up WiFi preferences for Link\n", 55 | "- While Link is searching, reset the bulbs: Unplug for 1s, plug in for 3s... and repeat 3-5 times.\n", 56 | "- When bulbs have been reset, they will flash to indicate that they have reset\n", 57 | "- Bulbs should appear in WeMo Link app search screen. If not, reset again.\n", 58 | "\n" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 1, 64 | "metadata": { 65 | "collapsed": false, 66 | "deletable": true, 67 | "editable": true 68 | }, 69 | "outputs": [ 70 | { 71 | "name": "stdout", 72 | "output_type": "stream", 73 | "text": [ 74 | "Current environment directory:/usr/local/Cellar/python/2.7.12/Frameworks/Python.framework/Versions/2.7\n", 75 | "System version: 2.7.12 (default, Aug 23 2016, 08:54:23) \n", 76 | "[GCC 4.2.1 Compatible Apple LLVM 7.3.0 (clang-703.0.31)]\n", 77 | "Current working directory: /Users/emunsing/GoogleDrive/Papers/EnergyBlockchain/Coding\n" 78 | ] 79 | } 80 | ], 81 | "source": [ 82 | "import pywemo\n", 83 | "import os, sys, time\n", 84 | "import numpy as np\n", 85 | "import matplotlib.pyplot as plt\n", 86 | "% matplotlib inline\n", 87 | "\n", 88 | "print(\"Current environment directory:\" + sys.prefix)\n", 89 | "print(\"System version: \"+sys.version)\n", 90 | "print(\"Current working directory: \"+os.getcwd())" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "After making sure that we're on the same network as the WeMo bridge and that the bridge is on (using the WeMo app), we need to discover the WeMo devices on the network." 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 2, 103 | "metadata": { 104 | "collapsed": false, 105 | "deletable": true, 106 | "editable": true 107 | }, 108 | "outputs": [ 109 | { 110 | "name": "stderr", 111 | "output_type": "stream", 112 | "text": [ 113 | "/usr/local/lib/python2.7/site-packages/pywemo/ssdp.py:142: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.\n", 114 | " if tree:\n" 115 | ] 116 | }, 117 | { 118 | "data": { 119 | "text/plain": [ 120 | "[]" 121 | ] 122 | }, 123 | "execution_count": 2, 124 | "metadata": {}, 125 | "output_type": "execute_result" 126 | } 127 | ], 128 | "source": [ 129 | "devices = pywemo.discover_devices()\n", 130 | "devices" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": {}, 136 | "source": [ 137 | "Great- we have a WeMo Link, which has some child Lights: we get details on that by accessing the `.Lights` property, which returns a dictionary keyed with the serial numbers of the lights:" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": 3, 143 | "metadata": { 144 | "collapsed": false 145 | }, 146 | "outputs": [ 147 | { 148 | "data": { 149 | "text/plain": [ 150 | "{'8418260000053C9D': ,\n", 151 | " '94103EF6BF42C290': ,\n", 152 | " '94103EF6BF439599': }" 153 | ] 154 | }, 155 | "execution_count": 3, 156 | "metadata": {}, 157 | "output_type": "execute_result" 158 | } 159 | ], 160 | "source": [ 161 | "devices[0].Lights" 162 | ] 163 | }, 164 | { 165 | "cell_type": "markdown", 166 | "metadata": {}, 167 | "source": [ 168 | "Serial numbers are a bit difficult to remember, and what matters is the `LIGHT` objects... so let's zip these up into a dictionary with names of our own creation:" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": 4, 174 | "metadata": { 175 | "collapsed": false 176 | }, 177 | "outputs": [ 178 | { 179 | "data": { 180 | "text/plain": [ 181 | "{'bathroom': ,\n", 182 | " 'bedroom': ,\n", 183 | " 'kitchen': }" 184 | ] 185 | }, 186 | "execution_count": 4, 187 | "metadata": {}, 188 | "output_type": "execute_result" 189 | } 190 | ], 191 | "source": [ 192 | "lights = dict(zip(['bedroom','kitchen','bathroom'],devices[0].Lights.values()))\n", 193 | "lights" 194 | ] 195 | }, 196 | { 197 | "cell_type": "markdown", 198 | "metadata": {}, 199 | "source": [ 200 | "We can now turn these on and off:" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": 5, 206 | "metadata": { 207 | "collapsed": false 208 | }, 209 | "outputs": [ 210 | { 211 | "data": { 212 | "text/plain": [ 213 | "" 214 | ] 215 | }, 216 | "execution_count": 5, 217 | "metadata": {}, 218 | "output_type": "execute_result" 219 | } 220 | ], 221 | "source": [ 222 | "lights['bedroom'].toggle()\n", 223 | "time.sleep(3)\n", 224 | "lights['bedroom'].toggle()" 225 | ] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "metadata": {}, 230 | "source": [ 231 | "Note that we need to sleep between commands, as the lights won't buffer commands but instead will execute the requests as soon as they are recieved.\n", 232 | "\n", 233 | "If we don't want to toggle them, we can force them to be either on or off:" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": 6, 239 | "metadata": { 240 | "collapsed": false 241 | }, 242 | "outputs": [ 243 | { 244 | "data": { 245 | "text/plain": [ 246 | "" 247 | ] 248 | }, 249 | "execution_count": 6, 250 | "metadata": {}, 251 | "output_type": "execute_result" 252 | } 253 | ], 254 | "source": [ 255 | "lights['bedroom'].turn_on(transition=2)\n", 256 | "time.sleep(2.1)\n", 257 | "lights['bedroom'].turn_off()" 258 | ] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "metadata": {}, 263 | "source": [ 264 | "For color-tunable lightbulbs, the `.set_temperature()` (and `.set_color`) methods provide the ability to specify a temperature. I've only worked with the temperature tunable Sylvania Lightify bulb, which appears to be tunable in the range of 2700-6500 Kelvin." 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": 7, 270 | "metadata": { 271 | "collapsed": true 272 | }, 273 | "outputs": [], 274 | "source": [ 275 | "lights['bathroom'].turn_off()\n", 276 | "lights['bathroom'].set_temperature(kelvin=2700, delay=False)\n", 277 | "lights['bathroom'].turn_on()\n", 278 | "time.sleep(0.5)\n", 279 | "lights['bathroom'].set_temperature(kelvin=6500,transition=2,delay=False)\n", 280 | "time.sleep(2.2)\n", 281 | "lights['bathroom'].set_temperature(kelvin=2700,transition=2,delay=False)\n", 282 | "time.sleep(2.2)" 283 | ] 284 | }, 285 | { 286 | "cell_type": "markdown", 287 | "metadata": {}, 288 | "source": [ 289 | "We can additionally control of brightness using the optional `level` argument of the `turn_on` method. Values for `level` should be in the range 0-255. Add this to a loop, and we can start to have some fun visualizations:" 290 | ] 291 | }, 292 | { 293 | "cell_type": "code", 294 | "execution_count": 8, 295 | "metadata": { 296 | "collapsed": false 297 | }, 298 | "outputs": [ 299 | { 300 | "data": { 301 | "text/plain": [ 302 | "[]" 303 | ] 304 | }, 305 | "execution_count": 8, 306 | "metadata": {}, 307 | "output_type": "execute_result" 308 | }, 309 | { 310 | "data": { 311 | "image/png": "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\n", 312 | "text/plain": [ 313 | "" 314 | ] 315 | }, 316 | "metadata": {}, 317 | "output_type": "display_data" 318 | } 319 | ], 320 | "source": [ 321 | "max_brightness = 255\n", 322 | "x = np.arange(0,2,0.01)\n", 323 | "brightness = np.cos(2*np.pi * x)*max_brightness/2. + max_brightness/2.\n", 324 | "plt.plot(brightness)" 325 | ] 326 | }, 327 | { 328 | "cell_type": "code", 329 | "execution_count": 9, 330 | "metadata": { 331 | "collapsed": false 332 | }, 333 | "outputs": [], 334 | "source": [ 335 | "for y in brightness:\n", 336 | " lights['bathroom'].turn_on(level=y,transition=0.1,force_update=True)\n", 337 | " time.sleep(0.11)" 338 | ] 339 | }, 340 | { 341 | "cell_type": "markdown", 342 | "metadata": {}, 343 | "source": [ 344 | "Note that the light shuts off when `level=0` and switches back on as soon as level is positive- this is not a continuous mapping as might be expected. Also, you may want to cap the brightness of the lightbulbs, or use a shade (an upside down paper coffee cup is a convenient lampshade)\n", 345 | "\n", 346 | "Put it together, and we can have a simple combined visualization with changing both temperature and brightness:" 347 | ] 348 | }, 349 | { 350 | "cell_type": "code", 351 | "execution_count": 10, 352 | "metadata": { 353 | "collapsed": false 354 | }, 355 | "outputs": [], 356 | "source": [ 357 | "temp_range = 6500-2700\n", 358 | "temp_base = 2700\n", 359 | "temperature = np.cos(np.pi * x)*temp_range*0.5 + temp_range*0.5 + temp_base\n", 360 | "\n", 361 | "for i in range(len(x)):\n", 362 | " lights['bathroom'].set_temperature(kelvin=temperature[i], delay=False)\n", 363 | " lights['bathroom'].turn_on(level=brightness[i],transition=0.1,force_update=True)" 364 | ] 365 | }, 366 | { 367 | "cell_type": "markdown", 368 | "metadata": {}, 369 | "source": [ 370 | "I haven't played with Groups, but the functionality is supported- check out `bridge.py` in the pywemo project!" 371 | ] 372 | } 373 | ], 374 | "metadata": { 375 | "kernelspec": { 376 | "display_name": "Python 2", 377 | "language": "python", 378 | "name": "python2" 379 | }, 380 | "language_info": { 381 | "codemirror_mode": { 382 | "name": "ipython", 383 | "version": 2 384 | }, 385 | "file_extension": ".py", 386 | "mimetype": "text/x-python", 387 | "name": "python", 388 | "nbconvert_exporter": "python", 389 | "pygments_lexer": "ipython2", 390 | "version": "2.7.12" 391 | } 392 | }, 393 | "nbformat": 4, 394 | "nbformat_minor": 1 395 | } 396 | --------------------------------------------------------------------------------