> You cannot get educated by this self-propagating system in which people study > to pass exams, and teach others to pass exams, but nobody knows anything. You > learn something by doing it yourself, by asking questions, by thinking, and by > experimenting. > - Richard Feynman In the spirit of learning by doing, I decided to implement a simple blockchain server. More work remains, but I'm tired after working on this for ~2-3h. I'd like to reimplement this from memory using a statically typed language like Haskell. I'd also like to implement node discovery (https://en.bitcoin.it/wiki/Satoshi_Client_Node_Discovery) because that is still something I don't quite understand. But I'm signing-off for now... Change-Id: I74f424e7f52ffbf81eaad420d7d5205da66d33b5 Reviewed-on: https://cl.tvl.fyi/c/depot/+/4802 Tested-by: BuildkiteCI Reviewed-by: wpcarro <wpcarro@gmail.com> Autosubmit: wpcarro <wpcarro@gmail.com>
		
			
				
	
	
		
			263 lines
		
	
	
	
		
			7.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			263 lines
		
	
	
	
		
			7.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from flask import Flask, jsonify, request
 | |
| from hashlib import sha256
 | |
| from datetime import datetime
 | |
| from urllib.parse import urlparse
 | |
| 
 | |
| import json
 | |
| import requests
 | |
| import uuid
 | |
| 
 | |
| ################################################################################
 | |
| # Helper Functions
 | |
| ################################################################################
 | |
| 
 | |
| def hash(x):
 | |
|   return sha256(x).hexdigest()
 | |
| 
 | |
| def is_pow_valid(guess, prev_proof):
 | |
|   """
 | |
|   Return true if the hash of `guess` + `prev_proof` has 4x leading zeros.
 | |
|   """
 | |
|   return hash(str(guess + prev_proof).encode("utf8"))[:4] == "0000"
 | |
| 
 | |
| ################################################################################
 | |
| # Classes
 | |
| ################################################################################
 | |
| 
 | |
| class Node(object):
 | |
|   def __init__(self, host="0.0.0.0", port=8000):
 | |
|     self.app = Flask(__name__)
 | |
|     self.define_api()
 | |
|     self.identifier = str(uuid.uuid4())
 | |
|     self.blockchain = Blockchain()
 | |
|     self.neighbors = set()
 | |
| 
 | |
|   def add_neighbors(self, urls=None):
 | |
|     for url in urls:
 | |
|       parsed = urlparse(url)
 | |
|       if not parsed.netloc:
 | |
|         raise ValueError("Must pass valid URLs for neighbors")
 | |
|       self.neighbors.add(parsed.netloc)
 | |
| 
 | |
|   def decode_chain(chain_json):
 | |
|     return Blockchain(
 | |
|         blocks=[
 | |
|             Block(
 | |
|                 index=block["index"],
 | |
|                 ts=block["ts"],
 | |
|                 transactions=[
 | |
|                     Transaction(
 | |
|                         origin=tx["origin"],
 | |
|                         target=tx["target"],
 | |
|                         amount=tx["amount"])
 | |
|                         for tx in block["ts"]
 | |
|                 ],
 | |
|                 proof=block["proof"],
 | |
|                 prev_hash=block["prev_hash"])
 | |
|                 for block in chain_json["blocks"]
 | |
|         ],
 | |
|         transactions=[
 | |
|             Transaction(
 | |
|                 origin=tx["origin"],
 | |
|                 target=tx["target"],
 | |
|                 amount=tx["amount"])
 | |
|                 for tx in chain_json["transactions"]
 | |
|         ])
 | |
| 
 | |
|   def resolve_conflicts(self):
 | |
|     auth_chain, auth_length = self.blockchain, len(self.blockchain)
 | |
| 
 | |
|     for neighbor in self.neighbors:
 | |
|       res = requests.get(f"http://{neighbor}/chain")
 | |
|       if res.status_code == 200 and res.json()["length"] > auth_length:
 | |
|          decoded_chain = decode_chain(res.json()["chain"])
 | |
|          if Blockchain.is_valid(decoded_chain):
 | |
|            auth_length = res.json()["length"]
 | |
|            auth_chain = decoded_chain
 | |
| 
 | |
|       self.blockchain = auth_chain
 | |
| 
 | |
|   def define_api(self):
 | |
|     def msg(x):
 | |
|       return jsonify({"message": x})
 | |
| 
 | |
|     ############################################################################
 | |
|     # /
 | |
|     ############################################################################
 | |
| 
 | |
|     @self.app.route("/healthz", methods={"GET"})
 | |
|     def healthz():
 | |
|       return "ok"
 | |
| 
 | |
|     @self.app.route("/reset", methods={"GET"})
 | |
|     def reset():
 | |
|       self.blockchain = Blockchain()
 | |
|       return msg("Success")
 | |
| 
 | |
|     @self.app.route("/mine", methods={"GET"})
 | |
|     def mine():
 | |
|       # calculate POW
 | |
|       proof = self.blockchain.prove_work()
 | |
| 
 | |
|       # reward miner
 | |
|       self.blockchain.add_transaction(
 | |
|           origin="0", # zero signifies that this is a newly minted coin
 | |
|           target=self.identifier,
 | |
|           amount=1)
 | |
| 
 | |
|       # publish new block
 | |
|       self.blockchain.add_block(proof=proof)
 | |
|       return msg("Success")
 | |
| 
 | |
|     ############################################################################
 | |
|     # /transactions
 | |
|     ############################################################################
 | |
| 
 | |
|     @self.app.route("/transactions/new", methods={"POST"})
 | |
