feat(wpcarro/scratch): create a proof-of-concept blockchain server
> 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>
This commit is contained in:
		
							parent
							
								
									afabc77f74
								
							
						
					
					
						commit
						9098920f0a
					
				
					 3 changed files with 286 additions and 0 deletions
				
			
		
							
								
								
									
										13
									
								
								users/wpcarro/scratch/blockchain/default.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								users/wpcarro/scratch/blockchain/default.nix
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | { pkgs, ... }: | ||||||
|  | 
 | ||||||
|  | let | ||||||
|  |   pypkgs = pkgs.python3Packages; | ||||||
|  | in pkgs.python3Packages.buildPythonApplication { | ||||||
|  |   pname = "main"; | ||||||
|  |   src = ./.; | ||||||
|  |   version = "0.0.1"; | ||||||
|  |   propagatedBuildInputs = with pypkgs; [ | ||||||
|  |     flask | ||||||
|  |     requests | ||||||
|  |   ]; | ||||||
|  | } | ||||||
							
								
								
									
										263
									
								
								users/wpcarro/scratch/blockchain/main.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								users/wpcarro/scratch/blockchain/main.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,263 @@ | ||||||
|  | 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() | ||||||
							
								
								
									
										10
									
								
								users/wpcarro/scratch/blockchain/setup.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								users/wpcarro/scratch/blockchain/setup.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | from setuptools import setup | ||||||
|  | 
 | ||||||
|  | setup( | ||||||
|  |     name='main', | ||||||
|  |     version='0.0.1', | ||||||
|  |     py_modules=['main'], | ||||||
|  |     entry_points={ | ||||||
|  |       'console_scripts': ['main = main:run'] | ||||||
|  |     }, | ||||||
|  | ) | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue