Skip to content

Commit c9ebe8e

Browse files
committed
Complete section: Connect the Blockchain and Cryptocurrency
1 parent 9fd4ff2 commit c9ebe8e

9 files changed

Lines changed: 223 additions & 9 deletions

File tree

backend/app/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
app = Flask(__name__)
1414
blockchain = Blockchain()
15-
wallet = Wallet()
15+
wallet = Wallet(blockchain)
1616
transaction_pool = TransactionPool()
1717
pubsub = PubSub(blockchain, transaction_pool)
1818

@@ -26,7 +26,9 @@ def route_blockchain():
2626

2727
@app.route('/blockchain/mine')
2828
def route_blockchain_mine():
29-
blockchain.add_block(transaction_pool.transaction_data())
29+
transaction_data = transaction_pool.transaction_data()
30+
transaction_data.append(Transaction.reward_transaction(wallet).to_json())
31+
blockchain.add_block(transaction_data)
3032
block = blockchain.chain[-1]
3133
pubsub.broadcast_block(block)
3234
transaction_pool.clear_blockchain_transactions(blockchain)
@@ -55,6 +57,10 @@ def route_wallet_transact():
5557

5658
return jsonify(transaction.to_json())
5759

60+
@app.route('/wallet/info')
61+
def route_wallet_info():
62+
return jsonify({ 'address': wallet.address, 'balance': wallet.balance })
63+
5864
ROOT_PORT = 5000
5965
PORT = ROOT_PORT
6066

backend/blockchain/blockchain.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from backend.blockchain.block import Block
2+
from backend.wallet.transaction import Transaction
3+
from backend.wallet.wallet import Wallet
4+
from backend.config import MINING_REWARD_INPUT
25

