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 )
0 commit comments