-
-
Notifications
You must be signed in to change notification settings - Fork 17
Reject transactions with negative fees #109
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
Closed
Jiyacodex
wants to merge
2
commits into
StabilityNexus:main
from
Jiyacodex:fix-negative-fee-validation
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -332,3 +332,4 @@ __pycache__/ | |
| *.so | ||
| *bore.zip | ||
| *bore_bin | ||
| .venv/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import unittest | ||
| from nacl.signing import SigningKey | ||
| from nacl.encoding import HexEncoder | ||
|
|
||
| from minichain import State, Transaction | ||
|
|
||
|
|
||
| class TestNegativeFeePrevention(unittest.TestCase): | ||
| def setUp(self): | ||
| self.state = State() | ||
| self.state.chain_id = "minichain-default" | ||
|
|
||
| # Setup Alice with a small balance | ||
| self.alice_sk = SigningKey.generate() | ||
| self.alice_pk = self.alice_sk.verify_key.encode(encoder=HexEncoder).decode() | ||
| self.state.credit_mining_reward(self.alice_pk, 10) | ||
|
|
||
| self.bob_pk = "b" * 64 | ||
|
|
||
| def _make_tx(self, amount, fee, nonce=0): | ||
| tx = Transaction( | ||
| sender=self.alice_pk, | ||
| receiver=self.bob_pk, | ||
| amount=amount, | ||
| nonce=nonce, | ||
| fee=fee, | ||
| chain_id="minichain-default", | ||
| ) | ||
| tx.sign(self.alice_sk) | ||
| return tx | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Core exploit scenario | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def test_negative_fee_is_rejected(self): | ||
| """Negative fee MUST be rejected; balance must not be inflated.""" | ||
| initial_balance = self.state.get_account(self.alice_pk)["balance"] | ||
|
|
||
| tx = self._make_tx(amount=0, fee=-1000) | ||
| receipt = self.state.validate_and_apply(tx) | ||
|
|
||
| self.assertIsNone(receipt, "validate_and_apply should return None for negative fee") | ||
| self.assertEqual( | ||
| self.state.get_account(self.alice_pk)["balance"], | ||
| initial_balance, | ||
| "Alice's balance must remain unchanged after a rejected negative-fee tx", | ||
| ) | ||
|
|
||
| def test_negative_fee_does_not_change_nonce(self): | ||
| """Rejected tx must not increment the sender nonce.""" | ||
| initial_nonce = self.state.get_account(self.alice_pk)["nonce"] | ||
| tx = self._make_tx(amount=0, fee=-1) | ||
| self.state.validate_and_apply(tx) | ||
| self.assertEqual( | ||
| self.state.get_account(self.alice_pk)["nonce"], | ||
| initial_nonce, | ||
| "Nonce must not be incremented for rejected transactions", | ||
| ) | ||
|
|
||
| def test_large_negative_fee_is_rejected(self): | ||
| """Even a very large negative fee (millions of coins) must be rejected.""" | ||
| tx = self._make_tx(amount=0, fee=-10_000_000) | ||
| receipt = self.state.validate_and_apply(tx) | ||
| self.assertIsNone(receipt) | ||
| self.assertEqual(self.state.get_account(self.alice_pk)["balance"], 10) | ||
|
|
||
| def test_fee_of_zero_is_accepted(self): | ||
| """Fee of exactly 0 is valid and should be accepted.""" | ||
| tx = self._make_tx(amount=5, fee=0) | ||
| receipt = self.state.validate_and_apply(tx) | ||
| self.assertIsNotNone(receipt, "Fee of 0 should be accepted") | ||
| self.assertEqual(receipt.status, 1) | ||
|
|
||
| def test_positive_fee_is_accepted(self): | ||
| """A normal positive fee should be accepted and correctly deducted.""" | ||
| tx = self._make_tx(amount=3, fee=2) | ||
| receipt = self.state.validate_and_apply(tx) | ||
| self.assertIsNotNone(receipt, "Positive fee should be accepted") | ||
| self.assertEqual(receipt.status, 1) | ||
| self.assertEqual(self.state.get_account(self.alice_pk)["balance"], 5) | ||
|
|
||
| def test_float_fee_is_rejected(self): | ||
| """Float fees (e.g. -0.5, 1.5) must be rejected as non-integer.""" | ||
| for bad_fee in [-0.5, 1.5, 0.1]: | ||
| with self.subTest(fee=bad_fee): | ||
| tx = Transaction( | ||
| sender=self.alice_pk, | ||
| receiver=self.bob_pk, | ||
| amount=0, | ||
| nonce=0, | ||
| fee=bad_fee, | ||
| chain_id="minichain-default", | ||
| ) | ||
| tx.sign(self.alice_sk) | ||
| receipt = self.state.validate_and_apply(tx) | ||
| self.assertIsNone(receipt, f"Float fee {bad_fee} should be rejected") | ||
|
|
||
| def test_negative_nonce_is_rejected(self): | ||
| """A negative nonce must also be rejected.""" | ||
| tx = Transaction( | ||
| sender=self.alice_pk, | ||
| receiver=self.bob_pk, | ||
| amount=0, | ||
| nonce=-1, | ||
| fee=0, | ||
| chain_id="minichain-default", | ||
| ) | ||
| tx.sign(self.alice_sk) | ||
| receipt = self.state.validate_and_apply(tx) | ||
| self.assertIsNone(receipt, "Negative nonce should be rejected") | ||
|
|
||
| def test_bool_fee_is_rejected(self): | ||
| """bool is a subclass of int in Python; True/False must not pass as fee.""" | ||
| for bad_fee in [True, False]: | ||
| with self.subTest(fee=bad_fee): | ||
| tx = self._make_tx(amount=0, fee=bad_fee) | ||
| receipt = self.state.validate_and_apply(tx) | ||
| self.assertIsNone(receipt, f"bool fee {bad_fee} should be rejected") | ||
|
|
||
| def test_apply_transaction_directly_rejects_negative_fee(self): | ||
| """Even when validate_and_apply is bypassed, apply_transaction's | ||
| underlying verify_transaction_logic must reject negative fees.""" | ||
| initial_balance = self.state.get_account(self.alice_pk)["balance"] | ||
| tx = self._make_tx(amount=0, fee=-1000) | ||
| receipt = self.state.apply_transaction(tx) | ||
| self.assertIsNone(receipt, "apply_transaction must reject negative fee directly") | ||
| self.assertEqual( | ||
| self.state.get_account(self.alice_pk)["balance"], | ||
| initial_balance, | ||
| "Balance must not inflate when apply_transaction is called directly", | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.