diff --git a/main.rb b/main.rb index 023b57b..cd41538 100644 --- a/main.rb +++ b/main.rb @@ -28,12 +28,15 @@ block_data = parse_json_body chain_id = params[:id] blockchain = find_block_chain(chain_id) - block = blockchain.add_block(block_data['data']) + difficulty = validate_difficulty(block_data['difficulty']) + block = blockchain.add_block(block_data['data'], difficulty: difficulty) { chain_id: chain_id, block_id: block.id.to_s, - block_hash: block._hash + block_hash: block._hash, + nonce: block.nonce, + difficulty: block.difficulty }.to_json end @@ -54,6 +57,29 @@ }.to_json end +get '/chain/:id/block/:block_id' do + chain_id = params[:id] + block_id = params[:block_id] + blockchain = find_block_chain(chain_id) + block = blockchain.blocks.find(block_id) + raise 'Block not found' unless block + + { + chain_id: chain_id, + block: { + id: block.id.to_s, + index: block.index, + data: block.data, + hash: block._hash, + previous_hash: block.previous_hash, + nonce: block.nonce, + difficulty: block.difficulty, + timestamp: block.created_at.to_i, + valid_hash: block.valid_hash? + } + }.to_json +end + helpers do def parse_json_body JSON.parse(request.body.read) @@ -65,4 +91,11 @@ def find_block_chain(chain_id) blockchain end + + def validate_difficulty(difficulty) + difficulty = difficulty.nil? ? 2 : difficulty.to_i + halt 422, { error: 'Difficulty must be a positive integer' }.to_json if difficulty <= 0 + halt 422, { error: 'Difficulty must be between 1 and 10' }.to_json if difficulty > 10 + difficulty + end end diff --git a/src/block.rb b/src/block.rb index 8d1483c..7ce8fcd 100644 --- a/src/block.rb +++ b/src/block.rb @@ -18,10 +18,16 @@ class Block # @return [Object] The information that is stored inside the `Block`. # @!attribute [r] previous_hash # @return [String] The `hash` of the previous `Block` in the `Blockchain`. + # @!attribute [r] nonce + # @return [Integer] The number used once for Proof of Work mining. + # @!attribute [r] difficulty + # @return [Integer] The number of leading zeros required in the hash. field :index, type: Integer field :data, type: String field :previous_hash, type: String field :_hash, type: String, as: :hash + field :nonce, type: Integer, default: 0 + field :difficulty, type: Integer, default: 2 belongs_to :blockchain @@ -30,18 +36,43 @@ class Block # Calculates the SHA256 hash of the block. # - # The `hash` is generated from the `Block`'s `index`, `timestamp`, transaction `data` - # and the `hash` of the previous block. + # The `hash` is generated from the `Block`'s `index`, `timestamp`, transaction `data`, + # the `hash` of the previous block, and the `nonce`. # # @return [String] The `hash` of the block def calculate_hash set_created_at - self._hash = Digest::SHA256.hexdigest("#{index}#{created_at.to_i}#{data}#{previous_hash}") + self._hash = Digest::SHA256.hexdigest("#{index}#{created_at.to_i}#{data}#{previous_hash}#{nonce}") end # Validates the integrity of the `Block`'s data. # @return [Boolean] `true` if the `Block`'s data is valid, `false` otherwise. def valid_data?(data) - Digest::SHA256.hexdigest("#{index}#{created_at.to_i}#{data}#{previous_hash}") == _hash + Digest::SHA256.hexdigest("#{index}#{created_at.to_i}#{data}#{previous_hash}#{nonce}") == _hash + end + + # Mines the block using Proof of Work algorithm. + # + # Increments the nonce until a hash is found that starts with the required number + # of leading zeros specified by the difficulty level. + # + # @return [String] The mined hash with required difficulty + def mine_block + target = '0' * difficulty + loop do + calculate_hash + break if _hash.start_with?(target) + + self.nonce += 1 + end + _hash + end + + # Validates that the block's hash meets the difficulty requirement. + # + # @return [Boolean] `true` if the hash has the required leading zeros, `false` otherwise. + def valid_hash? + target = '0' * difficulty + _hash.start_with?(target) end end diff --git a/src/blockchain.rb b/src/blockchain.rb index dc33bc5..0e128e6 100644 --- a/src/blockchain.rb +++ b/src/blockchain.rb @@ -17,10 +17,20 @@ class Blockchain # Add a new Block to this Blockchain # # @param data [Object] the data that needs to be added to the new Block - def add_block(data) + # @param difficulty [Integer] the mining difficulty (number of leading zeros) + # @return [Block] the newly created and mined Block + def add_block(data, difficulty: 2) integrity_valid? or raise 'Blockchain is not valid' last_block = blocks.last - blocks.create(index: last_block.index + 1, data:, previous_hash: last_block._hash) + block = blocks.build( + index: last_block.index + 1, + data:, + previous_hash: last_block._hash, + difficulty: + ) + block.mine_block + block.save! + block end # Get the last block of this Blockchain @@ -37,7 +47,8 @@ def last_block def integrity_valid? blocks.each_cons(2).all? do |previous_block, current_block| previous_block._hash == current_block.previous_hash && - current_block._hash == current_block.calculate_hash + current_block._hash == current_block.calculate_hash && + current_block.valid_hash? end end