├── 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 |

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