36
class Blockchain:
47
"""
@@ -65,6 +68,53 @@ def is_valid_chain(chain):
6568
last_block = chain[i-1]
6669
Block.is_valid_block(last_block, block)
6770

71+
Blockchain.is_valid_transaction_chain(chain)
72+
73+
@staticmethod
74+
def is_valid_transaction_chain(chain):
75+
"""
76+
Enforce the rules of a chain composed of blocks of transactions.
77+
- Each transaction must only appear once in the chain.
78+
- There can only be one mining reward per block.
79+
- Each transaction must be valid.
80+
"""
81+
transaction_ids = set()
82+
83+
for i in range(len(chain)):
84+
block = chain[i]
85+
has_mining_reward = False
86+
87+
for transaction_json in block.data:
88+
transaction = Transaction.from_json(transaction_json)
89+
90+
if transaction.id in transaction_ids:
91+
raise Exception(f'Transaction {transaction.id} is not unique')
92+
93+
transaction_ids.add(transaction.id)
94+
95+
if transaction.input == MINING_REWARD_INPUT:
96+
if has_mining_reward:
97+
raise Exception(
98+
'There can only be one mining reward per block. '\
99+
f'Check block with hash: {block.hash}'
100+
)
101+
102+
has_mining_reward = True
103+
else:
104+
historic_blockchain = Blockchain()
105+
historic_blockchain.chain = chain[0:i]
106+
historic_balance = Wallet.calculate_balance(
107+
historic_blockchain,
108+
transaction.input['address']
109+
)
110+
111+
if historic_balance != transaction.input['amount']:
112+
raise Exception(
113+
f'Transaction {transaction.id} has an invalid '\
114+
'input amount'
115+
)
116+
117+
Transaction.is_valid_transaction(transaction)
68118

69119
def main():
70120
blockchain = Blockchain()

backend/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55

66
MINE_RATE = 4 * SECONDS
77

8-
STARTING_BALANCE = 1000
8+
STARTING_BALANCE = 1000
9+
10+
MINING_REWARD = 50
11+
MINING_REWARD_INPUT = { 'address': '*--official-mining-reward--*' }

backend/scripts/test_app.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ def post_wallet_transact(recipient, amount):
1717
json={ 'recipient': recipient, 'amount': amount }
1818
).json()
1919

20+
def get_wallet_info():
21+
return requests.get(f'{BASE_URL}/wallet/info').json()
22+
2023
start_blockchain = get_blockchain()
2124
print(f'start_blockchain: {start_blockchain}')
2225

@@ -30,4 +33,7 @@ def post_wallet_transact(recipient, amount):
3033

3134
time.sleep(1)
3235
mined_block = get_blockchain_mine()
33-
print(f'\nmined_block: {mined_block}')
36+
print(f'\nmined_block: {mined_block}')
37+
38+
wallet_info = get_wallet_info()
39+
print(f'\nwallet_info: {wallet_info}')

backend/tests/blockchain/test_blockchain.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from backend.blockchain.blockchain import Blockchain
44
from backend.blockchain.block import GENESIS_DATA
5+
from backend.wallet.wallet import Wallet
6+
from backend.wallet.transaction import Transaction
57

68
def test_blockchain_instance():
79
blockchain = Blockchain()
@@ -18,7 +20,7 @@ def test_add_block():
1820
def blockchain_three_blocks():
1921
blockchain = Blockchain()
2022
for i in range(3):
21-
blockchain.add_block(i)
23+
blockchain.add_block([Transaction(Wallet(), 'recipient', i).to_json()])
2224
return blockchain
2325

2426
def test_is_valid_chain(blockchain_three_blocks):
@@ -47,4 +49,42 @@ def test_replace_chain_bad_chain(blockchain_three_blocks):
4749
blockchain_three_blocks.chain[1].hash = 'evil_hash'
4850

4951
with pytest.raises(Exception, match='The incoming chain is invalid'):
50-
blockchain.replace_chain(blockchain_three_blocks.chain)
52+
blockchain.replace_chain(blockchain_three_blocks.chain)
53+
54+
def test_valid_transaction_chain(blockchain_three_blocks):
55+
Blockchain.is_valid_transaction_chain(blockchain_three_blocks.chain)
56+
57+
def test_is_valid_transaction_chain_duplicate_transactions(blockchain_three_blocks):
58+
transaction = Transaction(Wallet(), 'recipient', 1).to_json()
59+
blockchain_three_blocks.add_block([transaction, transaction])
60+
61+
with pytest.raises(Exception, match='is not unique'):
62+
Blockchain.is_valid_transaction_chain(blockchain_three_blocks.chain)
63+
64+
def test_is_valid_transaction_chain_multiple_rewards(blockchain_three_blocks):
65+
reward_1 = Transaction.reward_transaction(Wallet()).to_json()
66+
reward_2 = Transaction.reward_transaction(Wallet()).to_json()
67+
blockchain_three_blocks.add_block([reward_1, reward_2])
68+
69+
with pytest.raises(Exception, match='one mining reward per block'):
70+
Blockchain.is_valid_transaction_chain(blockchain_three_blocks.chain)
71+
72+
def test_is_valid_transaction_chain_bad_transaction(blockchain_three_blocks):
73+
bad_transaction = Transaction(Wallet(), 'recipient', 1)
74+
bad_transaction.input['signature'] = Wallet().sign(bad_transaction.output)
75+
blockchain_three_blocks.add_block([bad_transaction.to_json()])
76+
77+
with pytest.raises(Exception):
78+
Blockchain.is_valid_transaction_chain(blockchain_three_blocks.chain)
79+
80+
def test_is_valid_transaction_chain_bad_historic_balance(blockchain_three_blocks):
81+
wallet = Wallet()
82+
bad_transaction = Transaction(wallet, 'recipient', 1)
83+
bad_transaction.output[wallet.address] = 9000
84+
bad_transaction.input['amount'] = 9001
85+
bad_transaction.input['signature'] = wallet.sign(bad_transaction.output)
86+
87+
blockchain_three_blocks.add_block([bad_transaction.to_json()])
88+
89+
with pytest.raises(Exception, match='has an invalid input amount'):
90+
Blockchain.is_valid_transaction_chain(blockchain_three_blocks.chain)

backend/tests/wallet/test_transaction.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from backend.wallet.transaction import Transaction
44
from backend.wallet.wallet import Wallet
5+
from backend.config import MINING_REWARD, MINING_REWARD_INPUT
56

67
def test_transaction():
78
sender_wallet = Wallet()
@@ -82,3 +83,29 @@ def test_valid_transaction_with_invalid_signature():
8283

8384
with pytest.raises(Exception, match='Invalid signature'):
8485
Transaction.is_valid_transaction(transaction)
86+
87+
def test_reward_transaction():
88+
miner_wallet = Wallet()
89+
transaction = Transaction.reward_transaction(miner_wallet)
90+
91+
assert transaction.input == MINING_REWARD_INPUT
92+
assert transaction.output[miner_wallet.address] == MINING_REWARD
93+
94+
def test_valid_reward_transaction():
95+
reward_transaction = Transaction.reward_transaction(Wallet())
96+
Transaction.is_valid_transaction(reward_transaction)
97+
98+
def test_invalid_reward_transaction_extra_recipient():
99+
reward_transaction = Transaction.reward_transaction(Wallet())
100+
reward_transaction.output['extra_recipient'] = 60
101+
102+
with pytest.raises(Exception, match='Invalid mining reward'):
103+
Transaction.is_valid_transaction(reward_transaction)
104+
105+
def test_invalid_reward_transaction_invalid_amount():
106+
miner_wallet = Wallet()
107+
reward_transaction = Transaction.reward_transaction(miner_wallet)
108+
reward_transaction.output[miner_wallet.address] = 9001
109+
110+
with pytest.raises(Exception, match='Invalid mining reward'):
111+
Transaction.is_valid_transaction(reward_transaction)

backend/tests/wallet/test_wallet.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from backend.wallet.wallet import Wallet
2+
from backend.wallet.transaction import Transaction
3+
from backend.blockchain.blockchain import Blockchain
4+
from backend.config import STARTING_BALANCE
25

36
def test_verify_valid_signature():
47
data = { 'foo': 'test_data' }
@@ -12,4 +15,38 @@ def test_verify_invalid_signature():
1215
wallet = Wallet()
1316
signature = wallet.sign(data)
1417

15-
assert not Wallet.verify(Wallet().public_key, data, signature)
18+
assert not Wallet.verify(Wallet().public_key, data, signature)
19+
20+
def test_calcalate_balance():
21+
blockchain = Blockchain()
22+
wallet = Wallet()
23+
24+
assert Wallet.calculate_balance(blockchain, wallet.address) == STARTING_BALANCE
25+
26+
amount = 50
27+
transaction = Transaction(wallet, 'recipient', amount)
28+
blockchain.add_block([transaction.to_json()])
29+
30+
assert Wallet.calculate_balance(blockchain, wallet.address) == \
31+
STARTING_BALANCE - amount
32+
33+
received_amount_1 = 25
34+
received_transaction_1 = Transaction(
35+
Wallet(),
36+
wallet.address,
37+
received_amount_1
38+
)
39+
40+
received_amount_2 = 43
41+
received_transaction_2 = Transaction(
42+
Wallet(),
43+
wallet.address,
44+
received_amount_2
45+
)
46+
47+
blockchain.add_block(
48+
[received_transaction_1.to_json(), received_transaction_2.to_json()]
49+
)
50+
51+
assert Wallet.calculate_balance(blockchain, wallet.address) == \
52+
STARTING_BALANCE - amount + received_amount_1 + received_amount_2

backend/wallet/transaction.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import uuid
33

44
from backend.wallet.wallet import Wallet
5+
from backend.config import MINING_REWARD, MINING_REWARD_INPUT
56

67
class Transaction:
78
"""
@@ -88,6 +89,11 @@ def is_valid_transaction(transaction):
8889
Validate a transaction.
8990
Raise an exception for invalid transactions.
9091
"""
92+
if transaction.input == MINING_REWARD_INPUT:
93+
if list(transaction.output.values()) != [MINING_REWARD]:
94+
raise Exception('Invalid mining reward')
95+
return
96+
9197
output_total = sum(transaction.output.values())
9298

9399
if transaction.input['amount'] != output_total:
@@ -100,6 +106,16 @@ def is_valid_transaction(transaction):
100106
):
101107
raise Exception('Invalid signature')
102108

109+
@staticmethod
110+
def reward_transaction(miner_wallet):
111+
"""
112+
Generate a reward transaction that award the miner.
113+
"""
114+
output = {}
115+
output[miner_wallet.address] = MINING_REWARD
116+
117+
return Transaction(input=MINING_REWARD_INPUT, output=output)
118+
103119
def main():
104120
transaction = Transaction(Wallet(), 'recipient', 15)
105121
print(f'transaction.__dict__: {transaction.__dict__}')

backend/wallet/wallet.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@ class Wallet:
1717
Keeps track of the miner's balance.
1818
Allows a miner to authorize transactions.
1919
"""
20-
def __init__(self):
20+
def __init__(self, blockchain=None):
21+
self.blockchain = blockchain
2122
self.address = str(uuid.uuid4())[0:8]
22-
self.balance = STARTING_BALANCE
2323
self.private_key = ec.generate_private_key(
2424
ec.SECP256K1(),
2525
default_backend()
2626
)
2727
self.public_key = self.private_key.public_key()
2828
self.serialize_public_key()
2929

30+
@property
31+
def balance(self):
32+
return Wallet.calculate_balance(self.blockchain, self.address)
33+
3034
def sign(self, data):
3135
"""
3236
Generate a signature based on the data using the local private key.
@@ -67,6 +71,31 @@ def verify(public_key, data, signature):
6771
except InvalidSignature:
6872
return False
6973

74+
@staticmethod
75+
def calculate_balance(blockchain, address):
76+
"""
77+
Calculate the balance of the given address considering the transaction
78+
data within the blockchain.
79+
80+
The balance is found by adding the output values that belong to the
81+
address since the most recent transaction by that address.
82+
"""
83+
balance = STARTING_BALANCE
84+
85+
if not blockchain:
86+
return balance
87+
88+
for block in blockchain.chain:
89+
for transaction in block.data:
90+
if transaction['input']['address'] == address:
91+
# Any time the address conducts a new transaction it resets
92+
# its balance
93+
balance = transaction['output'][address]
94+
elif address in transaction['output']:
95+
balance += transaction['output'][address]
96+
97+
return balance
98+
7099
def main():
71100
wallet = Wallet()
72101
print(f'wallet.__dict__: {wallet.__dict__}')

0 commit comments

Comments
 (0)