diff --git a/nostr_core_lib b/nostr_core_lib index f3068f8..a8dc2ed 160000 --- a/nostr_core_lib +++ b/nostr_core_lib @@ -1 +1 @@ -Subproject commit f3068f82f3ba5f9f8680af33a1081a9d6e92810d +Subproject commit a8dc2ed0464f4220ef306fa1ce07673dd4e94880 diff --git a/relay.pid b/relay.pid index 62f7fdf..d5cf0d0 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -3580654 +84610 diff --git a/src/config.c b/src/config.c index b32f27a..391fded 100644 --- a/src/config.c +++ b/src/config.c @@ -9,6 +9,9 @@ #ifdef VERSION #undef VERSION #endif +#ifdef VERSION_MAJOR +#undef VERSION_MAJOR +#endif #ifdef VERSION_MINOR #undef VERSION_MINOR #endif diff --git a/src/main.h b/src/main.h index 608d2a7..2fd15d8 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,12 @@ // Version information (auto-updated by build system) #define VERSION_MAJOR 1 #define VERSION_MINOR 0 -#define VERSION_PATCH 3 -#define VERSION "v1.0.3" +#define VERSION_PATCH 4 +#define VERSION "v1.0.4" + +// Avoid VERSION_MAJOR redefinition warning from nostr_core_lib +#undef VERSION_MAJOR +#define VERSION_MAJOR 1 // Relay metadata (authoritative source for NIP-11 information) #define RELAY_NAME "C-Relay" diff --git a/src/websockets.c b/src/websockets.c index 67faecf..4f555e4 100644 --- a/src/websockets.c +++ b/src/websockets.c @@ -471,18 +471,717 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso case LWS_CALLBACK_RECEIVE: if (len > 0) { + DEBUG_TRACE("LWS_CALLBACK_RECEIVE: received %zu bytes", len); + // Check if client is rate limited for malformed requests if (is_client_rate_limited_for_malformed_requests(pss)) { send_notice_message(wsi, "error: too many malformed requests - temporarily blocked"); return 0; } + // Check if this is a fragmented message + int is_first_fragment = lws_is_first_fragment(wsi); + int is_final_fragment = lws_is_final_fragment(wsi); + size_t remaining_payload = lws_remaining_packet_payload(wsi); + + DEBUG_TRACE("Fragment info: first=%d, final=%d, remaining=%zu, reassembly_active=%d", + is_first_fragment, is_final_fragment, remaining_payload, pss->reassembly_active); + + // Handle message reassembly for fragmented messages + // Only use reassembly if message is actually fragmented (not both first and final) + int is_fragmented = (is_first_fragment && !is_final_fragment) || pss->reassembly_active; + if (is_fragmented) { + // Start or continue reassembly + if (is_first_fragment) { + // First fragment - initialize reassembly buffer + if (pss->reassembly_buffer) { + DEBUG_WARN("Starting new reassembly but buffer already exists - cleaning up"); + free(pss->reassembly_buffer); + } + pss->reassembly_buffer = NULL; + pss->reassembly_size = 0; + pss->reassembly_capacity = 0; + pss->reassembly_active = 1; + DEBUG_TRACE("Starting message reassembly"); + } + + // Ensure buffer has enough capacity + size_t needed_capacity = pss->reassembly_size + len + 1; // +1 for null terminator + if (needed_capacity > pss->reassembly_capacity) { + size_t new_capacity = pss->reassembly_capacity == 0 ? 8192 : pss->reassembly_capacity * 2; + while (new_capacity < needed_capacity) { + new_capacity *= 2; + } + + char* new_buffer = realloc(pss->reassembly_buffer, new_capacity); + if (!new_buffer) { + DEBUG_ERROR("Failed to allocate reassembly buffer (capacity %zu)", new_capacity); + // Clean up and abort reassembly + free(pss->reassembly_buffer); + pss->reassembly_buffer = NULL; + pss->reassembly_size = 0; + pss->reassembly_capacity = 0; + pss->reassembly_active = 0; + send_notice_message(wsi, "error: message too large - memory allocation failed"); + return 0; + } + pss->reassembly_buffer = new_buffer; + pss->reassembly_capacity = new_capacity; + DEBUG_TRACE("Expanded reassembly buffer to %zu bytes", new_capacity); + } + + // Append fragment to buffer + memcpy(pss->reassembly_buffer + pss->reassembly_size, in, len); + pss->reassembly_size += len; + + // Check if this is the final fragment + if (is_final_fragment) { + // Message complete - process it + pss->reassembly_buffer[pss->reassembly_size] = '\0'; + pss->reassembly_active = 0; + + DEBUG_TRACE("Message reassembly complete: total size %zu bytes", pss->reassembly_size); + + // Process the complete message + char* complete_message = pss->reassembly_buffer; + size_t message_len = pss->reassembly_size; + + // Reset reassembly state (but keep buffer for reuse if needed) + pss->reassembly_size = 0; + + // Parse JSON message + DEBUG_TRACE("Parsing reassembled JSON message of length %zu", message_len); + cJSON* json = cJSON_Parse(complete_message); + + // Process the message (same logic as before) + if (json && cJSON_IsArray(json)) { + // Get message type + cJSON* type = cJSON_GetArrayItem(json, 0); + if (type && cJSON_IsString(type)) { + const char* msg_type = cJSON_GetStringValue(type); + + if (strcmp(msg_type, "EVENT") == 0) { + // Handle EVENT message + cJSON* event = cJSON_GetArrayItem(json, 1); + if (event && cJSON_IsObject(event)) { + // Extract event JSON string for unified validator + char *event_json_str = cJSON_Print(event); + if (!event_json_str) { + DEBUG_ERROR("Failed to serialize event JSON for validation"); + cJSON* error_response = cJSON_CreateArray(); + cJSON_AddItemToArray(error_response, cJSON_CreateString("OK")); + cJSON_AddItemToArray(error_response, cJSON_CreateString("unknown")); + cJSON_AddItemToArray(error_response, cJSON_CreateBool(0)); + cJSON_AddItemToArray(error_response, cJSON_CreateString("error: failed to process event")); + + char *error_str = cJSON_Print(error_response); + if (error_str) { + size_t error_len = strlen(error_str); + // Use proper message queue system instead of direct lws_write + if (queue_message(wsi, pss, error_str, error_len, LWS_WRITE_TEXT) != 0) { + DEBUG_ERROR("Failed to queue error response message"); + } + free(error_str); + } + cJSON_Delete(error_response); + cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + return 0; + } + + // Call unified validator with JSON string + size_t event_json_len = strlen(event_json_str); + int validation_result = nostr_validate_unified_request(event_json_str, event_json_len); + + // Map validation result to old result format (0 = success, -1 = failure) + int result = (validation_result == NOSTR_SUCCESS) ? 0 : -1; + + // Generate error message based on validation result + char error_message[512] = {0}; + if (result != 0) { + switch (validation_result) { + case NOSTR_ERROR_INVALID_INPUT: + strncpy(error_message, "invalid: malformed event structure", sizeof(error_message) - 1); + break; + case NOSTR_ERROR_EVENT_INVALID_SIGNATURE: + strncpy(error_message, "invalid: signature verification failed", sizeof(error_message) - 1); + break; + case NOSTR_ERROR_EVENT_INVALID_ID: + strncpy(error_message, "invalid: event id verification failed", sizeof(error_message) - 1); + break; + case NOSTR_ERROR_EVENT_INVALID_PUBKEY: + strncpy(error_message, "invalid: invalid pubkey format", sizeof(error_message) - 1); + break; + case -103: // NOSTR_ERROR_EVENT_EXPIRED + strncpy(error_message, "rejected: event expired", sizeof(error_message) - 1); + break; + case -102: // NOSTR_ERROR_NIP42_DISABLED + strncpy(error_message, "auth-required: NIP-42 authentication required", sizeof(error_message) - 1); + break; + case -101: // NOSTR_ERROR_AUTH_REQUIRED + strncpy(error_message, "blocked: pubkey not authorized", sizeof(error_message) - 1); + break; + default: + strncpy(error_message, "error: validation failed", sizeof(error_message) - 1); + break; + } + } + + // Cleanup event JSON string + free(event_json_str); + + // Check for NIP-70 protected events + if (result == 0) { + // Check if event has protected tag ["-"] + int is_protected_event = 0; + cJSON* tags = cJSON_GetObjectItem(event, "tags"); + if (tags && cJSON_IsArray(tags)) { + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags) { + if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 1) { + cJSON* tag_name = cJSON_GetArrayItem(tag, 0); + if (tag_name && cJSON_IsString(tag_name) && + strcmp(cJSON_GetStringValue(tag_name), "-") == 0) { + is_protected_event = 1; + break; + } + } + } + } + + if (is_protected_event) { + // Check if protected events are enabled using config + int protected_events_enabled = get_config_bool("nip70_protected_events_enabled", 0); + + if (!protected_events_enabled) { + // Protected events not supported + result = -1; + strncpy(error_message, "blocked: protected events not supported", sizeof(error_message) - 1); + error_message[sizeof(error_message) - 1] = '\0'; + DEBUG_WARN("Protected event rejected: protected events not enabled"); + } else { + // Protected events enabled - check authentication + cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); + const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; + + if (!pss || !pss->authenticated || + !event_pubkey || strcmp(pss->authenticated_pubkey, event_pubkey) != 0) { + // Not authenticated or pubkey mismatch + result = -1; + strncpy(error_message, "auth-required: protected event requires authentication", sizeof(error_message) - 1); + error_message[sizeof(error_message) - 1] = '\0'; + DEBUG_WARN("Protected event rejected: authentication required"); + } + } + } + } + + // Check for admin events (kind 23456) and intercept them + if (result == 0) { + cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); + if (kind_obj && cJSON_IsNumber(kind_obj)) { + int event_kind = (int)cJSON_GetNumberValue(kind_obj); + + DEBUG_TRACE("Processing event kind %d, message length: %zu", event_kind, message_len); + + // Log reception of Kind 23456 events + if (event_kind == 23456) { + DEBUG_LOG("Admin event (kind 23456) received"); + } + + if (event_kind == 23456) { + // Enhanced admin event security - check authorization first + + char auth_error[512] = {0}; + int auth_result = is_authorized_admin_event(event, auth_error, sizeof(auth_error)); + + if (auth_result != 0) { + // Authorization failed - log and reject + DEBUG_WARN("Admin event authorization failed"); + result = -1; + size_t error_len = strlen(auth_error); + size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; + memcpy(error_message, auth_error, copy_len); + error_message[copy_len] = '\0'; + } else { + // Authorization successful - process through admin API + + char admin_error[512] = {0}; + int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi); + + // Log results for Kind 23456 events + if (event_kind == 23456) { + if (admin_result != 0) { + char error_result_msg[512]; + if (strlen(admin_error) > 0) { + // Safely truncate admin_error if too long + size_t max_error_len = sizeof(error_result_msg) - 50; // Leave room for prefix + size_t error_len = strlen(admin_error); + if (error_len > max_error_len) { + error_len = max_error_len; + } + char truncated_error[512]; + memcpy(truncated_error, admin_error, error_len); + truncated_error[error_len] = '\0'; + + // Use a safer approach to avoid truncation warning + size_t prefix_len = snprintf(error_result_msg, sizeof(error_result_msg), + "ERROR: Kind %d event processing failed: ", event_kind); + if (prefix_len < sizeof(error_result_msg)) { + size_t remaining = sizeof(error_result_msg) - prefix_len; + size_t copy_len = strlen(truncated_error); + if (copy_len >= remaining) { + copy_len = remaining - 1; + } + memcpy(error_result_msg + prefix_len, truncated_error, copy_len); + error_result_msg[prefix_len + copy_len] = '\0'; + } + } else { + snprintf(error_result_msg, sizeof(error_result_msg), + "ERROR: Kind %d event processing failed", event_kind); + } + DEBUG_ERROR(error_result_msg); + } + } + + if (admin_result != 0) { + DEBUG_ERROR("Failed to process admin event"); + result = -1; + size_t error_len = strlen(admin_error); + size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; + memcpy(error_message, admin_error, copy_len); + error_message[copy_len] = '\0'; + } else { + // Admin events are processed by the admin API, not broadcast to subscriptions + } + } + } else if (event_kind == 1059) { + // Check for NIP-17 gift wrap admin messages + + char nip17_error[512] = {0}; + cJSON* response_event = process_nip17_admin_message(event, nip17_error, sizeof(nip17_error), wsi); + + if (!response_event) { + // Check if this is an error or if the command was already handled + if (strlen(nip17_error) > 0) { + // There was an actual error + DEBUG_ERROR("NIP-17 admin message processing failed"); + result = -1; + size_t error_len = strlen(nip17_error); + size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; + memcpy(error_message, nip17_error, copy_len); + error_message[copy_len] = '\0'; + + } else { + // No error message means the command was already handled (plain text commands) + // Store the original gift wrap event in database + if (store_event(event) != 0) { + DEBUG_ERROR("Failed to store gift wrap event in database"); + result = -1; + strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1); + } + } + } else { + // Store the original gift wrap event in database (unlike kind 23456) + if (store_event(event) != 0) { + DEBUG_ERROR("Failed to store gift wrap event in database"); + result = -1; + strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1); + cJSON_Delete(response_event); + } else { + // Broadcast RESPONSE event to matching persistent subscriptions + broadcast_event_to_subscriptions(response_event); + + // Clean up response event + cJSON_Delete(response_event); + } + } + } else if (event_kind == 14) { + // Check for DM stats commands addressed to relay + + char dm_error[512] = {0}; + int dm_result = process_dm_stats_command(event, dm_error, sizeof(dm_error), wsi); + + if (dm_result != 0) { + DEBUG_ERROR("DM stats command processing failed"); + result = -1; + size_t error_len = strlen(dm_error); + size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; + memcpy(error_message, dm_error, copy_len); + error_message[copy_len] = '\0'; + + } else { + // Store the DM event in database + if (store_event(event) != 0) { + DEBUG_ERROR("Failed to store DM event in database"); + result = -1; + strncpy(error_message, "error: failed to store DM event", sizeof(error_message) - 1); + } else { + // Broadcast DM event to matching persistent subscriptions + broadcast_event_to_subscriptions(event); + } + } + } else { + // Check if this is an ephemeral event (kinds 20000-29999) + // Per NIP-01: ephemeral events are broadcast but never stored + if (event_kind >= 20000 && event_kind < 30000) { + DEBUG_TRACE("Ephemeral event (kind %d) - broadcasting without storage", event_kind); + // Broadcast directly to subscriptions without database storage + broadcast_event_to_subscriptions(event); + } else { + DEBUG_TRACE("Storing regular event in database"); + // Regular event - store in database and broadcast + if (store_event(event) != 0) { + DEBUG_ERROR("Failed to store event in database"); + result = -1; + strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); + } else { + DEBUG_LOG("Event stored and broadcast (kind %d)", event_kind); + // Broadcast event to matching persistent subscriptions + broadcast_event_to_subscriptions(event); + } + } + } + } else { + // Event without valid kind - try normal storage + DEBUG_WARN("Event without valid kind - trying normal storage"); + if (store_event(event) != 0) { + DEBUG_ERROR("Failed to store event without kind in database"); + result = -1; + strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); + } else { + broadcast_event_to_subscriptions(event); + } + } + } + + // Send OK response + cJSON* event_id = cJSON_GetObjectItem(event, "id"); + if (event_id && cJSON_IsString(event_id)) { + cJSON* response = cJSON_CreateArray(); + cJSON_AddItemToArray(response, cJSON_CreateString("OK")); + cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id))); + cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0)); + cJSON_AddItemToArray(response, cJSON_CreateString(strlen(error_message) > 0 ? error_message : "")); + + char *response_str = cJSON_Print(response); + if (response_str) { + size_t response_len = strlen(response_str); + + // DEBUG: Log WebSocket frame details before sending + DEBUG_TRACE("WS_FRAME_SEND: type=OK len=%zu data=%.100s%s", + response_len, + response_str, + response_len > 100 ? "..." : ""); + + // Queue message for proper libwebsockets pattern + if (queue_message(wsi, pss, response_str, response_len, LWS_WRITE_TEXT) != 0) { + DEBUG_ERROR("Failed to queue OK response message"); + } + + free(response_str); + } + cJSON_Delete(response); + } + } + } else if (strcmp(msg_type, "REQ") == 0) { + DEBUG_TRACE("REQ message received, starting processing"); + + // Check NIP-42 authentication for REQ subscriptions if required + if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) { + DEBUG_TRACE("REQ rejected: NIP-42 authentication required"); + if (!pss->auth_challenge_sent) { + send_nip42_auth_challenge(wsi, pss); + } else { + send_notice_message(wsi, "NIP-42 authentication required for subscriptions"); + DEBUG_WARN("REQ rejected: NIP-42 authentication required"); + } + cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + return 0; + } + + DEBUG_TRACE("REQ message passed authentication check"); + + // Handle REQ message + cJSON* sub_id = cJSON_GetArrayItem(json, 1); + + if (sub_id && cJSON_IsString(sub_id)) { + const char* subscription_id = cJSON_GetStringValue(sub_id); + + DEBUG_TRACE("Processing REQ message for subscription %s", subscription_id); + + // Validate subscription ID before processing + if (!subscription_id) { + DEBUG_TRACE("REQ rejected: NULL subscription ID"); + send_notice_message(wsi, "error: invalid subscription ID"); + DEBUG_WARN("REQ rejected: NULL subscription ID"); + record_malformed_request(pss); + cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + return 0; + } + + // Validate subscription ID + if (!validate_subscription_id(subscription_id)) { + DEBUG_TRACE("REQ rejected: invalid subscription ID format"); + send_notice_message(wsi, "error: invalid subscription ID"); + DEBUG_WARN("REQ rejected: invalid subscription ID"); + cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + return 0; + } + + DEBUG_TRACE("REQ subscription ID validated: %s", subscription_id); + + // Create array of filter objects from position 2 onwards + cJSON* filters = cJSON_CreateArray(); + if (!filters) { + DEBUG_TRACE("REQ failed: could not create filters array"); + send_notice_message(wsi, "error: failed to process filters"); + DEBUG_ERROR("REQ failed: could not create filters array"); + cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + return 0; + } + + int json_size = cJSON_GetArraySize(json); + int filter_count = 0; + for (int i = 2; i < json_size; i++) { + cJSON* filter = cJSON_GetArrayItem(json, i); + if (filter) { + cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1)); + filter_count++; + } + } + + DEBUG_TRACE("REQ created %d filters from message", filter_count); + + // Validate filters before processing + char filter_error[512] = {0}; + if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) { + DEBUG_TRACE("REQ rejected: filter validation failed - %s", filter_error); + send_notice_message(wsi, filter_error); + DEBUG_WARN("REQ rejected: invalid filters"); + record_malformed_request(pss); + cJSON_Delete(filters); + cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + return 0; + } + + DEBUG_TRACE("REQ filters validated successfully"); + + DEBUG_TRACE("About to call handle_req_message for subscription %s", subscription_id); + handle_req_message(subscription_id, filters, wsi, pss); + DEBUG_TRACE("handle_req_message completed for subscription %s", subscription_id); + + // Clean up the filters array we created + cJSON_Delete(filters); + + DEBUG_LOG("REQ subscription %s processed, sending EOSE", subscription_id); + + // Send EOSE (End of Stored Events) + cJSON* eose_response = cJSON_CreateArray(); + if (eose_response) { + cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE")); + cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id)); + + char *eose_str = cJSON_Print(eose_response); + if (eose_str) { + size_t eose_len = strlen(eose_str); + + // DEBUG: Log WebSocket frame details before sending + DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s", + eose_len, + eose_str, + eose_len > 100 ? "..." : ""); + + // Queue message for proper libwebsockets pattern + if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) { + DEBUG_ERROR("Failed to queue EOSE message"); + } + + free(eose_str); + } + cJSON_Delete(eose_response); + } + } else { + send_notice_message(wsi, "error: missing or invalid subscription ID in REQ"); + DEBUG_WARN("REQ rejected: missing or invalid subscription ID"); + } + } else if (strcmp(msg_type, "COUNT") == 0) { + // Check NIP-42 authentication for COUNT requests if required + if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) { + if (!pss->auth_challenge_sent) { + send_nip42_auth_challenge(wsi, pss); + } else { + send_notice_message(wsi, "NIP-42 authentication required for count requests"); + DEBUG_WARN("COUNT rejected: NIP-42 authentication required"); + } + cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + return 0; + } + + // Handle COUNT message + cJSON* sub_id = cJSON_GetArrayItem(json, 1); + + if (sub_id && cJSON_IsString(sub_id)) { + const char* subscription_id = cJSON_GetStringValue(sub_id); + + // Create array of filter objects from position 2 onwards + cJSON* filters = cJSON_CreateArray(); + int json_size = cJSON_GetArraySize(json); + for (int i = 2; i < json_size; i++) { + cJSON* filter = cJSON_GetArrayItem(json, i); + if (filter) { + cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1)); + } + } + + // Validate filters before processing + char filter_error[512] = {0}; + if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) { + send_notice_message(wsi, filter_error); + DEBUG_WARN("COUNT rejected: invalid filters"); + record_malformed_request(pss); + cJSON_Delete(filters); + cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + return 0; + } + + handle_count_message(subscription_id, filters, wsi, pss); + + // Clean up the filters array we created + cJSON_Delete(filters); + } + } else if (strcmp(msg_type, "CLOSE") == 0) { + // Handle CLOSE message + cJSON* sub_id = cJSON_GetArrayItem(json, 1); + if (sub_id && cJSON_IsString(sub_id)) { + const char* subscription_id = cJSON_GetStringValue(sub_id); + + // Validate subscription ID before processing + if (!subscription_id) { + send_notice_message(wsi, "error: invalid subscription ID in CLOSE"); + DEBUG_WARN("CLOSE rejected: NULL subscription ID"); + cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + return 0; + } + + // Validate subscription ID + if (!validate_subscription_id(subscription_id)) { + send_notice_message(wsi, "error: invalid subscription ID in CLOSE"); + DEBUG_WARN("CLOSE rejected: invalid subscription ID"); + cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + return 0; + } + + // CRITICAL FIX: Mark subscription as inactive in global manager FIRST + // This prevents other threads from accessing it during removal + pthread_mutex_lock(&g_subscription_manager.subscriptions_lock); + + subscription_t* target_sub = g_subscription_manager.active_subscriptions; + while (target_sub) { + if (strcmp(target_sub->id, subscription_id) == 0 && target_sub->wsi == wsi) { + target_sub->active = 0; // Mark as inactive immediately + break; + } + target_sub = target_sub->next; + } + + pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock); + + // Now safe to remove from session list + if (pss) { + pthread_mutex_lock(&pss->session_lock); + + struct subscription** current = &pss->subscriptions; + while (*current) { + if (strcmp((*current)->id, subscription_id) == 0) { + struct subscription* to_remove = *current; + *current = to_remove->session_next; + pss->subscription_count--; + break; + } + current = &((*current)->session_next); + } + + pthread_mutex_unlock(&pss->session_lock); + } + + // Finally remove from global manager (which will free it) + remove_subscription_from_manager(subscription_id, wsi); + + // Subscription closed + } else { + send_notice_message(wsi, "error: missing or invalid subscription ID in CLOSE"); + DEBUG_WARN("CLOSE rejected: missing or invalid subscription ID"); + } + } else if (strcmp(msg_type, "AUTH") == 0) { + // Handle NIP-42 AUTH message + if (cJSON_GetArraySize(json) >= 2) { + cJSON* auth_payload = cJSON_GetArrayItem(json, 1); + + if (cJSON_IsString(auth_payload)) { + // AUTH challenge response: ["AUTH", ] (unusual) + handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload)); + } else if (cJSON_IsObject(auth_payload)) { + // AUTH signed event: ["AUTH", ] (standard NIP-42) + handle_nip42_auth_signed_event(wsi, pss, auth_payload); + } else { + send_notice_message(wsi, "Invalid AUTH message format"); + DEBUG_WARN("Received AUTH message with invalid payload type"); + } + } else { + send_notice_message(wsi, "AUTH message requires payload"); + DEBUG_WARN("Received AUTH message without payload"); + } + } else { + // Unknown message type + char unknown_msg[128]; + snprintf(unknown_msg, sizeof(unknown_msg), "Unknown message type: %.32s", msg_type); + DEBUG_WARN(unknown_msg); + send_notice_message(wsi, "Unknown message type"); + } + } + } + + // Clean up the reassembled message + if (json) cJSON_Delete(json); + // Note: complete_message points to reassembly_buffer, which is managed separately + // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED + + return 0; // Fragmented message processed + } else { + // Not the final fragment - continue accumulating + DEBUG_TRACE("Accumulated %zu bytes so far, waiting for more fragments", pss->reassembly_size); + return 0; + } + } + + // Handle non-fragmented messages (original code path) char *message = malloc(len + 1); if (message) { memcpy(message, in, len); message[len] = '\0'; // Parse JSON message (this is the normal program flow) + DEBUG_TRACE("Parsing JSON message of length %zu", strlen(message)); cJSON* json = cJSON_Parse(message); if (json && cJSON_IsArray(json)) { // Get message type @@ -682,7 +1381,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso if (kind_obj && cJSON_IsNumber(kind_obj)) { int event_kind = (int)cJSON_GetNumberValue(kind_obj); - DEBUG_TRACE("Processing event kind %d", event_kind); + DEBUG_TRACE("Processing event kind %d, message length: %zu", event_kind, strlen(message)); // Log reception of Kind 23456 events if (event_kind == 23456) { @@ -885,10 +1584,13 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso } } } else if (strcmp(msg_type, "REQ") == 0) { + DEBUG_TRACE("REQ message received, starting processing"); + // Log the full REQ message for debugging // Check NIP-42 authentication for REQ subscriptions if required if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) { + DEBUG_TRACE("REQ rejected: NIP-42 authentication required"); if (!pss->auth_challenge_sent) { send_nip42_auth_challenge(wsi, pss); } else { @@ -900,6 +1602,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso return 0; } + DEBUG_TRACE("REQ message passed authentication check"); + // Handle REQ message cJSON* sub_id = cJSON_GetArrayItem(json, 1); @@ -910,6 +1614,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso // Validate subscription ID before processing if (!subscription_id) { + DEBUG_TRACE("REQ rejected: NULL subscription ID"); send_notice_message(wsi, "error: invalid subscription ID"); DEBUG_WARN("REQ rejected: NULL subscription ID"); record_malformed_request(pss); @@ -920,6 +1625,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso // Validate subscription ID if (!validate_subscription_id(subscription_id)) { + DEBUG_TRACE("REQ rejected: invalid subscription ID format"); send_notice_message(wsi, "error: invalid subscription ID"); DEBUG_WARN("REQ rejected: invalid subscription ID"); cJSON_Delete(json); @@ -927,9 +1633,12 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso return 0; } + DEBUG_TRACE("REQ subscription ID validated: %s", subscription_id); + // Create array of filter objects from position 2 onwards cJSON* filters = cJSON_CreateArray(); if (!filters) { + DEBUG_TRACE("REQ failed: could not create filters array"); send_notice_message(wsi, "error: failed to process filters"); DEBUG_ERROR("REQ failed: could not create filters array"); cJSON_Delete(json); @@ -938,16 +1647,21 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso } int json_size = cJSON_GetArraySize(json); + int filter_count = 0; for (int i = 2; i < json_size; i++) { cJSON* filter = cJSON_GetArrayItem(json, i); if (filter) { cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1)); + filter_count++; } } + DEBUG_TRACE("REQ created %d filters from message", filter_count); + // Validate filters before processing char filter_error[512] = {0}; if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) { + DEBUG_TRACE("REQ rejected: filter validation failed - %s", filter_error); send_notice_message(wsi, filter_error); DEBUG_WARN("REQ rejected: invalid filters"); record_malformed_request(pss); @@ -957,7 +1671,11 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso return 0; } + DEBUG_TRACE("REQ filters validated successfully"); + + DEBUG_TRACE("About to call handle_req_message for subscription %s", subscription_id); handle_req_message(subscription_id, filters, wsi, pss); + DEBUG_TRACE("handle_req_message completed for subscription %s", subscription_id); // Clean up the filters array we created cJSON_Delete(filters); @@ -1191,6 +1909,15 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso pss->message_queue_count = 0; pss->writeable_requested = 0; + // Clean up message reassembly buffer + if (pss->reassembly_buffer) { + free(pss->reassembly_buffer); + pss->reassembly_buffer = NULL; + } + pss->reassembly_size = 0; + pss->reassembly_capacity = 0; + pss->reassembly_active = 0; + // Clean up session subscriptions - copy IDs first to avoid use-after-free pthread_mutex_lock(&pss->session_lock); diff --git a/src/websockets.h b/src/websockets.h index f561308..eae8465 100644 --- a/src/websockets.h +++ b/src/websockets.h @@ -21,10 +21,10 @@ // Filter validation constants #define MAX_FILTERS_PER_REQUEST 10 -#define MAX_AUTHORS_PER_FILTER 100 -#define MAX_IDS_PER_FILTER 100 -#define MAX_KINDS_PER_FILTER 50 -#define MAX_TAG_VALUES_PER_FILTER 100 +#define MAX_AUTHORS_PER_FILTER 1000 +#define MAX_IDS_PER_FILTER 1000 +#define MAX_KINDS_PER_FILTER 500 +#define MAX_TAG_VALUES_PER_FILTER 1000 #define MAX_KIND_VALUE 65535 #define MAX_TIMESTAMP_VALUE 2147483647 // Max 32-bit signed int #define MAX_LIMIT_VALUE 5000 @@ -73,6 +73,12 @@ struct per_session_data { struct message_queue_node* message_queue_tail; // Tail of message queue int message_queue_count; // Number of messages in queue int writeable_requested; // Flag: 1 if writeable callback requested + + // Message reassembly for handling fragmented WebSocket messages + char* reassembly_buffer; // Buffer for accumulating message fragments (NULL when not reassembling) + size_t reassembly_size; // Current size of accumulated data + size_t reassembly_capacity; // Allocated capacity of reassembly buffer + int reassembly_active; // Flag: 1 if currently reassembling a message }; // NIP-11 HTTP session data structure for managing buffer lifetime diff --git a/tests/test_requests.mjs b/tests/test_requests.mjs new file mode 100644 index 0000000..c9afe23 --- /dev/null +++ b/tests/test_requests.mjs @@ -0,0 +1,747 @@ + +/** + * Nostr Relay Pubkey Filter Test + * Tests how many pubkeys different relays can handle in a single filter request + */ + +import { WebSocket } from 'ws'; + +// Configuration +const RELAYS = [ + // "wss://relay.laantungir.net" + "ws://127.0.0.1:8888" +]; + +// Test parameters +const STEP_SIZE = 25; // Increment pubkey count by 25 each test +const MAX_PUBKEYS = 500; // Maximum pubkeys to test +const EVENT_KIND = 1; // Kind 1 = text notes +const EVENT_LIMIT = 2; // Only request 2 events per test + +// Generate test pubkey arrays of increasing sizes +function generateTestPubkeyArrays() { + const testArrays = []; + for (let count = STEP_SIZE; count <= MAX_PUBKEYS; count += STEP_SIZE) { + testArrays.push(PUBKEYS.slice(0, count)); + } + return testArrays; +} + +const PUBKEYS = [ + "85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204", + "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", + "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57", + "2645caf5706a31767c921532975a079f85950e1006bd5065f5dd0213e6848a96", + "8fe3f243e91121818107875d51bca4f3fcf543437aa9715150ec8036358939c5", + "83e818dfbeccea56b0f551576b3fd39a7a50e1d8159343500368fa085ccd964b", + "a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98", + "e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411", + "2cde0e02bda47eaeeed65e341619cc5f2afce990164669da4e1e5989180a96b9", + "edcd20558f17d99327d841e4582f9b006331ac4010806efa020ef0d40078e6da", + "34d2f5274f1958fcd2cb2463dabeaddf8a21f84ace4241da888023bf05cc8095", + "c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5", + "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9", + "e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42", + "1306edd66f1da374adc417cf884bbcff57c6399656236c1f872ee10403c01b2d", + "eaf27aa104833bcd16f671488b01d65f6da30163b5848aea99677cc947dd00aa", + "472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e", + "be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479", + "c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15", + "c037a6897df86bfd4df5496ca7e2318992b4766897fb18fbd1d347a4f4459f5e", + "e41e883f1ef62485a074c1a1fa1d0a092a5d678ad49bedc2f955ab5e305ba94e", + "020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e", + "e2f28c1ac6dff5a7b755635af4c8436d2fec89b888a9d9548a51b2c63f779555", + "29fbc05acee671fb579182ca33b0e41b455bb1f9564b90a3d8f2f39dee3f2779", + "090254801a7e8e5085b02e711622f0dfa1a85503493af246aa42af08f5e4d2df", + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", + "6b0d4c8d9dc59e110d380b0429a02891f1341a0fa2ba1b1cf83a3db4d47e3964", + "b0b8fbd9578ac23e782d97a32b7b3a72cda0760761359bd65661d42752b4090a", + "b7996c183e036df27802945b80bbdc8b0bf5971b6621a86bf3569c332117f07d", + "1833ee04459feb2ca4ae690d5f31269ad488c69e5fe903a42b532c677c4a8170", + "4adb4ff2dc72bbf1f6da19fc109008a25013c837cf712016972fad015b19513f", + "c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0", + "368f4e0027fd223fdb69b6ec6e1c06d1f027a611b1ed38eeb32493eb2878bb35", + "703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898", + "50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63", + "6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3", + "a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f", + "1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b", + "19fefd7f39c96d2ff76f87f7627ae79145bc971d8ab23205005939a5a913bc2f", + "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", + "a3b0ce5d70d0db22885706b2b1f144c6864a7e4828acff3f8f01ca6b3f54ad15", + "aef0d6b212827f3ba1de6189613e6d4824f181f567b1205273c16895fdaf0b23", + "826e9f895b81ab41a4522268b249e68d02ca81608def562a493cee35ffc5c759", + "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", + "e1055729d51e037b3c14e8c56e2c79c22183385d94aadb32e5dc88092cd0fef4", + "27f211f4542fd89d673cfad15b6d838cc5d525615aae8695ed1dcebc39b2dadb", + "eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f", + "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", + "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", + "00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700", + "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", + "22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954", + "175f568d77fb0cb7400f0ddd8aed1738cd797532b314ef053a1669d4dba7433a", + "6c535d95a8659b234d5a0805034f5f0a67e3c0ceffcc459f61f680fe944424bf", + "9579444852221038dcba34512257b66a1c6e5bdb4339b6794826d4024b3e4ce9", + "58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196", + "b8e6bf46e109314616fe24e6c7e265791a5f2f4ec95ae8aa15d7107ad250dc63", + "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240", + "387519cafd325668ecffe59577f37238638da4cf2d985b82f932fc81d33da1e8", + "b9e76546ba06456ed301d9e52bc49fa48e70a6bf2282be7a1ae72947612023dc", + "4d62dd5e6ac55ae2405940f59f6f030a994ec2b3ecc5556c8dc542cce20e46dd", + "9c612f8b770f0e3fd35cdac2bc57fcee8561e560504ea25c8b9eff8e03512b3e", + "3eeb3de14ec5c48c6c4c9ff80908c4186170eabb74b2a6705a7db9f9922cd61e", + "51d7f1b736d1958fa56f113e82a27987a3aca4f0e6d237fa8fc369cc1608c5c0", + "c2622c916d9b90e10a81b2ba67b19bdfc5d6be26c25756d1f990d3785ce1361b", + "b111d517452f9ef015e16d60ae623a6b66af63024eec941b0653bfee0dd667d4", + "d897efcd971f8e5eae08c86b7da66f89b30e761a4a86ac41d907425d15b630fe", + "9dea27855974a08fceb48c40fab8432c1a8e3a53a1da22a1ad568595d6010649", + "47b630bbcdfa88b1c85f84aa3b68fe6c0102b651ba5d9a23cbd2d07b4f6eecc1", + "eb0dc09a61fdfc0df5db1f20c7fc7d83f00c690580fea2e5bac8f99c13f65065", + "5c0775b1ae0a5140da9599aa9cd1c5beea55c2d55a5d681808525eb5fce37b32", + "b474e6999980aa9e8c9dd6e3720fb03136bfa05aba5fab1634dc0bd8767d412f", + "759f7abf05ca710bf2c8da7ad7a9a7df6d0c85db7b2217da524e94e3627b2fbd", + "060e7c6ed0dbeb9c8cdc61445ee38b9b08d899d6b617e28064b0916e243ddddb", + "f728d9e6e7048358e70930f5ca64b097770d989ccd86854fe618eda9c8a38106", + "bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce", + "b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27", + "c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6", + "ad46db12ee250a108756ab4f0f3007b04d7e699f45eac3ab696077296219d207", + "59fbee7369df7713dbbfa9bbdb0892c62eba929232615c6ff2787da384cb770f", + "d7f0e3917c466f1e2233e9624fbd6d4bd1392dbcfcaf3574f457569d496cb731", + "e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2", + "6f35047caf7432fc0ab54a28fed6c82e7b58230bf98302bf18350ff71e10430a", + "fdd5e8f6ae0db817be0b71da20498c1806968d8a6459559c249f322fa73464a7", + "883fea4c071fda4406d2b66be21cb1edaf45a3e058050d6201ecf1d3596bbc39", + "a1808558470389142e297d4729e081ab8bdff1ab50d0ebe22ffa78958f7a6ab7", + "330fb1431ff9d8c250706bbcdc016d5495a3f744e047a408173e92ae7ee42dac", + "a4cb51f4618cfcd16b2d3171c466179bed8e197c43b8598823b04de266cef110", + "9c163c7351f8832b08b56cbb2e095960d1c5060dd6b0e461e813f0f07459119e", + "0a722ca20e1ccff0adfdc8c2abb097957f0e0bf32db18c4281f031756d50eb8d", + "5cad82c898ee66013711945d687f7d9549f645a0118467dae2f5e274c598d6ff", + "03b593ef3d95102b54bdff77728131a7c3bdfe9007b0b92cd7c4ad4a0021de25", + "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", + "60d53675f07dee9e7d77910efa44682d87cb532313ba66b8f4449d649172296b", + "d3ab33199eb48c6f785072b4a66a8e57814e35d31375cca8c3ceeecc171f30ba", + "772bd267dffbff318d1a89f257c3371410111a8b89571dbbefa77af6bfa179f3", + "11b9a89404dbf3034e7e1886ba9dc4c6d376f239a118271bd2ec567a889850ce", + "0497384b57b43c107a778870462901bf68e0e8583b32e2816563543c059784a4", + "5d9ba2c5ee0e86e2c4477b145eb301f2df06063a19f4c4ab9042bd347188ec8e", + "5683ffc7ff8a732565135aad56cdff94ebacd9a616d1313aea8ad48a446bfe99", + "3004d45a0ab6352c61a62586a57c50f11591416c29db1143367a4f0623b491ca", + "b24e32ee9a1c18f2771b53345ed8dbc55b59cbe958e5a165dc01704c3aaa6196", + "0a2df905acd5b5be3214a84cb2d4f61b0efb4d9bf05739d51112252504959688", + "95361a2b42a26c22bac3b6b6ba4c5cac4d36906eb0cfb98268681c45a301c518", + "b07d216f2f0422ec0252dd81a6513b8d0b0c7ef85291fbf5a85ef23f8df78fa7", + "064de2497ce621aee2a5b4b926a08b1ca01bce9da85b0c714e883e119375140c", + "5a8e581f16a012e24d2a640152ad562058cb065e1df28e907c1bfa82c150c8ba", + "a36bdc7952e973b31cb32d4ce3ce21447db66c3149c1b7a3d2450f77f9c7e8f9", + "e03cfe011d81424bb60a12e9eb0cb0c9c688c34712c3794c0752e0718b369ef2", + "2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884", + "b9003833fabff271d0782e030be61b7ec38ce7d45a1b9a869fbdb34b9e2d2000", + "4379e76bfa76a80b8db9ea759211d90bb3e67b2202f8880cc4f5ffe2065061ad", + "76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa", + "d307643547703537dfdef811c3dea96f1f9e84c8249e200353425924a9908cf8", + "da0cc82154bdf4ce8bf417eaa2d2fa99aa65c96c77867d6656fccdbf8e781b18", + "3511ad63cd9ad760780044b7c815ee55e8e00722b5de271c47ff29367653456c", + "f9acb0b034c4c1177e985f14639f317ef0fedee7657c060b146ee790024317ec", + "0c371f5ed95076613443e8331c4b60828ed67bcdefaa1698fb5ce9d7b3285ffb", + "ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49", + "053935081a69624466034446eda3374d905652ddbf8217c88708182687a33066", + "a305cc8926861bdde5c71bbb6fd394bb4cea6ef5f5f86402b249fc5ceb0ce220", + "03a6e50be223dbb49e282764388f6f2ca8826eae8c5a427aa82bb1b61e51d5e6", + "a197639863cf175adc96348382a73b4a4a361c6b2e6fc1de61a14244a2f926a1", + "3ca7ca157b5975ace02225caf99fdce43f11207c072cb4899c80a414a9c7539d", + "02d9f5676fffc339ffe94dfab38bebe21ce117c6f1509d9922a82d454f420da2", + "08634a74c9d14479b462389b307695815f9a189e8fb6e058b92e18bd3f537405", + "ec7de4aa8758ba9e09a8c89d2757a1fa0e2cc61c20b757af52ae058931c1a33f", + "2250f69694c2a43929e77e5de0f6a61ae5e37a1ee6d6a3baef1706ed9901248b", + "a9434ee165ed01b286becfc2771ef1705d3537d051b387288898cc00d5c885be", + "bd625f1b8c49a79f075f3ebd2d111ff625504cf2ad12442fd70d191dd2f4a562", + "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", + "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", + "11d0b66747887ba9a6d34b23eb31287374b45b1a1b161eac54cb183c53e00ef7", + "2544cfcb89d7c2f8d3a31ea2ed386ac5189a18f484672436580eec215f9b039c", + "d4338b7c3306491cfdf54914d1a52b80a965685f7361311eae5f3eaff1d23a5b", + "c43e382ee4835010b9fad18e0a6f50f1ae143b98e089b8bb974232fce4d1f295", + "92de68b21302fa2137b1cbba7259b8ba967b535a05c6d2b0847d9f35ff3cf56a", + "55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185", + "2af01e0d6bd1b9fbb9e3d43157d64590fb27dcfbcabe28784a5832e17befb87b", + "35b23cd02d2d75e55cee38fdee26bc82f1d15d3c9580800b04b0da2edb7517ea", + "7e0c255fd3d0f9b48789a944baf19bf42c205a9c55199805eb13573b32137488", + "ee0e01eb17fc6cb4cd2d9300d2f8945b51056514f156c6bc6d491b74496d161a", + "cbc5ef6b01cbd1ffa2cb95a954f04c385a936c1a86e1bb9ccdf2cf0f4ebeaccb", + "ec6e36d5c9eb874f1db4253ef02377f7cc70697cda40fbfb24ded6b5d14cce4c", + "2779f3d9f42c7dee17f0e6bcdcf89a8f9d592d19e3b1bbd27ef1cffd1a7f98d1", + "976713246c36db1a4364f917b98633cbe0805d46af880f6b50a505d4eb32ed47", + "8766a54ef9a170b3860bc66fd655abb24b5fda75d7d7ff362f44442fbdeb47b9", + "ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14", + "e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb", + "07eced8b63b883cedbd8520bdb3303bf9c2b37c2c7921ca5c59f64e0f79ad2a6", + "532d830dffe09c13e75e8b145c825718fc12b0003f61d61e9077721c7fff93cb", + "1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d", + "c708943ea349519dcf56b2a5c138fd9ed064ad65ddecae6394eabd87a62f1770", + "ccaa58e37c99c85bc5e754028a718bd46485e5d3cb3345691ecab83c755d48cc", + "5b0e8da6fdfba663038690b37d216d8345a623cc33e111afd0f738ed7792bc54", + "f2c96c97f6419a538f84cf3fa72e2194605e1848096e6e5170cce5b76799d400", + "aa55a479ad6934d0fd78f3dbd88515cd1ca0d7a110812e711380d59df7598935", + "bd9eb657c25b4f6cda68871ce26259d1f9bc62420487e3224905b674a710a45a", + "69a80567e79b6b9bc7282ad595512df0b804784616bedb623c122fad420a2635", + "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52", + "df173277182f3155d37b330211ba1de4a81500c02d195e964f91be774ec96708", + "675b84fe75e216ab947c7438ee519ca7775376ddf05dadfba6278bd012e1d728", + "91c9a5e1a9744114c6fe2d61ae4de82629eaaa0fb52f48288093c7e7e036f832", + "24ebb6b58d0b984a965b76f82ce9eff8795cc95085a4d09dedc56949ed596ada", + "bb1cf5250435ff475cd8b32acb23e3ee7bbe8fc38f6951704b4798513947672c", + "922945779f93fd0b3759f1157e3d9fa20f3fd24c4b8f2bcf520cacf649af776d", + "3d842afecd5e293f28b6627933704a3fb8ce153aa91d790ab11f6a752d44a42d", + "e8d67c435a4a59304e1414280e952efe17be4254fca27916bf63f9f73e54aba4", + "c1fc7771f5fa418fd3ac49221a18f19b42ccb7a663da8f04cbbf6c08c80d20b1", + "8eee8f5a002e533e9f9ffef14c713da449c23f56f4415e7995552075a02d1d37", + "c998a5739f04f7fff202c54962aa5782b34ecb10d6f915bdfdd7582963bf9171", + "a536ab1f7f3c0133baadbdf472b1ac7ad4b774ed432c1989284193572788bca0", + "c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965", + "e6ee5b449c220defea6373b8a7e147cabd67c2bdb5016886bf6096a3c7435a61", + "a619cf1a888a73211bbf32e0c438319f23e91444d45d7bc88816ed5fcb7e8fa3", + "56a6b75373c8f7b93c53bcae86d8ffbaba9f2a1b38122054fcdb7f3bf645b727", + "89bfe407c647eb1888871f756516bb1906254fba3132d516ce9099614e37d10c", + "b7ed68b062de6b4a12e51fd5285c1e1e0ed0e5128cda93ab11b4150b55ed32fc", + "4657dfe8965be8980a93072bcfb5e59a65124406db0f819215ee78ba47934b3e", + "d61f3bc5b3eb4400efdae6169a5c17cabf3246b514361de939ce4a1a0da6ef4a", + "58ead82fa15b550094f7f5fe4804e0fe75b779dbef2e9b20511eccd69e6d08f9", + "fcf6fee0e959c7195dadc5f36fe5a873003b389e7033293b06057c821fcbc9c5", + "6681268ace4748d41a4cfcc1e64006fb935bbc359782b3d9611f64d51c6752d9", + "e76450df94f84c1c0b71677a45d75b7918f0b786113c2d038e6ab8841b99f276", + "a44dbc9aaa357176a7d4f5c3106846ea096b66de0b50ee39aff54baab6c4bf4b", + "281e109d2a2899bb0555cf0c3a69b24b3debd61885ca29ef39b95b632be25fe7", + "5be6446aa8a31c11b3b453bf8dafc9b346ff328d1fa11a0fa02a1e6461f6a9b1", + "e1ff3bfdd4e40315959b08b4fcc8245eaa514637e1d4ec2ae166b743341be1af", + "0d6c8388dcb049b8dd4fc8d3d8c3bb93de3da90ba828e4f09c8ad0f346488a33", + "9be0be0e64d38a29a9cec9a5c8ef5d873c2bfa5362a4b558da5ff69bc3cbb81e", + "c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11", + "4c7f826edf647462f744b3f16d485f53c797eabdb21cc8a7bb0713283b88e629", + "d1621db4d91b23180707b8d4eb9b599fa8ec1dfc2453793a1f83878bd4bbc9d8", + "4b74667f89358cd582ad82b16a2d24d5bfcb89ac4b1347ee80e5674a13ba78b2", + "83d999a148625c3d2bb819af3064c0f6a12d7da88f68b2c69221f3a746171d19", + "b6494a74d18a2dfa3f80ced9fadae35807716fce1071e4de19e2d746b6d87606", + "b9c411db4036219e3dfcbe28d60e550b46cce86260fcf2c65d281258e437556f", + "2590201e2919a8aa6568c88900192aa54ef00e6c0974a5b0432f52614a841ec8", + "c15a5a65986e7ab4134dee3ab85254da5c5d4b04e78b4f16c82837192d355185", + "dab6c6065c439b9bafb0b0f1ff5a0c68273bce5c1959a4158ad6a70851f507b6", + "baf27a4cc4da49913e7fdecc951fd3b971c9279959af62b02b761a043c33384c", + "6c237d8b3b120251c38c230c06d9e48f0d3017657c5b65c8c36112eb15c52aeb", + "f173040998481bcb2534a53433eafb8d6ea4c7b0e1fc64572830471fe43fc77d", + "36732cc35fe56185af1b11160a393d6c73a1fe41ddf1184c10394c28ca5d627b", + "126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f", + "457e17b7ea97a845a0d1fa8feda9976596678e3a8af46dc6671d40e050ce857d", + "1739d937dc8c0c7370aa27585938c119e25c41f6c441a5d34c6d38503e3136ef", + "b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0", + "33bd77e5394520747faae1394a4af5fa47f404389676375b6dc7be865ed81452", + "fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3", + "4f83ef69228b3e09b0abc11ded9e6b85319c0b7fef1a044b8ee9970e38441817", + "4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0", + "d8f38b894b42f7008305cebf17b48925654f22b180c5861b81141f80ccf72848", + "9c557e253213c127a86e333ff01c9f12f63091957efafd878d220a0e2cb1193e", + "9eab64e92219ccedb15ea9b75ababaa4ae831451019394e0e3336390c3a742d8", + "43dedbafef3748c3f9146b961c9b87a3f5cdb1ccb50b4f5890e408702a27a506", + "17717ad4d20e2a425cda0a2195624a0a4a73c4f6975f16b1593fc87fa46f2d58", + "ee6ea13ab9fe5c4a68eaf9b1a34fe014a66b40117c50ee2a614f4cda959b6e74", + "4d023ce9dfd75a7f3075b8e8e084008be17a1f750c63b5de721e6ef883adc765", + "d91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075", + "4eb88310d6b4ed95c6d66a395b3d3cf559b85faec8f7691dafd405a92e055d6d", + "0aeb0814c99a13df50643ca27b831a92aaae6366f54e9c276166347aa037d63a", + "16f1a0100d4cfffbcc4230e8e0e4290cc5849c1adc64d6653fda07c031b1074b", + "148d1366a5e4672b1321adf00321778f86a2371a4bdbe99133f28df0b3d32fa1", + "7076f6592de184f9e912c617c46e5e83bad91d3b7f88b7b54cc73bf8ca493321", + "dea51494fec5947d27ca659b73dd281ff5bdba3f89f5da1977a731ad0c22e725", + "aa97e3d392b97c21327081cdb3cb674dfa8c9c663493db43799a4079555ad1b1", + "b7dfbebf760efb2c756b3cae22baafdbbdf55abb782f85e93b4db804d5cba7e3", + "ca696d7edb4a86be7c7d9872bd2f9a44440cf8e2de7853536cbb3a60ae89641f", + "ff27d01cb1e56fb58580306c7ba76bb037bf211c5b573c56e4e70ca858755af0", + "58dece9ff68d021afe76d549b9e48e6cb7b06a5c14cdf45332c8ed7321d6f211", + "78688c1f371e7b923d95368c9298cca06c1ec0a89ea897aa181bd60091121fea", + "9e8dd91d21e867dec464868a8d1f4a27c0e113c53e32f2bec0a7c6e25ad2e9d5", + "6d028aa49aa1f584b3d35aee9fcee8e3c0d81108114289aa046a7969b21eb5f5", + "2c470abbac95a49cd0ed5b3b9e628ffda1dbb03c14caba1a225a9b8bf1dc9d5f", + "9d7af6946b320b3ba6b4d386de2b2cf3f8ac52fdcb63f3343d1a8362693a3ce5", + "d7a4345c3ead1ea7a34bd6aae43c63cbd81941d9ba019fe972843e5ce78e3187", + "00f471f6312ce408f7eb56592a2b6c6b5f54ac2967c77f4c1498903b598e1b16", + "41e9e2c8398583b90204f8e35c2a4c036aeebac6d05dbdc3e7fb44a1d6bd65a2", + "0d978064b9054c023111926050d983573dac2aff16bb8a7497fde8ad725357c0", + "507f5bf8367c1883f115ddf9ee95f79ea693c720eb5a5a8718443a99fa308954", + "9a090f86adf9fbdc37de2a14745e73d6e1fd096d3da0670b6795ce5ad3cfeea3", + "f2b7c5787424c8f9cf6c4480eb99f4a3770cc06337a4f0d1b109ba849b464193", + "6ffe93bb72d4ac788fd8be2dadf5bb4a2f14a330d530b0e68bd733c9744c6619", + "3ebc74907d1f928f209ef210e872cac033eaf3ff89e6853286d45d91e351ef9e", + "da56c54b5e6749d65ad038c196478794af94e4fa5a4efdc20b49981e4ec566c3", + "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5", + "5b705e6cb602425c019202dd070a0c009b040ac19960eeef2d8a8fab25c1efe5", + "9ec7a778167afb1d30c4833de9322da0c08ba71a69e1911d5578d3144bb56437", + "e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f", + "40b9c85fffeafc1cadf8c30a4e5c88660ff6e4971a0dc723d5ab674b5e61b451", + "efe5d120df0cc290fa748727fb45ac487caad346d4f2293ab069e8f01fc51981", + "408f636bd26fcc5f29889033b447cb2411f60ab1b8a5fc8cb3842dab758fdeb5", + "d8a2c33f2e2ff3a9d4ff2a5593f3d5a59e9167fa5ded063d0e49891776611e0c", + "02a11d1545114ab63c29958093c91b9f88618e56fee037b9d2fabcff32f62ea9", + "1bbd7fdf68eaf5c19446c3aaf63b39dd4a8e33548bc96f6bd239a4124d8f229e", + "726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11", + "167e7fe01a76b6bec9d2a9b196b18c72e150e985fbeb46ee651869e7b4032785", + "c88f94f0a391b9aaa1ffefd645253b1a968b0a422a876ea48920a95d45c33f47", + "cd169bd8fbd5179e2a8d498ffc31d3ae0e40825ff2b8a85ea359c4455a107ca8", + "fd38f135ef675eac5e93d5b2a738c41777c250188031caf1dcf07b1687a1fe49", + "6b4c612991132cf4c6c390dceaae75041b9954ba4f9c465aca70beb350815a57", + "8fec426247845bdd26f36ae4f737508c15dbec07d43ce18f8c136ab9e35ac212", + "af551accea482000bdccb34bd3c521558e1f353773a3caed83a147921c369ea1", + "a664a4973cd23e9f3b35a62429f7671aba2c2ae68c03313913b5c2d77269d906", + "18f54af1e10c5bb7a35468b0f62b295d12347903c9f95738d065c84bef1402ef", + "be39043cc12efbddfee564d95da751a71df6c139e2def45c431cadeb4a573ca3", + "01ddee289b1a2e90874ca3428a7a414764a6cad1abfaa985c201e7aada16d38c", + "da25cf7b457bddb6b7bc8e1b0146c0fa85373807d6efdac955199fd01fd53c1f", + "ec380784d96b93d404166a6bf1a778227a94a02bdf499f94b5f48f61f5b1350f", + "6538925ebfb661f418d8c7d074bee2e8afd778701dd89070c2da936d571e55c3", + "9edd72eb23222c969379d90d60ec82891b7c827188bb28510a863f59cb697b0a", + "09222857afceb23c66c99fc93d8e5ebda6d7aad901eb38af73c508f117685012", + "744ecc9a119a92da88b1f448b4030cdbc2fec5c37ea06ebdd026e742b002af7f", + "f531a8672baa2415b271e866dbe11fb08640f6c0e5d98f918bd0308e7169b5b7", + "44dc1c2db9c3fbd7bee9257eceb52be3cf8c40baf7b63f46e56b58a131c74f0b", + "89e14be49ed0073da83b678279cd29ba5ad86cf000b6a3d1a4c3dc4aa4fdd02c", + "8fb140b4e8ddef97ce4b821d247278a1a4353362623f64021484b372f948000c", + "72f9755501e1a4464f7277d86120f67e7f7ec3a84ef6813cc7606bf5e0870ff3", + "3d99feac152027ede63326aa4f43d4ca88e4cd27296b96fe18c55d496a8f6340", + "2540d50aeb9be889c3bd050c9cc849b57b156a2759b48084a83db59aa9056eb4", + "b66be78da89991544a05c3a2b63da1d15eefe8e9a1bb6a4369f8616865bd6b7c", + "2f5de0003db84ecd5449128350c66c7fb63e9d02b250d84af84f463e2f9bcef1", + "2045369fc115b138d1438f98d3c29916986c9fde6b8203f7ff8699f0faee1c93", + "ae1008d23930b776c18092f6eab41e4b09fcf3f03f3641b1b4e6ee3aa166d760", + "1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139", + "b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a", + "ac3f6afe17593f61810513dac9a1e544e87b9ce91b27d37b88ec58fbaa9014aa", + "d1f3c71639ae3bba17ffc6c8deb1fdb3a56506b3492213d033528cc291523704", + "6b4a29bbd43d1d0eeead384f512dbb591ce9407d27dba48ad54b00d9d2e1972b", + "84de08882b6e36705cf6592ee58e632dd6e092dd61c13192fc80cbbc0cbc82cc", + "d3d74124ddfb5bdc61b8f18d17c3335bbb4f8c71182a35ee27314a49a4eb7b1d", + "a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d", + "eb7246eb8e26b0c48dd4f9c2a822a0f4d5c84138937195090932b61a2d756051", + "683211bd155c7b764e4b99ba263a151d81209be7a566a2bb1971dc1bbd3b715e", + "5468bceeb74ce35cb4173dcc9974bddac9e894a74bf3d44f9ca8b7554605c9ed", + "78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d", + "2754fc862d6bc0b7c3971046612d942563d181c187a391e180ed6b00f80e7e5b", + "f1725586a402c06aec818d1478a45aaa0dc16c7a9c4869d97c350336d16f8e43", + "6a359852238dc902aed19fbbf6a055f9abf21c1ca8915d1c4e27f50df2f290d9", + "9e4954853fca260cecf983f098e5204c68b2bdfebde91f1f7b25c10b566d50f8", + "3356de61b39647931ce8b2140b2bab837e0810c0ef515bbe92de0248040b8bdd", + "3e294d2fd339bb16a5403a86e3664947dd408c4d87a0066524f8a573ae53ca8e", + "21335073401a310cc9179fe3a77e9666710cfdf630dfd840f972c183a244b1ad", + "987096ef8a2853fea1a31b0ed5276503da291536f167bbf7f3f991c9f05d6d7f", + "7a78fbfec68c2b3ab6084f1f808321ba3b5ea47502c41115902013e648e76288", + "c12a2bcb002fd74b4d342f9b942c24c44cc46d5ed39201245a8b6f4162e7efce", + "8867bed93e89c93d0d8ac98b2443c5554799edb9190346946b12e03f13664450", + "9b61cd02adac4b18fbcc06237e7469b07e276faf6ec4ecb34b030c2e385892a0", + "0463223adf38df9a22a7fb07999a638fdd42d8437573e0bf19c43e013b14d673", + "9989500413fb756d8437912cc32be0730dbe1bfc6b5d2eef759e1456c239f905", + "17e2889fba01021d048a13fd0ba108ad31c38326295460c21e69c43fa8fbe515", + "6c6c253fe26a5b2abf440124e35dcaa39e891cd28274431ba49da5c11d89747d", + "9d065f84c0cba7b0ef86f5d2d155e6ce01178a8a33e194f9999b7497b1b2201b", + "5ffb8e1b6b629c0e34a013f9298ebb0759b98a3d24029916321d5eb4255b6735", + "3fc5f8553abd753ac47967c4c468cfd08e8cb9dee71b79e12d5adab205bc04d3", + "ff82c8b53aa53a9705200690b91c572e2e4918f1a88de5d923ac06fa4560fa19", + "4d4ab737e2fbb5af0fd590b4b7e8c6fe76d3a02a9791ef7fdacf601f9e50fad8", + "5eca50a04afaefe55659fb74810b42654e2268c1acca6e53801b9862db74a83a", + "d700fc10d457eeae4f02eb04d715a054837e68a2e2d010971382c5e1016dc99e", + "af321973db865bb33fbc50a4de67fc0e6808d812c6e4dfd9cbc2fd50275b1dfd", + "bbf923aa9246065f88c40c7d9bf61cccc0ff3fcff065a8cb2ff4cfbb62088f1e", + "268b948b5aab4bab0e5430ee49e3cff11776cf183df93b32159f9670ed541495", + "3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594", + "4d4fb5ff0afb8c04e6c6e03f51281b664576f985e5bc34a3a7ee310a1e821f47", + "9b12847f3d28bf8850ebc03f8d495a1ae8f9a2c86dbda295c90556619a3ee831", + "733c5427f55ceba01a0f6607ab0fd11832bbb27d7db17b570e7eb7b68a081d9a", + "4bc7982c4ee4078b2ada5340ae673f18d3b6a664b1f97e8d6799e6074cb5c39d", + "afa0f26dbf3e674630d1cd6499e86c14f316cd4f78c6ab73bb85b00aa9c50a57", + "c301f13372c8f0d9bc8186d874fa45fa33aede13e66f4187a3bd22ee41c95b2a", + "548a29f145187fc97689f8ae67944627723c315c163b0dbb88842e50c681d7ca", + "d0a1ffb8761b974cec4a3be8cbcb2e96a7090dcf465ffeac839aa4ca20c9a59e", + "faaf47af27e3de06e83f346fc6ccea0aabfc7520d82ffe90c48dfcd740c69caa", + "3eacaa768326d7dce80f6ee17ada199bebe7eb3c1a60b39b14e0a58bbac66fe4", + "7f5237e9f77a22c4a89624c7ac31cae797d8ac4144b02493890d54fee7399bcd", + "d84517802a434757c56ae8642bffb4d26e5ade0712053750215680f5896e579b", + "bdb96ad31ac6af123c7683c55775ee2138da0f8f011e3994d56a27270e692575", + "aab1b0caf13b9bd26a62cf8b3b20f9bfaa0e56f3ec42196a00fedf432e07d739", + "c230edd34ca5c8318bf4592ac056cde37519d395c0904c37ea1c650b8ad4a712", + "ce41c1698a8c042218bc586f0b9ec8d5bffa3dcbcea09bd59db9d0d92c3fc0b4", + "b9a537523bba2fcdae857d90d8a760de4f2139c9f90d986f747ce7d0ec0d173d", + "1a6e0aeff1dba7ba121fbeb33bf3162901495df3fcb4e4a40423e1c10edf0dca", + "21b419102da8fc0ba90484aec934bf55b7abcf75eedb39124e8d75e491f41a5e", + "2183e94758481d0f124fbd93c56ccaa45e7e545ceeb8d52848f98253f497b975", + "e3f98bfb9cbeb7563a139983602e50f616cb7ebb06c3295b8ee377328f051206", + "b5b9b84d1723994d06013606227fb5b91f9de8820b04cf848d1dccc23d054f39", + "07adfda9c5adc80881bb2a5220f6e3181e0c043b90fa115c4f183464022968e6", + "facdaf1ce758bdf04cdf1a1fa32a3564a608d4abc2481a286ffc178f86953ef0", + "efc83f01c8fb309df2c8866b8c7924cc8b6f0580afdde1d6e16e2b6107c2862c", + "52b4a076bcbbbdc3a1aefa3735816cf74993b1b8db202b01c883c58be7fad8bd", + "c6f7077f1699d50cf92a9652bfebffac05fc6842b9ee391089d959b8ad5d48fd", + "e7424ad457e512fdf4764a56bf6d428a06a13a1006af1fb8e0fe32f6d03265c7", + "27797bd4e5ee52db0a197668c92b9a3e7e237e1f9fa73a10c38d731c294cfc9a", + "7bdef7bdebb8721f77927d0e77c66059360fa62371fdf15f3add93923a613229", + "3335d373e6c1b5bc669b4b1220c08728ea8ce622e5a7cfeeb4c0001d91ded1de", + "645681b9d067b1a362c4bee8ddff987d2466d49905c26cb8fec5e6fb73af5c84", + "06b7819d7f1c7f5472118266ed7bca8785dceae09e36ea3a4af665c6d1d8327c", + "7a6b8c7de171955c214ded7e35cc782cd6dddfd141abb1929c632f69348e6f49", + "eb882b0bb659bf72235020a0b884c4a7d817e0af3903715736b146188b1d0868", + "2ae6c71323a225ecfa8cf655600ebbe12b1019ff36bf02726d82d095aab29729", + "c2f85a06279a7bfa7f2477a3cee907990231a13d17b54524738504bd12e0c86c", + "f1b911af1c7a56073e3b83ba7eaa681467040e0fbbdd265445aa80e65c274c22", + "a54c2ae6ec6ac06b4d7b45c483eab86ac226b8ecfa99163ef7cc000da9b40895", + "bbc73cae41502ddad7a4112586dcaf4422810d60aa4b57c637ccd1a746b07844", + "218238431393959d6c8617a3bd899303a96609b44a644e973891038a7de8622d", + "59cacbd83ad5c54ad91dacf51a49c06e0bef730ac0e7c235a6f6fa29b9230f02", + "ba80990666ef0b6f4ba5059347beb13242921e54669e680064ca755256a1e3a6", + "031db544f3158556508b321db59afd62c5bb1592021e5dfd9ff87dca0ad27d8c", + "ee85604f8ec6e4e24f8eaf2a624d042ebd431dae448fe11779adcfb6bb78575e", + "266ee74062e8dae0aeddfcd0f72725107598efaa80c1a7176d6ee6dd302bce4c", + "b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450", + "dbe0605a9c73172bad7523a327b236d55ea4b634e80e78a9013db791f8fd5b2c", + "1e53e900c3bbc5ead295215efe27b2c8d5fbd15fb3dd810da3063674cb7213b2", + "832a2b3cef4b1754c4a7572964a44db64d19edf627ec45179b519d0a5eae8199", + "4c800257a588a82849d049817c2bdaad984b25a45ad9f6dad66e47d3b47e3b2f", + "3743244390be53473a7e3b3b8d04dce83f6c9514b81a997fb3b123c072ef9f78", + "f96c3d76497074c4c83a7b3823380e77dc73d5a9494fd2e053e4a1453e17824b", + "d04ecf33a303a59852fdb681ed8b412201ba85d8d2199aec73cb62681d62aa90", + "0cca6201658d5d98239c1511ef402562ff7d72446fb201a8d1857c39e369c9fa", + "61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9", + "7adb520c3ac7cb6dc8253508df0ce1d975da49fefda9b5c956744a049d230ace", + "7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19", + "9ec078ef9ca31e1bdbb97175dde1cb00bf9f7225e6f622ccc8d367302e220497", + "93e174736c4719f80627854aca8b67efd0b59558c8ece267a8eccbbd2e7c5535", + "e62f419a0e16607b96ff10ecb00249af7d4b69c7d121e4b04130c61cc998c32e", + "171ddd43dab1af0d1fb14029287152a4c89296890e0607cf5e7ba73c73fdf1a5", + "604e96e099936a104883958b040b47672e0f048c98ac793f37ffe4c720279eb2", + "7726c437ccf791f6ded97dbac1846e62019e5fbd24f42e9db2f640f231c3c09a", + "1bf9f239dca1636149bc2f3fc334077ae959ea9607cacf945ef8f8bb227dc5e1", + "fcd818454002a6c47a980393f0549ac6e629d28d5688114bb60d831b5c1832a7", + "56172b53f730750b40e63c501b16068dd96a245e7f0551675c0fec9817ee96e0", + "260d3a820b7f8de20f4972725999b1af88b0cc5554ca38f9681c8d657e043cc3", + "9ba8c688f091ca48de2b0f9bc998e3bc36a0092149f9201767da592849777f1c", + "61594d714aa94fe551f604123578c4a6592145f4228ad8601224b1b89ce401b0", + "416ca193aa5448b8cca1f09642807765cc0ee299609f972df0614cfb8ea2f2b1", + "9b6d95b76a01191a4c778185681ed7f3bced2fffa8e41516ec78240b213285f5", + "ee0304bae0d4679bb34347ce3b1b80482262b9812bd0c0d5e19a5e2445043b75", + "8027a1877f39e603dafc63279e004b4ed9df861d18ce81d9c43c7d7135da8f65", + "42b409ff9b261a985227b1ab92707e706777ac14de24654d7e05f0501b37e003", + "99097983b74c70800b182abc6f64046ab70407e9cabcd6cf570a38ada9ef75d5", + "de8ca7a6b3f7314e91921d4dc5e915fb7bc2bd32129ea6966322effa48050c4c", + "dcb302978215f54f33c3d2d7157ef69fd5058cf488fc43dd75c32b5dcaf47e7a", + "7c765d407d3a9d5ea117cb8b8699628560787fc084a0c76afaa449bfbd121d84", + "06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71", + "59f1b5faf29904fe94a6a042e2d82d80d68fc16ad7651eba90a8de39f63f8fe8", + "174398550d1468a41b98a09f496c38d3694feadef0f0073fd557610384bafb10", + "00d52016bd7e4aae8bf8eaa23f42276b649fe557483b5d7684702633dd0fd944", + "9a39bf837c868d61ed8cce6a4c7a0eb96f5e5bcc082ad6afdd5496cb614a23fb", + "74ffc51cc30150cf79b6cb316d3a15cf332ab29a38fec9eb484ab1551d6d1856", + "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", + "6c6e3e05e1c9d2aae0ed2431544aea411771dd9d81017539af0fd818b2389f28", + "23a2cf63ec81e65561acafc655898d2fd0ef190084653fa452413f75e5a3d5bc", + "f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8", + "e3aefda887252a72cee3578d33b2dcd90e9fe53b8bed6347ef5e26f74211adbb", + "6b4ec98f02e647e01440b473bbd92a7fae21e01b6aa6c65e32db94a36092272e", + "623ed218de81311783656783d6ce690b521a89c4dc09f28962e5bfd4fa549249", + "9ce71f1506ccf4b99f234af49bd6202be883a80f95a155c6e9a1c36fd7e780c7", + "139fcc6bb304b2879974c59cda61d86d7816ad4ac0f38ee7a724df488060e65d", + "0e8c41eb946657188ea6f2aac36c25e393fff4d4149a83679220d66595ff0faa", + "59ffbe1fc829decf90655438bd2df3a7b746ef4a04634d4ee9e280bb6ce5f14e", + "e4f695f05bb05b231255ccce3d471b8d79c64a65bccc014662d27f0f7e921092", + "39cc53c9e3f7d4980b21bea5ebc8a5b9cdf7fa6539430b5a826e8ad527168656", + "685fb49563864326e78df461468795b7e47849a27e713281cd8bb75c0547936d", + "05e4649832dfb8d1bfa81ea7cbf1c92c4f1cd5052bfc8d5465ba744aa6fa5eb8", + "e5cece49ae3fc2c81f50c8e7a93a5fb1e1585380c467e4822234b64a94add617", + "dda028cd1b806b4d494cc7f2789b6c2bd7e3c28ff6a267d03acc5ac6e69a05e0", + "046c436b2a525059867b40c81e469b6d83001442fc65312c87a7ce7abeb022ff", + "676ffea2ec31426a906d7795d7ebae2ba5e61f0b9fa815995b4a299dd085d510", + "15b5cf6cdf4fd1c02f28bcce0f197cafae4c8c7c66a3e2e23af9fe610875315e", + "c0fb5367cfcb803c5383f98e26524bed9176e6211588f53ec63fe6079cbfd3df", + "7ab00f596b0286b77f78af567ee1be2536feee41daee67bd872f1480b7aa65b9", + "e8d66519e43b1214ac68f9f2bdbc4386d41ac66b20c5a260b9b04102784074e9", + "e6618db6961dc7b91478e0fa78c4c1b6699009981526693bd5e273972550860c", + "b1e1185884a6d14bbfce3899cb53e8183adde642f264d0ff4f1745371e06134c", + "cae5b7ea348afefc4c102bb7b125c4928f114739a27b877c6bcfbe5a79280384", + "ecbe372132a9323b813eeb48f8dfcedaeca00e2887af181b063c6cfa13ed8ea1", + "52387c6b99cc42aac51916b08b7b51d2baddfc19f2ba08d82a48432849dbdfb2", + "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da", + "f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9", + "fd0266485777bd73e97c7c37f520c83c82e362fe4c25a6be93f3380083d4646b", + "433e80c14ff7b8e16e179ccec35f55833df7dd5a5a063d23117b4b01b6f97170", + "b7c6f6915cfa9a62fff6a1f02604de88c23c6c6c6d1b8f62c7cc10749f307e81", + "ddf03aca85ade039e6742d5bef3df352df199d0d31e22b9858e7eda85cb3bbbe", + "d36e8083fa7b36daee646cb8b3f99feaa3d89e5a396508741f003e21ac0b6bec", + "ec79b568bdea63ca6091f5b84b0c639c10a0919e175fa09a4de3154f82906f25", + "8cd2d0f8310f7009e94f50231870756cb39ba68f37506044910e2f71482b1788", + "0eef96197f5c6be3859b6817e6a5736685856c416e29a2925bd5a15b2a57c8b1", + "04918dfc36c93e7db6cc0d60f37e1522f1c36b64d3f4b424c532d7c595febbc5", + "c8383d81dd24406745b68409be40d6721c301029464067fcc50a25ddf9139549", + "3b3a42d34cf0a1402d18d536c9d2ac2eb1c6019a9153be57084c8165d192e325", + "da18e9860040f3bf493876fc16b1a912ae5a6f6fa8d5159c3de2b8233a0d9851", + "e3fc673fc5f99cc554d0ff47756795647d25cb6e6658f912d114ae6429d35d35", + "3aa5817273c3b2f94f491840e0472f049d0f10009e23de63006166bca9b36ea3", + "bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747", + "1096f6be0a4d7f0ecc2df4ed2c8683f143efc81eeba3ece6daadd2fca74c7ecc", + "d76726da1b64e8679d8b6e66facf551ba96f2612de5a171fac818ee85ce3e5fe", + "27487c9600b16b24a1bfb0519cfe4a5d1ad84959e3cce5d6d7a99d48660a1f78", + "5d3ab876c206a37ad3b094e20bfc3941df3fa21a15ac8ea76d6918473789669a", + "6b1b8dac34ffc61d464dfeef00e4a84a604e172ef6391fb629293d6f1666148c", + "6fb266012c3008303e54ae55140b46957e9978098401dda34f4d921a275bf8bb", + "53a91e3a64d1f658e983ac1e4f9e0c697f8f33e01d8debe439f4c1a92113f592", + "5082984480f3b27891840a2037512739149678efc2ac981ca8cd016d02304efd", + "7b849efa5604b58d50c419637b9873847dbf957081d526136c3a49b7357cd617", + "f53b9d91a8cd177fb4a1cf081a1b6d58759a381ef120a7c5a18c0e70cae80983", + "cfd7df62799a22e384a4ab5da8c4026c875b119d0f47c2716b20cdac9cc1f1a6", + "d83b5ef189df7e884627294b752969547814c3cfe38995cf207c040e03bbe7a4", + "96f652249b0946e1575d78a8bc7450123c8e64f1c56f6b2f93bc23fb249ed85a", + "d60bdad03468f5f8c85b1b10db977e310a5aafab33750dfadb37488b02bfc8d7", + "9839f160d893daae661c84168e07f46f0e1e9746feb8439a6d76738b4ad32eaa", + "453a656903a031395d450f318211a6ec54cd79049a851f92cd6702c65ff5f5bd", + "1634b87b5fcfd4a6c4ff2f2de17450ccce46f9abe0b02a71876c596ec165bfed", + "24480686b56234a240fd9827209b584847f3d4f9657f0d9a97aec5320a264acb", + "f4d1866e8599563c52ceeedf11c28b8567e465c6e9a91df92add535d57f02ab0", + "805b34f708837dfb3e7f05815ac5760564628b58d5a0ce839ccbb6ef3620fac3", + "659a74f6cfbc7c252c58d93452b9d9575e36c464aa6544c6375227c9166a6ed9", + "75d737c3472471029c44876b330d2284288a42779b591a2ed4daa1c6c07efaf7", + "dac1d8c5a9fe94f224e095b52577c33c2cc2b8f3a2d6ad9cbd46845af8c987f0", + "be7358c4fe50148cccafc02ea205d80145e253889aa3958daafa8637047c840e", + "30e8cbf1427c137fa60674a639431c19a9d6f4c07fd2959df83158e674fccbaa", + "7f573f55d875ce8edc528edf822949fd2ab9f9c65d914a40225663b0a697be07", + "781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5", + "d82a91e1013170b10ca7fa0ec800fd0dc6e9335b70c303dadba603fc36802b5f", + "a4237e420cdb0b3231d171fe879bcae37a2db7abf2f12a337b975337618c3ac2", + "7ff4d89f90845ac4d7a50a259163798e0f446e61d4c943cc89637beff567ad02", + "48dbb5e717a6221d64fd13ba12794bc28e5067ac1d7632ee9437d533772750df", + "efba340bd479176486e5a2281c97ac4a90fdcf86ec9c13a78c3182ab877cd19b", + "1021c8921548fa89abb4cc7e8668a3a8dcebae0a4c323ffeaf570438832d6993", + "c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345", + "4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8", + "e568a76a4f8836be296d405eb41034260d55e2361e4b2ef88350a4003bbd5f9b", + "ebdee92945ef05283be0ac3de25787c81a6a58a10f568f9c6b61d9dd513adbad", + "6e8f3edfa28bfc8057d735794f76b697bcf18fb894a5a37a132693ebda31a464", + "576d23dc3db2056d208849462fee358cf9f0f3310a2c63cb6c267a4b9f5848f9", + "18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08", + "a9046cc9175dc5a45fb93a2c890f9a8b18c707fa6d695771aab9300081d3e21a", + "7a69e5f62fcc20e81beea7461a945e6531f8c7944200d0b3cb4cc63556c44106", + "fd0266485777bd73e97c7c37f520c83c82e362fe4c25a6be93f3380083d4646b", + "4b29db7a76f3b4fbc0a4fffc092e41c14f1a1a975a462d87e82827af03719cb2", + "df1a6cb6c95a5bdd2a69e4fa921061da950fc0bb0b3529d04ca75e0c11f871df", + "08bfc00b7f72e015f45c326f486bec16e4d5236b70e44543f1c5e86a8e21c76a", + "1e908fbc1d131c17a87f32069f53f64f45c75f91a2f6d43f8aa6410974da5562", + "b3a737d014a7e75f44b0f5afbd752f9bcc2abe54f60dbbebc3681b6e16611967", + "d3052ca3e3d523b1ec80671eb1bba0517a2f522e195778dc83dd03a8d84a170e", + "b98ded4ceaea20790dbcb3c31400692009d34c7f9927c286835a99b7481a5c22", + "9e1e498420bc7c35f3e3a78d20045b4c8343986dae48739759bccb2a27e88c53", + "141d2053cb29535ad45aa9e865cdec492524f0ec0066496b98b7099daab5d658", + "8722c3843c85ddd6162a5cb506e1cb4d6ab0cafb966034f426e55a2ef89a345e", + "52dfd21724329920c5c95f5361464e468584136d30030eb29247a7fe6c2c6e36", + "d82a91e1013170b10ca7fa0ec800fd0dc6e9335b70c303dadba603fc36802b5f", + "6bbb7d71eaa2544215a877e136cd7f490f4625eb56459a0da856cc8296d5df30", + "1ebb28301aa1a48248d3723a0ea434bb7d4612ec920fa749583e3f41ce25849f", + "00000001505e7e48927046e9bbaa728b1f3b511227e2200c578d6e6bb0c77eb9", + "a01b5ba26421374250442e0d23f96e6a4bce429e0175cd0769ad8c585dd5a892", + "26d6a946675e603f8de4bf6f9cef442037b70c7eee170ff06ed7673fc34c98f1", + "1dd7992ea0ecbda7480ceed35748c3655691133c8c78af947fd24299db8f481f", + "cdee943cbb19c51ab847a66d5d774373aa9f63d287246bb59b0827fa5e637400", + "3d03c53608415b1d718c7786ee10bdb4e67bced32207e32880ee9e44301a19ec", + "170dc4045d6c06275b40bd39f68ca31dbb962094e9763ee460f8341bd40bebca", + "db1abbff170320730e5ace672ad7217161b8935afc1a896ae2fecf903c159932", + "b0ac2c26eabdb0e0a9b0d10fd1458ca73c575b19d65e13f0e7484cbee84038b3", + "c1e7fc21b4f9c199e6086e095639f0f16a4e4884544547ce8a653ed7b5b6c4a7", + "813c2662366a12f6337b951c048552fd3c4894e403cab701634dcd803786dc09", + "fd0bcf8cd1aee83fe75e6c0fdfc543845e5bc3f50d26d2d5e5c6d3fa521f98c0", + "45fd1955f590da87c1fd2edb99d17accf235ec3ebf0afe1d3306ade42693c6e9", + "27938497226683c701e2843c6db580e2f0e25f5a198f4c3397e3a0a27764215d", + "2321edfd415f9558605b4d7a7083c52624e8922ae86bb2ae359fbf829724111a", + "0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd", + "1f8e182bf72d61cb78dcd6b96dd3be8b874b8881da6630757b6508970f67230c", + "c6603b0f1ccfec625d9c08b753e4f774eaf7d1cf2769223125b5fd4da728019e", + "296842eaaed9be5ae0668da09fe48aac0521c4af859ad547d93145e5ac34c17e", + "88f8707a45e825a13ed383332abe6e2f104ab44d877918be22550083a2b59e60", + "27a20b41a66b35d442302a50ca1baad72c2ed844c8d1224c9f6d50a12752084e", + "280e847ef0c82a2a7c4e877c91cd7567474c1431b815d27bbc1017e147d9d77c", + "ad9d42203fd2480ea2e5c4c64593a027708aebe2b02aa60bd7b1d666daa5b08d", + "bb0174ae21a6cac1a0a9c8b4ac6ebfda56ce51605c315b1824970bc275f7239a", + "edb470271297ac5a61f277f3cd14de54c67eb5ccd20ef0d9df29be18685bb004", + "9609b093450dd7e0afb389619acdaf2e6a0d6817c93552f3911e05b50ae73e3d", + "3e33fd7124f174fc535151937f8718634dd9d856143d4cefb5a10ddaf2f615c0", + "463555bb4b0f80fd1376fae628fabfaf7e5e31cd2741d80aa4d225c926bc287e", + "916cb5ff07d3b51cef7f6b6b7f5479b1001b401c0e82558ee1a22504c7d507c9", + "cc5f259f036683798e4a52071dbb97238702ffb6f0c85af6d273c8ddbe5c0afb", + "2ad91f1dca2dcd5fc89e7208d1e5059f0bac0870d63fc3bac21c7a9388fa18fd", + "8bf629b3d519a0f8a8390137a445c0eb2f5f2b4a8ed71151de898051e8006f13", + "94215f42a96335c87fcb9e881a0bbb62b9a795519e109cf5f9d2ef617681f622", + "bcbf9644d3f475d00eb9c6e467385ce16d4546c1a24222ccfa542bf776eaba95", + "ba5115c37b0f911e530ed6c487ccbd9b737da33fd4b88a9f590860378c06af62", + "609f186ca023d658c0fe019570472f59565c8be1dc163b1541fac9d90aa4e8af", + "4e5622b575cdbb4d5ded093e48c68cd3f724fad547142f0c1b0aec2f2b2a0b2e", + "4df7b43b3a4db4b99e3dbad6bd0f84226726efd63ae7e027f91acbd91b4dba48", + "bcbf9644d3f475d00eb9c6e467385ce16d4546c1a24222ccfa542bf776eaba95", + "3a06add309fd8419ea4d4e475e9c0dff5909c635d9769bf0728232f3a0683a84", + "d2384c4ac623eb9f15ae4cb32ee7a9b03e0202802d52f2395f2ee8f6433151aa", + "1d4cc828b657da8c7a101e8657365459b9dc74139bed5d35bd8295b00be2a1ae", + "76fcec0e0638351f1d0e0dc4ebaf6dd3d67404126d664547674070f3175273d9", + "6707c39e6c53ef945c5df29af78667dc941ed80094994bd264fd806a6e0a3230", + "80482e60178c2ce996da6d67577f56a2b2c47ccb1c84c81f2b7960637cb71b78", + "147784df719c09fad62bff0493a60b4f5dbbe8579e73f00d207350e8ffdfd65f", + "afc0295d2c6e0a1820c214c07312070a4070d52083163d7fe410fa02bf85d9d2", + "6a5e3cc17279cbdf051c06d96e3f843cdb296f351d8ca35a6a190c0ab90dbf9a", + "3b7fc823611f1aeaea63ee3bf69b25b8aa16ec6e81d1afc39026808fe194354f", + "d96fe9c5478d1bb36e9ec40cc678b0bf7ff67e017922a151f925640a8884f291", + "1d80e5588de010d137a67c42b03717595f5f510e73e42cfc48f31bae91844d59", + "06dde95f0268ce40128bf73ca6e85567b8567688ea52f24dcd5734e77c50f2d9", + "f683e87035f7ad4f44e0b98cfbd9537e16455a92cd38cefc4cb31db7557f5ef2", + "036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58", + "7cc328a08ddb2afdf9f9be77beff4c83489ff979721827d628a542f32a247c0e", + "f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba", + "fb61b93d864e4f0eba766bb8556f2dc0262e8e985012e29ba28508dd52067d98", + "0e52122d1eb95cdd8ba5f65815f7d1c9125a8c14d82989eae52ab369eea6c7e4", + "04dcaf2552801937d1c20b69adf89646f21b53c17906271d22c7be9bcadb96c0", + "0ee827a36e8bb0cfc483cf1872781182c4a16c58acba3ae2d7b155e0370e93b8", + "adc14fa3ad590856dd8b80815d367f7c1e6735ad00fd98a86d002fbe9fb535e1", + "2bc9be7569515701581d5d765422a17ee3500d8e1f4e7aa53f6be86ae6ba274d", + "9a21569255d0a3a9e75f1de2e4c883c9be2e5615887f22b2ecf6b1813bcd587d", + "3f3ff7adb39159c42c0aa16d53c0483bfbfad57df22a9e9e9364a306741eb2cf", + "e9aa50decff01f2cec1ec2b2e0b34332cf9c92cafdac5a7cc0881a6d26b59854", + "c7eda660a6bc8270530e82b4a7712acdea2e31dc0a56f8dc955ac009efd97c86", + "787338757fc25d65cd929394d5e7713cf43638e8d259e8dcf5c73b834eb851f2", + "7e8ffe414a53ce18a85536eda74d3cbda0da6a98d4fe6c514948002472178c95", + "6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3", + "0c371f5ed95076613443e8331c4b60828ed67bcdefaa1698fb5ce9d7b3285ffb", + "daa41bedb68591363bf4407f687cb9789cc543ed024bb77c22d2c84d88f54153", + "12ee03d11684a125dd87be879c28190415be3f3b1eca6b4ed743bd74ffd880e6", + "00dfb20815a71e24572394efdfbf772e56a507921b8287201ab8937496ee8e6d", + "94a6a78a5aebbba40bd1aaa2234810132c2d8004bb9177616c413d3c0ddf320e" +]; +const PUBKEY_ARRAYS = generateTestPubkeyArrays(); + +// Main testing function +async function testRelayPubkeyLimits() { + console.log('Starting Nostr Relay Pubkey Filter Test'); + console.log(`Testing ${RELAYS.length} relays with up to ${MAX_PUBKEYS} pubkeys`); + console.log(`Incrementing by ${STEP_SIZE} pubkeys per test, requesting ${EVENT_LIMIT} events per test\n`); + + const results = {}; + + // Initialize results for each relay + for (const relayUrl of RELAYS) { + results[relayUrl] = { + maxPubkeys: 0, + failures: 0, + lastSuccess: 0 + }; + } + + // Test each pubkey array size + for (const pubkeyArray of PUBKEY_ARRAYS) { + const pubkeyCount = pubkeyArray.length; + console.log(`\nTesting with ${pubkeyCount} pubkeys...`); + + // Test each relay with this pubkey count + for (const relayUrl of RELAYS) { + if (results[relayUrl].failures >= 2) { + console.log(` ${relayUrl}: Skipping (already failed 2 times)`); + continue; + } + + try { + const success = await testRelayWithPubkeys(relayUrl, pubkeyArray); + if (success) { + results[relayUrl].maxPubkeys = pubkeyCount; + results[relayUrl].lastSuccess = pubkeyCount; + results[relayUrl].failures = 0; // Reset failures on success + console.log(` ${relayUrl}: ✓ Success`); + } else { + results[relayUrl].failures++; + console.log(` ${relayUrl}: ✗ Failed (${results[relayUrl].failures}/2)`); + } + } catch (error) { + results[relayUrl].failures++; + console.log(` ${relayUrl}: ✗ Error (${results[relayUrl].failures}/2): ${error.message}`); + } + } + } + + // Print final results + console.log('\n=== FINAL RESULTS ==='); + for (const relayUrl of RELAYS) { + const result = results[relayUrl]; + console.log(`${relayUrl}: ${result.maxPubkeys} pubkeys (last success: ${result.lastSuccess})`); + } +} + +// Test a single relay with a specific pubkey array +async function testRelayWithPubkeys(relayUrl, pubkeys) { + return new Promise((resolve) => { + const ws = new WebSocket(relayUrl); + let receivedEOSE = false; + let subscriptionId = 'test_' + Math.random().toString(36).substr(2, 9); + let timeoutId; + + const cleanup = () => { + clearTimeout(timeoutId); + ws.close(); + }; + + ws.on('open', () => { + console.log(`Connected to ${relayUrl}`); + // Send subscription request + const filter = { + kinds: [EVENT_KIND], + authors: pubkeys, + limit: EVENT_LIMIT + }; + const subscriptionMessage = ['REQ', subscriptionId, filter]; + const messageString = JSON.stringify(subscriptionMessage); + const messageSize = Buffer.byteLength(messageString, 'utf8'); + console.log(`Sending request with ${pubkeys.length} pubkeys (${messageSize} bytes)`); + ws.send(messageString); + + // Set timeout for EOSE response + timeoutId = setTimeout(() => { + if (!receivedEOSE) { + console.log('Timeout waiting for EOSE'); + cleanup(); + resolve(false); + } + }, 10000); // 10 second timeout + }); + + ws.on('message', (data) => { + try { + const message = JSON.parse(data.toString()); + if (message[0] === 'EVENT' && message[1] === subscriptionId) { + // Skip event content, just log that we received an event + console.log(`Received EVENT for subscription ${subscriptionId}`); + } else if (message[0] === 'EOSE' && message[1] === subscriptionId) { + console.log(`Received EOSE: ${JSON.stringify(message)}`); + receivedEOSE = true; + cleanup(); + resolve(true); + } else { + console.log(`Received other message: ${JSON.stringify(message)}`); + } + } catch (e) { + console.log(`Received non-JSON: ${data.toString()}`); + } + }); + + ws.on('error', (error) => { + console.log(`WebSocket error: ${error.message}`); + cleanup(); + resolve(false); + }); + + // Overall connection timeout + setTimeout(() => { + if (!receivedEOSE) { + cleanup(); + resolve(false); + } + }, 15000); // 15 second total timeout + }); +} + +// Run the test +testRelayPubkeyLimits().catch(console.error); \ No newline at end of file