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…
Reference in a new issue