mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-07 09:23:06 +02:00
fix: resolve room-server push ACKs through the dispatcher
Room server pushes waited on dispatcher.wait_for_ack, but nothing in the repeater could ever resolve it, so every push timed out and re-pushed on the backoff schedule (issue #286's duplicate floods — worst for virtual companions on the same instance, whose ACKs never even cross the air): - no core AckHandler is registered (all RX lands in the router fallback), so received ACK CRCs were never fed to dispatcher ACK matching - inject_packet waited on packet.get_crc(), a packet-hash CRC that no ACK sender produces; the crypto CRC only the room server knows was ignored - PATH returns (how a flood-received DM is ACKed) were decrypted by PathHelper, but it read the encoded path_len wire byte as a raw count — an empty 3-byte-hash path (0x80) parsed as a 128-byte truncated payload — and the router's PATH branch never ran PathHelper for room/repeater destinations when any companion bridge existed Fixes: - router ACK branch feeds discrete ACK CRCs (RF and locally injected) to dispatcher._register_ack_received - PATH packets addressed to a local server identity are processed by PathHelper regardless of companion bridges; PathHelper decodes the encoded path_len via PathUtils, keeps the encoded byte in out_path_len, and registers the embedded ACK via ack_received_fn - inject_packet accepts expected_ack_crc/ack_timeout; the room server passes its crypto CRC and a hop-count-based timeout (the encoded byte would have produced a ~4-minute direct-push wait) Verified live on a real mesh: push to a same-instance virtual companion resolves via the PATH-embedded ACK (encoded path_len 0x80) and push to a firmware client resolves via the discrete RF ACK, no re-push loops. Fixes #286 Fixes #341
This commit is contained in:
@@ -67,6 +67,74 @@ async def test_path_helper_updates_client_out_path_on_valid_decrypt():
|
||||
assert isinstance(client.last_activity, int)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_helper_registers_embedded_ack():
|
||||
"""Firmware path returns embed the delivery ACK after the path
|
||||
(extra_type=PAYLOAD_TYPE_ACK + 4-byte CRC); it must reach ack_received_fn
|
||||
so local waiters (e.g. room server pushes) resolve."""
|
||||
client = _FakeClient(pubkey=bytes([0x22]) + b"x" * 31, shared_secret=b"k" * 32)
|
||||
acl = _FakeACL([client])
|
||||
ack_fn = AsyncMock()
|
||||
helper = PathHelper(acl_dict={0x11: acl}, ack_received_fn=ack_fn)
|
||||
|
||||
packet = _PathPacket(payload=b"\x11\x22\xaa\xbb\xcc")
|
||||
# path_len(2) + path + extra_type(PAYLOAD_TYPE_ACK=3) + crc(4, LE)
|
||||
decrypted = b"\x02\x99\x88" + bytes([0x03]) + bytes.fromhex("4dabaf95")
|
||||
with patch(
|
||||
"openhop_core.protocol.crypto.CryptoUtils.mac_then_decrypt",
|
||||
return_value=decrypted,
|
||||
):
|
||||
await helper.process_path_packet(packet)
|
||||
|
||||
ack_fn.assert_awaited_once_with(0x95AFAB4D)
|
||||
assert client.out_path_len == 2 # path update still applied
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_helper_handles_encoded_path_len_with_embedded_ack():
|
||||
"""path_len in a path return is the ENCODED wire byte: with 3-byte hashes an
|
||||
empty path is 0x80, not 0x00. Reading it as a raw count (128) made the
|
||||
helper bail as 'truncated' before the embedded ACK was registered, so
|
||||
room-server pushes to same-instance companions timed out forever.
|
||||
Bytes below are a decrypted path return captured from a live mesh."""
|
||||
client = _FakeClient(pubkey=bytes([0x22]) + b"x" * 31, shared_secret=b"k" * 32)
|
||||
acl = _FakeACL([client])
|
||||
ack_fn = AsyncMock()
|
||||
helper = PathHelper(acl_dict={0x11: acl}, ack_received_fn=ack_fn)
|
||||
|
||||
packet = _PathPacket(payload=b"\x11\x22\xaa\xbb\xcc")
|
||||
# path_len 0x80 (3-byte hashes, 0 hops) + extra_type ACK + crc + AES padding
|
||||
decrypted = bytes.fromhex("80038d48208500000000000000000000")
|
||||
with patch(
|
||||
"openhop_core.protocol.crypto.CryptoUtils.mac_then_decrypt",
|
||||
return_value=decrypted,
|
||||
):
|
||||
await helper.process_path_packet(packet)
|
||||
|
||||
ack_fn.assert_awaited_once_with(0x8520488D)
|
||||
assert client.out_path_len == 0x80 # encoded byte preserved
|
||||
assert bytes(client.out_path) == b""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_helper_ignores_non_ack_extra():
|
||||
client = _FakeClient(pubkey=bytes([0x22]) + b"x" * 31, shared_secret=b"k" * 32)
|
||||
acl = _FakeACL([client])
|
||||
ack_fn = AsyncMock()
|
||||
helper = PathHelper(acl_dict={0x11: acl}, ack_received_fn=ack_fn)
|
||||
|
||||
packet = _PathPacket(payload=b"\x11\x22\xaa\xbb\xcc")
|
||||
# extra_type 0x08 (PATH) instead of ACK: nothing to register
|
||||
decrypted = b"\x02\x99\x88" + bytes([0x08]) + b"\x01\x02\x03\x04"
|
||||
with patch(
|
||||
"openhop_core.protocol.crypto.CryptoUtils.mac_then_decrypt",
|
||||
return_value=decrypted,
|
||||
):
|
||||
await helper.process_path_packet(packet)
|
||||
|
||||
ack_fn.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_helper_returns_false_for_non_matching_or_invalid_inputs():
|
||||
client = _FakeClient(pubkey=bytes([0x22]) + b"x" * 31, shared_secret=b"k" * 32)
|
||||
|
||||
Reference in New Issue
Block a user