From a7ff041a482b9a2b17f7ae3c0e8a48dd9ff52489 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Thu, 12 Mar 2026 20:33:52 -0700 Subject: [PATCH] Drop out channel hash helper --- app/decoder.py | 11 +---------- app/repository/messages.py | 13 +++++++++++++ app/routers/debug.py | 6 ++++++ app/routers/messages.py | 8 -------- tests/test_api.py | 10 +++++++++- tests/test_decoder.py | 15 --------------- tests/test_real_crypto.py | 4 +--- tests/test_send_messages.py | 11 ----------- 8 files changed, 30 insertions(+), 48 deletions(-) diff --git a/app/decoder.py b/app/decoder.py index 314f1ed..d2a83fb 100644 --- a/app/decoder.py +++ b/app/decoder.py @@ -85,15 +85,6 @@ class PacketInfo: path_hash_size: int = 1 # Bytes per hop: 1, 2, or 3 -def calculate_channel_hash(channel_key: bytes) -> str: - """ - Calculate the channel hash from a 16-byte channel key. - Returns the first byte of SHA256(key) as hex. - """ - hash_bytes = hashlib.sha256(channel_key).digest() - return format(hash_bytes[0], "02x") - - def extract_payload(raw_packet: bytes) -> bytes | None: """ Extract just the payload from a raw packet, skipping header and path. @@ -233,7 +224,7 @@ def try_decrypt_packet_with_channel_key( return None packet_channel_hash = format(packet_info.payload[0], "02x") - expected_hash = calculate_channel_hash(channel_key) + expected_hash = format(hashlib.sha256(channel_key).digest()[0], "02x") if packet_channel_hash != expected_hash: return None diff --git a/app/repository/messages.py b/app/repository/messages.py index 23b4a85..6949dc1 100644 --- a/app/repository/messages.py +++ b/app/repository/messages.py @@ -857,6 +857,19 @@ class MessageRepository: "top_senders_24h": top_senders, } + @staticmethod + async def count_channels_with_incoming_messages() -> int: + """Count distinct channel conversations with at least one incoming message.""" + cursor = await db.conn.execute( + """ + SELECT COUNT(DISTINCT conversation_key) AS cnt + FROM messages + WHERE type = 'CHAN' AND outgoing = 0 + """ + ) + row = await cursor.fetchone() + return int(row["cnt"]) if row and row["cnt"] is not None else 0 + @staticmethod async def get_most_active_rooms(sender_key: str, limit: int = 5) -> list[tuple[str, str, int]]: """Get channels where a contact has sent the most messages. diff --git a/app/routers/debug.py b/app/routers/debug.py index 9ca3ee3..ac6641d 100644 --- a/app/routers/debug.py +++ b/app/routers/debug.py @@ -13,6 +13,7 @@ from pydantic import BaseModel, Field from app.config import get_recent_log_lines, settings from app.radio_sync import get_contacts_selected_for_radio_sync, get_radio_channel_limit +from app.repository import MessageRepository from app.routers.health import HealthResponse, build_health_data from app.services.radio_runtime import radio_runtime @@ -34,6 +35,7 @@ class DebugRuntimeInfo(BaseModel): connection_desired: bool setup_in_progress: bool setup_complete: bool + channels_with_incoming_messages: int max_channels: int path_hash_mode: int path_hash_mode_supported: bool @@ -269,6 +271,9 @@ async def debug_support_snapshot() -> DebugSnapshotResponse: """Return a support/debug snapshot with recent logs and live radio state.""" health_data = await build_health_data(radio_runtime.is_connected, radio_runtime.connection_info) radio_probe = await _probe_radio() + channels_with_incoming_messages = ( + await MessageRepository.count_channels_with_incoming_messages() + ) return DebugSnapshotResponse( captured_at=datetime.now(timezone.utc).isoformat(), application=_build_application_info(), @@ -278,6 +283,7 @@ async def debug_support_snapshot() -> DebugSnapshotResponse: connection_desired=radio_runtime.connection_desired, setup_in_progress=radio_runtime.is_setup_in_progress, setup_complete=radio_runtime.is_setup_complete, + channels_with_incoming_messages=channels_with_incoming_messages, max_channels=radio_runtime.max_channels, path_hash_mode=radio_runtime.path_hash_mode, path_hash_mode_supported=radio_runtime.path_hash_mode_supported, diff --git a/app/routers/messages.py b/app/routers/messages.py index 3b8d12f..5b8a259 100644 --- a/app/routers/messages.py +++ b/app/routers/messages.py @@ -138,7 +138,6 @@ async def send_channel_message(request: SendChannelMessageRequest) -> Message: require_connected() # Get channel info from our database - from app.decoder import calculate_channel_hash from app.repository import ChannelRepository db_channel = await ChannelRepository.get_by_key(request.channel_key) @@ -155,13 +154,6 @@ async def send_channel_message(request: SendChannelMessageRequest) -> Message: status_code=400, detail=f"Invalid channel key format: {request.channel_key}" ) from None - expected_hash = calculate_channel_hash(key_bytes) - logger.info( - "Sending to channel %s (%s) via managed radio slot, key hash: %s", - request.channel_key, - db_channel.name, - expected_hash, - ) return await send_channel_message_to_channel( channel=db_channel, channel_key_upper=request.channel_key.upper(), diff --git a/tests/test_api.py b/tests/test_api.py index aa943f6..eea9048 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -127,6 +127,13 @@ class TestDebugEndpoint: channel_key = "CD" * 16 await _insert_contact(contact_key, "Alice", last_contacted=1700000000) await ChannelRepository.upsert(key=channel_key, name="#flightless", on_radio=False) + await MessageRepository.create( + msg_type="CHAN", + text="Alice: hello", + received_at=1700000001, + conversation_key=channel_key, + sender_timestamp=1700000001, + ) radio_manager.max_channels = 2 radio_manager.path_hash_mode = 1 @@ -187,6 +194,7 @@ class TestDebugEndpoint: assert payload["application"]["commit_hash"] == "deadbeef" assert payload["runtime"]["channel_slot_reuse_enabled"] is True + assert payload["runtime"]["channels_with_incoming_messages"] == 1 assert any("support snapshot marker" in line for line in payload["logs"]) radio_probe = payload["radio_probe"] @@ -226,6 +234,7 @@ class TestDebugEndpoint: payload = response.json() assert payload["radio_probe"]["performed"] is False assert payload["radio_probe"]["errors"] == ["Radio not connected"] + assert payload["runtime"]["channels_with_incoming_messages"] == 0 class TestRadioDisconnectedHandler: @@ -328,7 +337,6 @@ class TestMessagesEndpoint: radio_manager._meshcore = mock_mc with ( _patch_require_connected(mock_mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event") as mock_broadcast, ): response = await client.post( diff --git a/tests/test_decoder.py b/tests/test_decoder.py index 11b2879..a357a0b 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -14,7 +14,6 @@ from app.decoder import ( PayloadType, RouteType, _clamp_scalar, - calculate_channel_hash, decrypt_direct_message, decrypt_group_text, derive_public_key, @@ -34,22 +33,8 @@ class TestChannelKeyDerivation: channel_name = "#test" expected_key = hashlib.sha256(channel_name.encode("utf-8")).digest()[:16] - # Verify the derived key produces the expected channel hash - result_hash = calculate_channel_hash(expected_key) - expected_hash = format(hashlib.sha256(expected_key).digest()[0], "02x") - assert result_hash == expected_hash assert len(expected_key) == 16 - def test_channel_hash_calculation(self): - """Channel hash is the first byte of SHA256(key) as hex.""" - key = bytes(16) # All zeros - expected_hash = format(hashlib.sha256(key).digest()[0], "02x") - - result = calculate_channel_hash(key) - - assert result == expected_hash - assert len(result) == 2 # Two hex chars - class TestPacketParsing: """Test raw packet header parsing.""" diff --git a/tests/test_real_crypto.py b/tests/test_real_crypto.py index 489be9e..514a345 100644 --- a/tests/test_real_crypto.py +++ b/tests/test_real_crypto.py @@ -210,12 +210,10 @@ class TestChannelDecryption: def test_channel_hash_matches_packet(self): """Channel hash in packet matches hash computed from key.""" - from app.decoder import calculate_channel_hash - info = parse_packet(CHANNEL_PACKET) assert info is not None packet_hash = format(info.payload[0], "02x") - expected_hash = calculate_channel_hash(CHANNEL_KEY) + expected_hash = format(sha256(CHANNEL_KEY).digest()[0], "02x") assert packet_hash == expected_hash def test_wrong_channel_key_fails(self): diff --git a/tests/test_send_messages.py b/tests/test_send_messages.py index 7042c6a..93cf9a7 100644 --- a/tests/test_send_messages.py +++ b/tests/test_send_messages.py @@ -209,7 +209,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event", side_effect=capture_broadcast), ): request = SendChannelMessageRequest(channel_key=chan_key, text="!lasttime5 someone") @@ -234,7 +233,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event"), ): request = SendChannelMessageRequest(channel_key=chan_key, text="acked now") @@ -262,7 +260,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event", side_effect=capture_broadcast), ): request = SendChannelMessageRequest(channel_key=chan_key, text="hello") @@ -293,7 +290,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event"), ): request = SendChannelMessageRequest(channel_key=chan_key, text="hello") @@ -317,7 +313,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event"), ): request = SendChannelMessageRequest(channel_key=chan_key, text="hello") @@ -339,7 +334,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event"), pytest.raises(HTTPException) as exc_info, ): @@ -362,7 +356,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event"), ): await send_channel_message( @@ -391,7 +384,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event"), ): await send_channel_message( @@ -433,7 +425,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event"), ): await send_channel_message( @@ -458,7 +449,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event"), patch("app.radio.settings.force_channel_slot_reconfigure", True), ): @@ -487,7 +477,6 @@ class TestOutgoingChannelBroadcast: with ( patch("app.routers.messages.require_connected", return_value=mc), patch.object(radio_manager, "_meshcore", mc), - patch("app.decoder.calculate_channel_hash", return_value="abcd"), patch("app.routers.messages.broadcast_event"), pytest.raises(HTTPException) as exc_info, ):