|     def new_transaction():
 | |
|       payload = request.get_json()
 | |
| 
 | |
|       self.blockchain.add_transaction(
 | |
|           origin=payload["origin"],
 | |
|           target=payload["target"],
 | |
|           amount=payload["amount"])
 | |
|       return msg("Success")
 | |
| 
 | |
|     ############################################################################
 | |
|     # /blocks
 | |
|     ############################################################################
 | |
| 
 | |
|     @self.app.route("/chain", methods={"GET"})
 | |
|     def view_blocks():
 | |
|       return jsonify({
 | |
|           "length": len(self.blockchain),
 | |
|           "chain": self.blockchain.dictify(),
 | |
|       })
 | |
| 
 | |
|     ############################################################################
 | |
|     # /nodes
 | |
|     ############################################################################
 | |
|     @self.app.route("/node/neighbors", methods={"GET"})
 | |
|     def view_neighbors():
 | |
|       return jsonify({"neighbors": list(self.neighbors)})
 | |
| 
 | |
|     @self.app.route("/node/register", methods={"POST"})
 | |
|     def register_nodes():
 | |
|       payload = request.get_json()["neighbors"]
 | |
|       payload = set(payload) if payload else set()
 | |
|       self.add_neighbors(payload)
 | |
|       return msg("Success")
 | |
| 
 | |
|     @self.app.route("/node/resolve", methods={"GET"})
 | |
|     def resolve_nodes():
 | |
|       self.resolve_conflicts()
 | |
|       return msg("Success")
 | |
| 
 | |
|   def run(self):
 | |
|     self.app.run(host="0.0.0.0", port=8000)
 | |
| 
 | |
| 
 | |
| class Blockchain(object):
 | |
|   def __init__(self, blocks=None, transactions=None):
 | |
|     self.blocks = blocks or []
 | |
|     self.transactions = transactions or []
 | |
|     self.add_block()
 | |
| 
 | |
|   def __len__(self):
 | |
|     return len(self.blocks)
 | |
| 
 | |
|   def __iter__(self):
 | |
|     for block in self.blocks:
 | |
|       yield block
 | |
| 
 | |
|   def prove_work(self):
 | |
|     guess, prev_proof = 0, self.blocks[-1].proof or 0
 | |
|     while not is_pow_valid(guess, prev_proof):
 | |
|       guess += 1
 | |
|     return guess
 | |
| 
 | |
|   def add_block(self, prev_hash=None, proof=None):
 | |
|     b = Block(
 | |
|         index=len(self),
 | |
|         transactions=self.transactions,
 | |
|         prev_hash=self.blocks[-1].hash() if self.blocks else None,
 | |
|         proof=proof)
 | |
|     self.blocks.append(b)
 | |
|     return b
 | |
| 
 | |
|   def adopt_blocks(self, json_blocks):
 | |
|     pass
 | |
| 
 | |
|   def add_transaction(self, origin=None, target=None, amount=None):
 | |
|     tx = Transaction(origin=origin, target=target, amount=amount)
 | |
|     self.transactions.append(tx)
 | |
| 
 | |
|   @staticmethod
 | |
|   def is_valid(chain):
 | |
|     prev_block = next(chain)
 | |
| 
 | |
|     for block in chain:
 | |
|       if block.prev_hash != prev_block.hash() or not is_pow_valid(prev_block.proof, block.proof):
 | |
|         return False
 | |
|       prev_block = block
 | |
| 
 | |
|     return True
 | |
| 
 | |
|   def dictify(self):
 | |
|     return {
 | |
|         "blocks": [block.dictify() for block in self.blocks],
 | |
|         "transactions": [tx.dictify() for tx in self.transactions],
 | |
|     }
 | |
| 
 | |
| 
 | |
| class Block(object):
 | |
|   def __init__(self, index=None, ts=None, transactions=None, proof=None, prev_hash=None):
 | |
|     self.index = index
 | |
|     self.ts = ts or str(datetime.now())
 | |
|     self.transactions = transactions
 | |
|     self.proof = proof
 | |
|     self.prev_hash = prev_hash
 | |
| 
 | |
|   def hash(self):
 | |
|     return sha256(self.jsonify().encode()).hexdigest()
 | |
| 
 | |
|   def dictify(self):
 | |
|     return {
 | |
|         "index": self.index,
 | |
|         "ts": self.ts,
 | |
|         "transactions": [tx.dictify() for tx in self.transactions],
 | |
|         "proof": self.proof,
 | |
|         "prev_hash": self.prev_hash,
 | |
|     }
 | |
| 
 | |
|   def jsonify(self):
 | |
|     return json.dumps(self.dictify(), sort_keys=True)
 | |
| 
 | |
| class Transaction(object):
 | |
|   def __init__(self, origin=None, target=None, amount=None):
 | |
|     if None in {origin, target, amount}:
 | |
|       raise ValueError("To create a Transaction, you must provide origin, target, and amount")
 | |
| 
 | |
|     self.origin = origin
 | |
|     self.target = target
 | |
|     self.amount = amount
 | |
| 
 | |
|   def dictify(self):
 | |
|     return {
 | |
|         "origin": self.origin,
 | |
|         "target": self.target,
 | |
|         "amount": self.amount,
 | |
|     }
 | |
| 
 | |
|   def jsonify(self):
 | |
|     return json.dumps(self.dictify(), sort_keys=True)
 | |
| 
 | |
| ################################################################################
 | |
| # Main
 | |
| ################################################################################
 | |
| 
 | |
| def run():
 | |
|   Node(host="0.0.0.0", port=8000).run()
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|   run()
 |