From 6faa9de9943af699bb8b77ba19ab09fb36a0121b Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 15:05:14 +0530 Subject: [PATCH 01/16] optimize: implement lazy hashing cache for Transaction IDs --- minichain/transaction.py | 19 ++++++++++------- tests/test_cache.py | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 tests/test_cache.py diff --git a/minichain/transaction.py b/minichain/transaction.py index 27f41f3..38aeb24 100644 --- a/minichain/transaction.py +++ b/minichain/transaction.py @@ -19,6 +19,9 @@ def __init__(self, sender, receiver, amount, nonce, data=None, signature=None, t else: self.timestamp = round(timestamp * 1000) # Seconds → ms self.signature = signature # Hex str + + # 1. Initialize the cache placeholder + self._cached_tx_id = None def to_dict(self): return { @@ -60,8 +63,11 @@ def hash_payload(self): @property def tx_id(self): - """Deterministic identifier for the signed transaction.""" - return canonical_json_hash(self.to_dict()) + """Deterministic identifier for the signed transaction, cached for performance.""" + # 2. Check the cache before re-calculating + if self._cached_tx_id is None: + self._cached_tx_id = canonical_json_hash(self.to_dict()) + return self._cached_tx_id def sign(self, signing_key: SigningKey): # Validate that the signing key matches the sender @@ -69,6 +75,9 @@ def sign(self, signing_key: SigningKey): raise ValueError("Signing key does not match sender") signed = signing_key.sign(self.hash_payload) self.signature = signed.signature.hex() + + # 3. Invalidate the cache because the signature (and thus the tx_id) changed + self._cached_tx_id = None def verify(self): if not self.signature: @@ -80,8 +89,4 @@ def verify(self): return True except (BadSignatureError, CryptoError, ValueError, TypeError): - # Covers: - # - Invalid signature - # - Malformed public key hex - # - Invalid hex in signature - return False + return False \ No newline at end of file diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..c84413a --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,44 @@ +from minichain.transaction import Transaction +from nacl.signing import SigningKey +from nacl.encoding import HexEncoder + +def test_tx_caching(): + # 1. Setup a dummy transaction + sk = SigningKey.generate() + sender_hex = sk.verify_key.encode(encoder=HexEncoder).decode() + + tx = Transaction( + sender=sender_hex, + receiver="receiver_addr", + amount=100, + nonce=1 + ) + + print(f"--- Initial State ---") + print(f"Cache value: {tx._cached_tx_id}") # Should be None + + # 2. First access (triggers calculation) + first_id = tx.tx_id + print(f"\n--- After First Access ---") + print(f"Calculated ID: {first_id}") + print(f"Cache value: {tx._cached_tx_id}") # Should now be the hash + + # 3. Second access (should be instant) + second_id = tx.tx_id + print(f"\n--- After Second Access ---") + print(f"Is it the same ID? {first_id == second_id}") + + # 4. Signing (should clear cache) + print(f"\n--- Signing Transaction ---") + tx.sign(sk) + print(f"Cache value after sign: {tx._cached_tx_id}") # Should be None again + + # 5. Access after signing (re-calculates with signature) + signed_id = tx.tx_id + print(f"\n--- After Accessing Signed TX ---") + print(f"New Signed ID: {signed_id}") + print(f"Cache value: {tx._cached_tx_id}") + print(f"Did ID change? {signed_id != first_id}") + +if __name__ == "__main__": + test_tx_caching() \ No newline at end of file From 8912e1600e42d83b10156d1c3104551d7003a052 Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 15:11:42 +0530 Subject: [PATCH 02/16] test: refactor diagnostic prints to assertions and fix linting --- tests/test_cache.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index c84413a..4c48e75 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -14,31 +14,23 @@ def test_tx_caching(): nonce=1 ) - print(f"--- Initial State ---") - print(f"Cache value: {tx._cached_tx_id}") # Should be None + # 2. Assert Initial State (The "None" check you were worried about) + assert tx._cached_tx_id is None - # 2. First access (triggers calculation) + # 3. First access (triggers calculation) first_id = tx.tx_id - print(f"\n--- After First Access ---") - print(f"Calculated ID: {first_id}") - print(f"Cache value: {tx._cached_tx_id}") # Should now be the hash + assert tx._cached_tx_id == first_id + assert tx._cached_tx_id is not None - # 3. Second access (should be instant) + # 4. Second access (should be an instant lookup) second_id = tx.tx_id - print(f"\n--- After Second Access ---") - print(f"Is it the same ID? {first_id == second_id}") - - # 4. Signing (should clear cache) - print(f"\n--- Signing Transaction ---") + assert second_id == first_id + + # 5. Signing (must invalidate/clear the cache) tx.sign(sk) - print(f"Cache value after sign: {tx._cached_tx_id}") # Should be None again + assert tx._cached_tx_id is None - # 5. Access after signing (re-calculates with signature) + # 6. Access after signing (must re-calculate) signed_id = tx.tx_id - print(f"\n--- After Accessing Signed TX ---") - print(f"New Signed ID: {signed_id}") - print(f"Cache value: {tx._cached_tx_id}") - print(f"Did ID change? {signed_id != first_id}") - -if __name__ == "__main__": - test_tx_caching() \ No newline at end of file + assert tx._cached_tx_id == signed_id + assert signed_id != first_id \ No newline at end of file From 43507e7bb651def4be703207336ed28591cafc48 Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 15:22:45 +0530 Subject: [PATCH 03/16] refactor: implement guarded __setattr__ for robust cache invalidation --- minichain/transaction.py | 36 ++++++++++++++++++---------------- tests/test_cache.py | 42 ++++++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/minichain/transaction.py b/minichain/transaction.py index 38aeb24..fb38998 100644 --- a/minichain/transaction.py +++ b/minichain/transaction.py @@ -6,23 +6,30 @@ class Transaction: + # 1. List the fields that, if changed, should break the cache + _TX_FIELDS = {"sender", "receiver", "amount", "nonce", "data", "timestamp", "signature"} + def __init__(self, sender, receiver, amount, nonce, data=None, signature=None, timestamp=None): - self.sender = sender # Public key (Hex str) - self.receiver = receiver # Public key (Hex str) or None for Deploy + # We set these first + self.sender = sender + self.receiver = receiver self.amount = amount self.nonce = nonce - self.data = data # Preserve None (do NOT normalize to "") - if timestamp is None: - self.timestamp = round(time.time() * 1000) # New tx: seconds → ms - elif timestamp > 1e12: - self.timestamp = int(timestamp) # Already in ms (from network) - else: - self.timestamp = round(timestamp * 1000) # Seconds → ms - self.signature = signature # Hex str + self.data = data + self.timestamp = timestamp if timestamp else round(time.time() * 1000) + self.signature = signature - # 1. Initialize the cache placeholder + # Initialize cache last self._cached_tx_id = None + # 2. The "Watcher" function + def __setattr__(self, name, value): + # Perform the actual assignment + super().__setattr__(name, value) + # If a core field was changed, and the cache exists, kill the cache + if name in self._TX_FIELDS and hasattr(self, "_cached_tx_id"): + super().__setattr__("_cached_tx_id", None) + def to_dict(self): return { "sender": self.sender, @@ -63,21 +70,16 @@ def hash_payload(self): @property def tx_id(self): - """Deterministic identifier for the signed transaction, cached for performance.""" - # 2. Check the cache before re-calculating if self._cached_tx_id is None: self._cached_tx_id = canonical_json_hash(self.to_dict()) return self._cached_tx_id def sign(self, signing_key: SigningKey): - # Validate that the signing key matches the sender if signing_key.verify_key.encode(encoder=HexEncoder).decode() != self.sender: raise ValueError("Signing key does not match sender") signed = signing_key.sign(self.hash_payload) + # Setting this now automatically clears the cache because of __setattr__! self.signature = signed.signature.hex() - - # 3. Invalidate the cache because the signature (and thus the tx_id) changed - self._cached_tx_id = None def verify(self): if not self.signature: diff --git a/tests/test_cache.py b/tests/test_cache.py index 4c48e75..17413d3 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -3,34 +3,42 @@ from nacl.encoding import HexEncoder def test_tx_caching(): - # 1. Setup a dummy transaction + """Verifies standard lifecycle caching: None -> Filled -> Cleared by Sign.""" sk = SigningKey.generate() sender_hex = sk.verify_key.encode(encoder=HexEncoder).decode() - tx = Transaction( - sender=sender_hex, - receiver="receiver_addr", - amount=100, - nonce=1 - ) + tx = Transaction(sender=sender_hex, receiver="addr", amount=100, nonce=1) - # 2. Assert Initial State (The "None" check you were worried about) + # 1. Initial State assert tx._cached_tx_id is None - # 3. First access (triggers calculation) + # 2. First access (triggers calculation) first_id = tx.tx_id assert tx._cached_tx_id == first_id - assert tx._cached_tx_id is not None - - # 4. Second access (should be an instant lookup) - second_id = tx.tx_id - assert second_id == first_id - # 5. Signing (must invalidate/clear the cache) + # 3. Signing (must clear cache automatically via __setattr__) tx.sign(sk) assert tx._cached_tx_id is None - # 6. Access after signing (must re-calculate) + # 4. Re-calculate after sign signed_id = tx.tx_id + assert signed_id != first_id assert tx._cached_tx_id == signed_id - assert signed_id != first_id \ No newline at end of file + +def test_tx_mutation_clears_cache(): + """Verifies that direct field updates also clear the cache (Bulletproof check).""" + tx = Transaction(sender="alice", receiver="bob", amount=100, nonce=1) + + # 1. Fill the cache + original_id = tx.tx_id + assert tx._cached_tx_id is not None + + # 2. Mutate a field directly (e.g., changing the amount) + tx.amount = 500 + + # 3. ASSERT: Cache must be None immediately after mutation + assert tx._cached_tx_id is None + + # 4. ASSERT: New ID must be different from the old one + new_id = tx.tx_id + assert new_id != original_id \ No newline at end of file From 7c7d51cf37472db9227e96b5d1926ffe40df795b Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 15:36:53 +0530 Subject: [PATCH 04/16] refactor: use guarded __setattr__ for automated cache invalidation --- minichain/transaction.py | 63 +++++++++++----------------------------- tests/test_cache.py | 23 ++++----------- 2 files changed, 22 insertions(+), 64 deletions(-) diff --git a/minichain/transaction.py b/minichain/transaction.py index fb38998..2edd757 100644 --- a/minichain/transaction.py +++ b/minichain/transaction.py @@ -6,11 +6,14 @@ class Transaction: - # 1. List the fields that, if changed, should break the cache _TX_FIELDS = {"sender", "receiver", "amount", "nonce", "data", "timestamp", "signature"} + def __setattr__(self, name, value): + super().__setattr__(name, value) + if name in self._TX_FIELDS and hasattr(self, "_cached_tx_id"): + super().__setattr__("_cached_tx_id", None) + def __init__(self, sender, receiver, amount, nonce, data=None, signature=None, timestamp=None): - # We set these first self.sender = sender self.receiver = receiver self.amount = amount @@ -18,54 +21,26 @@ def __init__(self, sender, receiver, amount, nonce, data=None, signature=None, t self.data = data self.timestamp = timestamp if timestamp else round(time.time() * 1000) self.signature = signature - - # Initialize cache last self._cached_tx_id = None - # 2. The "Watcher" function - def __setattr__(self, name, value): - # Perform the actual assignment - super().__setattr__(name, value) - # If a core field was changed, and the cache exists, kill the cache - if name in self._TX_FIELDS and hasattr(self, "_cached_tx_id"): - super().__setattr__("_cached_tx_id", None) - def to_dict(self): - return { - "sender": self.sender, - "receiver": self.receiver, - "amount": self.amount, - "nonce": self.nonce, - "data": self.data, - "timestamp": self.timestamp, - "signature": self.signature, - } + return {"sender": self.sender, "receiver": self.receiver, "amount": self.amount, + "nonce": self.nonce, "data": self.data, "timestamp": self.timestamp, + "signature": self.signature} def to_signing_dict(self): - return { - "sender": self.sender, - "receiver": self.receiver, - "amount": self.amount, - "nonce": self.nonce, - "data": self.data, - "timestamp": self.timestamp, - } + return {"sender": self.sender, "receiver": self.receiver, "amount": self.amount, + "nonce": self.nonce, "data": self.data, "timestamp": self.timestamp} @classmethod def from_dict(cls, payload: dict): - return cls( - sender=payload["sender"], - receiver=payload.get("receiver"), - amount=payload["amount"], - nonce=payload["nonce"], - data=payload.get("data"), - signature=payload.get("signature"), - timestamp=payload.get("timestamp"), - ) + return cls(sender=payload["sender"], receiver=payload.get("receiver"), + amount=payload["amount"], nonce=payload["nonce"], + data=payload.get("data"), signature=payload.get("signature"), + timestamp=payload.get("timestamp")) @property def hash_payload(self): - """Returns the bytes to be signed.""" return canonical_json_bytes(self.to_signing_dict()) @property @@ -77,18 +52,14 @@ def tx_id(self): def sign(self, signing_key: SigningKey): if signing_key.verify_key.encode(encoder=HexEncoder).decode() != self.sender: raise ValueError("Signing key does not match sender") - signed = signing_key.sign(self.hash_payload) - # Setting this now automatically clears the cache because of __setattr__! - self.signature = signed.signature.hex() + self.signature = signing_key.sign(self.hash_payload).signature.hex() def verify(self): if not self.signature: return False - try: - verify_key = VerifyKey(self.sender, encoder=HexEncoder) - verify_key.verify(self.hash_payload, bytes.fromhex(self.signature)) + VerifyKey(self.sender, encoder=HexEncoder).verify( + self.hash_payload, bytes.fromhex(self.signature)) return True - except (BadSignatureError, CryptoError, ValueError, TypeError): return False \ No newline at end of file diff --git a/tests/test_cache.py b/tests/test_cache.py index 17413d3..fcec4e4 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -2,43 +2,30 @@ from nacl.signing import SigningKey from nacl.encoding import HexEncoder + def test_tx_caching(): - """Verifies standard lifecycle caching: None -> Filled -> Cleared by Sign.""" sk = SigningKey.generate() sender_hex = sk.verify_key.encode(encoder=HexEncoder).decode() - tx = Transaction(sender=sender_hex, receiver="addr", amount=100, nonce=1) - # 1. Initial State assert tx._cached_tx_id is None - - # 2. First access (triggers calculation) first_id = tx.tx_id assert tx._cached_tx_id == first_id + assert tx.tx_id == first_id # second access, same result - # 3. Signing (must clear cache automatically via __setattr__) tx.sign(sk) assert tx._cached_tx_id is None - # 4. Re-calculate after sign signed_id = tx.tx_id assert signed_id != first_id assert tx._cached_tx_id == signed_id + def test_tx_mutation_clears_cache(): - """Verifies that direct field updates also clear the cache (Bulletproof check).""" tx = Transaction(sender="alice", receiver="bob", amount=100, nonce=1) - - # 1. Fill the cache original_id = tx.tx_id assert tx._cached_tx_id is not None - - # 2. Mutate a field directly (e.g., changing the amount) + tx.amount = 500 - - # 3. ASSERT: Cache must be None immediately after mutation assert tx._cached_tx_id is None - - # 4. ASSERT: New ID must be different from the old one - new_id = tx.tx_id - assert new_id != original_id \ No newline at end of file + assert tx.tx_id != original_id \ No newline at end of file From 07f11c68e5fb0082347f36de180d3bbd94739250 Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 15:44:44 +0530 Subject: [PATCH 05/16] fix: frozenset for _TX_FIELDS and is not None timestamp check --- minichain/transaction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minichain/transaction.py b/minichain/transaction.py index 2edd757..d66928b 100644 --- a/minichain/transaction.py +++ b/minichain/transaction.py @@ -6,7 +6,7 @@ class Transaction: - _TX_FIELDS = {"sender", "receiver", "amount", "nonce", "data", "timestamp", "signature"} + _TX_FIELDS = frozenset({"sender", "receiver", "amount", "nonce", "data", "timestamp", "signature"}) def __setattr__(self, name, value): super().__setattr__(name, value) @@ -19,7 +19,7 @@ def __init__(self, sender, receiver, amount, nonce, data=None, signature=None, t self.amount = amount self.nonce = nonce self.data = data - self.timestamp = timestamp if timestamp else round(time.time() * 1000) + self.timestamp = timestamp if timestamp is not None else round(time.time() * 1000) self.signature = signature self._cached_tx_id = None From 76e691cc53efaa2b2908bd8ab7ee1cfd77da2b2b Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 16:08:33 +0530 Subject: [PATCH 06/16] chore: trigger CodeRabbit re-review From 655d49d1c24d5391bbd91bf69211f7e5a4e44890 Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 16:18:08 +0530 Subject: [PATCH 07/16] fix: use try/except/else in verify() for TRY300 --- minichain/transaction.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/minichain/transaction.py b/minichain/transaction.py index d66928b..e1405b2 100644 --- a/minichain/transaction.py +++ b/minichain/transaction.py @@ -60,6 +60,7 @@ def verify(self): try: VerifyKey(self.sender, encoder=HexEncoder).verify( self.hash_payload, bytes.fromhex(self.signature)) - return True except (BadSignatureError, CryptoError, ValueError, TypeError): - return False \ No newline at end of file + return False + else: + return True \ No newline at end of file From cf4a0bac91394a33fd8b1fa418a84d03f79d635e Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 16:35:04 +0530 Subject: [PATCH 08/16] chore: trigger CodeRabbit re-review From 7d3dc7fe1f8fd4f34539d4eefb17fa1c41963bde Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 16:48:47 +0530 Subject: [PATCH 09/16] fix: normalize timestamps and seal tx fields after signing --- minichain/transaction.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/minichain/transaction.py b/minichain/transaction.py index e1405b2..298056e 100644 --- a/minichain/transaction.py +++ b/minichain/transaction.py @@ -8,20 +8,28 @@ class Transaction: _TX_FIELDS = frozenset({"sender", "receiver", "amount", "nonce", "data", "timestamp", "signature"}) - def __setattr__(self, name, value): + def __setattr__(self, name, value) -> None: + if name in self._TX_FIELDS and getattr(self, "_sealed", False): + raise AttributeError(f"Transaction is sealed; cannot modify '{name}'") super().__setattr__(name, value) if name in self._TX_FIELDS and hasattr(self, "_cached_tx_id"): super().__setattr__("_cached_tx_id", None) + @staticmethod + def _normalize_ts(ts) -> int: + ts = int(ts) + return ts * 1000 if ts < 1e12 else ts + def __init__(self, sender, receiver, amount, nonce, data=None, signature=None, timestamp=None): self.sender = sender self.receiver = receiver self.amount = amount self.nonce = nonce self.data = data - self.timestamp = timestamp if timestamp is not None else round(time.time() * 1000) + self.timestamp = self._normalize_ts(timestamp) if timestamp is not None else round(time.time() * 1000) self.signature = signature self._cached_tx_id = None + self._sealed = False def to_dict(self): return {"sender": self.sender, "receiver": self.receiver, "amount": self.amount, @@ -53,6 +61,7 @@ def sign(self, signing_key: SigningKey): if signing_key.verify_key.encode(encoder=HexEncoder).decode() != self.sender: raise ValueError("Signing key does not match sender") self.signature = signing_key.sign(self.hash_payload).signature.hex() + self._sealed = True def verify(self): if not self.signature: From b18d6af0f974fd2865b5fc0d1a062e9114f37b89 Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 18:31:00 +0530 Subject: [PATCH 10/16] feat: add state sealing and robust cache invalidation --- tests/test_cache.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index fcec4e4..c1cf2d0 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,3 +1,4 @@ +import pytest from minichain.transaction import Transaction from nacl.signing import SigningKey from nacl.encoding import HexEncoder @@ -28,4 +29,20 @@ def test_tx_mutation_clears_cache(): tx.amount = 500 assert tx._cached_tx_id is None - assert tx.tx_id != original_id \ No newline at end of file + assert tx.tx_id != original_id + +def test_signed_tx_is_sealed(): + # 1. Generate a real key + sk = SigningKey.generate() + # 2. Get the actual hex address for that key + sender_hex = sk.verify_key.encode(encoder=HexEncoder).decode() + + # 3. Use that real address as the sender + tx = Transaction(sender=sender_hex, receiver="bob", amount=100, nonce=1) + + # 4. Now the signature will be accepted + tx.sign(sk) + + # 5. Assert that it is indeed sealed + with pytest.raises(AttributeError, match="Transaction is sealed"): + tx.amount = 500 \ No newline at end of file From 8988a3361e1c51d471587224d44a80c5b92fae99 Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 18:44:30 +0530 Subject: [PATCH 11/16] feat: add mock-based verification for hashing efficiency --- tests/test_cache.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index c1cf2d0..f7a39c3 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -32,17 +32,12 @@ def test_tx_mutation_clears_cache(): assert tx.tx_id != original_id def test_signed_tx_is_sealed(): - # 1. Generate a real key + """Verifies that a signed transaction cannot be modified.""" sk = SigningKey.generate() - # 2. Get the actual hex address for that key sender_hex = sk.verify_key.encode(encoder=HexEncoder).decode() - - # 3. Use that real address as the sender tx = Transaction(sender=sender_hex, receiver="bob", amount=100, nonce=1) - # 4. Now the signature will be accepted tx.sign(sk) - # 5. Assert that it is indeed sealed with pytest.raises(AttributeError, match="Transaction is sealed"): tx.amount = 500 \ No newline at end of file From 19fd40af161835001ea7ae2406f83ff482e8df6e Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 18:49:01 +0530 Subject: [PATCH 12/16] test: implement mock-based call counting to verify hashing efficiency --- tests/test_cache.py | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index f7a39c3..40c7a41 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,38 +1,43 @@ import pytest +from unittest.mock import patch from minichain.transaction import Transaction from nacl.signing import SigningKey from nacl.encoding import HexEncoder - -def test_tx_caching(): +def test_tx_caching_efficiency(): + """ + Verifies that the expensive hashing math is only performed once + and skipped on subsequent accesses (Memoization proof). + """ sk = SigningKey.generate() sender_hex = sk.verify_key.encode(encoder=HexEncoder).decode() tx = Transaction(sender=sender_hex, receiver="addr", amount=100, nonce=1) - assert tx._cached_tx_id is None - first_id = tx.tx_id - assert tx._cached_tx_id == first_id - assert tx.tx_id == first_id # second access, same result - - tx.sign(sk) - assert tx._cached_tx_id is None + # We 'patch' the hashing function to count how many times it's called + with patch('minichain.transaction.canonical_json_hash') as mock_hash: + # Give it a dummy return value so the code doesn't crash + mock_hash.return_value = "mocked_hash_value" - signed_id = tx.tx_id - assert signed_id != first_id - assert tx._cached_tx_id == signed_id + # 1. First Access: Should trigger the hash calculation + res1 = tx.tx_id + assert res1 == "mocked_hash_value" + assert mock_hash.call_count == 1 + # 2. Second Access: Should return the cached value (count remains 1) + res2 = tx.tx_id + assert res2 == "mocked_hash_value" + assert mock_hash.call_count == 1 # <--- THIS proves the cache worked! -def test_tx_mutation_clears_cache(): - tx = Transaction(sender="alice", receiver="bob", amount=100, nonce=1) - original_id = tx.tx_id - assert tx._cached_tx_id is not None + # 3. Invalidate via Mutation: Changing a field clears the cache + tx.amount = 200 + assert tx._cached_tx_id is None - tx.amount = 500 - assert tx._cached_tx_id is None - assert tx.tx_id != original_id + # 4. Third Access: Should trigger calculation again (count becomes 2) + _ = tx.tx_id + assert mock_hash.call_count == 2 def test_signed_tx_is_sealed(): - """Verifies that a signed transaction cannot be modified.""" + """Verifies that a signed transaction cannot be modified (Sealing check).""" sk = SigningKey.generate() sender_hex = sk.verify_key.encode(encoder=HexEncoder).decode() tx = Transaction(sender=sender_hex, receiver="bob", amount=100, nonce=1) From 9b20f494a2c95dba3b32b230b50bf1ad6f858b6a Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 18:57:01 +0530 Subject: [PATCH 13/16] test: implement mock-based call counting to verify hashing efficiency --- tests/test_cache.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index 40c7a41..4566cfa 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -37,12 +37,25 @@ def test_tx_caching_efficiency(): assert mock_hash.call_count == 2 def test_signed_tx_is_sealed(): - """Verifies that a signed transaction cannot be modified (Sealing check).""" + """Verifies that a signed transaction clears cache, changes ID, and cannot be modified.""" sk = SigningKey.generate() sender_hex = sk.verify_key.encode(encoder=HexEncoder).decode() tx = Transaction(sender=sender_hex, receiver="bob", amount=100, nonce=1) + # 1. Grab the ID before signing + unsigned_id = tx.tx_id + assert tx._cached_tx_id == unsigned_id + + # 2. Sign it tx.sign(sk) + # 3. Prove signing killed the old cache + assert tx._cached_tx_id is None + + # 4. Prove the new ID is totally different + signed_id = tx.tx_id + assert signed_id != unsigned_id + + # 5. Prove it's locked down (Sealed) with pytest.raises(AttributeError, match="Transaction is sealed"): tx.amount = 500 \ No newline at end of file From 5a5d751c416481180fbbd5cec7cb9e246f5931fa Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 19:08:50 +0530 Subject: [PATCH 14/16] chore: trigger CodeRabbit re-review From 3e93e0124617889c6d1644e4ca06b959471e28c3 Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 19:18:45 +0530 Subject: [PATCH 15/16] test: add parameterized loop to verify comprehensive cache invalidation --- tests/test_cache.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index 4566cfa..0e7265e 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -15,7 +15,6 @@ def test_tx_caching_efficiency(): # We 'patch' the hashing function to count how many times it's called with patch('minichain.transaction.canonical_json_hash') as mock_hash: - # Give it a dummy return value so the code doesn't crash mock_hash.return_value = "mocked_hash_value" # 1. First Access: Should trigger the hash calculation @@ -28,13 +27,31 @@ def test_tx_caching_efficiency(): assert res2 == "mocked_hash_value" assert mock_hash.call_count == 1 # <--- THIS proves the cache worked! - # 3. Invalidate via Mutation: Changing a field clears the cache - tx.amount = 200 - assert tx._cached_tx_id is None + # 3. Comprehensive Invalidation: Changing ANY field must clear the cache + mutations = { + "sender": "new_sender_hex", + "receiver": "new_receiver", + "amount": 200, + "nonce": 2, + "data": "new_data", + "timestamp": 1234567890, + "signature": "fake_signature_hex" + } - # 4. Third Access: Should trigger calculation again (count becomes 2) - _ = tx.tx_id - assert mock_hash.call_count == 2 + expected_calls = 1 + for field, new_value in mutations.items(): + # Mutate the field dynamically + setattr(tx, field, new_value) + + # Prove the cache was instantly killed + assert tx._cached_tx_id is None, f"Cache failed to clear when mutating {field}" + + # Access ID again, which forces a re-calculation + _ = tx.tx_id + + # Prove the hashing math ran exactly one more time + expected_calls += 1 + assert mock_hash.call_count == expected_calls, f"Hash did not recalculate for {field}" def test_signed_tx_is_sealed(): """Verifies that a signed transaction clears cache, changes ID, and cannot be modified.""" From 445fcf5069e6b13407ad3bc704e1968f85c4d8b3 Mon Sep 17 00:00:00 2001 From: sanaica Date: Thu, 26 Mar 2026 20:01:08 +0530 Subject: [PATCH 16/16] chore: trigger CodeRabbit re-review