diff --git a/backend/consensus/base.py b/backend/consensus/base.py index 846dd9279..240414635 100644 --- a/backend/consensus/base.py +++ b/backend/consensus/base.py @@ -337,8 +337,8 @@ def contract_snapshot_factory( """ try: contract_address = to_checksum_address(contract_address) - except Exception: - pass + except (ValueError, TypeError): + return None # Check if the transaction is a contract deployment and the contract address matches the transaction's to address if ( transaction.type == TransactionType.DEPLOY_CONTRACT diff --git a/backend/consensus/vrf.py b/backend/consensus/vrf.py index 55c953600..6135e0d2d 100644 --- a/backend/consensus/vrf.py +++ b/backend/consensus/vrf.py @@ -6,18 +6,26 @@ def get_validators_for_transaction( nodes: list[dict], num_validators: int | None = None, - rng=np.random.default_rng(seed=int(datetime.now().timestamp())), + rng: np.random.Generator | None = None, ) -> list[dict]: """ Returns subset of validators for a transaction. The selelction and order is given by a random sampling based on the stake of the validators. """ + if rng is None: + rng = np.random.default_rng(seed=int(datetime.now().timestamp())) + if num_validators is None: num_validators = DEFAULT_VALIDATORS_COUNT num_validators = min(num_validators, len(nodes)) total_stake = sum(validator["stake"] for validator in nodes) + if total_stake == 0: + raise ValueError( + "Cannot select validators: all stakes are zero. " + "At least one validator must have a positive stake." + ) probabilities = [validator["stake"] / total_stake for validator in nodes] selected_validators = rng.choice( diff --git a/backend/consensus/worker_service.py b/backend/consensus/worker_service.py index 6b41ea5e9..47cada199 100644 --- a/backend/consensus/worker_service.py +++ b/backend/consensus/worker_service.py @@ -588,7 +588,7 @@ def health_check(): "cpu_percent": round(process.cpu_percent(), 2), "memory_percent": round(process.memory_percent(), 2), } - except: + except Exception: pass # Probe local GenVM manager responsiveness. diff --git a/backend/database_handler/accounts_manager.py b/backend/database_handler/accounts_manager.py index 6169b5171..c15dd06a2 100644 --- a/backend/database_handler/accounts_manager.py +++ b/backend/database_handler/accounts_manager.py @@ -45,7 +45,7 @@ def create_new_account_with_address(self, address: str) -> Account: try: address = to_checksum_address(address) except Exception: - pass + raise ValueError(f"Failed to checksum address: {address}") existing_account = ( self.session.query(CurrentState).filter(CurrentState.id == address).first() @@ -67,7 +67,7 @@ def get_account(self, account_address: str) -> CurrentState | None: try: normalized = to_checksum_address(account_address) except Exception: - normalized = account_address + raise ValueError(f"Failed to checksum address: {account_address}") account = ( self.session.query(CurrentState) .filter(CurrentState.id == normalized) diff --git a/backend/protocol_rpc/transactions_parser.py b/backend/protocol_rpc/transactions_parser.py index 1a0f85126..517f407b9 100644 --- a/backend/protocol_rpc/transactions_parser.py +++ b/backend/protocol_rpc/transactions_parser.py @@ -604,13 +604,17 @@ def decode_method_send_data(self, data: str) -> DecodedMethodSendData: def decode_method_call_data(self, data: str) -> DecodedMethodCallData: raw_bytes = eth_utils.hexadecimal.decode_hex(data) + if len(raw_bytes) == 0: + return DecodedMethodCallData(b"") + # Remove the null byte if raw_bytes[-1] == 0: raw_bytes = raw_bytes[:-1] # Try to decode the outer list first if raw_bytes[0] >= 0xF8: # Long list - raw_bytes = raw_bytes[2:] # Skip list prefix and length + num_length_bytes = raw_bytes[0] - 0xF7 + raw_bytes = raw_bytes[1 + num_length_bytes:] # Skip prefix + length bytes elif raw_bytes[0] >= 0xC0: # Short list raw_bytes = raw_bytes[1:] # Skip list prefix diff --git a/tests/unit/test_vrf.py b/tests/unit/test_vrf.py index 95473770e..4760b3d35 100644 --- a/tests/unit/test_vrf.py +++ b/tests/unit/test_vrf.py @@ -1,5 +1,6 @@ from backend.consensus.vrf import get_validators_for_transaction from unittest.mock import Mock +import pytest def list_of_dicts_to_set(list_of_dicts: list[dict]) -> set: @@ -66,3 +67,21 @@ def choice_mock(a, p, size, replace): rng.choice.assert_called_once() assert validators == [{"stake": 3}, {"stake": 2}, {"stake": 1}] + + +def test_get_validators_zero_stake_raises(): + """When all stakes are zero, selection should raise ValueError.""" + nodes = [{"stake": 0}, {"stake": 0}] + with pytest.raises(ValueError, match="all stakes are zero"): + get_validators_for_transaction(nodes, 1) + + +def test_get_validators_rng_not_shared_between_calls(): + """Each call without an explicit rng should get a fresh generator.""" + nodes = [{"stake": 1}, {"stake": 2}, {"stake": 3}] + # Call twice without passing rng — the default must be created fresh each time. + v1 = get_validators_for_transaction(nodes, 1) + v2 = get_validators_for_transaction(nodes, 1) + # Both should succeed (no shared mutable state corruption). + assert len(v1) == 1 + assert len(v2) == 1