Drop out channel hash helper

This commit is contained in:
Jack Kingsman
2026-03-12 20:33:52 -07:00
parent 5a580b9c01
commit a7ff041a48
8 changed files with 30 additions and 48 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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,

View File

@@ -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(),

View File

@@ -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(

View File

@@ -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."""

View File

@@ -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):

View File

@@ -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,
):