Skip to content

Commit 533f928

Browse files
committed
Added Conflict Resolution
1 parent e9824f4 commit 533f928

File tree

4 files changed

+162
-110
lines changed

4 files changed

+162
-110
lines changed

blockchain.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
MINING_REWARD = 10
1616

1717
# Configure Proof-Of-Work Difficulty: Number of leading zeros required in the hash
18-
POW_LEADING_ZEROS = 2
18+
POW_LEADING_ZEROS = 3
1919

2020

2121
class Blockchain:
@@ -26,6 +26,7 @@ def __init__(self, hosting_node_id):
2626
self.__chain = [genesis_block]
2727
self.__hosting_node = hosting_node_id
2828
self.__peer_nodes = set()
29+
self.resolve_conflicts = False # Set to True, when there is a conflict to be resolved
2930

3031
# List of new unhandled transactions that are waiting to be processed (mined)
3132
# and included into the blockchain
@@ -49,6 +50,11 @@ def get_chain_dict(self):
4950
return dict_chain
5051

5152

53+
def get_length(self):
54+
"""Returns the length of the current blockchain"""
55+
return len(self.__chain)
56+
57+
5258
def get_last_block(self):
5359
"""Returns a copy of the last block of the current blockchain"""
5460
return self.__chain[:][-1]
@@ -221,14 +227,13 @@ def add_block(self, block):
221227
Arguments:
222228
:block: The block to add that was broadcast by another node
223229
"""
224-
transactions = [Transaction(tx.sender, tx.recipient, tx.amount, tx.signature) for tx in block['transactions']]
230+
transactions = [Transaction.from_dict(tx) for tx in block['transactions']]
225231
proof_is_valid = Verification.valid_proof(transactions[:-1], block['previous_hash'], block['proof'], self.mining_difficulty)
226232
hashes_match = hash_block(self.__chain[-1]) == block['previous']
227233
if not proof_is_valid or not hashes_match:
228234
return False
229235

230-
block_object = Block(block['index'], block['previous_hash'], block['transactions'], block['proof'], block['timestamp'])
231-
self.__chain.append(block_object)
236+
self.__chain.append(Block.from_dict(block))
232237

233238
# Update open-transactions: remove any transaction that is already mined...
234239
stored_transactions = self.__open_transactions[:]
@@ -244,6 +249,30 @@ def add_block(self, block):
244249
return True
245250

246251

252+
def resolve(self):
253+
"""Resolves any conflict in the current blockchain"""
254+
winner_chain = self.__chain
255+
replace = False
256+
for node in self.__peer_nodes:
257+
url = 'http://{}/chain'.format(node)
258+
try:
259+
response = requests.get(url)
260+
node_chain = response.json()
261+
node_chain = [Block.from_dict(block) for block in node_chain]
262+
node_chain_length = len(node_chain)
263+
local_chain_length = len(self.__chain)
264+
if node_chain_length > local_chain_length and Verification.verify_chain(node_chain, self.mining_difficulty):
265+
winner_chain = node_chain
266+
replace = True
267+
except requests.exceptions.ConnectionError:
268+
continue
269+
self.resolve_conflicts = False
270+
if replace:
271+
self.__chain = winner_chain
272+
self.__open_transactions = [] # Invalidate any previously open transactions
273+
self.save_data()
274+
return replace
275+
247276

248277
def get_peer_nodes(self):
249278
"""Returns a list of connected peer nodes"""
@@ -276,10 +305,14 @@ def broadcast(self, endpoint, data):
276305
url = 'http://{}/{}'.format(node, endpoint)
277306
try:
278307
response = requests.post(url, json=data)
279-
if response.status_code >= 400:
280-
# print('Transaction declined, needs resolving')
308+
if response.status_code == 409:
309+
self.resolve_conflicts = True
310+
return False
311+
elif response.status_code >= 400:
312+
# TODO: print('Transaction declined, needs resolving')
281313
return False
282314
except requests.exceptions.ConnectionError:
283315
# Node is offline?
284316
continue
285-
return True
317+
return True
318+

node.py

Lines changed: 78 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,30 @@
1313

1414

1515

16+
def resp(status_code=200, message='Success', data=None):
17+
"""Helper function to return jsonified response"""
18+
response = { 'message': message }
19+
if data:
20+
response.update(data)
21+
return jsonify(response), status_code
22+
23+
1624
def validate_request_data(data, required_fields=None):
1725
"""Helper function to validates the request data.
1826
returns None if data is valid, i.e., data is present and
1927
required_fields, if any, are also present.
20-
Otherwise, returns a jsonify() response object with 400 status.
28+
Otherwise, returns a jsonified response object
29+
with 400 status and an error message.
2130
2231
Arguments:
2332
:data: Dictionary of request parameters to validate
2433
:required_fields: List of fields that are required to be present
2534
"""
2635
if not data:
27-
response = {'message': 'No data found'}
28-
return jsonify(response), 400
36+
return resp(400, 'No data found')
2937
if required_fields:
3038
if not all(field in data for field in required_fields):
31-
response = {'message': 'Required data is misssing'}
32-
return jsonify(response), 400
39+
return resp(400, 'Required data is misssing')
3340
return None
3441

3542

@@ -54,10 +61,7 @@ def get_transactions():
5461
def add_transaction():
5562
"""TODO: Validate input parameters"""
5663
if wallet.public_key == None:
57-
response = {
58-
'message': 'No wallet set up.'
59-
}
60-
return jsonify(response), 400
64+
return resp(400, 'No wallet set up')
6165

6266
data = request.get_json()
6367
required_fields = ['recipient', 'amount']
@@ -70,20 +74,15 @@ def add_transaction():
7074
signature = wallet.sign_transactions(wallet.public_key, recipient, amount)
7175
success = blockchain.add_transaction(sender=wallet.public_key, recipient=recipient, amount=amount, signature=signature)
7276
if success:
73-
response = {
74-
'message': 'Transaction added successfully.',
75-
'transaction': blockchain.get_open_transactions()[-1].to_dict(),
76-
'balance': blockchain.get_balance()
77-
}
78-
return jsonify(response), 201
77+
return resp(201, 'Transaction added successfully.', {
78+
'transaction': blockchain.get_open_transactions()[-1].to_dict(),
79+
'balance': blockchain.get_balance()
80+
})
7981
else:
80-
response = {
81-
'message': 'Failed to create a transaction'
82-
}
83-
return jsonify(response), 500
82+
return resp(500, 'Failed to create a transaction')
8483

8584

86-
@app.route('/broadcast_transaction', methods=['POST'])
85+
@app.route('/broadcast-transaction', methods=['POST'])
8786
def broadcast_transaction():
8887
"""Receive broadcast of newly added transactions from other nodes"""
8988
data = request.get_json()
@@ -94,20 +93,15 @@ def broadcast_transaction():
9493

9594
success = blockchain.add_transaction(sender=data['sender'], recipient=data['recipient'], amount=data['amount'], signature=data['signature'], is_receiving=True)
9695
if success:
97-
response = {
98-
'message': 'Transaction added successfully.',
99-
'transaction': blockchain.get_open_transactions()[-1].to_dict(),
100-
'balance': blockchain.get_balance()
101-
}
102-
return jsonify(response), 201
96+
return resp(201, 'Transaction added successfully.', {
97+
'transaction': blockchain.get_open_transactions()[-1].to_dict(),
98+
'balance': blockchain.get_balance()
99+
})
103100
else:
104-
response = {
105-
'message': 'Failed to create a transaction'
106-
}
107-
return jsonify(response), 500
101+
return resp(500, 'Failed to create a transaction.')
108102

109103

110-
@app.route('/broadcast_block', methods=['POST'])
104+
@app.route('/broadcast-block', methods=['POST'])
111105
def broadcast_block():
112106
"""Receive broadcast of newly mined blocks from other nodes"""
113107
data = request.get_json()
@@ -120,17 +114,25 @@ def broadcast_block():
120114
last_local_block_index = blockchain.get_last_block()['index']
121115
if block['index'] == last_local_block_index + 1:
122116
if blockchain.add_block(block):
123-
response = {'message': 'Block added successfully'}
124-
return jsonify(response), 201
117+
return resp(201, 'Block added successfully.')
125118
else:
126-
response = {'message': 'Block seems invalid'}
127-
return jsonify(response), 500
119+
return resp(409, 'Block seems invalid.') # Invalid data (conflict)
128120
elif block['index'] > last_local_block_index:
129-
pass
121+
# TODO: Our chain is shorter.. We need to fix it!
122+
blockchain.resolve_conflicts = True
123+
return resp(200, 'Bockchain seems to differ from local blockchain.') # Not an error (Current node needs to fix the issue)
130124
else:
131125
# Our blockchain is more recent (has longer chain)
132-
response = {'message': 'Bockchain seems to be shorter. Block not added'}
133-
return jsonify(response), 409 # Invalid data
126+
return resp(409, 'Bockchain seems to be shorter. Block not added.') # Invalid data (conflict)
127+
128+
129+
@app.route('/resolve-conflicts', methods=['POST'])
130+
def resolve_conflicts():
131+
replaced = blockchain.resolve()
132+
if replaced:
133+
return resp(200, 'Chain was replaced.')
134+
else:
135+
return resp(200, 'Local chain kept.')
134136

135137

136138
@app.route('/wallet', methods=['POST'])
@@ -139,70 +141,57 @@ def create_keys():
139141
if wallet.save_keys():
140142
global blockchain
141143
blockchain = Blockchain(wallet.public_key)
142-
response = {
143-
'public_key': wallet.public_key,
144-
'private_key': wallet.private_key,
145-
'balance': blockchain.get_balance()
146-
}
147-
return jsonify(response), 201
144+
return resp(201, '', {
145+
'public_key': wallet.public_key,
146+
'private_key': wallet.private_key,
147+
'balance': blockchain.get_balance()
148+
})
148149
else:
149-
response = {
150-
'message': 'Failed to save the wallet keys.'
151-
}
152-
return jsonify(response), 500
150+
return resp(500, 'Failed to save the wallet keys.')
153151

154152

155153
@app.route('/wallet', methods=['GET'])
156154
def load_keys():
157155
if wallet.load_keys():
158156
global blockchain
159157
blockchain = Blockchain(wallet.public_key)
160-
response = {
161-
'public_key': wallet.public_key,
162-
'private_key': wallet.private_key,
163-
'balance': blockchain.get_balance()
164-
}
165-
return jsonify(response), 200
158+
return resp(200, '', {
159+
'public_key': wallet.public_key,
160+
'private_key': wallet.private_key,
161+
'balance': blockchain.get_balance()
162+
})
166163
else:
167-
response = {
168-
'message': 'Failed to load the wallet keys.'
169-
}
170-
return jsonify(response), 500
164+
return resp(500, 'Failed to load the wallet keys.')
171165

172166

173167
@app.route('/balance', methods=['GET'])
174168
def get_balance():
175169
balance = blockchain.get_balance()
176170
if balance != None:
177-
response = {
178-
'message': 'Balance fetched successfully',
179-
'balance': blockchain.get_balance()
180-
}
181-
return jsonify(response), 200
171+
return resp(200, 'Balance fetched successfully.', {
172+
'balance': blockchain.get_balance()
173+
})
182174
else:
183-
response = {
184-
'message': 'Failed to load balance.',
185-
'is_wallet_setup': wallet.public_key != None
186-
}
187-
return jsonify(response), 500
175+
return resp(500, 'Failed to load balance.', {
176+
'is_wallet_setup': wallet.public_key != None
177+
})
188178

189179

190180
@app.route('/mine', methods=['POST'])
191181
def mine():
182+
if blockchain.resolve_conflicts:
183+
return resp(409, 'Block not added due to conflicts.')
184+
192185
block = blockchain.mine_block()
193186
if block != None:
194-
response = {
195-
'message': 'Mining successful. Block added.',
196-
'added_block': block.to_dict(),
197-
'balance': blockchain.get_balance()
198-
}
199-
return jsonify(response), 201
187+
return resp(201, 'Mining successful. Block added.', {
188+
'added_block': block.to_dict(),
189+
'balance': blockchain.get_balance()
190+
})
200191
else:
201-
response = {
202-
'message': 'Mining failed. Block not added.',
203-
'is_wallet_setup': wallet.public_key != None
204-
}
205-
return jsonify(response), 500
192+
return resp(500, 'Mining failed. Block not added.', {
193+
'is_wallet_setup': wallet.public_key != None
194+
})
206195

207196

208197
@app.route('/chain', methods=['GET'])
@@ -212,10 +201,9 @@ def get_chain():
212201

213202
@app.route('/nodes', methods=['GET'])
214203
def get_nodes():
215-
response = {
216-
'peer_nodes': blockchain.get_peer_nodes()
217-
}
218-
return jsonify(response), 200
204+
return resp(200, '', {
205+
'peer_nodes': blockchain.get_peer_nodes()
206+
})
219207

220208

221209
@app.route('/node', methods=['POST'])
@@ -227,26 +215,19 @@ def add_node():
227215
return invalid_data_response
228216

229217
blockchain.add_peer_node(data['node'])
230-
response = {
231-
'message': 'Node added successfully',
232-
'peer_nodes': blockchain.get_peer_nodes()
233-
}
234-
return jsonify(response), 201
218+
return resp(201, 'Node added successfully.', {
219+
'peer_nodes': blockchain.get_peer_nodes()
220+
})
235221

236222

237223
@app.route('/node/<node_url>', methods=['DELETE'])
238224
def remove_node(node_url):
239225
if not node_url:
240-
response = {
241-
'message': 'No node data found'
242-
}
243-
return jsonify(response), 400
226+
return resp(400, 'No node data found')
244227
blockchain.remove_peer_node(node_url)
245-
response = {
246-
'message': 'Node removed successfully',
247-
'peer_nodes': blockchain.get_peer_nodes()
248-
}
249-
return jsonify(response), 200
228+
return resp(200, 'Node removed successfully.', {
229+
'peer_nodes': blockchain.get_peer_nodes()
230+
})
250231

251232

252233

node_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def command_line_interface(self):
7676
print("\nMining started...")
7777
block = self.blockchain.mine_block()
7878
if block != None:
79-
print("Mining done! Proof={}, Balance={:.2f}".format(block['proof'], self.blockchain.get_balance()))
79+
print("Mining done! Proof={}, Balance={:.2f}".format(block.proof, self.blockchain.get_balance()))
8080
print("Added Block: ", block)
8181
else:
8282
print("Mining failed!")

0 commit comments

Comments
 (0)