mirror of
https://github.com/torlando-tech/columba.git
synced 2025-12-22 05:37:07 +00:00
perf: Optimize peer identity restoration from minutes to milliseconds
"Apply Changes" was taking 4.5+ minutes with ~4000 peer identities because the previous implementation created full RNS objects for each peer: - Created RNS.Identity from public key - Created RNS.Destination for LXMF delivery - Registered with RNS.Transport - Called RNS.Identity.recall() to verify But RNS's Identity.known_destinations is just a dict that remember() populates directly. This change bypasses the expensive object creation and directly populates the dict. ## Changes ### Python (`reticulum_wrapper.py`) - Add `bulk_restore_announce_identities()`: Direct dict population for announces where destination_hash is already available (no computation needed) - Add `bulk_restore_peer_identities()`: Lightweight hash computation for peer identities - computes LXMF delivery destination hash from public key without creating full RNS objects ### Kotlin - Add `restoreAnnounceIdentities()` to AIDL interface, binder, and protocol - Update `MessagingManager` to call new bulk Python methods - Update `InterfaceConfigManager` to use appropriate bulk restore for each type ### Tests - Add 25 new Python tests for bulk restore functions covering: - Success cases with valid data - Invalid inputs (missing fields, invalid hex, invalid base64) - Empty lists - Large batch performance - Equivalence with Identity.remember() dict format ## Performance Results (tested with ~4000 identities) | Operation | Before | After | |-----------|--------|-------| | 1832 peer identities | ~4.5 min | 88ms | | 2080 announce identities | (included above) | 142ms | | **Total** | ~4.5 min | **~230ms** | 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -200,11 +200,21 @@ interface IReticulumService {
|
||||
|
||||
/**
|
||||
* Restore peer identities to enable message sending to previously known peers.
|
||||
* @param peerIdentitiesJson JSON array containing objects with 'hash' and 'public_key' fields
|
||||
* Uses bulk restore for performance optimization.
|
||||
* @param peerIdentitiesJson JSON array containing objects with 'identity_hash' and 'public_key' fields
|
||||
* @return JSON string with result: {"success": true/false, "restored_count": N, "error": "..."}
|
||||
*/
|
||||
String restorePeerIdentities(String peerIdentitiesJson);
|
||||
|
||||
/**
|
||||
* Restore announce identities to enable message sending to announced peers.
|
||||
* Uses bulk restore with direct dict population for maximum performance.
|
||||
* For announces, we have destination_hash directly - no hash computation needed.
|
||||
* @param announcesJson JSON array containing objects with 'destination_hash' and 'public_key' fields
|
||||
* @return JSON string with result: {"success": true/false, "restored_count": N, "error": "..."}
|
||||
*/
|
||||
String restoreAnnounceIdentities(String announcesJson);
|
||||
|
||||
/**
|
||||
* Force service process to exit (for clean restart).
|
||||
* This shutdowns RNS and exits the process immediately.
|
||||
|
||||
@@ -1274,7 +1274,60 @@ class ServiceReticulumProtocol(
|
||||
} else {
|
||||
val error = result.optString("error", "Unknown error")
|
||||
Log.e(TAG, "Failed to restore peer identities: $error")
|
||||
throw RuntimeException("Failed to restore peer identities: $error")
|
||||
error("Failed to restore peer identities: $error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore announce identities from stored public keys to enable message sending to announced peers.
|
||||
* Uses bulk restore with direct dict population for maximum performance.
|
||||
*/
|
||||
fun restoreAnnounceIdentities(announces: List<Pair<String, ByteArray>>): Result<Int> {
|
||||
return runCatching {
|
||||
val service = this.service ?: throw IllegalStateException("Service not bound")
|
||||
|
||||
Log.d(TAG, "restoreAnnounceIdentities: Processing ${announces.size} announce identities")
|
||||
|
||||
// Build JSON array of announce identities
|
||||
val announcesArray = JSONArray()
|
||||
for ((index, pair) in announces.withIndex()) {
|
||||
val (destHashStr, publicKey) = pair
|
||||
|
||||
// Log details for first few announces
|
||||
if (index < 3) {
|
||||
Log.d(TAG, "restoreAnnounceIdentities: [$index] destHash=$destHashStr, publicKeyLength=${publicKey.size}")
|
||||
}
|
||||
|
||||
val base64Key = publicKey.toBase64()
|
||||
if (base64Key == null) {
|
||||
Log.e(TAG, "restoreAnnounceIdentities: [$index] Failed to encode public key to base64 for hash $destHashStr")
|
||||
continue
|
||||
}
|
||||
|
||||
val announceObj =
|
||||
JSONObject().apply {
|
||||
put("destination_hash", destHashStr)
|
||||
put("public_key", base64Key)
|
||||
}
|
||||
announcesArray.put(announceObj)
|
||||
}
|
||||
|
||||
Log.d(TAG, "restoreAnnounceIdentities: Built JSON array with ${announcesArray.length()} announces")
|
||||
|
||||
val resultJson = service.restoreAnnounceIdentities(announcesArray.toString())
|
||||
Log.d(TAG, "restoreAnnounceIdentities: Got result from service: $resultJson")
|
||||
|
||||
val result = JSONObject(resultJson)
|
||||
|
||||
if (result.optBoolean("success", false)) {
|
||||
val restoredCount = result.optInt("restored_count", 0)
|
||||
Log.d(TAG, "Restored $restoredCount announce identities")
|
||||
restoredCount
|
||||
} else {
|
||||
val error = result.optString("error", "Unknown error")
|
||||
Log.e(TAG, "Failed to restore announce identities: $error")
|
||||
error("Failed to restore announce identities: $error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,8 +255,8 @@ class InterfaceConfigManager
|
||||
throw Exception("Failed to initialize Reticulum: ${error.message}", error)
|
||||
}
|
||||
|
||||
// Step 10: Restore peer identities
|
||||
Log.d(TAG, "Step 10: Restoring peer identities...")
|
||||
// Step 10: Restore peer identities (uses bulk restore with lightweight hash computation)
|
||||
Log.d(TAG, "Step 10: Bulk restoring peer identities...")
|
||||
try {
|
||||
val peerIdentities = conversationRepository.getAllPeerIdentities()
|
||||
Log.d(TAG, "Retrieved ${peerIdentities.size} peer identities from database")
|
||||
@@ -264,45 +264,45 @@ class InterfaceConfigManager
|
||||
if (peerIdentities.isNotEmpty() && reticulumProtocol is ServiceReticulumProtocol) {
|
||||
reticulumProtocol.restorePeerIdentities(peerIdentities)
|
||||
.onSuccess { count ->
|
||||
Log.d(TAG, "✓ Restored $count peer identities")
|
||||
Log.d(TAG, "✓ Bulk restored $count peer identities")
|
||||
}
|
||||
.onFailure { error ->
|
||||
Log.w(TAG, "Failed to restore peer identities: ${error.message}", error)
|
||||
Log.w(TAG, "Failed to bulk restore peer identities: ${error.message}", error)
|
||||
// Not fatal - continue
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "No peer identities to restore")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error restoring peer identities", e)
|
||||
Log.w(TAG, "Error bulk restoring peer identities", e)
|
||||
// Not fatal - continue
|
||||
}
|
||||
|
||||
// Step 10b: Restore announce peer identities
|
||||
Log.d(TAG, "Step 10b: Restoring announce peer identities...")
|
||||
// Step 10b: Restore announce identities (uses fast bulk restore with direct dict population)
|
||||
Log.d(TAG, "Step 10b: Bulk restoring announce identities...")
|
||||
try {
|
||||
val announces = database.announceDao().getAllAnnouncesSync()
|
||||
Log.d(TAG, "Retrieved ${announces.size} announce peer identities from database")
|
||||
Log.d(TAG, "Retrieved ${announces.size} announce identities from database")
|
||||
|
||||
if (announces.isNotEmpty() && reticulumProtocol is ServiceReticulumProtocol) {
|
||||
// Map announces to peer identity format (destinationHash, publicKey)
|
||||
val announcePeerIdentities =
|
||||
// Map announces to (destinationHash, publicKey) - no hash computation needed
|
||||
val announceIdentities =
|
||||
announces.map { announce ->
|
||||
announce.destinationHash to announce.publicKey
|
||||
}
|
||||
reticulumProtocol.restorePeerIdentities(announcePeerIdentities)
|
||||
reticulumProtocol.restoreAnnounceIdentities(announceIdentities)
|
||||
.onSuccess { count ->
|
||||
Log.d(TAG, "✓ Restored $count announce peer identities")
|
||||
Log.d(TAG, "✓ Bulk restored $count announce identities")
|
||||
}
|
||||
.onFailure { error ->
|
||||
Log.w(TAG, "Failed to restore announce peer identities: ${error.message}", error)
|
||||
Log.w(TAG, "Failed to bulk restore announce identities: ${error.message}", error)
|
||||
// Not fatal - continue
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "No announce peer identities to restore")
|
||||
Log.d(TAG, "No announce identities to restore")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error restoring announce peer identities", e)
|
||||
Log.w(TAG, "Error bulk restoring announce identities", e)
|
||||
// Not fatal - continue
|
||||
}
|
||||
|
||||
|
||||
@@ -351,6 +351,10 @@ class ReticulumServiceBinder(
|
||||
return messagingManager.restorePeerIdentities(peerIdentitiesJson)
|
||||
}
|
||||
|
||||
override fun restoreAnnounceIdentities(announcesJson: String): String {
|
||||
return messagingManager.restoreAnnounceIdentities(announcesJson)
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// Callback Methods
|
||||
// ===========================================
|
||||
|
||||
@@ -259,22 +259,23 @@ class MessagingManager(private val wrapperManager: PythonWrapperManager) {
|
||||
|
||||
/**
|
||||
* Restore peer identities to enable message sending to previously known peers.
|
||||
* Uses bulk restore for performance optimization.
|
||||
*
|
||||
* @param peerIdentitiesJson JSON array containing objects with 'hash' and 'public_key' fields
|
||||
* @param peerIdentitiesJson JSON array containing objects with 'identity_hash' and 'public_key' fields
|
||||
* @return JSON string with result
|
||||
*/
|
||||
fun restorePeerIdentities(peerIdentitiesJson: String): String {
|
||||
return wrapperManager.withWrapper { wrapper ->
|
||||
try {
|
||||
Log.d(TAG, "Restoring peer identities")
|
||||
val result = wrapper.callAttr("restore_all_peer_identities", peerIdentitiesJson)
|
||||
Log.d(TAG, "Bulk restoring peer identities")
|
||||
val result = wrapper.callAttr("bulk_restore_peer_identities", peerIdentitiesJson)
|
||||
|
||||
// Python returns {"success_count": int, "errors": list}
|
||||
val successCount = result.getDictValue("success_count")?.toInt() ?: 0
|
||||
val errors = result.getDictValue("errors")?.toString() ?: "[]"
|
||||
|
||||
val success = successCount > 0
|
||||
Log.d(TAG, "Restored $successCount peer identities")
|
||||
val success = successCount > 0 || peerIdentitiesJson == "[]"
|
||||
Log.d(TAG, "Bulk restored $successCount peer identities")
|
||||
|
||||
if (errors != "[]") {
|
||||
Log.e(TAG, "Some identities failed to restore: $errors")
|
||||
@@ -288,7 +289,53 @@ class MessagingManager(private val wrapperManager: PythonWrapperManager) {
|
||||
}
|
||||
}.toString()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error restoring peer identities", e)
|
||||
Log.e(TAG, "Error bulk restoring peer identities", e)
|
||||
JSONObject().apply {
|
||||
put("success", false)
|
||||
put("error", e.message)
|
||||
}.toString()
|
||||
}
|
||||
} ?: run {
|
||||
JSONObject().apply {
|
||||
put("success", false)
|
||||
put("error", "Wrapper not initialized")
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore announce identities to enable message sending to announced peers.
|
||||
* Uses bulk restore with direct dict population for maximum performance.
|
||||
*
|
||||
* @param announcesJson JSON array containing objects with 'destination_hash' and 'public_key' fields
|
||||
* @return JSON string with result
|
||||
*/
|
||||
fun restoreAnnounceIdentities(announcesJson: String): String {
|
||||
return wrapperManager.withWrapper { wrapper ->
|
||||
try {
|
||||
Log.d(TAG, "Bulk restoring announce identities")
|
||||
val result = wrapper.callAttr("bulk_restore_announce_identities", announcesJson)
|
||||
|
||||
// Python returns {"success_count": int, "errors": list}
|
||||
val successCount = result.getDictValue("success_count")?.toInt() ?: 0
|
||||
val errors = result.getDictValue("errors")?.toString() ?: "[]"
|
||||
|
||||
val success = successCount > 0 || announcesJson == "[]"
|
||||
Log.d(TAG, "Bulk restored $successCount announce identities")
|
||||
|
||||
if (errors != "[]") {
|
||||
Log.e(TAG, "Some announce identities failed to restore: $errors")
|
||||
}
|
||||
|
||||
JSONObject().apply {
|
||||
put("success", success)
|
||||
put("restored_count", successCount)
|
||||
if (!success) {
|
||||
put("error", if (errors != "[]") errors else "No identities restored")
|
||||
}
|
||||
}.toString()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error bulk restoring announce identities", e)
|
||||
JSONObject().apply {
|
||||
put("success", false)
|
||||
put("error", e.message)
|
||||
|
||||
@@ -3245,6 +3245,185 @@ class ReticulumWrapper:
|
||||
log_error("ReticulumWrapper", "restore_all_peer_identities", f"Error restoring peer identities: {e}")
|
||||
return {"success_count": 0, "errors": [str(e)]}
|
||||
|
||||
def bulk_restore_announce_identities(self, announce_data) -> Dict:
|
||||
"""
|
||||
Bulk restore announce identities by directly populating Identity.known_destinations.
|
||||
For announces, we have destination_hash directly - no computation needed.
|
||||
This is much faster than creating full RNS Identity/Destination objects.
|
||||
|
||||
Args:
|
||||
announce_data: JSON string or List of dicts with 'destination_hash' and 'public_key' keys
|
||||
|
||||
Returns:
|
||||
Dict with 'success_count' and 'errors' list
|
||||
"""
|
||||
try:
|
||||
if not RETICULUM_AVAILABLE:
|
||||
return {"success_count": 0, "errors": ["Reticulum not available"]}
|
||||
|
||||
# Parse JSON string if needed
|
||||
if isinstance(announce_data, str):
|
||||
import json
|
||||
announce_data = json.loads(announce_data)
|
||||
|
||||
log_debug("ReticulumWrapper", "bulk_restore_announce_identities",
|
||||
f"Bulk restoring {len(announce_data)} announce identities")
|
||||
|
||||
import time
|
||||
import base64
|
||||
|
||||
success_count = 0
|
||||
errors = []
|
||||
expected_key_size = RNS.Identity.KEYSIZE // 8 # Convert bits to bytes (512 bits = 64 bytes)
|
||||
|
||||
for i, announce in enumerate(announce_data):
|
||||
try:
|
||||
dest_hash_str = announce.get('destination_hash')
|
||||
public_key_str = announce.get('public_key')
|
||||
|
||||
if not dest_hash_str:
|
||||
errors.append(f"Announce {i}: missing destination_hash")
|
||||
continue
|
||||
if not public_key_str:
|
||||
errors.append(f"Announce {i}: missing public_key")
|
||||
continue
|
||||
|
||||
# Convert hex string to bytes
|
||||
dest_hash = bytes.fromhex(dest_hash_str)
|
||||
|
||||
# Decode base64 string to bytes
|
||||
public_key = base64.b64decode(public_key_str)
|
||||
|
||||
# Validate public key size
|
||||
if len(public_key) != expected_key_size:
|
||||
errors.append(f"Announce {i}: Invalid public key size {len(public_key)}, expected {expected_key_size}")
|
||||
continue
|
||||
|
||||
# Directly populate Identity.known_destinations
|
||||
# Format: [timestamp, packet_hash, public_key, app_data]
|
||||
RNS.Identity.known_destinations[dest_hash] = [
|
||||
time.time(), # timestamp
|
||||
None, # packet_hash (not needed for recall)
|
||||
public_key, # public key bytes
|
||||
None # app_data
|
||||
]
|
||||
|
||||
# Also store in local identities cache for wrapper lookups
|
||||
# Create a lightweight identity object for local cache
|
||||
identity = RNS.Identity(create_keys=False)
|
||||
identity.load_public_key(public_key)
|
||||
self.identities[dest_hash_str] = identity
|
||||
|
||||
success_count += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Error processing announce {i}: {e}")
|
||||
|
||||
log_info("ReticulumWrapper", "bulk_restore_announce_identities",
|
||||
f"Bulk restored {success_count} announce identities, {len(errors)} errors")
|
||||
return {"success_count": success_count, "errors": errors}
|
||||
|
||||
except Exception as e:
|
||||
log_error("ReticulumWrapper", "bulk_restore_announce_identities",
|
||||
f"Error bulk restoring announce identities: {e}")
|
||||
return {"success_count": 0, "errors": [str(e)]}
|
||||
|
||||
def bulk_restore_peer_identities(self, peer_data) -> Dict:
|
||||
"""
|
||||
Bulk restore peer identities using lightweight hash computation.
|
||||
For peer identities, we must compute destination_hash from identity_hash.
|
||||
This is faster than creating full RNS Identity/Destination objects.
|
||||
|
||||
Args:
|
||||
peer_data: JSON string or List of dicts with 'identity_hash' and 'public_key' keys
|
||||
|
||||
Returns:
|
||||
Dict with 'success_count' and 'errors' list
|
||||
"""
|
||||
try:
|
||||
if not RETICULUM_AVAILABLE:
|
||||
return {"success_count": 0, "errors": ["Reticulum not available"]}
|
||||
|
||||
# Parse JSON string if needed
|
||||
if isinstance(peer_data, str):
|
||||
import json
|
||||
peer_data = json.loads(peer_data)
|
||||
|
||||
log_debug("ReticulumWrapper", "bulk_restore_peer_identities",
|
||||
f"Bulk restoring {len(peer_data)} peer identities")
|
||||
|
||||
import time
|
||||
import base64
|
||||
|
||||
success_count = 0
|
||||
errors = []
|
||||
expected_key_size = RNS.Identity.KEYSIZE // 8 # Convert bits to bytes
|
||||
|
||||
# Precompute the LXMF name_hash (constant for all LXMF delivery destinations)
|
||||
# This is: hash("lxmf.delivery")[:NAME_HASH_LENGTH//8]
|
||||
lxmf_name = "lxmf.delivery"
|
||||
lxmf_name_hash = RNS.Identity.full_hash(lxmf_name.encode("utf-8"))[:(RNS.Identity.NAME_HASH_LENGTH // 8)]
|
||||
|
||||
for i, peer in enumerate(peer_data):
|
||||
try:
|
||||
identity_hash_str = peer.get('identity_hash')
|
||||
public_key_str = peer.get('public_key')
|
||||
|
||||
if not identity_hash_str:
|
||||
errors.append(f"Peer {i}: missing identity_hash")
|
||||
continue
|
||||
if not public_key_str:
|
||||
errors.append(f"Peer {i}: missing public_key")
|
||||
continue
|
||||
|
||||
# Decode base64 string to bytes
|
||||
public_key = base64.b64decode(public_key_str)
|
||||
|
||||
# Validate public key size
|
||||
if len(public_key) != expected_key_size:
|
||||
errors.append(f"Peer {i}: Invalid public key size {len(public_key)}, expected {expected_key_size}")
|
||||
continue
|
||||
|
||||
# Compute the identity hash from the public key (this is the authoritative hash)
|
||||
# Identity hash = truncated_hash(public_key)
|
||||
actual_identity_hash = RNS.Identity.truncated_hash(public_key)
|
||||
|
||||
# Compute the LXMF delivery destination hash
|
||||
# dest_hash = full_hash(name_hash + identity_hash)[:TRUNCATED_HASHLENGTH//8]
|
||||
addr_hash_material = lxmf_name_hash + actual_identity_hash
|
||||
dest_hash = RNS.Identity.full_hash(addr_hash_material)[:RNS.Reticulum.TRUNCATED_HASHLENGTH // 8]
|
||||
|
||||
# Directly populate Identity.known_destinations
|
||||
# Format: [timestamp, packet_hash, public_key, app_data]
|
||||
RNS.Identity.known_destinations[dest_hash] = [
|
||||
time.time(), # timestamp
|
||||
None, # packet_hash (not needed for recall)
|
||||
public_key, # public key bytes
|
||||
None # app_data
|
||||
]
|
||||
|
||||
# Also store in local identities cache for wrapper lookups
|
||||
identity = RNS.Identity(create_keys=False)
|
||||
identity.load_public_key(public_key)
|
||||
|
||||
# Store by multiple keys for lookup flexibility
|
||||
self.identities[actual_identity_hash.hex()] = identity
|
||||
self.identities[dest_hash.hex()] = identity
|
||||
|
||||
success_count += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Error processing peer {i}: {e}")
|
||||
|
||||
log_info("ReticulumWrapper", "bulk_restore_peer_identities",
|
||||
f"Bulk restored {success_count} peer identities, {len(errors)} errors")
|
||||
return {"success_count": success_count, "errors": errors}
|
||||
|
||||
except Exception as e:
|
||||
log_error("ReticulumWrapper", "bulk_restore_peer_identities",
|
||||
f"Error bulk restoring peer identities: {e}")
|
||||
return {"success_count": 0, "errors": [str(e)]}
|
||||
|
||||
def get_lxmf_destination(self) -> Dict:
|
||||
"""
|
||||
Get the local LXMF delivery destination hash.
|
||||
|
||||
@@ -1034,6 +1034,644 @@ class TestPeerIdentityIntegration(unittest.TestCase):
|
||||
self.assertGreater(len(wrapper.identities), 0)
|
||||
|
||||
|
||||
class TestBulkRestoreAnnounceIdentities(unittest.TestCase):
|
||||
"""Test the bulk_restore_announce_identities method for fast announce restoration"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
import tempfile
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures"""
|
||||
import shutil
|
||||
if os.path.exists(self.temp_dir):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', False)
|
||||
def test_bulk_restore_announces_reticulum_not_available(self):
|
||||
"""Test that bulk_restore_announce_identities returns error when Reticulum is not available"""
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
announce_data = [
|
||||
{"destination_hash": "aabbccdd" * 4, "public_key": base64.b64encode(b'key1' * 16).decode()}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertIn('errors', result)
|
||||
self.assertEqual(result['errors'], ["Reticulum not available"])
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_success(self, mock_rns):
|
||||
"""Test successful bulk restore of announce identities"""
|
||||
# Setup mock for known_destinations dict
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512 # 64 bytes
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
# Create test data - 64 byte public keys
|
||||
public_key_1 = b'k' * 64
|
||||
public_key_2 = b'm' * 64
|
||||
announce_data = [
|
||||
{
|
||||
"destination_hash": "aabbccdd" * 4, # 32 hex chars = 16 bytes
|
||||
"public_key": base64.b64encode(public_key_1).decode()
|
||||
},
|
||||
{
|
||||
"destination_hash": "11223344" * 4,
|
||||
"public_key": base64.b64encode(public_key_2).decode()
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
|
||||
# Both should succeed
|
||||
self.assertEqual(result['success_count'], 2)
|
||||
self.assertEqual(len(result['errors']), 0)
|
||||
|
||||
# Verify entries were added to Identity.known_destinations
|
||||
self.assertEqual(len(mock_rns.Identity.known_destinations), 2)
|
||||
|
||||
# Verify the dict entries have correct structure
|
||||
dest_hash_1 = bytes.fromhex("aabbccdd" * 4)
|
||||
self.assertIn(dest_hash_1, mock_rns.Identity.known_destinations)
|
||||
entry = mock_rns.Identity.known_destinations[dest_hash_1]
|
||||
self.assertEqual(len(entry), 4) # [time, packet_hash, public_key, app_data]
|
||||
self.assertEqual(entry[2], public_key_1) # public key at index 2
|
||||
self.assertIsNone(entry[1]) # packet_hash is None
|
||||
self.assertIsNone(entry[3]) # app_data is None
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_with_json_string(self, mock_rns):
|
||||
"""Test bulk_restore_announce_identities with JSON string input"""
|
||||
import json
|
||||
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
announce_data = [
|
||||
{
|
||||
"destination_hash": "aabbccdd" * 4,
|
||||
"public_key": base64.b64encode(b'x' * 64).decode()
|
||||
}
|
||||
]
|
||||
json_string = json.dumps(announce_data)
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(json_string)
|
||||
|
||||
self.assertEqual(result['success_count'], 1)
|
||||
self.assertEqual(len(result['errors']), 0)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_missing_destination_hash(self, mock_rns):
|
||||
"""Test bulk_restore_announce_identities with missing destination_hash field"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
announce_data = [
|
||||
{"public_key": base64.b64encode(b'k' * 64).decode()} # Missing destination_hash
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
self.assertIn('missing destination_hash', result['errors'][0])
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_missing_public_key(self, mock_rns):
|
||||
"""Test bulk_restore_announce_identities with missing public_key field"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
announce_data = [
|
||||
{"destination_hash": "aabbccdd" * 4} # Missing public_key
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
self.assertIn('missing public_key', result['errors'][0])
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_invalid_hex(self, mock_rns):
|
||||
"""Test bulk_restore_announce_identities with invalid hex in destination_hash"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
announce_data = [
|
||||
{
|
||||
"destination_hash": "ZZZZZZZZ" * 4, # Invalid hex
|
||||
"public_key": base64.b64encode(b'k' * 64).decode()
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
self.assertIn('Error processing announce', result['errors'][0])
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_invalid_base64(self, mock_rns):
|
||||
"""Test bulk_restore_announce_identities with invalid base64 in public_key"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
announce_data = [
|
||||
{
|
||||
"destination_hash": "aabbccdd" * 4,
|
||||
"public_key": "!!!INVALID_BASE64!!!"
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
self.assertIn('Error processing announce', result['errors'][0])
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_empty_list(self, mock_rns):
|
||||
"""Test bulk_restore_announce_identities with empty list"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities([])
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 0)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_large_batch(self, mock_rns):
|
||||
"""Test bulk_restore_announce_identities with large batch (1000 announces)"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
# Create 1000 announces
|
||||
announce_data = []
|
||||
for i in range(1000):
|
||||
announce_data.append({
|
||||
"destination_hash": f"{i:032x}", # 32 hex chars
|
||||
"public_key": base64.b64encode(f"key{i:06d}".ljust(64, '0').encode()).decode()
|
||||
})
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
|
||||
# All should succeed
|
||||
self.assertEqual(result['success_count'], 1000)
|
||||
self.assertEqual(len(result['errors']), 0)
|
||||
self.assertEqual(len(mock_rns.Identity.known_destinations), 1000)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_partial_success(self, mock_rns):
|
||||
"""Test bulk_restore_announce_identities with mix of valid and invalid entries"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
announce_data = [
|
||||
{
|
||||
"destination_hash": "aabbccdd" * 4,
|
||||
"public_key": base64.b64encode(b'k' * 64).decode()
|
||||
},
|
||||
{
|
||||
"destination_hash": "INVALID_HEX",
|
||||
"public_key": base64.b64encode(b'm' * 64).decode()
|
||||
},
|
||||
{
|
||||
"destination_hash": "11223344" * 4,
|
||||
"public_key": base64.b64encode(b'n' * 64).decode()
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 2)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_populates_local_cache(self, mock_rns):
|
||||
"""Test that bulk_restore_announce_identities also populates wrapper.identities cache"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
announce_data = [
|
||||
{
|
||||
"destination_hash": "aabbccdd" * 4,
|
||||
"public_key": base64.b64encode(b'k' * 64).decode()
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 1)
|
||||
# Check local cache is populated
|
||||
dest_hash_hex = "aabbccdd" * 4
|
||||
self.assertIn(dest_hash_hex, wrapper.identities)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_wrong_public_key_size(self, mock_rns):
|
||||
"""Test bulk_restore_announce_identities with wrong public key size"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512 # Expects 64 byte keys
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
announce_data = [
|
||||
{
|
||||
"destination_hash": "aabbccdd" * 4,
|
||||
"public_key": base64.b64encode(b'short').decode() # Too short
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
|
||||
# Should fail due to wrong key size
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
self.assertIn('public key size', result['errors'][0].lower())
|
||||
|
||||
|
||||
class TestBulkRestorePeerIdentities(unittest.TestCase):
|
||||
"""Test the bulk_restore_peer_identities method for fast peer identity restoration"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
import tempfile
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures"""
|
||||
import shutil
|
||||
if os.path.exists(self.temp_dir):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', False)
|
||||
def test_bulk_restore_peers_reticulum_not_available(self):
|
||||
"""Test that bulk_restore_peer_identities returns error when Reticulum is not available"""
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
peer_data = [
|
||||
{"identity_hash": "aabbccdd" * 4, "public_key": base64.b64encode(b'key1' * 16).decode()}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(peer_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertIn('errors', result)
|
||||
self.assertEqual(result['errors'], ["Reticulum not available"])
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_success(self, mock_rns):
|
||||
"""Test successful bulk restore of peer identities"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
mock_rns.Identity.NAME_HASH_LENGTH = 80
|
||||
mock_rns.Identity.full_hash = lambda x: b'h' * 32 # Mock hash function
|
||||
mock_rns.Reticulum.TRUNCATED_HASHLENGTH = 128 # 16 bytes
|
||||
mock_rns.Identity.truncated_hash = lambda x: b't' * 16
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
peer_data = [
|
||||
{
|
||||
"identity_hash": "aabbccdd" * 4, # 32 hex chars = 16 bytes
|
||||
"public_key": base64.b64encode(b'k' * 64).decode()
|
||||
},
|
||||
{
|
||||
"identity_hash": "11223344" * 4,
|
||||
"public_key": base64.b64encode(b'm' * 64).decode()
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(peer_data)
|
||||
|
||||
# Both should succeed
|
||||
self.assertEqual(result['success_count'], 2)
|
||||
self.assertEqual(len(result['errors']), 0)
|
||||
|
||||
# Verify entries were added to Identity.known_destinations
|
||||
self.assertGreater(len(mock_rns.Identity.known_destinations), 0)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_with_json_string(self, mock_rns):
|
||||
"""Test bulk_restore_peer_identities with JSON string input"""
|
||||
import json
|
||||
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
mock_rns.Identity.NAME_HASH_LENGTH = 80
|
||||
mock_rns.Identity.full_hash = lambda x: b'h' * 32
|
||||
mock_rns.Reticulum.TRUNCATED_HASHLENGTH = 128
|
||||
mock_rns.Identity.truncated_hash = lambda x: b't' * 16
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
peer_data = [
|
||||
{
|
||||
"identity_hash": "aabbccdd" * 4,
|
||||
"public_key": base64.b64encode(b'x' * 64).decode()
|
||||
}
|
||||
]
|
||||
json_string = json.dumps(peer_data)
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(json_string)
|
||||
|
||||
self.assertEqual(result['success_count'], 1)
|
||||
self.assertEqual(len(result['errors']), 0)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_missing_identity_hash(self, mock_rns):
|
||||
"""Test bulk_restore_peer_identities with missing identity_hash field"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
peer_data = [
|
||||
{"public_key": base64.b64encode(b'k' * 64).decode()} # Missing identity_hash
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(peer_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
self.assertIn('missing identity_hash', result['errors'][0])
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_missing_public_key(self, mock_rns):
|
||||
"""Test bulk_restore_peer_identities with missing public_key field"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
peer_data = [
|
||||
{"identity_hash": "aabbccdd" * 4} # Missing public_key
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(peer_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
self.assertIn('missing public_key', result['errors'][0])
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_invalid_hex_in_identity_hash_is_ignored(self, mock_rns):
|
||||
"""Test bulk_restore_peer_identities ignores invalid hex in identity_hash field.
|
||||
|
||||
The identity_hash field is not validated because the implementation computes
|
||||
the actual identity hash from the public key (which is the source of truth).
|
||||
This is by design - the stored identity_hash may be stale/incorrect.
|
||||
"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
mock_rns.Identity.NAME_HASH_LENGTH = 80
|
||||
mock_rns.Identity.full_hash = lambda x: b'h' * 32
|
||||
mock_rns.Reticulum.TRUNCATED_HASHLENGTH = 128
|
||||
mock_rns.Identity.truncated_hash = lambda x: b't' * 16
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
peer_data = [
|
||||
{
|
||||
"identity_hash": "ZZZZZZZZ" * 4, # Invalid hex - ignored since we compute from public_key
|
||||
"public_key": base64.b64encode(b'k' * 64).decode()
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(peer_data)
|
||||
|
||||
# Should succeed - identity_hash is not validated, we compute from public_key
|
||||
self.assertEqual(result['success_count'], 1)
|
||||
self.assertEqual(len(result['errors']), 0)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_invalid_base64(self, mock_rns):
|
||||
"""Test bulk_restore_peer_identities with invalid base64 in public_key"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
peer_data = [
|
||||
{
|
||||
"identity_hash": "aabbccdd" * 4,
|
||||
"public_key": "!!!INVALID_BASE64!!!"
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(peer_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
self.assertIn('Error processing peer', result['errors'][0])
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_empty_list(self, mock_rns):
|
||||
"""Test bulk_restore_peer_identities with empty list"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities([])
|
||||
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 0)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_large_batch(self, mock_rns):
|
||||
"""Test bulk_restore_peer_identities with large batch (1000 peers)"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
mock_rns.Identity.NAME_HASH_LENGTH = 80
|
||||
mock_rns.Identity.full_hash = lambda x: b'h' * 32
|
||||
mock_rns.Reticulum.TRUNCATED_HASHLENGTH = 128
|
||||
mock_rns.Identity.truncated_hash = lambda x: b't' * 16
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
# Create 1000 peers
|
||||
peer_data = []
|
||||
for i in range(1000):
|
||||
peer_data.append({
|
||||
"identity_hash": f"{i:032x}", # 32 hex chars
|
||||
"public_key": base64.b64encode(f"key{i:06d}".ljust(64, '0').encode()).decode()
|
||||
})
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(peer_data)
|
||||
|
||||
# All should succeed
|
||||
self.assertEqual(result['success_count'], 1000)
|
||||
self.assertEqual(len(result['errors']), 0)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_partial_success(self, mock_rns):
|
||||
"""Test bulk_restore_peer_identities with mix of valid and invalid entries.
|
||||
|
||||
Note: Invalid identity_hash does NOT cause failure - we compute from public_key.
|
||||
Only invalid public_key (bad base64) causes failure.
|
||||
"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
mock_rns.Identity.NAME_HASH_LENGTH = 80
|
||||
mock_rns.Identity.full_hash = lambda x: b'h' * 32
|
||||
mock_rns.Reticulum.TRUNCATED_HASHLENGTH = 128
|
||||
mock_rns.Identity.truncated_hash = lambda x: b't' * 16
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
peer_data = [
|
||||
{
|
||||
"identity_hash": "aabbccdd" * 4,
|
||||
"public_key": base64.b64encode(b'k' * 64).decode()
|
||||
},
|
||||
{
|
||||
"identity_hash": "11223344" * 4,
|
||||
"public_key": "INVALID_BASE64!!!" # This causes failure
|
||||
},
|
||||
{
|
||||
"identity_hash": "55667788" * 4,
|
||||
"public_key": base64.b64encode(b'n' * 64).decode()
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(peer_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 2)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_populates_local_cache(self, mock_rns):
|
||||
"""Test that bulk_restore_peer_identities also populates wrapper.identities cache"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
mock_rns.Identity.NAME_HASH_LENGTH = 80
|
||||
mock_rns.Identity.full_hash = lambda x: b'h' * 32
|
||||
mock_rns.Reticulum.TRUNCATED_HASHLENGTH = 128
|
||||
mock_rns.Identity.truncated_hash = lambda x: b't' * 16
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
peer_data = [
|
||||
{
|
||||
"identity_hash": "aabbccdd" * 4,
|
||||
"public_key": base64.b64encode(b'k' * 64).decode()
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(peer_data)
|
||||
|
||||
self.assertEqual(result['success_count'], 1)
|
||||
# Check local cache is populated (either by identity_hash or computed dest_hash)
|
||||
self.assertGreater(len(wrapper.identities), 0)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_peers_wrong_public_key_size(self, mock_rns):
|
||||
"""Test bulk_restore_peer_identities with wrong public key size"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512 # Expects 64 byte keys
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
peer_data = [
|
||||
{
|
||||
"identity_hash": "aabbccdd" * 4,
|
||||
"public_key": base64.b64encode(b'short').decode() # Too short
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_peer_identities(peer_data)
|
||||
|
||||
# Should fail due to wrong key size
|
||||
self.assertEqual(result['success_count'], 0)
|
||||
self.assertEqual(len(result['errors']), 1)
|
||||
self.assertIn('public key size', result['errors'][0].lower())
|
||||
|
||||
|
||||
class TestBulkRestoreEquivalence(unittest.TestCase):
|
||||
"""Test that bulk restore produces equivalent results to individual store"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures"""
|
||||
import tempfile
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures"""
|
||||
import shutil
|
||||
if os.path.exists(self.temp_dir):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
@patch('reticulum_wrapper.RNS')
|
||||
@patch('reticulum_wrapper.RETICULUM_AVAILABLE', True)
|
||||
def test_bulk_restore_announces_same_format_as_remember(self, mock_rns):
|
||||
"""Test that bulk_restore_announce_identities produces same dict format as Identity.remember()"""
|
||||
mock_rns.Identity.known_destinations = {}
|
||||
mock_rns.Identity.KEYSIZE = 512
|
||||
|
||||
wrapper = reticulum_wrapper.ReticulumWrapper(self.temp_dir)
|
||||
|
||||
public_key = b'k' * 64
|
||||
announce_data = [
|
||||
{
|
||||
"destination_hash": "aabbccdd" * 4,
|
||||
"public_key": base64.b64encode(public_key).decode()
|
||||
}
|
||||
]
|
||||
|
||||
result = wrapper.bulk_restore_announce_identities(announce_data)
|
||||
self.assertEqual(result['success_count'], 1)
|
||||
|
||||
# Check the format matches what Identity.remember() would produce
|
||||
dest_hash = bytes.fromhex("aabbccdd" * 4)
|
||||
entry = mock_rns.Identity.known_destinations[dest_hash]
|
||||
|
||||
# Format should be: [timestamp, packet_hash, public_key, app_data]
|
||||
self.assertIsInstance(entry[0], float) # timestamp
|
||||
self.assertIsNone(entry[1]) # packet_hash
|
||||
self.assertEqual(entry[2], public_key) # public_key
|
||||
self.assertIsNone(entry[3]) # app_data
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run tests with verbose output
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
Reference in New Issue
Block a user