-
-
Notifications
You must be signed in to change notification settings - Fork 18
fix: validate proof of work on block acceptance #115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
799550d
24636cf
9423173
add5a77
1fed17c
fda0c04
472fff1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,7 +11,15 @@ | |||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def validate_block_link_and_hash(previous_block, block): | ||||||||||||||||||||||
| class InvalidProofOfWorkError(ValueError): | ||||||||||||||||||||||
| pass | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def validate_difficulty(difficulty, max_difficulty): | ||||||||||||||||||||||
| if type(difficulty) is not int or difficulty < 1 or difficulty > max_difficulty: | ||||||||||||||||||||||
| raise ValueError(f"invalid difficulty {difficulty}") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def validate_block(previous_block, block, expected_difficulty): | ||||||||||||||||||||||
| if block.previous_hash != previous_block.hash: | ||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||
| f"invalid previous hash {block.previous_hash} != {previous_block.hash}" | ||||||||||||||||||||||
|
|
@@ -26,17 +34,26 @@ def validate_block_link_and_hash(previous_block, block): | |||||||||||||||||||||
| if block.hash != expected_hash: | ||||||||||||||||||||||
| raise ValueError(f"invalid hash {block.hash}") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| target = "0" * (block.difficulty or 1) | ||||||||||||||||||||||
| if not block.hash.startswith(target): | ||||||||||||||||||||||
| raise ValueError(f"invalid Proof of Work: hash {block.hash} does not satisfy difficulty {block.difficulty}") | ||||||||||||||||||||||
| if block.difficulty != expected_difficulty: | ||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||
| f"invalid difficulty {block.difficulty} != expected {expected_difficulty}" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In addition to the check in line 39, we must also check that
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Otherwise, the miner can circumvent the difficulty check, by simply writing a small difficulty in
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Zahnentferner you're right. When I reviewed the attack was blocked by the separate difficulty checks in add_block and resolve_conflicts in the initial commit, but the architecture was flawed which I didn't notice since the checks already protected. |
||||||||||||||||||||||
| if not block.hash.startswith("0" * expected_difficulty): | ||||||||||||||||||||||
| raise InvalidProofOfWorkError( | ||||||||||||||||||||||
| f"invalid PoW: hash {block.hash} does not satisfy difficulty {expected_difficulty}" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if block.timestamp <= previous_block.timestamp: | ||||||||||||||||||||||
| raise ValueError(f"invalid timestamp: {block.timestamp} is not strictly greater than previous block timestamp {previous_block.timestamp}") | ||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||
| f"invalid timestamp: {block.timestamp} is not strictly greater than previous block timestamp {previous_block.timestamp}" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| max_allowed_time = int(time.time() * 1000) + 15000 | ||||||||||||||||||||||
| if block.timestamp > max_allowed_time: | ||||||||||||||||||||||
| raise ValueError(f"invalid timestamp: {block.timestamp} is too far in the future (max allowed: {max_allowed_time})") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||
| f"invalid timestamp: {block.timestamp} is too far in the future (max allowed: {max_allowed_time})" | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class Blockchain: | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
|
|
@@ -130,7 +147,13 @@ def get_total_work(self, chain_list=None): | |||||||||||||||||||||
| if chain_list is None: | ||||||||||||||||||||||
| with self._lock: | ||||||||||||||||||||||
| chain_list = self.chain | ||||||||||||||||||||||
| return sum(2 ** (block.difficulty or 1) for block in chain_list) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| for block in chain_list: | ||||||||||||||||||||||
| if not isinstance(block.hash, str): | ||||||||||||||||||||||
| raise ValueError(f"invalid or missing hash on block {block.index}") | ||||||||||||||||||||||
| validate_difficulty(block.difficulty, len(block.hash)) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return sum(2 ** block.difficulty for block in chain_list) | ||||||||||||||||||||||
|
Comment on lines
+151
to
+156
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Reject malformed hashes before measuring total work.
Proposed fix for block in chain_list:
- validate_difficulty(block.difficulty, len(block.hash))
+ if not isinstance(block.hash, str) or len(block.hash) != 64:
+ raise ValueError("invalid block hash")
+ validate_difficulty(block.difficulty, 64)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def _next_difficulty(self, difficulty, avg_block_time): | ||||||||||||||||||||||
| """Advance the EMA difficulty control after a block, returning the new value.""" | ||||||||||||||||||||||
|
|
@@ -150,16 +173,15 @@ def _apply_block(self, prev_block, block, state, difficulty, avg_block_time): | |||||||||||||||||||||
| from .validators import ValidationStatus | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try: | ||||||||||||||||||||||
| validate_block_link_and_hash(prev_block, block) | ||||||||||||||||||||||
| validate_block(prev_block, block, difficulty) | ||||||||||||||||||||||
| except InvalidProofOfWorkError as exc: | ||||||||||||||||||||||
| logger.warning("Block %s rejected: %s", block.index, exc) | ||||||||||||||||||||||
| return ValidationStatus.INVALID, difficulty, avg_block_time | ||||||||||||||||||||||
| except ValueError as exc: | ||||||||||||||||||||||
| logger.warning("Block %s rejected: %s", block.index, exc) | ||||||||||||||||||||||
| status = ValidationStatus.INVALID if "hash" in str(exc) else ValidationStatus.FAILED | ||||||||||||||||||||||
| return status, difficulty, avg_block_time | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if block.difficulty != difficulty: | ||||||||||||||||||||||
| logger.warning("Block %s rejected: Invalid difficulty. Expected %s, got %s", block.index, difficulty, block.difficulty) | ||||||||||||||||||||||
| return ValidationStatus.INVALID, difficulty, avg_block_time | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| receipts = [] | ||||||||||||||||||||||
| for tx in block.transactions: | ||||||||||||||||||||||
| status, receipt = state.validate_and_apply_with_status(tx) | ||||||||||||||||||||||
|
|
@@ -224,9 +246,12 @@ def resolve_conflicts(self, new_chain_list) -> tuple[bool, list]: | |||||||||||||||||||||
| return False, [] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| with self._lock: | ||||||||||||||||||||||
| current_work = self.get_total_work() | ||||||||||||||||||||||
| new_work = self.get_total_work(new_chain_list) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try: | ||||||||||||||||||||||
| current_work = self.get_total_work() | ||||||||||||||||||||||
| new_work = self.get_total_work(new_chain_list) | ||||||||||||||||||||||
| except ValueError as exc: | ||||||||||||||||||||||
| logger.warning("Reorg rejected: %s", exc) | ||||||||||||||||||||||
| return False, [] | ||||||||||||||||||||||
| if new_work <= current_work: | ||||||||||||||||||||||
| logger.debug("Incoming chain (work: %s) is not heavier than local chain (work: %s). Rejecting.", new_work, current_work) | ||||||||||||||||||||||
| return False, [] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need this validation function? Don't we know that we will always call this function with arguments that satisfy the conditions in line 18?
Also, some conditions in line 18 seem redundant anyway. For example, if difficulty is greater than len(block.hash), line 39 will fail anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I'm overlooking something important, let's remove this validation function.