├── backend ├── __init__.py ├── util │ ├── __init__.py │ ├── crypto_hash.py │ └── hex_to_binary.py ├── wallet │ ├── __init__.py │ ├── transaction_pool.py │ ├── wallet.py │ └── transaction.py ├── blockchain │ ├── __init__.py │ ├── blockchain.py │ └── block.py ├── app │ ├── __main__.py │ └── __init__.py ├── tests │ ├── util │ │ ├── test_hex_to_binary.py │ │ └── test_crypto_hash.py │ ├── blockchain │ │ ├── test_blockchain.py │ │ └── test_block.py │ └── wallet │ │ ├── test_wallet.py │ │ ├── test_transaction_pool.py │ │ └── test_transaction.py ├── config.py ├── env.example ├── scripts │ ├── average_block_rate.py │ └── test_app.py └── pubsub.py ├── blockchain-env ├── bin │ ├── python │ ├── python3 │ ├── flask │ ├── pip │ ├── pip3 │ ├── pytest │ ├── py.test │ ├── activate.csh │ ├── activate │ └── activate.fish └── pyvenv.cfg ├── frontend ├── src │ ├── history.js │ ├── assets │ │ └── logo.png │ ├── config.js │ ├── components │ │ ├── Transaction.js │ │ ├── App.js │ │ ├── Block.js │ │ ├── TransactionPool.js │ │ ├── Blockchain.js │ │ └── ConductTransaction.js │ ├── index.css │ └── index.js ├── .gitignore ├── public │ └── index.html ├── package.json └── README.md ├── python_blockchain_logo.png ├── .gitignore ├── requirements.txt ├── PUBNUB_CONFIG.md └── README.md /backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/wallet/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/blockchain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blockchain-env/bin/python: -------------------------------------------------------------------------------- 1 | python3.13 -------------------------------------------------------------------------------- /blockchain-env/bin/python3: -------------------------------------------------------------------------------- 1 | python3.13 -------------------------------------------------------------------------------- /frontend/src/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | export default createBrowserHistory(); 4 | -------------------------------------------------------------------------------- /python_blockchain_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/15Dkatz/python-blockchain-tutorial/HEAD/python_blockchain_logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *__pycache__* 3 | *node_modules* 4 | blockchain-env/lib/python3.7 5 | *.DS_Store* 6 | 7 | *.env 8 | 9 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/15Dkatz/python-blockchain-tutorial/HEAD/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /backend/app/__main__.py: -------------------------------------------------------------------------------- 1 | from backend.app import app, PORT 2 | 3 | if __name__ == "__main__": 4 | app.run(host="0.0.0.0", port=PORT, debug=True) 5 | 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==8.4.2 2 | Flask==3.1.2 3 | Flask-Cors==3.0.8 4 | pubnub==10.4.1 5 | requests==2.32.5 6 | cryptography==46.0.3 7 | python-dotenv==1.0.0 8 | -------------------------------------------------------------------------------- /backend/tests/util/test_hex_to_binary.py: -------------------------------------------------------------------------------- 1 | from backend.util.hex_to_binary import hex_to_binary 2 | 3 | def test_hex_to_binary(): 4 | original_number = 789 5 | hex_number = hex(original_number)[2:] 6 | binary_number = hex_to_binary(hex_number) 7 | 8 | assert int(binary_number, 2) == original_number 9 | -------------------------------------------------------------------------------- /blockchain-env/bin/flask: -------------------------------------------------------------------------------- 1 | #!/Users/dk/Code/python-blockchain-tutorial/blockchain-env/bin/python3.13 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from flask.cli import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /backend/config.py: -------------------------------------------------------------------------------- 1 | NANOSECONDS = 1 2 | MICROSECONDS = 1000 * NANOSECONDS 3 | MILLISECONDS = 1000 * MICROSECONDS 4 | SECONDS = 1000 * MILLISECONDS 5 | 6 | MINE_RATE = 4 * SECONDS 7 | 8 | STARTING_BALANCE = 1000 9 | 10 | MINING_REWARD = 50 11 | MINING_REWARD_INPUT = { 'address': '*--official-mining-reward--*' } 12 | -------------------------------------------------------------------------------- /blockchain-env/bin/pip: -------------------------------------------------------------------------------- 1 | #!/Users/dk/Code/python-blockchain-tutorial/blockchain-env/bin/python3.13 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /blockchain-env/bin/pip3: -------------------------------------------------------------------------------- 1 | #!/Users/dk/Code/python-blockchain-tutorial/blockchain-env/bin/python3.13 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pip._internal.cli.main import main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(main()) 9 | -------------------------------------------------------------------------------- /blockchain-env/bin/pytest: -------------------------------------------------------------------------------- 1 | #!/Users/dk/Code/python-blockchain-tutorial/blockchain-env/bin/python3.13 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pytest import console_main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(console_main()) 9 | -------------------------------------------------------------------------------- /blockchain-env/bin/py.test: -------------------------------------------------------------------------------- 1 | #!/Users/dk/Code/python-blockchain-tutorial/blockchain-env/bin/python3.13 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import sys 5 | from pytest import console_main 6 | if __name__ == '__main__': 7 | sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) 8 | sys.exit(console_main()) 9 | -------------------------------------------------------------------------------- /backend/env.example: -------------------------------------------------------------------------------- 1 | # Environment Configuration 2 | # Copy this file to .env and update with your actual values 3 | 4 | # PubNub Configuration 5 | # Get your keys from: https://www.pubnub.com/ 6 | PUBNUB_PUBLISH_KEY=pub-c-your-actual-publish-key 7 | PUBNUB_SUBSCRIBE_KEY=sub-c-your-actual-subscribe-key 8 | PUBNUB_USER_ID=blockchain-node-1 9 | 10 | -------------------------------------------------------------------------------- /blockchain-env/pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /opt/homebrew/opt/python@3.13/bin 2 | include-system-site-packages = false 3 | version = 3.13.0 4 | executable = /opt/homebrew/Cellar/python@3.13/3.13.0_1/Frameworks/Python.framework/Versions/3.13/bin/python3.13 5 | command = /opt/homebrew/opt/python@3.13/bin/python3.13 -m venv /Users/dk/Code/python-blockchain-tutorial/blockchain-env 6 | -------------------------------------------------------------------------------- /frontend/src/config.js: -------------------------------------------------------------------------------- 1 | const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5050'; 2 | const NANOSECONDS_PY = 1; 3 | const MICROSECONDS_PY = 1000 * NANOSECONDS_PY; 4 | const MILLISECONDS_PY = 1000 * MICROSECONDS_PY; 5 | 6 | const MILLISECONDS_JS = 1; 7 | const SECONDS_JS = MILLISECONDS_JS * 1000; 8 | 9 | export { API_BASE_URL, MILLISECONDS_PY, SECONDS_JS }; 10 | -------------------------------------------------------------------------------- /backend/tests/util/test_crypto_hash.py: -------------------------------------------------------------------------------- 1 | from backend.util.crypto_hash import crypto_hash 2 | 3 | def test_crypto_hash(): 4 | # It should create the same hash with arguments of different data types in 5 | # any order 6 | assert crypto_hash(1, [2], 'three') == crypto_hash('three', 1, [2]) 7 | assert crypto_hash('foo') == 'b2213295d564916f89a6a42455567c87c3f480fcd7a1c15e220f17d7169a790b' 8 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /backend/tests/blockchain/test_blockchain.py: -------------------------------------------------------------------------------- 1 | from backend.blockchain.blockchain import Blockchain 2 | from backend.blockchain.block import GENESIS_DATA 3 | 4 | def test_blockchain_instance(): 5 | blockchain = Blockchain() 6 | assert blockchain.chain[0].hash == GENESIS_DATA['hash'] 7 | 8 | def test_add_block(): 9 | blockchain = Blockchain() 10 | data = 'test-data' 11 | blockchain.add_block(data) 12 | 13 | assert blockchain.chain[-1].data == data 14 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pychain 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /backend/tests/wallet/test_wallet.py: -------------------------------------------------------------------------------- 1 | from backend.wallet.wallet import Wallet 2 | 3 | def test_verify_valid_signature(): 4 | data = { 'foo': 'test_data' } 5 | wallet = Wallet() 6 | signature = wallet.sign(data) 7 | 8 | assert Wallet.verify(wallet.public_key, data, signature) 9 | 10 | def test_verify_invalid_signature(): 11 | data = { 'foo': 'test_data' } 12 | wallet = Wallet() 13 | signature = wallet.sign(data) 14 | 15 | assert not Wallet.verify(Wallet().public_key, data, signature) 16 | -------------------------------------------------------------------------------- /frontend/src/components/Transaction.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Transaction({ transaction }) { 4 | const { input, output } = transaction; 5 | const recipients = Object.keys(output); 6 | 7 | return ( 8 |
9 |
From: {input.address}
10 | { 11 | recipients.map(recipient => ( 12 |
13 | To: {recipient} | Sent: {output[recipient]} 14 |
15 | )) 16 | } 17 |
18 | ) 19 | } 20 | 21 | export default Transaction; 22 | -------------------------------------------------------------------------------- /backend/util/crypto_hash.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | 4 | def crypto_hash(*args): 5 | """ 6 | Return a sha-256 hash of the given arguments. 7 | """ 8 | stringified_args = sorted(map(lambda data: json.dumps(data), args)) 9 | joined_data = ''.join(stringified_args) 10 | 11 | return hashlib.sha256(joined_data.encode('utf-8')).hexdigest() 12 | 13 | def main(): 14 | print(f"crypto_hash('one', 2, [3]): {crypto_hash('one', 2, [3])}") 15 | print(f"crypto_hash(2, 'one', [3]): {crypto_hash(2, 'one', [3])}") 16 | 17 | if __name__ == '__main__': 18 | main() 19 | -------------------------------------------------------------------------------- /backend/scripts/average_block_rate.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from backend.blockchain.blockchain import Blockchain 4 | from backend.config import SECONDS 5 | 6 | blockchain = Blockchain() 7 | 8 | times = [] 9 | 10 | for i in range(1000): 11 | start_time = time.time_ns() 12 | blockchain.add_block(i) 13 | end_time = time.time_ns() 14 | 15 | time_to_mine = (end_time - start_time) / SECONDS 16 | times.append(time_to_mine) 17 | 18 | average_time = sum(times) / len(times) 19 | 20 | print(f'New block difficulty: {blockchain.chain[-1].difficulty}') 21 | print(f'Time to mine new block: {time_to_mine}s') 22 | print(f'Average time to add blocks: {average_time}s\n') 23 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #444; 3 | color: #fff; 4 | text-align: center; 5 | font-size: 18px; 6 | font-family: 'Quicksand'; 7 | padding-top: 5%; 8 | word-wrap: break-word; 9 | } 10 | 11 | .logo { 12 | width: 250px; 13 | height: 250px; 14 | } 15 | 16 | .App { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | 22 | .WalletInfo { 23 | width: 500px; 24 | } 25 | 26 | .Block { 27 | border: 1px solid #fff; 28 | padding: 10%; 29 | margin: 2%; 30 | } 31 | 32 | .Transaction { 33 | padding: 5%; 34 | } 35 | 36 | .Blockchain, .ConductTransaction, .TransactionPool { 37 | margin: 10%; 38 | margin-top: 5%; 39 | } 40 | 41 | a, a:hover { 42 | color: #e66; 43 | text-decoration: underline; 44 | } 45 | 46 | input, button { 47 | color: black; 48 | } -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 4 | import './index.css'; 5 | import App from './components/App'; 6 | import Blockchain from './components/Blockchain'; 7 | import ConductTransaction from './components/ConductTransaction'; 8 | import TransactionPool from './components/TransactionPool'; 9 | 10 | const root = ReactDOM.createRoot(document.getElementById('root')); 11 | root.render( 12 | 13 | 14 | } /> 15 | } /> 16 | } /> 17 | } /> 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "history": "^5.3.0", 7 | "react": "^18.2.0", 8 | "react-bootstrap": "^2.9.2", 9 | "react-dom": "^18.2.0", 10 | "react-router": "^6.20.1", 11 | "react-router-dom": "^6.20.1", 12 | "react-scripts": "5.0.1" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/scripts/test_app.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | 4 | from backend.wallet.wallet import Wallet 5 | 6 | BASE_URL = 'http://localhost:5050' 7 | 8 | def get_blockchain(): 9 | return requests.get(f'{BASE_URL}/blockchain').json() 10 | 11 | def get_blockchain_mine(): 12 | return requests.get(f'{BASE_URL}/blockchain/mine').json() 13 | 14 | def post_wallet_transact(recipient, amount): 15 | return requests.post( 16 | f'{BASE_URL}/wallet/transact', 17 | json={ 'recipient': recipient, 'amount': amount } 18 | ).json() 19 | 20 | start_blockchain = get_blockchain() 21 | print(f'start_blockchain: {start_blockchain}') 22 | 23 | recipient = Wallet().address 24 | post_wallet_transact_1 = post_wallet_transact(recipient, 21) 25 | print(f'\npost_wallet_transact_1: {post_wallet_transact_1}') 26 | 27 | time.sleep(1) 28 | post_wallet_transact_2 = post_wallet_transact(recipient, 13) 29 | print(f'\npost_wallet_transact_2: {post_wallet_transact_2}') 30 | 31 | time.sleep(1) 32 | mined_block = get_blockchain_mine() 33 | print(f'\nmined_block: {mined_block}') 34 | -------------------------------------------------------------------------------- /blockchain-env/bin/activate.csh: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate.csh" *from csh*. 2 | # You cannot run it directly. 3 | 4 | # Created by Davide Di Blasi . 5 | # Ported to Python 3.3 venv by Andrew Svetlov 6 | 7 | alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' 8 | 9 | # Unset irrelevant variables. 10 | deactivate nondestructive 11 | 12 | setenv VIRTUAL_ENV "/Users/dk/Code/python-blockchain-tutorial/blockchain-env" 13 | 14 | set _OLD_VIRTUAL_PATH="$PATH" 15 | setenv PATH "$VIRTUAL_ENV/bin:$PATH" 16 | setenv VIRTUAL_ENV_PROMPT "blockchain-env" 17 | 18 | 19 | set _OLD_VIRTUAL_PROMPT="$prompt" 20 | 21 | if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then 22 | set prompt = "(blockchain-env) $prompt" 23 | endif 24 | 25 | alias pydoc python -m pydoc 26 | 27 | rehash 28 | -------------------------------------------------------------------------------- /frontend/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import logo from '../assets/logo.png'; 4 | import { API_BASE_URL } from '../config'; 5 | 6 | function App() { 7 | const [walletInfo, setWalletInfo] = useState({}); 8 | 9 | useEffect(() => { 10 | fetch(`${API_BASE_URL}/wallet/info`) 11 | .then(response => response.json()) 12 | .then(json => setWalletInfo(json)); 13 | }, []); 14 | 15 | const { address, balance } = walletInfo; 16 | 17 | return ( 18 |
19 | application-logo 20 |

