Skip to content

Commit af2c842

Browse files
committed
add tinychain legacy
1 parent 5c003d8 commit af2c842

File tree

14 files changed

+1043
-0
lines changed

14 files changed

+1043
-0
lines changed

docs/tinychain-legacy/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__
2+
*.db
3+
*.log

docs/tinychain-legacy/Pipfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[[source]]
2+
url = "https://pypi.org/simple"
3+
verify_ssl = true
4+
name = "pypi"
5+
6+
[packages]
7+
pyparsing = "*"
8+
ecdsa = "*"
9+
pyyaml = "*"
10+
flask = "*"
11+
requests = "*"
12+
peewee = "*"
13+
sqlalchemy = "*"
14+
tabulate = "*"
15+
colored-traceback = "*"
16+
17+
[dev-packages]
18+
19+
[requires]
20+
python_version = "3.9"

docs/tinychain-legacy/Pipfile.lock

Lines changed: 518 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/tinychain-legacy/README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
tinychain
2+
=========
3+
4+
**an ultralight blockchain core, written in Python.**
5+
6+
tinychain is the smallest implementation of a blockchain (BFT replicated state machine) you will ever find. It reimplements the full bitcoin consensus (Nakamoto consensus) with a custom VM based on Brainfuck.
7+
8+
1366 lines of code so far. inspired by [geohot's tinygrad](https://github.com/geohot/tinygrad).
9+
10+
* cryptography
11+
* transactions
12+
* consensus - Nakamoto POW
13+
* VM’s - Brainfuck
14+
* state machine
15+
* gas markets
16+
* protocol, RPC, and P2P networking
17+
18+
Let the devs do what they do best - building cool stuff.
19+
20+
| **Area** | **Description** | **Status** |
21+
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
22+
| VM | [Brainfuck](https://en.wikipedia.org/wiki/Brainfuck) smart contracts | ✅⚠️ 60% Done |
23+
| Consensus | Bitcoin / Nakamoto / POW with ZK-friendly hash function | ⚠️ WIP |
24+
| Tokenomics | Ethereum-like - native token + fixed exchange rate to gas | ✅ Done |
25+
| Cryptography | ECDSA wallets, SECP256k1 curve (same as BTC), SHA-2 256 bit hash function | ✅ Done |
26+
| Networking | P2P and RPC servers both use HTTP, gossip network architecture | ⚠️ WIP |
27+
| ZK proofs | ZK for compression of tinychain. Use either [groth16](https://github.com/erhant/zkbrainfuck) or [halo2](https://github.com/cryptape/ckb-bf-zkvm) SNARK proofs for brainfuck. TBA we will rework consensus/crypto to use SNARK-friendly tech (MiMC/Poseidon hash function, SNARK-friendly signature scheme) | |
28+
29+
## Install.
30+
31+
Requirements:
32+
33+
* Python 3.
34+
* `pipenv`, `pip` or something like it.
35+
36+
Instructions:
37+
38+
```sh
39+
# Install dependencies.
40+
pipenv install
41+
pipenv shell
42+
```
43+
44+
## Usage.
45+
46+
Demo is a work-in-progress:
47+
48+
```py
49+
# Run two nodes which will mine and sync.
50+
PYTHONPATH=./src python3 src/tinychain/consensus/bitcoin.py
51+
PYTHONPATH=./src python3 src/tinychain/consensus/bitcoin.py
52+
```
53+
54+
## Why?
55+
56+
It takes too long to digest the architecture of any modern blockchain like Ethereum, Optimism, etc.
57+
58+
geohot took PyTorch and distilled it into >10,000 LOC. let's do the same for a blockchain.
59+
60+
maybe we'll learn some things along the way.
61+
62+
## What is a blockchain?
63+
64+
It's really quite an interesting combination of many things.
65+
66+
* a blockchain is a P2P system based on a programmable database
67+
* users can run programs on this database
68+
* they run these programs by cryptographically signing transactions
69+
* users pay nodes in tokens for running the network
70+
* how is the cost of running transactions measured?
71+
* the programs run inside a VM, which has a metering facility for resource usage in terms of computation and storage
72+
* the unit of account for metering is called gas
73+
* gas is bought in an algorithmic market for the blockchain's native token. This is usually implemented as a "gas price auction"
74+
* the order in which these transactions are run is determined according to a consensus algorithm.
75+
* the consensus algorithm elects a node which is given the power to decide the sequence of transactions for that epoch
76+
* bitcoin uses proof-of-work, meaning that the more CPU's you have, the more likely you are to become the leader
77+
* given the sequence of transactions, we can run the state machine
78+
* the state machine bundles the VM, with a shared context of accounts and their gas credits
79+
* and this is all bundled together in the node, which provides facilities for querying the state of database
80+
81+
The goal of this project is to elucidate the primitives throughout this invention, in a very simple way, so you can experiment and play with different VM's and code.
82+
83+
## Roadmap.
84+
85+
- [x] VM
86+
- [ ] smart contracts
87+
- [x] wallet
88+
- [x] transactions
89+
- [ ] CLI
90+
- [x] state machine
91+
- [x] sequencer
92+
- [x] accounts / gas
93+
- [ ] node
94+
- [ ] consensus
95+
- [ ] networking
96+
97+
See `node.py` for design.
98+
99+
## Feature set.
100+
101+
- **VM** and **state machine model**. Brainfuck is used as the programming runtime. It includes its own gas metering - 1 gas for computation, 3 gas for writing to memory. There is no in-built object model for now - there is only the Brainfuck VM, and its memory. Any program can write to memory and overwrite other Brainfuck.
102+
103+
- **Gas market / tokenomics**. Like Ethereum, this chain has a token and an internal unit of account called gas. There is no progressive gas auctions (PGA's) yet - for now it is a fixed exchange rate (see `gas_market.py`).
104+
105+
- **Consensus**. Bitcoin/Nakamoto consensus is currently being implemented, meaning the network runs via proof-of-work. In future, I hope to implement Tendermint proof-of-stake (see `consensus/tendermint.py` for more) with the token being staked actually hosted on an Ethereum network (L1/L2).
106+
107+
- **Cryptography**. ECDSA is used for public-private cryptography, with the SECP256k1 curve (used by Bitcoin) and the SHA-2 hash function with size set to 256 bits.
108+
109+
- **Networking**. The P2P protocol and node API server both run over HTTP. This was easy.
110+
111+
```
112+
curl -X GET http://0.0.0.0:5100/api/machine_eval -H "Content-Type: application/json" -d '{"from_acc":"","to_acc":"","data":"+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++."}'
113+
```

docs/tinychain-legacy/docs/bitcoin.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Nakamoto consensus.
2+
3+
Bitcoin consensus is really quite simple.
4+
5+
```
6+
Block
7+
Prev block hash
8+
Transactions
9+
Nonce
10+
11+
Hashcash - a work algorithm, where you can measure CPU expenditure.
12+
13+
Longest chain consensus
14+
Longest = chain with the most accumulated work
15+
16+
What is work?
17+
Accumulated hashpower (measured by hashcash solutions) that follows the difficulty readjustment schedule
18+
19+
Difficulty readjustment - in order to regulate the block production of the network,
20+
the hashcash difficulty target is adjusted every epoch to target an epoch length of 2 weeks
21+
22+
Epochs are defined every N blocks
23+
Each block includes a timestamp, which is regulated by the network to be close to real clock time
24+
25+
26+
Our algorithm for running this is really simple:
27+
- maintain a block DAG structure
28+
- each block has a parent and a nullable child
29+
- each block has its height (depth in DAG path)
30+
- each block has its accumulated difficulty stored with it
31+
- each block has its index
32+
33+
The "tip" of the chain refers to the block with the most accumulated difficulty - this is the longest chain rule
34+
35+
There are only 3 routines:
36+
- mine - produce blocks, gossip them
37+
- verify_block - verify the POW solution, the transactions and their signatures
38+
- ingest_block - add the block to the DAG
39+
```
40+
41+
42+
## Bitcoin architecture.
43+
44+
```
45+
Miner thread
46+
-> works on mining next block
47+
-> mine(block) takes a block, which includes existing details like difficulty target
48+
-> can be restarted
49+
-> start_mining:
50+
takes the current mempool (txs)
51+
and the current tip (block)
52+
and mines until we get a signal to stop
53+
then restarts
54+
55+
Before we mine, we figure out what the current difficulty is.
56+
57+
When we receive a new block:
58+
- download any parents until we reach a common block
59+
- then verify the earliest block to the latest
60+
- we also recompute the difficulty for the epochs ingested
61+
- when this is all done, we restart the miner on the freshest tip.
62+
63+
When we mine a new block:
64+
- broadcast the block
65+
- mine on this new tip
66+
```
67+
68+
Finally networking:
69+
70+
```
71+
ConsensusEngine calls protocol to gossip blocks
72+
Node uses ConsensusEngine to Sequence transactions
73+
Node uses transaction Sequence to run replicated State Machine
74+
Protocol uses Wire protocol to communicate over JSON-RPC HTTPS
75+
Node uses Protocol to receive user requests to send transcations
76+
Node receives user transactions and gossips them over network
77+
78+
79+
This doesn't even do fee markets yet. But it should soon.
80+
81+
```
82+
83+
84+
And then on top of this- the launch:
85+
86+
```
87+
People will download the "brainnode" software
88+
89+
```

docs/tinychain-legacy/docs/bitcoin.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use crate::block::{Block, BlockHash};
2+
pub mod block;
3+
4+
use std::fs;
5+
use std::cmp;
6+
use sha2::{Sha256, Digest};
7+
use std::time::{SystemTime};
8+
use ethereum_types::{U256};
9+
10+
const DICT_PATH: &str = "data/dict.txt";
11+
12+
// Returns the base-n representation of a number according to an
13+
// alphabet specified in `symbols`. The length of symbols is the
14+
// "base".
15+
//
16+
// e.g. symbols = ["0","1"] is the binary numeral system.
17+
//
18+
fn to_digits(num: u32, symbols: &Vec<&str>) -> String {
19+
let base = u32::try_from(symbols.len()).unwrap();
20+
let mut digits = String::new();
21+
let mut n = num;
22+
23+
while n > 0 {
24+
let i = n.rem_euclid(base);
25+
if !digits.is_empty() {
26+
digits += " ";
27+
}
28+
digits += symbols[usize::try_from(i).unwrap()];
29+
n /= base;
30+
}
31+
32+
digits
33+
}
34+
35+
fn sha256_digest(content: &String) -> U256 {
36+
let mut hasher = Sha256::new();
37+
hasher.update(content.clone());
38+
let digest = hasher.finalize();
39+
return U256::from(digest.as_slice());
40+
}
41+
42+
// fn current_time() -> u64 {
43+
// match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
44+
// Ok(n) => n.as_secs(),
45+
// Err(_) => panic!("SystemTime before UNIX EPOCH!"),
46+
// }
47+
// }
48+
49+
fn solve_pow(target: U256, block: &Block, dict: &Vec<&str>) -> u32 {
50+
let mut nonce_idx: u32 = 0;
51+
let _base = dict.len();
52+
53+
let mut block2 = block.clone();
54+
55+
loop {
56+
block2.nonce = nonce_idx;
57+
58+
// Build the outrage string.
59+
// Convert nonce number into alphabet of the outrage wordlist.
60+
// let outrage = to_digits(nonce_idx, &dict).as_bytes();
61+
62+
// Compute SHA256 digest.
63+
let mut hasher = Sha256::new();
64+
let buf = serde_json::to_vec(&block2).unwrap();
65+
hasher.update(buf);
66+
let digest = hasher.finalize();
67+
68+
// Convert to U256 number.
69+
let guess = U256::from(digest.as_slice());
70+
71+
if guess < target {
72+
// println!("solved target={} value={} dist={} nonce=\"{}\"", target, guess, target - guess, outrage);
73+
// target = target / 2;
74+
return nonce_idx
75+
}
76+
77+
nonce_idx += 1;
78+
}
79+
}
80+
81+
fn main() {
82+
let word_dict = fs::read_to_string(DICT_PATH).expect("Unable to read file");
83+
let dict: Vec<&str> = word_dict.lines().collect();
84+
let NULL_HASH: U256 = U256::from(0);
85+
86+
println!("Loaded dictionary of {} words", dict.len());
87+
88+
//
89+
// Mining loop.
90+
//
91+
92+
// This implements a proof-of-work algorithm, whereby the miner
93+
// searches for a value (`guess`) that is less than a `target`.
94+
// The lower the target, the higher the difficulty involved in the search process.
95+
// Unlike the usual PoW algorithms, the input content to the hash function is human-readable outrage propaganda.
96+
// A nonce is generated, which is used to index into a dictionary of outrage content, and then thereby
97+
// hashed.
98+
let genesis_block = Block {
99+
prev_block_hash: NULL_HASH,
100+
number: 0,
101+
dict_hash: sha256_digest(&word_dict.to_string()),
102+
nonce: 0
103+
};
104+
let mut prev_block = genesis_block;
105+
let mut target: U256 = U256::from_dec_str("4567192616659071619386515177772132323232230222220222226193865124364247891968").unwrap();
106+
107+
let EPOCH_LENGTH = 5;
108+
let EPOCH_TARGET_TIMESPAN_SECONDS = 5;
109+
let mut epoch_start_block_mined_at = SystemTime::now();
110+
111+
112+
println!("Starting miner...");
113+
println!(" difficulty = {}", target);
114+
println!();
115+
116+
loop {
117+
let mut pending_block = Block {
118+
prev_block_hash: prev_block.block_hash(),
119+
number: prev_block.number + 1,
120+
dict_hash: sha256_digest(&word_dict.to_string()),
121+
nonce: 0
122+
};
123+
124+
let guess = solve_pow(target, &pending_block, &dict);
125+
let outrage = to_digits(guess, &dict);
126+
println!("solved target={} value={} dist={} nonce=\"{}\"", target, guess, target - guess, outrage);
127+
128+
// Seal block.
129+
pending_block.nonce = guess;
130+
pending_block.prev_block_hash = prev_block.block_hash();
131+
132+
prev_block = pending_block;
133+
println!("mined block #{} hash={}\n", prev_block.number, prev_block.block_hash());
134+
135+
// Update difficulty/target.
136+
if prev_block.number % EPOCH_LENGTH == 0 {
137+
// 5 seconds for epoch of 5 blocks
138+
139+
let mut timespan = SystemTime::now().duration_since(epoch_start_block_mined_at).unwrap().as_secs();
140+
if timespan < EPOCH_TARGET_TIMESPAN_SECONDS/4 {
141+
timespan = EPOCH_TARGET_TIMESPAN_SECONDS/4;
142+
}
143+
if timespan > EPOCH_TARGET_TIMESPAN_SECONDS*4 {
144+
timespan = EPOCH_TARGET_TIMESPAN_SECONDS*4;
145+
}
146+
147+
let epoch = prev_block.number / EPOCH_LENGTH;
148+
let factor = timespan / EPOCH_TARGET_TIMESPAN_SECONDS;
149+
target = target * timespan / EPOCH_TARGET_TIMESPAN_SECONDS;
150+
151+
println!("epoch #{}", epoch);
152+
println!("adjusting difficulty timespan={}s factor={}", timespan, (timespan as f64) / (EPOCH_TARGET_TIMESPAN_SECONDS as f64));
153+
154+
epoch_start_block_mined_at = SystemTime::now();
155+
}
156+
}
157+
}

docs/tinychain-legacy/docs/missing.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Missing pieces
2+
==============
3+
4+
## ECDSA public key recovery
5+
6+
In Ethereum/Bitcoin, we can recover the public key from a signature.
7+
8+
The way this works - ECDSA public keys have two points (R1, R2), and on each point there is an (X,Y). For any given signature, you can recover two public keys - since ECDSA is based on squaring and there are two solutions to the square root (+ve and -ve). By encoding a value when you create the signature which specifies the parity (positive/negative) of one of these points, you can reliably recover the correct public key from the signature.
9+
10+
I had some trouble implementing this.

0 commit comments

Comments
 (0)