> 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()
 |