Skip to content
This repository was archived by the owner on Apr 3, 2026. It is now read-only.

Commit 9cb4b76

Browse files
authored
feat: Exercise #8. Modules and HTTP
BREAKING CHANGE: Last exercise added
2 parents 4c28c55 + b119a15 commit 9cb4b76

12 files changed

Lines changed: 1349 additions & 9 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from time import time
2+
# CLASS IMPORTS
3+
from classes.printable import Printable
4+
5+
class Block(Printable):
6+
def __init__(self, index, previous_hash, transactions, proof, timestamp=None):
7+
self.index = index
8+
self.previous_hash = previous_hash
9+
self.transactions = transactions
10+
self.proof = proof
11+
self.timestamp = time() if timestamp is None else timestamp
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import functools
2+
import json
3+
import requests
4+
# CLASS IMPORTS
5+
from classes.block import Block
6+
from classes.transaction import Transaction
7+
from classes.verification import Verification
8+
# OTHER IMPORTS
9+
from utils import hash_block, add__line
10+
11+
MINING_REWARD = 10
12+
13+
class Blockchain:
14+
def __init__(self, public_key, node_id):
15+
genesis_block = Block(0, '', [], 100)
16+
self.public_key = public_key
17+
self.chain = [genesis_block]
18+
self.open_transactions = []
19+
# You create a set of nodes to include unique values (which will not be added
20+
# if are repeated)
21+
self.peer_nodes = set()
22+
self.node_id = node_id
23+
self.resolve_conflicts = False
24+
self.load_data()
25+
26+
@property
27+
def chain(self):
28+
return self.__chain[:]
29+
30+
@property
31+
def open_transactions(self):
32+
return self.__open_transactions[:]
33+
34+
@chain.setter
35+
def chain(self, val):
36+
self.__chain = val
37+
38+
@open_transactions.setter
39+
def open_transactions(self, val):
40+
self.__open_transactions = val
41+
42+
def save_data(self):
43+
savable_chain = [block.__dict__ for block in [Block(block_el.index, block_el.previous_hash, [tx.__dict__ for tx in block_el.transactions], block_el.proof, block_el.timestamp) for block_el in self.__chain]]
44+
savable_transactions = [tx.__dict__ for tx in self.__open_transactions]
45+
46+
with open(f'blockchain-{self.node_id}.txt', mode='w') as f:
47+
f.write(json.dumps(savable_chain))
48+
f.write('\n')
49+
f.write(json.dumps(savable_transactions))
50+
f.write('\n')
51+
# Whey you create the file (or save it), you change the peer nodes to a list
52+
# as another list of information to be saved
53+
f.write(json.dumps(list(self.peer_nodes)))
54+
55+
def load_data(self):
56+
try:
57+
with open(f'blockchain-{self.node_id}.txt', mode='r') as f:
58+
file_content = f.readlines()
59+
60+
loaded_blockchain = json.loads(file_content[0])
61+
loaded_open_transactions = json.loads(file_content[1])
62+
# When you load the file, its third line now will be all the loaded peer lines
63+
loaded_peer_nodes = json.loads(file_content[2])
64+
updated_blockchain = []
65+
updated_transactions = []
66+
67+
for current_block in loaded_blockchain:
68+
block_transactions = [Transaction(tx['sender'], tx['recipient'], tx['signature'], tx['amount']) for tx in current_block['transactions']]
69+
updated_block = Block(
70+
current_block['proof'],
71+
current_block['previous_hash'],
72+
block_transactions,
73+
current_block['proof'])
74+
updated_blockchain.append(updated_block)
75+
76+
for tx in loaded_open_transactions:
77+
updated_transaction = Transaction(tx['sender'], tx['recipient'], tx['signature'], tx['amount'])
78+
updated_transactions.append(updated_transaction)
79+
80+
self.chain = updated_blockchain
81+
self.open_transactions = updated_transactions
82+
# After loading them, you adjust them again as a set of values instead a list
83+
self.peer_nodes = set(loaded_peer_nodes)
84+
except (IOError, IndexError):
85+
# Exception logic has been handled in class initializer
86+
pass
87+
finally:
88+
print('Cleanup')
89+
90+
def get_balance(self, sender=None):
91+
if sender == None:
92+
if self.public_key == None:
93+
return None
94+
participant = self.public_key
95+
else:
96+
participant = sender
97+
98+
participant = self.public_key
99+
tx_sender = [[tx.amount for tx in block.transactions
100+
if tx.sender == participant] for block in self.__chain]
101+
open_tx_sender = [tx.amount
102+
for tx in self.__open_transactions if tx.sender == participant]
103+
tx_sender.append(open_tx_sender)
104+
print(tx_sender)
105+
amount_sent = functools.reduce(lambda tx_sum, tx_amt: tx_sum + sum(tx_amt)
106+
if len(tx_amt) > 0 else tx_sum + 0, tx_sender, 0)
107+
tx_recipient = [[tx.amount for tx in block.transactions
108+
if tx.recipient == participant] for block in self.__chain]
109+
amount_received = functools.reduce(lambda tx_sum, tx_amt: tx_sum + sum(tx_amt)
110+
if len(tx_amt) > 0 else tx_sum + 0, tx_recipient, 0)
111+
return amount_received - amount_sent
112+
113+
def take_last_blockchain_value(self):
114+
if len(self.__chain) < 1:
115+
return None
116+
117+
return self.__chain[-1]
118+
119+
def add_transaction(self, recipient, sender, signature, amount=1.0, is_receiving=False):
120+
"""
121+
Add a new transaction to the list of open transactions (which will be added to the next mined block)
122+
123+
Arguments:
124+
:sender: The sender of the coins.
125+
:recipient: The recipient of the coins.
126+
:amount: The amount of the transaction.
127+
"""
128+
new_transaction = Transaction(sender, recipient, signature, amount)
129+
130+
if Verification.verify_transaction(new_transaction, self.get_balance):
131+
self.__open_transactions.append(new_transaction)
132+
self.save_data()
133+
134+
if not is_receiving:
135+
for peer_node_url in self.peer_nodes:
136+
broadcast_url = f'http://{peer_node_url}/broadcast-transaction'
137+
transaction_payload = { 'sender': sender, 'recipient': recipient, 'signature': signature, 'amount': amount }
138+
139+
try:
140+
internal_response = requests.post(broadcast_url, json=transaction_payload)
141+
142+
if internal_response.status_code == 400 or internal_response.status_code == 500:
143+
print('Transaction declined, needs resolving')
144+
return False
145+
except requests.exceptions.ConnectionError:
146+
continue
147+
return True
148+
else:
149+
return False
150+
151+
def get_open_transactions(self):
152+
return self.__open_transactions[:]
153+
154+
def mine_block(self):
155+
if self.public_key == None:
156+
return None
157+
158+
last_block = self.__chain[-1]
159+
hashed_block = hash_block(last_block)
160+
proof_of_work_value = self.proof_of_work()
161+
162+
reward_transaction = Transaction('MINING', self.public_key, '', MINING_REWARD)
163+
164+
transactions = self.__open_transactions.copy()
165+
transactions.append(reward_transaction)
166+
new_block = Block(
167+
len(self.__chain),
168+
hashed_block,
169+
transactions,
170+
proof_of_work_value
171+
)
172+
self.__chain.append(new_block)
173+
self.__open_transactions = []
174+
self.save_data()
175+
176+
for node_url in self.peer_nodes:
177+
try:
178+
broadcast_url = f'http://{node_url}/broadcast-block'
179+
block_dict = new_block.__dict__.copy()
180+
block_dict['transactions'] = [tx.__dict__ for tx in block_dict['transactions']]
181+
182+
internal_response = requests.post(broadcast_url, json={ 'block': block_dict })
183+
184+
if internal_response.status_code == 400 or internal_response.status_code == 500:
185+
print('Block declined, needs resolving')
186+
if internal_response.status_code == 409:
187+
self.resolve_conflicts = True
188+
except requests.exceptions.ConnectionError:
189+
continue
190+
191+
return new_block
192+
193+
def add_block(self, new_block):
194+
transaction_list = [Transaction(
195+
tx['sender'],
196+
tx['recipient'],
197+
tx['signature'],
198+
tx['amount']
199+
) for tx in new_block['transactions']]
200+
proof_is_valid = Verification.valid_proof(transaction_list[-1], new_block['previous_hash'], new_block['proof'])
201+
hashes_match = hash_block(self.chain[-1]) == new_block['previous_hash']
202+
203+
if not proof_is_valid or not hashes_match:
204+
return False
205+
206+
converted_block = Block(
207+
new_block['index'],
208+
new_block['previous_hash'],
209+
transaction_list,
210+
new_block['proof'],
211+
new_block['timestamp'])
212+
213+
self.__chain.append(converted_block)
214+
stored_transactions = self.__open_transactions[:]
215+
216+
for incoming_tx in new_block['transactions']:
217+
for open_tx in stored_transactions:
218+
if open_tx.sender == incoming_tx['sender'] and open_tx.recipient == incoming_tx['recipient'] and open_tx.signature == incoming_tx['signature']:
219+
try:
220+
self.__open_transactions.remove(open_tx)
221+
except:
222+
print('Item was already removed')
223+
224+
self.save_data()
225+
226+
return True
227+
228+
def proof_of_work(self):
229+
last_block = self.__chain[-1]
230+
hash_last_block = hash_block(last_block)
231+
proof = 0
232+
while not Verification.valid_proof(self.__open_transactions, hash_last_block, proof):
233+
proof += 1
234+
return proof
235+
236+
def resolve(self):
237+
winner_chain = self.chain
238+
chain_has_been_replaced = False
239+
240+
for node in self.peer_nodes:
241+
url = f'http://{node}/chain'
242+
try:
243+
chain_response = requests.post(url)
244+
node_chain = chain_response.json()
245+
node_chain = [Block(
246+
block['index'],
247+
block['previous_hash'],
248+
[Transaction(
249+
block_tx['sender'],
250+
block_tx['recipient'],
251+
block_tx['signature'],
252+
block_tx['amount']
253+
) for block_tx in block['transactions']],
254+
block['proof'],
255+
block['timestamp'],
256+
) for block in node_chain]
257+
258+
node_chain_length = len(node_chain)
259+
local_chain_length = len(winner_chain)
260+
261+
if node_chain_length > local_chain_length and Verification.verify_chain(node_chain):
262+
winner_chain = node_chain
263+
chain_has_been_replaced = True
264+
except requests.exceptions.ConnectionError:
265+
continue
266+
267+
self.resolve_conflicts = False
268+
self.chain = winner_chain
269+
270+
if chain_has_been_replaced:
271+
self.open_transactions = []
272+
273+
self.save_data()
274+
275+
return chain_has_been_replaced
276+
277+
def add_peer_node(self, node):
278+
self.peer_nodes.add(node)
279+
self.save_data()
280+
281+
def remove_peer_node(self, node):
282+
self.peer_nodes.discard(node)
283+
self.save_data()
284+
285+
def get_all_nodes(self):
286+
return list(self.peer_nodes)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class Printable:
2+
def __repr__(self):
3+
return str(self.__dict__)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from collections import OrderedDict
2+
# CLASS IMPORTS
3+
from classes.printable import Printable
4+
5+
class Transaction(Printable):
6+
def __init__(self, sender, recipient, signature, amount):
7+
self.sender = sender
8+
self.recipient = recipient
9+
self.amount = amount
10+
self.signature = signature
11+
12+
def to_ordered_dict(self):
13+
return OrderedDict([
14+
('sender', self.sender),
15+
('recipient', self.recipient),
16+
('amount', self.amount)])
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# OTHER IMPORTS
2+
from utils import hash_block, hash_string_256
3+
from classes.wallet import Wallet
4+
5+
class Verification:
6+
@classmethod
7+
def verify_chain(cls, blockchain):
8+
""" The function helps to verify the integrity of the blockchain by checking if each block's previous hash matches the hash of the previous block. """
9+
for (index, block) in enumerate(blockchain):
10+
if index == 0:
11+
continue
12+
if block.previous_hash != hash_block(blockchain[index - 1]):
13+
return False
14+
if not cls.valid_proof(block.transactions[:-1], block.previous_hash, block.proof):
15+
print('Proof of work is invalid')
16+
return False
17+
return True
18+
19+
@staticmethod
20+
def verify_transaction(transaction, get_balance, check_funds=True):
21+
if check_funds:
22+
sender_balance = get_balance(transaction.sender)
23+
return sender_balance >= transaction.amount and Wallet.verify_transaction(transaction)
24+
else:
25+
Wallet.verify_transaction(transaction)
26+
27+
@classmethod
28+
def verify_transactions(self, open_transactions):
29+
""" The function verifies all open transactions to ensure they are valid. """
30+
return all([tx for tx in open_transactions if not self.verify_transaction(tx)])
31+
32+
@staticmethod
33+
def valid_proof(transactions, last_hash, proof):
34+
guess = (str([tx.to_ordered_dict() for tx in transactions]) + str(last_hash) + str(proof)).encode()
35+
guess_hash = hash_string_256(guess)
36+
37+
return guess_hash[0:2] == '00'

0 commit comments

Comments
 (0)