Welcome to pychain

21 |
22 | Blockchain 23 | Conduct a Transaction 24 | Transaction Pool 25 |
26 |
27 |
Address: {address}
28 |
Balance: {balance}
29 |
30 |
31 | ); 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /backend/util/hex_to_binary.py: -------------------------------------------------------------------------------- 1 | from backend.util.crypto_hash import crypto_hash 2 | 3 | HEX_TO_BINARY_CONVERSION_TABLE = { 4 | '0': '0000', 5 | '1': '0001', 6 | '2': '0010', 7 | '3': '0011', 8 | '4': '0100', 9 | '5': '0101', 10 | '6': '0110', 11 | '7': '0111', 12 | '8': '1000', 13 | '9': '1001', 14 | 'a': '1010', 15 | 'b': '1011', 16 | 'c': '1100', 17 | 'd': '1101', 18 | 'e': '1110', 19 | 'f': '1111' 20 | } 21 | 22 | def hex_to_binary(hex_string): 23 | binary_string = '' 24 | 25 | for character in hex_string: 26 | binary_string += HEX_TO_BINARY_CONVERSION_TABLE[character] 27 | 28 | return binary_string 29 | 30 | def main(): 31 | number = 451 32 | hex_number = hex(number)[2:] 33 | print(f'hex_number: {hex_number}') 34 | 35 | binary_number = hex_to_binary(hex_number) 36 | print(f'binary_number: {binary_number}') 37 | 38 | original_number = int(binary_number, 2) 39 | print(f'original_number: {original_number}') 40 | 41 | hex_to_binary_crypto_hash = hex_to_binary(crypto_hash('test-data')) 42 | print(f'hex_to_binary_crypto_hash: {hex_to_binary_crypto_hash}') 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /backend/tests/wallet/test_transaction_pool.py: -------------------------------------------------------------------------------- 1 | from backend.wallet.transaction_pool import TransactionPool 2 | from backend.wallet.transaction import Transaction 3 | from backend.wallet.wallet import Wallet 4 | from backend.blockchain.blockchain import Blockchain 5 | 6 | def test_set_transaction(): 7 | transaction_pool = TransactionPool() 8 | transaction = Transaction(Wallet(), 'recipient', 1) 9 | transaction_pool.set_transaction(transaction) 10 | 11 | assert transaction_pool.transaction_map[transaction.id] == transaction 12 | 13 | def test_clear_blockchain_transactions(): 14 | transaction_pool = TransactionPool() 15 | transaction_1 = Transaction(Wallet(), 'recipient', 1) 16 | transaction_2 = Transaction(Wallet(), 'recipient', 2) 17 | 18 | transaction_pool.set_transaction(transaction_1) 19 | transaction_pool.set_transaction(transaction_2) 20 | 21 | blockchain = Blockchain() 22 | blockchain.add_block([transaction_1.to_json(), transaction_2.to_json()]) 23 | 24 | assert transaction_1.id in transaction_pool.transaction_map 25 | assert transaction_2.id in transaction_pool.transaction_map 26 | 27 | transaction_pool.clear_blockchain_transactions(blockchain) 28 | 29 | assert not transaction_1.id in transaction_pool.transaction_map 30 | assert not transaction_2.id in transaction_pool.transaction_map 31 | -------------------------------------------------------------------------------- /backend/wallet/transaction_pool.py: -------------------------------------------------------------------------------- 1 | class TransactionPool: 2 | def __init__(self): 3 | self.transaction_map = {} 4 | 5 | def set_transaction(self, transaction): 6 | """ 7 | Set a transaction in the transaction pool. 8 | """ 9 | self.transaction_map[transaction.id] = transaction 10 | 11 | 12 | def existing_transaction(self, address): 13 | """ 14 | Find a transaction generated by the address in the transaction pool 15 | """ 16 | for transaction in self.transaction_map.values(): 17 | if transaction.input['address'] == address: 18 | return transaction 19 | 20 | def transaction_data(self): 21 | """ 22 | Return the transactions of thje transaction pool represented in their 23 | json serialized form. 24 | """ 25 | return list(map( 26 | lambda transaction: transaction.to_json(), 27 | self.transaction_map.values() 28 | )) 29 | 30 | def clear_blockchain_transactions(self, blockchain): 31 | """ 32 | Delete blockchain recorded transactions from the transaction pool. 33 | """ 34 | for block in blockchain.chain: 35 | for transaction in block.data: 36 | try: 37 | del self.transaction_map[transaction['id']] 38 | except KeyError: 39 | pass 40 | -------------------------------------------------------------------------------- /frontend/src/components/Block.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | import { MILLISECONDS_PY } from '../config'; 4 | import Transaction from './Transaction'; 5 | 6 | function ToggleTransactionDisplay({ block }) { 7 | const [displayTransaction, setDisplayTransaction] = useState(false); 8 | const { data } = block; 9 | 10 | const toggleDisplayTransaction = () => { 11 | setDisplayTransaction(!displayTransaction); 12 | } 13 | 14 | if (displayTransaction) { 15 | return ( 16 |
17 | { 18 | data.map(transaction => ( 19 |
20 |
21 | 22 |
23 | )) 24 | } 25 |
26 | 33 |
34 | ) 35 | } 36 | 37 | return ( 38 |
39 |
40 | 47 |
48 | ) 49 | } 50 | 51 | function Block({ block }) { 52 | const { timestamp, hash } = block; 53 | const hashDisplay = `${hash.substring(0, 15)}...`; 54 | const timestampDisplay = new Date(timestamp / MILLISECONDS_PY).toLocaleString(); 55 | 56 | return ( 57 |
58 |
Hash: {hashDisplay}
59 |
Timestamp: {timestampDisplay}
60 | 61 |
62 | ) 63 | } 64 | 65 | export default Block; 66 | -------------------------------------------------------------------------------- /frontend/src/components/TransactionPool.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { Button } from 'react-bootstrap'; 4 | import Transaction from './Transaction'; 5 | import { API_BASE_URL, SECONDS_JS } from '../config'; 6 | 7 | const POLL_INTERVAL = 10 * SECONDS_JS; 8 | 9 | function TransactionPool() { 10 | const navigate = useNavigate(); 11 | const [transactions, setTransactions] = useState([]); 12 | 13 | const fetchTransactions = () => { 14 | fetch(`${API_BASE_URL}/transactions`) 15 | .then(response => response.json()) 16 | .then(json => { 17 | console.log('transactions json', json); 18 | 19 | setTransactions(json); 20 | }); 21 | } 22 | 23 | useEffect(() => { 24 | fetchTransactions(); 25 | 26 | const intervalId = setInterval(fetchTransactions, POLL_INTERVAL); 27 | 28 | return () => clearInterval(intervalId); 29 | }, []); 30 | 31 | const fetchMineBlock = () => { 32 | fetch(`${API_BASE_URL}/blockchain/mine`) 33 | .then(() => { 34 | alert('Success!'); 35 | 36 | navigate('/blockchain'); 37 | }); 38 | } 39 | 40 | return ( 41 |
42 | Home 43 |
44 |

Transaction Pool

45 |
46 | { 47 | transactions.map(transaction => ( 48 |
49 |
50 | 51 |
52 | )) 53 | } 54 |
55 |
56 | 62 |
63 | ) 64 | } 65 | 66 | export default TransactionPool; 67 | -------------------------------------------------------------------------------- /frontend/src/components/Blockchain.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Button } from 'react-bootstrap'; 4 | import { API_BASE_URL } from '../config'; 5 | import Block from './Block'; 6 | 7 | const PAGE_RANGE = 3; 8 | 9 | function Blockchain() { 10 | const [blockchain, setBlockchain] = useState([]); 11 | const [blockchainLength, setBlockchainLength] = useState(0); 12 | 13 | const fetchBlockchainPage = ({ start, end }) => { 14 | fetch(`${API_BASE_URL}/blockchain/range?start=${start}&end=${end}`) 15 | .then(response => response.json()) 16 | .then(json => setBlockchain(json)); 17 | } 18 | 19 | useEffect(() => { 20 | fetchBlockchainPage({ start: 0, end: PAGE_RANGE }); 21 | 22 | fetch(`${API_BASE_URL}/blockchain/length`) 23 | .then(response => response.json()) 24 | .then(json => setBlockchainLength(json)); 25 | }, []); 26 | 27 | const buttonNumbers = []; 28 | for (let i=0; i 34 | Home 35 |
36 |

Blockchain

37 |
38 | { 39 | blockchain.map(block => ) 40 | } 41 |
42 |
43 | { 44 | buttonNumbers.map(number => { 45 | const start = number * PAGE_RANGE; 46 | const end = (number+1) * PAGE_RANGE; 47 | 48 | return ( 49 | fetchBlockchainPage({ start, end })}> 50 | {' '} 53 | 54 | ) 55 | }) 56 | } 57 |
58 | 59 | ) 60 | } 61 | 62 | export default Blockchain; 63 | -------------------------------------------------------------------------------- /blockchain-env/bin/activate: -------------------------------------------------------------------------------- 1 | # This file must be used with "source bin/activate" *from bash* 2 | # You cannot run it directly 3 | 4 | deactivate () { 5 | # reset old environment variables 6 | if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then 7 | PATH="${_OLD_VIRTUAL_PATH:-}" 8 | export PATH 9 | unset _OLD_VIRTUAL_PATH 10 | fi 11 | if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then 12 | PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" 13 | export PYTHONHOME 14 | unset _OLD_VIRTUAL_PYTHONHOME 15 | fi 16 | 17 | # Call hash to forget past commands. Without forgetting 18 | # past commands the $PATH changes we made may not be respected 19 | hash -r 2> /dev/null 20 | 21 | if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then 22 | PS1="${_OLD_VIRTUAL_PS1:-}" 23 | export PS1 24 | unset _OLD_VIRTUAL_PS1 25 | fi 26 | 27 | unset VIRTUAL_ENV 28 | unset VIRTUAL_ENV_PROMPT 29 | if [ ! "${1:-}" = "nondestructive" ] ; then 30 | # Self destruct! 31 | unset -f deactivate 32 | fi 33 | } 34 | 35 | # unset irrelevant variables 36 | deactivate nondestructive 37 | 38 | # on Windows, a path can contain colons and backslashes and has to be converted: 39 | case "$(uname)" in 40 | CYGWIN*|MSYS*) 41 | # transform D:\path\to\venv to /d/path/to/venv on MSYS 42 | # and to /cygdrive/d/path/to/venv on Cygwin 43 | VIRTUAL_ENV=$(cygpath "/Users/dk/Code/python-blockchain-tutorial/blockchain-env") 44 | export VIRTUAL_ENV 45 | ;; 46 | *) 47 | # use the path as-is 48 | export VIRTUAL_ENV="/Users/dk/Code/python-blockchain-tutorial/blockchain-env" 49 | ;; 50 | esac 51 | 52 | _OLD_VIRTUAL_PATH="$PATH" 53 | PATH="$VIRTUAL_ENV/bin:$PATH" 54 | export PATH 55 | 56 | VIRTUAL_ENV_PROMPT="blockchain-env" 57 | export VIRTUAL_ENV_PROMPT 58 | 59 | # unset PYTHONHOME if set 60 | # this will fail if PYTHONHOME is set to the empty string (which is bad anyway) 61 | # could use `if (set -u; : $PYTHONHOME) ;` in bash 62 | if [ -n "${PYTHONHOME:-}" ] ; then 63 | _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" 64 | unset PYTHONHOME 65 | fi 66 | 67 | if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then 68 | _OLD_VIRTUAL_PS1="${PS1:-}" 69 | PS1="(blockchain-env) ${PS1:-}" 70 | export PS1 71 | fi 72 | 73 | # Call hash to forget past commands. Without forgetting 74 | # past commands the $PATH changes we made may not be respected 75 | hash -r 2> /dev/null 76 | -------------------------------------------------------------------------------- /blockchain-env/bin/activate.fish: -------------------------------------------------------------------------------- 1 | # This file must be used with "source /bin/activate.fish" *from fish* 2 | # (https://fishshell.com/). You cannot run it directly. 3 | 4 | function deactivate -d "Exit virtual environment and return to normal shell environment" 5 | # reset old environment variables 6 | if test -n "$_OLD_VIRTUAL_PATH" 7 | set -gx PATH $_OLD_VIRTUAL_PATH 8 | set -e _OLD_VIRTUAL_PATH 9 | end 10 | if test -n "$_OLD_VIRTUAL_PYTHONHOME" 11 | set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME 12 | set -e _OLD_VIRTUAL_PYTHONHOME 13 | end 14 | 15 | if test -n "$_OLD_FISH_PROMPT_OVERRIDE" 16 | set -e _OLD_FISH_PROMPT_OVERRIDE 17 | # prevents error when using nested fish instances (Issue #93858) 18 | if functions -q _old_fish_prompt 19 | functions -e fish_prompt 20 | functions -c _old_fish_prompt fish_prompt 21 | functions -e _old_fish_prompt 22 | end 23 | end 24 | 25 | set -e VIRTUAL_ENV 26 | set -e VIRTUAL_ENV_PROMPT 27 | if test "$argv[1]" != "nondestructive" 28 | # Self-destruct! 29 | functions -e deactivate 30 | end 31 | end 32 | 33 | # Unset irrelevant variables. 34 | deactivate nondestructive 35 | 36 | set -gx VIRTUAL_ENV "/Users/dk/Code/python-blockchain-tutorial/blockchain-env" 37 | 38 | set -gx _OLD_VIRTUAL_PATH $PATH 39 | set -gx PATH "$VIRTUAL_ENV/bin" $PATH 40 | set -gx VIRTUAL_ENV_PROMPT "blockchain-env" 41 | 42 | # Unset PYTHONHOME if set. 43 | if set -q PYTHONHOME 44 | set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME 45 | set -e PYTHONHOME 46 | end 47 | 48 | if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" 49 | # fish uses a function instead of an env var to generate the prompt. 50 | 51 | # Save the current fish_prompt function as the function _old_fish_prompt. 52 | functions -c fish_prompt _old_fish_prompt 53 | 54 | # With the original prompt function renamed, we can override with our own. 55 | function fish_prompt 56 | # Save the return status of the last command. 57 | set -l old_status $status 58 | 59 | # Output the venv prompt; color taken from the blue of the Python logo. 60 | printf "%s(%s)%s " (set_color 4B8BBE) "blockchain-env" (set_color normal) 61 | 62 | # Restore the return status of the previous command. 63 | echo "exit $old_status" | . 64 | # Output the original/"old" prompt. 65 | _old_fish_prompt 66 | end 67 | 68 | set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" 69 | end 70 | -------------------------------------------------------------------------------- /frontend/src/components/ConductTransaction.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { FormGroup, FormControl, Button } from 'react-bootstrap'; 4 | import { API_BASE_URL } from '../config'; 5 | 6 | function ConductTransaction() { 7 | const navigate = useNavigate(); 8 | const [amount, setAmount] = useState(0); 9 | const [recipient, setRecipient] = useState(''); 10 | const [knownAddresses, setKnownAddresses] = useState([]); 11 | 12 | useEffect(() => { 13 | fetch(`${API_BASE_URL}/known-addresses`) 14 | .then(response => response.json()) 15 | .then(json => setKnownAddresses(json)); 16 | }, []); 17 | 18 | const updateRecipient = event => { 19 | setRecipient(event.target.value); 20 | } 21 | 22 | const updateAmount = event => { 23 | setAmount(Number(event.target.value)); 24 | } 25 | 26 | const submitTransaction = () => { 27 | fetch(`${API_BASE_URL}/wallet/transact`, { 28 | method: 'POST', 29 | headers: { 'Content-Type': 'application/json' }, 30 | body: JSON.stringify({ recipient, amount }) 31 | }).then(response => response.json()) 32 | .then(json => { 33 | console.log('submitTransaction json', json); 34 | 35 | alert('Success!'); 36 | 37 | navigate('/transaction-pool'); 38 | }); 39 | } 40 | 41 | return ( 42 |
43 | Home 44 |
45 |

