"""GarlicCloveBuilder — construct individual garlic cloves. Ported from net.i2p.router.message components. Builds CloveConfig objects for data delivery, ACK requests, and LeaseSet delivery — the three clove types in a typical outbound garlic message. """ from __future__ import annotations import os import struct import time from i2p_data.garlic_config import CloveConfig, GarlicConfig from i2p_data.i2np import DeliveryStatusMessage, DataMessage, DatabaseStoreMessage class GarlicCloveBuilder: """Build individual garlic cloves for different delivery purposes.""" @staticmethod def build_data_clove( dest_hash: bytes, payload: bytes, expiration: int | None = None, ) -> CloveConfig: """Build a DESTINATION-delivery clove wrapping payload in a DataMessage.""" if expiration is None: expiration = int(time.time() * 1000) + 60_000 # Wrap payload in I2NP DataMessage body format data_msg = DataMessage(payload) msg_bytes = data_msg.to_bytes() return CloveConfig.for_destination( dest_hash=dest_hash, message_data=msg_bytes, clove_id=int.from_bytes(os.urandom(4), "big"), expiration=expiration, ) @staticmethod def build_ack_clove( reply_token: int, our_ib_gateway: bytes, our_ib_tunnel_id: int, expiration: int | None = None, ) -> CloveConfig: """Build a TUNNEL-delivery ACK clove (DeliveryStatusMessage). The ACK is delivered via our inbound tunnel so the reply confirms the message reached the destination. """ if expiration is None: expiration = int(time.time() * 1000) + 60_000 # DeliveryStatusMessage with the reply token as msg_id ack_msg = DeliveryStatusMessage(reply_token, int(time.time() * 1000)) msg_bytes = ack_msg.to_bytes() return CloveConfig.for_tunnel( router_hash=our_ib_gateway, tunnel_id=our_ib_tunnel_id, message_data=msg_bytes, clove_id=int.from_bytes(os.urandom(4), "big"), expiration=expiration, ) @staticmethod def build_leaseset_clove( our_lease_set_bytes: bytes, expiration: int | None = None, ) -> CloveConfig: """Build a LOCAL-delivery clove carrying our LeaseSet. The receiver stores our LeaseSet locally so they can route replies back to us. """ if expiration is None: expiration = int(time.time() * 1000) + 60_000 # Wrap in DatabaseStoreMessage (type 1) key = b"\x00" * 32 # placeholder — receiver extracts from LS db_store = DatabaseStoreMessage(key, 1, 0, our_lease_set_bytes) msg_bytes = db_store.to_bytes() return CloveConfig.for_local( message_data=msg_bytes, clove_id=int.from_bytes(os.urandom(4), "big"), expiration=expiration, ) @staticmethod def create_garlic_config( dest_pub_key: bytes, data_clove: CloveConfig, ack_clove: CloveConfig | None = None, ls_clove: CloveConfig | None = None, msg_id: int | None = None, expiration: int | None = None, ) -> GarlicConfig: """Assemble cloves into a GarlicConfig in standard order. Order: ACK (if present), LeaseSet (if present), Data. """ if msg_id is None: msg_id = int.from_bytes(os.urandom(4), "big") if expiration is None: expiration = int(time.time() * 1000) + 60_000 config = GarlicConfig( recipient_public_key=dest_pub_key, message_id=msg_id, expiration=expiration, ) # Standard order: ACK first, then LS, then data if ack_clove is not None: config.add_clove(ack_clove) if ls_clove is not None: config.add_clove(ls_clove) config.add_clove(data_clove) return config