From ad3d94c3068c4aef420b903ebe7964c3d54cbcad Mon Sep 17 00:00:00 2001 From: Pedro Perafan Date: Sun, 9 Nov 2025 00:34:02 -0600 Subject: [PATCH 1/2] feat: Add Proof of Work implementation for v2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add nonce and difficulty fields to Block model - Implement mine_block method with PoW algorithm - Update calculate_hash to include nonce - Add valid_hash? method to verify difficulty requirement - Update Blockchain.add_block to mine blocks - Update integrity_valid? to check PoW validity - Add GET endpoint for block details with mining info - Add optional difficulty parameter to block creation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Gemfile.lock | 1 + main.rb | 32 +++++++++++++++++++++++++++++--- src/block.rb | 39 +++++++++++++++++++++++++++++++++++---- src/blockchain.rb | 17 ++++++++++++++--- 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f00e547..2bb64a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,6 +60,7 @@ GEM zeitwerk (2.6.12) PLATFORMS + arm64-darwin-25 x86_64-linux DEPENDENCIES diff --git a/main.rb b/main.rb index fd0c317..6d0b145 100644 --- a/main.rb +++ b/main.rb @@ -20,12 +20,15 @@ block_data = parse_json_body chain_id = params[:id] blockchain = find_block_chain(chain_id) - block = blockchain.add_block(block_data) + difficulty = block_data['difficulty'] || 2 + 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 @@ -37,7 +40,7 @@ block = blockchain.blocks.find(block_id) raise 'Block not found' unless block - valid = block.valid_data?(block_id, block_data) + valid = block.valid_data?(block_data['data']) { chain_id: chain_id, @@ -46,6 +49,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) 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 From 033586111622c2180850caeb0b01684bfc1812c8 Mon Sep 17 00:00:00 2001 From: Pedro Perafan Date: Sun, 9 Nov 2025 14:52:08 -0600 Subject: [PATCH 2/2] fix: Add difficulty validation to prevent invalid inputs - Add validate_difficulty helper method - Convert difficulty to integer and default to 2 if nil - Reject negative values with 422 status - Reject values > 10 to prevent excessive mining time - Prevents TypeError from string/float inputs Addresses CodeRabbit security review comment --- main.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/main.rb b/main.rb index bece6dd..cd41538 100644 --- a/main.rb +++ b/main.rb @@ -28,7 +28,7 @@ block_data = parse_json_body chain_id = params[:id] blockchain = find_block_chain(chain_id) - difficulty = block_data['difficulty'] || 2 + difficulty = validate_difficulty(block_data['difficulty']) block = blockchain.add_block(block_data['data'], difficulty: difficulty) { @@ -91,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