Conduct a Transaction

46 |
47 | 48 | 54 | 55 | 56 | 62 | 63 |
64 | 70 |
71 |
72 |

Known Addresses

73 |
74 | { 75 | knownAddresses.map((knownAddress, i) => ( 76 | 77 | {knownAddress}{i !== knownAddresses.length - 1 ? ', ' : ''} 78 | 79 | )) 80 | } 81 |
82 |
83 | ) 84 | } 85 | 86 | export default ConductTransaction; 87 | -------------------------------------------------------------------------------- /backend/blockchain/blockchain.py: -------------------------------------------------------------------------------- 1 | from backend.blockchain.block import Block 2 | 3 | class Blockchain: 4 | """ 5 | Blockchain: a public ledger of transactions. 6 | Implemented as a list of blocks - data sets of transactions 7 | """ 8 | def __init__(self): 9 | self.chain = [Block.genesis()] 10 | 11 | def add_block(self, data): 12 | self.chain.append(Block.mine_block(self.chain[-1], data)) 13 | 14 | def __repr__(self): 15 | return f'Blockchain: {self.chain}' 16 | 17 | def replace_chain(self, chain): 18 | """ 19 | Replace the local chain with the incoming one if the following applies: 20 | - The incoming chain is longer than the local one. 21 | - The incoming chain is formatted properly. 22 | """ 23 | if len(chain) <= len(self.chain): 24 | raise Exception('Cannot replace. The incoming chain must be longer.') 25 | 26 | try: 27 | Blockchain.is_valid_chain(chain) 28 | except Exception as e: 29 | raise Exception(f'Cannot replace. The incoming chain is invalid: {e}') 30 | 31 | self.chain = chain 32 | 33 | def to_json(self): 34 | """ 35 | Serialize the blockchain into a list of blocks. 36 | """ 37 | return list(map(lambda block: block.to_json(), self.chain)) 38 | 39 | @staticmethod 40 | def from_json(chain_json): 41 | """ 42 | Deserialize a list of serialized blocks into a Blokchain instance. 43 | The result will contain a chain list of Block instances. 44 | """ 45 | blockchain = Blockchain() 46 | blockchain.chain = list( 47 | map(lambda block_json: Block.from_json(block_json), chain_json) 48 | ) 49 | 50 | return blockchain 51 | 52 | @staticmethod 53 | def is_valid_chain(chain): 54 | """ 55 | Validate the incoming chain. 56 | Enforce the following rules of the blockchain: 57 | - the chain must start with the genesis block 58 | - blocks must be formatted correctly 59 | """ 60 | if chain[0] != Block.genesis(): 61 | raise Exception('The genesis block must be valid') 62 | 63 | for i in range(1, len(chain)): 64 | block = chain[i] 65 | last_block = chain[i-1] 66 | Block.is_valid_block(last_block, block) 67 | 68 | 69 | def main(): 70 | blockchain = Blockchain() 71 | blockchain.add_block('one') 72 | blockchain.add_block('two') 73 | 74 | print(blockchain) 75 | print(f'blockchain.py ___name__: {__name__}') 76 | 77 | if __name__ == '__main__': 78 | main() 79 | -------------------------------------------------------------------------------- /backend/tests/blockchain/test_block.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import time 3 | 4 | from backend.blockchain.block import Block, GENESIS_DATA 5 | from backend.config import MINE_RATE, SECONDS 6 | from backend.util.hex_to_binary import hex_to_binary 7 | 8 | def test_mine_block(): 9 | last_block = Block.genesis() 10 | data = 'test-data' 11 | block = Block.mine_block(last_block, data) 12 | 13 | assert isinstance(block, Block) 14 | assert block.data == data 15 | assert block.last_hash == last_block.hash 16 | assert hex_to_binary(block.hash)[0:block.difficulty] == '0' * block.difficulty 17 | 18 | def test_genesis(): 19 | genesis = Block.genesis() 20 | 21 | assert isinstance(genesis, Block) 22 | for key, value in GENESIS_DATA.items(): 23 | getattr(genesis, key) == value 24 | 25 | def test_quickly_mined_block(): 26 | last_block = Block.mine_block(Block.genesis(), 'foo') 27 | mined_block = Block.mine_block(last_block, 'bar') 28 | 29 | assert mined_block.difficulty == last_block.difficulty + 1 30 | 31 | def test_slowly_mined_block(): 32 | last_block = Block.mine_block(Block.genesis(), 'foo') 33 | time.sleep(MINE_RATE / SECONDS) 34 | mined_block = Block.mine_block(last_block, 'bar') 35 | 36 | assert mined_block.difficulty == last_block.difficulty - 1 37 | 38 | def test_mined_block_difficulty_limits_at_1(): 39 | last_block = Block( 40 | time.time_ns(), 41 | 'test_last_hash', 42 | 'test_hash', 43 | 'test_data', 44 | 1, 45 | 0 46 | ) 47 | 48 | time.sleep(MINE_RATE / SECONDS) 49 | mined_block = Block.mine_block(last_block, 'bar') 50 | 51 | assert mined_block.difficulty == 1 52 | 53 | @pytest.fixture 54 | def last_block(): 55 | return Block.genesis() 56 | 57 | @pytest.fixture 58 | def block(last_block): 59 | return Block.mine_block(last_block, 'test_data') 60 | 61 | def test_is_valid_block(last_block, block): 62 | Block.is_valid_block(last_block, block) 63 | 64 | def test_is_valid_block_bad_last_hash(last_block, block): 65 | block.last_hash = 'evil_last_hash' 66 | 67 | with pytest.raises(Exception, match='last_hash must be correct'): 68 | Block.is_valid_block(last_block, block) 69 | 70 | def test_is_valid_block_bad_proof_of_work(last_block, block): 71 | block.hash = 'fff' 72 | 73 | with pytest.raises(Exception, match='proof of work requirement was not met'): 74 | Block.is_valid_block(last_block, block) 75 | 76 | def test_is_valid_block_jumped_difficulty(last_block, block): 77 | jumped_difficulty = 10 78 | block.difficulty = jumped_difficulty 79 | block.hash = f'{"0" * jumped_difficulty}111abc' 80 | 81 | with pytest.raises(Exception, match='difficulty must only adjust by 1'): 82 | Block.is_valid_block(last_block, block) 83 | 84 | def test_is_valid_block_bad_block_hash(last_block, block): 85 | block.hash = '0000000000000000bbbabc' 86 | 87 | with pytest.raises(Exception, match='block hash must be correct'): 88 | Block.is_valid_block(last_block, block) 89 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /backend/tests/wallet/test_transaction.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from backend.wallet.transaction import Transaction 4 | from backend.wallet.wallet import Wallet 5 | 6 | def test_transaction(): 7 | sender_wallet = Wallet() 8 | recipient = 'recipient' 9 | amount = 50 10 | transaction = Transaction(sender_wallet, recipient, amount) 11 | 12 | assert transaction.output[recipient] == amount 13 | assert transaction.output[sender_wallet.address] == sender_wallet.balance - amount 14 | 15 | assert 'timestamp' in transaction.input 16 | assert transaction.input['amount'] == sender_wallet.balance 17 | assert transaction.input['address'] == sender_wallet.address 18 | assert transaction.input['public_key'] == sender_wallet.public_key 19 | assert Wallet.verify( 20 | transaction.input['public_key'], 21 | transaction.output, 22 | transaction.input['signature'] 23 | ) 24 | 25 | def test_transaction_exceeds_balance(): 26 | with pytest.raises(Exception, match='Amount exceeds balance'): 27 | Transaction(Wallet(), 'recipient', 9001) 28 | 29 | def test_transaction_update_exceeds_balance(): 30 | sender_wallet = Wallet() 31 | transaction = Transaction(sender_wallet, 'recipient', 50) 32 | 33 | with pytest.raises(Exception, match='Amount exceeds balance'): 34 | transaction.update(sender_wallet, 'new_recipient', 9001) 35 | 36 | def test_transaction_update(): 37 | sender_wallet = Wallet() 38 | first_recipient = 'first_recipient' 39 | first_amount = 50 40 | transaction = Transaction(sender_wallet, first_recipient, first_amount) 41 | 42 | next_recipient = 'next_recipient' 43 | next_amount = 75 44 | transaction.update(sender_wallet, next_recipient, next_amount) 45 | 46 | assert transaction.output[next_recipient] == next_amount 47 | assert transaction.output[sender_wallet.address] ==\ 48 | sender_wallet.balance - first_amount - next_amount 49 | assert Wallet.verify( 50 | transaction.input['public_key'], 51 | transaction.output, 52 | transaction.input['signature'] 53 | ) 54 | 55 | to_first_again_amount = 25 56 | transaction.update(sender_wallet, first_recipient, to_first_again_amount) 57 | 58 | assert transaction.output[first_recipient] == \ 59 | first_amount + to_first_again_amount 60 | assert transaction.output[sender_wallet.address] ==\ 61 | sender_wallet.balance - first_amount - next_amount - to_first_again_amount 62 | assert Wallet.verify( 63 | transaction.input['public_key'], 64 | transaction.output, 65 | transaction.input['signature'] 66 | ) 67 | 68 | def test_valid_transaction(): 69 | Transaction.is_valid_transaction(Transaction(Wallet(), 'recipient', 50)) 70 | 71 | def test_valid_transaction_with_invalid_outputs(): 72 | sender_wallet = Wallet() 73 | transaction = Transaction(sender_wallet, 'recipient', 50) 74 | transaction.output[sender_wallet.address] = 9001 75 | 76 | with pytest.raises(Exception, match='Invalid transaction output values'): 77 | Transaction.is_valid_transaction(transaction) 78 | 79 | def test_valid_transaction_with_invalid_signature(): 80 | transaction = Transaction(Wallet(), 'recipient', 50) 81 | transaction.input['signature'] = Wallet().sign(transaction.output) 82 | 83 | with pytest.raises(Exception, match='Invalid signature'): 84 | Transaction.is_valid_transaction(transaction) 85 | -------------------------------------------------------------------------------- /backend/wallet/wallet.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | 4 | from backend.config import STARTING_BALANCE 5 | from cryptography.hazmat.backends import default_backend 6 | from cryptography.hazmat.primitives.asymmetric import ec 7 | from cryptography.hazmat.primitives.asymmetric.utils import ( 8 | encode_dss_signature, 9 | decode_dss_signature 10 | ) 11 | from cryptography.hazmat.primitives import hashes, serialization 12 | from cryptography.exceptions import InvalidSignature 13 | 14 | class Wallet: 15 | """ 16 | An individual wallet for a miner. 17 | Keeps track of the miner's balance. 18 | Allows a miner to authorize transactions. 19 | """ 20 | def __init__(self, blockchain=None): 21 | self.blockchain = blockchain 22 | self.address = str(uuid.uuid4())[0:8] 23 | self.private_key = ec.generate_private_key( 24 | ec.SECP256K1(), 25 | default_backend() 26 | ) 27 | self.public_key = self.private_key.public_key() 28 | self.serialize_public_key() 29 | 30 | @property 31 | def balance(self): 32 | return Wallet.calculate_balance(self.blockchain, self.address) 33 | 34 | def sign(self, data): 35 | """ 36 | Generate a signature based on the data using the local private key. 37 | """ 38 | return decode_dss_signature(self.private_key.sign( 39 | json.dumps(data).encode('utf-8'), 40 | ec.ECDSA(hashes.SHA256()) 41 | )) 42 | 43 | 44 | def serialize_public_key(self): 45 | """ 46 | Reset the public key to its serialized version. 47 | """ 48 | self.public_key = self.public_key.public_bytes( 49 | encoding=serialization.Encoding.PEM, 50 | format=serialization.PublicFormat.SubjectPublicKeyInfo 51 | ).decode('utf-8') 52 | 53 | @staticmethod 54 | def verify(public_key, data, signature): 55 | """ 56 | Verify a signature based on the original public key and data. 57 | """ 58 | deserialized_public_key = serialization.load_pem_public_key( 59 | public_key.encode('utf-8'), 60 | default_backend() 61 | ) 62 | 63 | (r, s) = signature 64 | 65 | try: 66 | deserialized_public_key.verify( 67 | encode_dss_signature(r, s), 68 | json.dumps(data).encode('utf-8'), 69 | ec.ECDSA(hashes.SHA256()) 70 | ) 71 | return True 72 | except InvalidSignature: 73 | return False 74 | 75 | @staticmethod 76 | def calculate_balance(blockchain, address): 77 | """ 78 | Calculate the balance of the given address considering the transaction 79 | data within the blockchain. 80 | 81 | The balance is found by adding the output values that belong to the 82 | address since the most recent transaction by that address. 83 | """ 84 | balance = STARTING_BALANCE 85 | 86 | if not blockchain: 87 | return balance 88 | 89 | for block in blockchain.chain: 90 | for transaction in block.data: 91 | if transaction['input']['address'] == address: 92 | # Any time the address conducts a new transaction it resets 93 | # its balance 94 | balance = transaction['output'][address] 95 | elif address in transaction['output']: 96 | balance += transaction['output'][address] 97 | 98 | return balance 99 | 100 | def main(): 101 | wallet = Wallet() 102 | print(f'wallet.__dict__: {wallet.__dict__}') 103 | 104 | data = { 'foo': 'bar' } 105 | signature = wallet.sign(data) 106 | print(f'signature: {signature}') 107 | 108 | should_be_valid = Wallet.verify(wallet.public_key, data, signature) 109 | print(f'should_be_valid: {should_be_valid}') 110 | 111 | should_be_invalid = Wallet.verify(Wallet().public_key, data, signature) 112 | print(f'should_be_invalid: {should_be_invalid}') 113 | 114 | if __name__ == '__main__': 115 | main() 116 | -------------------------------------------------------------------------------- /PUBNUB_CONFIG.md: -------------------------------------------------------------------------------- 1 | # PubNub Configuration Guide 2 | 3 | ## Overview 4 | 5 | The blockchain application uses PubNub for real-time peer-to-peer communication between nodes. PubNub enables the publish/subscribe pattern that allows blockchain nodes to: 6 | 7 | - Broadcast newly mined blocks to all peers 8 | - Share transactions across the network 9 | - Synchronize blockchain state in real-time 10 | 11 | ## Getting Your PubNub Keys 12 | 13 | 1. **Create a PubNub Account** 14 | - Visit [https://www.pubnub.com/](https://www.pubnub.com/) 15 | - Sign up for a free account (no credit card required) 16 | 17 | 2. **Create a New App** 18 | - Once logged in, click "Create New App" 19 | - Give it a name like "python-blockchain" 20 | 21 | 3. **Get Your Keys** 22 | - You'll see two important keys: 23 | - **Publish Key** - Used to send messages to channels 24 | - **Subscribe Key** - Used to receive messages from channels 25 | - Keep these keys handy! 26 | 27 | ### Using secrets.env (Recommended for Production) 28 | 29 | 1. **Copy the secrets template:** 30 | ```bash 31 | cp env.example .env 32 | ``` 33 | 34 | 2. **Edit secrets.env and add your keys:** 35 | ```bash 36 | PUBNUB_PUBLISH_KEY=pub-c-your-actual-publish-key 37 | PUBNUB_SUBSCRIBE_KEY=sub-c-your-actual-subscribe-key 38 | PUBNUB_USER_ID=blockchain-node-1 39 | ``` 40 | 41 | ### Backend Code 42 | 43 | The `backend/pubsub.py` file now reads configuration from environment variables. 44 | 45 | ## Multi-Node Configuration 46 | 47 | When running multiple nodes, ensure each has a unique `PUBNUB_USER_ID`: 48 | 49 | ```bash 50 | # Node 1 (main) 51 | export PUBNUB_USER_ID=blockchain-node-1 52 | python3 -m backend.app 53 | 54 | # Node 2 (peer) - in a different terminal 55 | export PUBNUB_USER_ID=blockchain-node-2 56 | export PEER=True 57 | export PEER_PORT=5001 58 | python3 -m backend.app 59 | 60 | # Node 3 (seeded) - in another terminal 61 | export PUBNUB_USER_ID=blockchain-node-3 62 | export PEER=True 63 | export SEED_DATA=True 64 | export PEER_PORT=5002 65 | python3 -m backend.app 66 | ``` 67 | 68 | Each node should have a unique user_id: 69 | - `blockchain-node-1` - Main node 70 | - `blockchain-peer-1` - Peer node 71 | - `blockchain-seed-1` - Seeded node 72 | 73 | ## PubNub Dashboard Monitoring 74 | 75 | You can monitor real-time activity in the PubNub dashboard: 76 | 77 | 1. Log in to [https://admin.pubnub.com/](https://admin.pubnub.com/) 78 | 2. Select your app 79 | 3. Go to "Debug Console" 80 | 4. Subscribe to your channels to see messages in real-time 81 | 82 | This is useful for: 83 | - Debugging connection issues 84 | - Monitoring message flow 85 | - Verifying blockchain synchronization 86 | 87 | ### Channels Used by This Application 88 | 89 | The blockchain application uses the following channels: 90 | 91 | - **`BLOCK`** - Broadcasts newly mined blocks to all nodes 92 | - **`TRANSACTION`** - Shares new transactions across the network 93 | - **`TEST`** - Used for testing connectivity (development only) 94 | 95 | ## Quick Start Guide 96 | 97 | ### First Time Setup 98 | 99 | 1. **Get your PubNub keys:** 100 | - Sign up at https://www.pubnub.com/ 101 | - Create a new app 102 | - Copy your Publish and Subscribe keys 103 | 104 | 2. **Configure your environment:** 105 | ```bash 106 | # Copy the example file 107 | cp env.example .env 108 | 109 | # Edit .env with your actual keys 110 | nano .env 111 | ``` 112 | 113 | 3. **Run the application:** 114 | ```bash 115 | python3 -m backend.app 116 | ``` 117 | 118 | ### Running Multiple Nodes 119 | 120 | To test blockchain synchronization with multiple nodes: 121 | 122 | ```bash 123 | # Terminal 1 - Main node 124 | export PUBNUB_USER_ID=node-1 125 | python3 -m backend.app 126 | 127 | # Terminal 2 - Peer node 128 | export PUBNUB_USER_ID=peer-1 129 | export PEER=True 130 | export PEER_PORT=5001 131 | python3 -m backend.app 132 | 133 | ``` 134 | 135 | ## Resources 136 | 137 | - [PubNub Python SDK Documentation](https://www.pubnub.com/docs/sdks/python) 138 | - [PubNub Publish/Subscribe Tutorial](https://www.pubnub.com/tutorials/python/) 139 | - [PubNub Best Practices](https://www.pubnub.com/docs/general/security/best-practices) 140 | - [PubNub Free Tier Limits](https://www.pubnub.com/pricing/) 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Python, JS, & React | Build a Blockchain & Cryptocurrency 2 | 3 | ![Course Logo](python_blockchain_logo.png) 4 | 5 | **The course is designed to help you achieve three main goals:** 6 | - Learn Python and Backend Web Development. 7 | - Build a Blockchain and Cryptocurrency Project that you can add to your portfolio. 8 | - Learn JavaScript, Frontend Web Development, React.js, and React Hooks. 9 | 10 | The course's main project is to build a blockchain and cryptocurrency. With a blockchain and cryptocurrency system as the main goal, you will go through a course journey that starts with backend development using Python. Then, you will transaction to frontend web development with JavaScript, React.js, and React Hooks. 11 | 12 | Check out the course: https://www.udemy.com/course/python-js-react-blockchain/?referralCode=9051A01550E782315B77 13 | 14 | **Here's an overview of the overall course journey:** 15 | - Get an introduction of the Python Fundamentals. 16 | - Begin building the Blockchain Application with Python. 17 | - Test the Application using Pytest. 18 | - Incorporate the crucial concept of Proof of Work into the Blockchain. 19 | - Enhance the application to prepare for networking. 20 | - Create the Blockchain network using Flask and Pub/Sub. 21 | - Integrate the Cryptocurrency, building Wallets, Keys, and Transactions. 22 | - Extend the network implementation with the cryptocurrency. 23 | - Transition from Python to JavaScript with a "From Python to JavaScript" introduction. 24 | - Establish frontend web development skills and begin coding with React.js. 25 | - Create the frontend portion for the blockchain portion of the system. 26 | - Complete the frontend by building a UI for the cryptocurrency portion of the system. 27 | 28 | **In addition, here are the skills that you'll gain from the course:** 29 | - How to build a blockchain and cryptocurrency system from scratch. 30 | - The fundamentals of python - data structures, object-oriented programming, modules, and more. 31 | - The ins and outs of hashing and sha256. 32 | - Encoding and decoding in utf-8. 33 | - Testing Python applications with pytest. 34 | - Python virtual environments. 35 | - The concept of proof of work, and how it pertains to mining blocks. 36 | - Conversion between hexadecimal to binary. 37 | - HTTP APIs and requests. 38 | - How to create APIs with Python Flask. 39 | - The publish/subscribe pattern to set up networks. 40 | - When to apply the concepts of serialization and deserialization. 41 | - Public/private keypairs and generating data signatures. 42 | - The fundamentals of JavaScript. 43 | - Frontend web development and how web applications are constructed. 44 | - The core concepts of React and React hooks. 45 | - How the React engine works under the hood, and how React applies hooks. 46 | - CORS - and how to get over the CORS error properly. 47 | - How to build a pagination system. 48 | 49 | *** 50 | 51 | #### Command Reference 52 | 53 | **Activate the virtual environment** 54 | ``` 55 | source blockchain-env/bin/activate 56 | ``` 57 | 58 | **Install all packages** 59 | ``` 60 | pip3 install -r requirements.txt 61 | ``` 62 | 63 | **Run the tests** 64 | 65 | Make sure to activate the virtual environment. 66 | 67 | ``` 68 | python3 -m pytest backend/tests 69 | ``` 70 | 71 | **Run the application and API** 72 | 73 | Make sure to activate the virtual environment. 74 | 75 | ``` 76 | python3 -m backend.app 77 | ``` 78 | 79 | **Run a peer instance** 80 | 81 | Make sure to activate the virtual environment. 82 | Choose a unique PUBNUB_USER_ID per peer. 83 | 84 | ``` 85 | export PEER=True && export PUBNUB_USER_ID=blockchain-peer-1 && python3 -m backend.app 86 | ``` 87 | 88 | **Run the frontend** 89 | 90 | In the frontend directory: 91 | ``` 92 | npm run start 93 | ``` 94 | 95 | **Seed the backend with data** 96 | 97 | Make sure to activate the virtual environment. 98 | 99 | ``` 100 | export SEED_DATA=True && python3 -m backend.app 101 | ``` 102 | 103 | ** PubNub Configuration** 104 | 105 | This application uses PubNub for real-time peer-to-peer communication between blockchain nodes. **You must configure PubNub to run the application.** 106 | 107 | **See [PUBNUB_CONFIG.md](PUBNUB_CONFIG.md) for detailed setup instructions.** 108 | 109 | 1. Get free PubNub keys at [https://www.pubnub.com/](https://www.pubnub.com/) 110 | 2. copy `backend/env.example` to `backend/.env` and configure it with your keys. 111 | -------------------------------------------------------------------------------- /backend/pubsub.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import requests 4 | 5 | from pubnub.pubnub import PubNub 6 | from pubnub.pnconfiguration import PNConfiguration 7 | from pubnub.callbacks import SubscribeCallback 8 | 9 | from backend.blockchain.block import Block 10 | from backend.blockchain.blockchain import Blockchain 11 | from backend.wallet.transaction import Transaction 12 | 13 | pnconfig = PNConfiguration() 14 | pnconfig.publish_key = os.environ.get('PUBNUB_PUBLISH_KEY') 15 | pnconfig.subscribe_key = os.environ.get('PUBNUB_SUBSCRIBE_KEY') 16 | pnconfig.user_id = os.environ.get('PUBNUB_USER_ID', 'blockchain-node-default') 17 | 18 | CHANNELS = { 19 | 'TEST': 'TEST', 20 | 'BLOCK': 'BLOCK', 21 | 'TRANSACTION': 'TRANSACTION' 22 | } 23 | 24 | class Listener(SubscribeCallback): 25 | def __init__(self, blockchain, transaction_pool): 26 | self.blockchain = blockchain 27 | self.transaction_pool = transaction_pool 28 | 29 | def message(self, pubnub, message_object): 30 | print(f'\n-- Channel: {message_object.channel} | Message: {message_object.message}') 31 | 32 | if message_object.channel == CHANNELS['BLOCK']: 33 | block = Block.from_json(message_object.message) 34 | potential_chain = self.blockchain.chain[:] 35 | potential_chain.append(block) 36 | 37 | try: 38 | self.blockchain.replace_chain(potential_chain) 39 | 40 | self.transaction_pool.clear_blockchain_transactions( 41 | self.blockchain 42 | ) 43 | 44 | print('\n -- Successfully replaced the local chain') 45 | except Exception as e: 46 | print(f'\n -- Did not replace chain: {e}') 47 | 48 | # If we can't validate the block, try to sync the full blockchain 49 | # This handles cases where we're missing previous blocks 50 | self.sync_blockchain() 51 | 52 | elif message_object.channel == CHANNELS['TRANSACTION']: 53 | transaction = Transaction.from_json(message_object.message) 54 | self.transaction_pool.set_transaction(transaction) 55 | print('\n -- Set the new transaction in the transaction pool') 56 | 57 | def sync_blockchain(self): 58 | """ 59 | Synchronize the local blockchain with the root node. 60 | This is called when we receive a block we can't validate. 61 | """ 62 | try: 63 | # Get the root backend host (main node) 64 | root_host = os.environ.get('ROOT_HOST', 'localhost') 65 | root_port = os.environ.get('ROOT_PORT', '5050') 66 | 67 | print(f'\n -- Attempting to sync blockchain from {root_host}:{root_port}') 68 | 69 | # Request the full blockchain from the root node 70 | response = requests.get(f'http://{root_host}:{root_port}/blockchain') 71 | result_blockchain = Blockchain.from_json(response.json()) 72 | 73 | # Replace our local chain with the synchronized chain 74 | self.blockchain.replace_chain(result_blockchain.chain) 75 | 76 | print(f'\n -- Successfully synchronized! Chain length: {len(self.blockchain.chain)}') 77 | except Exception as e: 78 | print(f'\n -- Could not synchronize blockchain: {e}') 79 | 80 | class PubSub(): 81 | """ 82 | Handles the publish/subscribe layer of the application. 83 | Provides communication between the nodes of the blockchain network. 84 | """ 85 | def __init__(self, blockchain, transaction_pool): 86 | self.pubnub = PubNub(pnconfig) 87 | self.pubnub.subscribe().channels(CHANNELS.values()).execute() 88 | self.pubnub.add_listener(Listener(blockchain, transaction_pool)) 89 | 90 | def publish(self, channel, message): 91 | """ 92 | Publish the message object to the channel. 93 | """ 94 | try: 95 | result = self.pubnub.publish().channel(channel).message(message).sync() 96 | print(f'\n-- Published to {channel}: {result.status.is_error()}') 97 | except Exception as e: 98 | print(f'\n-- Error publishing to {channel}: {e}') 99 | 100 | def broadcast_block(self, block): 101 | """ 102 | Broadcast a block object to all nodes. 103 | """ 104 | self.publish(CHANNELS['BLOCK'], block.to_json()) 105 | 106 | def broadcast_transaction(self, transaction): 107 | """ 108 | Broadcast a transaction to all nodes. 109 | """ 110 | self.publish(CHANNELS['TRANSACTION'], transaction.to_json()) 111 | 112 | def main(): 113 | pubsub = PubSub() 114 | 115 | time.sleep(1) 116 | 117 | pubsub.publish(CHANNELS['TEST'], { 'foo': 'bar' }) 118 | 119 | if __name__ == '__main__': 120 | main() 121 | -------------------------------------------------------------------------------- /backend/blockchain/block.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from backend.util.crypto_hash import crypto_hash 4 | from backend.util.hex_to_binary import hex_to_binary 5 | from backend.config import MINE_RATE 6 | 7 | GENESIS_DATA = { 8 | 'timestamp': 1, 9 | 'last_hash': 'genesis_last_hash', 10 | 'hash': 'genesis_hash', 11 | 'data': [], 12 | 'difficulty': 3, 13 | 'nonce': 'genesis_nonce' 14 | } 15 | 16 | class Block: 17 | """ 18 | Block: a unit of storage. 19 | Store transactions in a blockchain that supports a cryptocurrency. 20 | """ 21 | def __init__(self, timestamp, last_hash, hash, data, difficulty, nonce): 22 | self.timestamp = timestamp 23 | self.last_hash = last_hash 24 | self.hash = hash 25 | self.data = data 26 | self.difficulty = difficulty 27 | self.nonce = nonce 28 | 29 | def __repr__(self): 30 | return ( 31 | 'Block(' 32 | f'timestamp: {self.timestamp}, ' 33 | f'last_hash: {self.last_hash}, ' 34 | f'hash: {self.hash}, ' 35 | f'data: {self.data}, ' 36 | f'difficulty: {self.difficulty}, ' 37 | f'nonce: {self.nonce})' 38 | ) 39 | 40 | def __eq__(self, other): 41 | return self.__dict__ == other.__dict__ 42 | 43 | def to_json(self): 44 | """ 45 | Serialize the block into a dictionary of its attributes 46 | """ 47 | return self.__dict__ 48 | 49 | @staticmethod 50 | def mine_block(last_block, data): 51 | """ 52 | Mine a block based on the given last_block and data, until a block hash 53 | is found that meets the leading 0's proof of work requirement. 54 | """ 55 | timestamp = time.time_ns() 56 | last_hash = last_block.hash 57 | difficulty = Block.adjust_difficulty(last_block, timestamp) 58 | nonce = 0 59 | hash = crypto_hash(timestamp, last_hash, data, difficulty, nonce) 60 | 61 | while hex_to_binary(hash)[0:difficulty] != '0' * difficulty: 62 | nonce += 1 63 | timestamp = time.time_ns() 64 | difficulty = Block.adjust_difficulty(last_block, timestamp) 65 | hash = crypto_hash(timestamp, last_hash, data, difficulty, nonce) 66 | 67 | return Block(timestamp, last_hash, hash, data, difficulty, nonce) 68 | 69 | @staticmethod 70 | def genesis(): 71 | """ 72 | Generate the genesis block. 73 | """ 74 | return Block(**GENESIS_DATA) 75 | 76 | @staticmethod 77 | def from_json(block_json): 78 | """ 79 | Deserialize a block's json representation back into a block instance. 80 | """ 81 | return Block(**block_json) 82 | 83 | @staticmethod 84 | def adjust_difficulty(last_block, new_timestamp): 85 | """ 86 | Calculate the adjusted difficulty according to the MINE_RATE. 87 | Increase the difficulty for quickly mined blocks. 88 | Decrease the difficulty for slowly mined blocks. 89 | """ 90 | if (new_timestamp - last_block.timestamp) < MINE_RATE: 91 | return last_block.difficulty + 1 92 | 93 | if (last_block.difficulty - 1) > 0: 94 | return last_block.difficulty - 1 95 | 96 | return 1 97 | 98 | @staticmethod 99 | def is_valid_block(last_block, block): 100 | """ 101 | Validate block by enforcing the following rules: 102 | - the block must have the proper last_hash reference 103 | - the block must meet the proof of work requirement 104 | - the difficulty must only adjust by 1 105 | - the block hash must be a valid combination of the block fields 106 | """ 107 | if block.last_hash != last_block.hash: 108 | raise Exception('The block last_hash must be correct') 109 | 110 | if hex_to_binary(block.hash)[0:block.difficulty] != '0' * block.difficulty: 111 | raise Exception('The proof of work requirement was not met') 112 | 113 | if abs(last_block.difficulty - block.difficulty) > 1: 114 | raise Exception('The block difficulty must only adjust by 1') 115 | 116 | reconstructed_hash = crypto_hash( 117 | block.timestamp, 118 | block.last_hash, 119 | block.data, 120 | block.nonce, 121 | block.difficulty 122 | ) 123 | 124 | if block.hash != reconstructed_hash: 125 | raise Exception('The block hash must be correct') 126 | 127 | def main(): 128 | genesis_block = Block.genesis() 129 | bad_block = Block.mine_block(genesis_block, 'foo') 130 | bad_block.last_hash = 'evil_data' 131 | 132 | try: 133 | Block.is_valid_block(genesis_block, bad_block) 134 | except Exception as e: 135 | print(f'is_valid_block: {e}') 136 | 137 | if __name__ == '__main__': 138 | main() 139 | -------------------------------------------------------------------------------- /backend/wallet/transaction.py: -------------------------------------------------------------------------------- 1 | import time 2 | import uuid 3 | 4 | from backend.wallet.wallet import Wallet 5 | from backend.config import MINING_REWARD, MINING_REWARD_INPUT 6 | 7 | class Transaction: 8 | """ 9 | Document of an exchange in currency from a sender to one 10 | or more recipients. 11 | """ 12 | def __init__( 13 | self, 14 | sender_wallet=None, 15 | recipient=None, 16 | amount=None, 17 | id=None, 18 | output=None, 19 | input=None 20 | ): 21 | self.id = id or str(uuid.uuid4())[0:8] 22 | self.output = output or self.create_output( 23 | sender_wallet, 24 | recipient, 25 | amount 26 | ) 27 | self.input = input or self.create_input(sender_wallet, self.output) 28 | 29 | def create_output(self, sender_wallet, recipient, amount): 30 | """ 31 | Structure the output data for the transaction. 32 | """ 33 | if amount > sender_wallet.balance: 34 | raise Exception('Amount exceeds balance') 35 | 36 | output = {} 37 | output[recipient] = amount 38 | output[sender_wallet.address] = sender_wallet.balance - amount 39 | 40 | return output 41 | 42 | def create_input(self, sender_wallet, output): 43 | """ 44 | Structure the input data for the transaction. 45 | Sign the transaction and include the sender's public key and address 46 | """ 47 | return { 48 | 'timestamp': time.time_ns(), 49 | 'amount': sender_wallet.balance, 50 | 'address': sender_wallet.address, 51 | 'public_key': sender_wallet.public_key, 52 | 'signature': sender_wallet.sign(output) 53 | } 54 | 55 | def update(self, sender_wallet, recipient, amount): 56 | """ 57 | Update the transaction with an existing or new recipient. 58 | """ 59 | if amount > self.output[sender_wallet.address]: 60 | raise Exception('Amount exceeds balance') 61 | 62 | if recipient in self.output: 63 | self.output[recipient] = self.output[recipient] + amount 64 | else: 65 | self.output[recipient] = amount 66 | 67 | self.output[sender_wallet.address] = \ 68 | self.output[sender_wallet.address] - amount 69 | 70 | self.input = self.create_input(sender_wallet, self.output) 71 | 72 | def to_json(self): 73 | """ 74 | Serialize the transaction. 75 | Convert large signature integers to strings to prevent float conversion in JSON. 76 | """ 77 | transaction_dict = self.__dict__.copy() 78 | 79 | if self.input != None and isinstance(self.input, dict) and 'signature' in self.input: 80 | transaction_dict = { 81 | 'id': self.id, 82 | 'output': self.output.copy(), 83 | 'input': self.input.copy() 84 | } 85 | # Convert signature tuple/list to string representations 86 | sig = self.input['signature'] 87 | transaction_dict['input']['signature'] = [str(sig[0]), str(sig[1])] 88 | 89 | return transaction_dict 90 | 91 | @staticmethod 92 | def from_json(transaction_json): 93 | """ 94 | Deserialize a transaction's json representation back into a 95 | Transaction instance. 96 | Convert signature strings back to integers. 97 | """ 98 | # Make a copy to avoid mutating the original 99 | transaction_data = transaction_json.copy() 100 | 101 | # If there's a signature, convert strings back to integers 102 | if 'input' in transaction_data and transaction_data['input'] is not None: 103 | if isinstance(transaction_data['input'], dict) and 'signature' in transaction_data['input']: 104 | sig = transaction_data['input']['signature'] 105 | # Convert string representations back to integers 106 | if isinstance(sig[0], str): 107 | transaction_data['input'] = transaction_data['input'].copy() 108 | transaction_data['input']['signature'] = [int(sig[0]), int(sig[1])] 109 | 110 | return Transaction(**transaction_data) 111 | 112 | @staticmethod 113 | def is_valid_transaction(transaction): 114 | """ 115 | Validate a transaction. 116 | Raise an exception for invalid transactions. 117 | """ 118 | if transaction.input == MINING_REWARD_INPUT: 119 | if list(transaction.output.values()) != [MINING_REWARD]: 120 | raise Exception('Invalid mining reward') 121 | return 122 | 123 | output_total = sum(transaction.output.values()) 124 | 125 | if transaction.input['amount'] != output_total: 126 | raise Exception('Invalid transaction output values') 127 | 128 | if not Wallet.verify( 129 | transaction.input['public_key'], 130 | transaction.output, 131 | transaction.input['signature'] 132 | ): 133 | raise Exception('Invalid signature') 134 | 135 | @staticmethod 136 | def reward_transaction(miner_wallet): 137 | """ 138 | Generate a reward transaction that award the miner. 139 | """ 140 | output = {} 141 | output[miner_wallet.address] = MINING_REWARD 142 | 143 | return Transaction(input=MINING_REWARD_INPUT, output=output) 144 | 145 | def main(): 146 | transaction = Transaction(Wallet(), 'recipient', 15) 147 | print(f'transaction.__dict__: {transaction.__dict__}') 148 | 149 | transaction_json = transaction.to_json() 150 | restored_transaction = Transaction.from_json(transaction_json) 151 | print(f'restored_transaction.__dict__: {restored_transaction.__dict__}') 152 | 153 | if __name__ == '__main__': 154 | main() 155 | -------------------------------------------------------------------------------- /backend/app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import random 4 | import threading 5 | import time 6 | from pathlib import Path 7 | from dotenv import load_dotenv 8 | 9 | # Load environment variables FIRST, before any other backend imports 10 | env_path = Path(__file__).parent.parent / '.env' 11 | load_dotenv(dotenv_path=env_path) 12 | 13 | from flask import Flask, jsonify, request, Response 14 | from flask_cors import CORS 15 | import json 16 | 17 | from backend.blockchain.blockchain import Blockchain 18 | from backend.wallet.wallet import Wallet 19 | from backend.wallet.transaction import Transaction 20 | from backend.wallet.transaction_pool import TransactionPool 21 | from backend.pubsub import PubSub 22 | 23 | app = Flask(__name__) 24 | CORS(app, resources={ r'/*': { 'origins': 'http://localhost:3000' } }) 25 | 26 | def json_response(data, status=200): 27 | """ 28 | Create a JSON response that preserves large integers. 29 | Flask's default jsonify converts large ints to floats, which breaks 30 | cryptographic signatures. This function ensures integers are preserved. 31 | """ 32 | return Response( 33 | json.dumps(data, separators=(',', ':')), 34 | status=status, 35 | mimetype='application/json' 36 | ) 37 | blockchain = Blockchain() 38 | wallet = Wallet(blockchain) 39 | transaction_pool = TransactionPool() 40 | pubsub = PubSub(blockchain, transaction_pool) 41 | 42 | @app.route('/') 43 | def route_default(): 44 | return 'Welcome to the blockchain' 45 | 46 | @app.route('/blockchain') 47 | def route_blockchain(): 48 | return json_response(blockchain.to_json()) 49 | 50 | @app.route('/blockchain/range') 51 | def route_blockchain_range(): 52 | # http://localhost:5050/blockchain/range?start=2&end=5 53 | start = int(request.args.get('start')) 54 | end = int(request.args.get('end')) 55 | 56 | return jsonify(blockchain.to_json()[::-1][start:end]) 57 | 58 | @app.route('/blockchain/length') 59 | def route_blockchain_length(): 60 | return jsonify(len(blockchain.chain)) 61 | 62 | @app.route('/blockchain/mine') 63 | def route_blockchain_mine(): 64 | transaction_data = transaction_pool.transaction_data() 65 | transaction_data.append(Transaction.reward_transaction(wallet).to_json()) 66 | blockchain.add_block(transaction_data) 67 | block = blockchain.chain[-1] 68 | pubsub.broadcast_block(block) 69 | transaction_pool.clear_blockchain_transactions(blockchain) 70 | 71 | return json_response(block.to_json()) 72 | 73 | @app.route('/wallet/transact', methods=['POST']) 74 | def route_wallet_transact(): 75 | transaction_data = request.get_json() 76 | transaction = transaction_pool.existing_transaction(wallet.address) 77 | 78 | if transaction: 79 | transaction.update( 80 | wallet, 81 | transaction_data['recipient'], 82 | transaction_data['amount'] 83 | ) 84 | else: 85 | transaction = Transaction( 86 | wallet, 87 | transaction_data['recipient'], 88 | transaction_data['amount'] 89 | ) 90 | 91 | pubsub.broadcast_transaction(transaction) 92 | transaction_pool.set_transaction(transaction) 93 | 94 | return jsonify(transaction.to_json()) 95 | 96 | @app.route('/wallet/info') 97 | def route_wallet_info(): 98 | return jsonify({ 'address': wallet.address, 'balance': wallet.balance }) 99 | 100 | @app.route('/known-addresses') 101 | def route_known_addresses(): 102 | known_addresses = set() 103 | 104 | for block in blockchain.chain: 105 | for transaction in block.data: 106 | known_addresses.update(transaction['output'].keys()) 107 | 108 | return jsonify(list(known_addresses)) 109 | 110 | @app.route('/transactions') 111 | def route_transactions(): 112 | return jsonify(transaction_pool.transaction_data()) 113 | 114 | ROOT_PORT = 5050 115 | PORT = ROOT_PORT 116 | # In Docker, use service name supplied by an env variable instead of localhost 117 | root_host = os.environ.get('ROOT_HOST', 'localhost') 118 | 119 | if os.environ.get('PEER') == 'True': 120 | PORT = random.randint(5051, 6000) 121 | 122 | result = requests.get(f'http://{root_host}:{ROOT_PORT}/blockchain') 123 | result_blockchain = Blockchain.from_json(result.json()) 124 | 125 | try: 126 | blockchain.replace_chain(result_blockchain.chain) 127 | print('\n -- Successfully synchronized the local chain') 128 | except Exception as e: 129 | print(f'\n -- Error synchronizing: {e}') 130 | 131 | if os.environ.get('SEED_DATA') == 'True': 132 | for i in range(10): 133 | blockchain.add_block([ 134 | Transaction(Wallet(), Wallet().address, random.randint(2, 50)).to_json(), 135 | Transaction(Wallet(), Wallet().address, random.randint(2, 50)).to_json() 136 | ]) 137 | 138 | for i in range(3): 139 | transaction = Transaction(Wallet(), Wallet().address, random.randint(2, 50)) 140 | pubsub.broadcast_transaction(transaction) 141 | transaction_pool.set_transaction(transaction) 142 | 143 | def poll_root_blockchain(): 144 | poll_interval = int(os.environ.get('POLL_INTERVAL', '15')) 145 | root_host = os.environ.get('ROOT_HOST', 'localhost') 146 | 147 | print(f'\n -- Starting polling thread for {root_host}:{ROOT_PORT} every {poll_interval}s') 148 | 149 | while True: 150 | try: 151 | result = requests.get(f'http://{root_host}:{ROOT_PORT}/blockchain') 152 | result_blockchain = Blockchain.from_json(result.json()) 153 | blockchain.replace_chain(result_blockchain.chain) 154 | print(f'\n -- Successfully polled blockchain from {root_host}') 155 | except Exception as e: 156 | print(f'\n -- Error polling root blockchain: {e}') 157 | 158 | time.sleep(poll_interval) 159 | 160 | if os.environ.get('POLL_ROOT') == 'True': 161 | # Start polling in a background daemon thread so it doesn't block Flask 162 | polling_thread = threading.Thread(target=poll_root_blockchain, daemon=True) 163 | polling_thread.start() 164 | 165 | if __name__ == "__main__": 166 | app.run(host="0.0.0.0", port=PORT, debug=True) 167 | 168 | --------------------------------------------------------------------------------