diff --git a/VERSION b/VERSION index ef52a648..f9056827 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.6 +0.4.7 diff --git a/nostr_core/nip004.h b/nostr_core/nip004.h index d0065c9b..1cf28b30 100644 --- a/nostr_core/nip004.h +++ b/nostr_core/nip004.h @@ -13,7 +13,7 @@ extern "C" { #endif // NIP-04 constants -// #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 65535 +// #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 1048576 // 1MB // NIP-04 Constants // #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 16777216 // 16MB // #define NOSTR_NIP04_MAX_ENCRYPTED_SIZE 22369621 // ~21.3MB (accounts for base64 overhead + IV) diff --git a/nostr_core/nip044.c b/nostr_core/nip044.c index 9051dfb7..1825d4de 100644 --- a/nostr_core/nip044.c +++ b/nostr_core/nip044.c @@ -75,54 +75,55 @@ static size_t calc_padded_len(size_t unpadded_len) { return chunk * ((unpadded_len - 1) / chunk + 1); } -// NIP-44 padding (per spec) +// NIP-44 padding (modified to support 1MB payloads with 32-bit length field) static unsigned char* pad_plaintext(const char* plaintext, size_t* padded_len) { size_t unpadded_len = strlen(plaintext); - if (unpadded_len > 65535) { + if (unpadded_len > 1048576) { return NULL; } - + size_t padded_content_len = calc_padded_len(unpadded_len); - *padded_len = padded_content_len + 2; // Add 2 bytes for the length prefix + *padded_len = padded_content_len + 4; // Add 4 bytes for the length prefix (32-bit) unsigned char* padded = malloc(*padded_len); if (!padded) return NULL; - - // Write length prefix (big-endian u16) - padded[0] = (unpadded_len >> 8) & 0xFF; - padded[1] = unpadded_len & 0xFF; - + + // Write length prefix (big-endian u32) + padded[0] = (unpadded_len >> 24) & 0xFF; + padded[1] = (unpadded_len >> 16) & 0xFF; + padded[2] = (unpadded_len >> 8) & 0xFF; + padded[3] = unpadded_len & 0xFF; + // Copy plaintext and add zero-padding - memcpy(padded + 2, plaintext, unpadded_len); - memset(padded + 2 + unpadded_len, 0, padded_content_len - unpadded_len); - + memcpy(padded + 4, plaintext, unpadded_len); + memset(padded + 4 + unpadded_len, 0, padded_content_len - unpadded_len); + return padded; } -// NIP-44 unpadding (per spec) +// NIP-44 unpadding (modified to support 32-bit length field) static char* unpad_plaintext(const unsigned char* padded, size_t padded_len) { - if (padded_len < 4) return NULL; + if (padded_len < 6) return NULL; // Minimum: 4 bytes length + 2 bytes content - size_t unpadded_len = (padded[0] << 8) | padded[1]; + size_t unpadded_len = (padded[0] << 24) | (padded[1] << 16) | (padded[2] << 8) | padded[3]; size_t expected_padded_len = calc_padded_len(unpadded_len); - - if (padded_len != expected_padded_len + 2) { + if (padded_len != expected_padded_len + 4) { return NULL; } - if (unpadded_len > padded_len - 2) { + if (unpadded_len > padded_len - 4) { return NULL; } char* plaintext = malloc(unpadded_len + 1); if (!plaintext) return NULL; - + // Handle empty message case (unpadded_len can be 0) if (unpadded_len > 0) { - memcpy(plaintext, padded + 2, unpadded_len); + memcpy(plaintext, padded + 4, unpadded_len); } plaintext[unpadded_len] = '\0'; - + return plaintext; } @@ -131,15 +132,15 @@ static char* unpad_plaintext(const unsigned char* padded, size_t padded_len) { // ============================================================================= int nostr_nip44_encrypt_with_nonce(const unsigned char* sender_private_key, - const unsigned char* recipient_public_key, - const char* plaintext, - const unsigned char* nonce, - char* output, - size_t output_size) { + const unsigned char* recipient_public_key, + const char* plaintext, + const unsigned char* nonce, + char* output, + size_t output_size) { if (!sender_private_key || !recipient_public_key || !plaintext || !nonce || !output) { return NOSTR_ERROR_INVALID_INPUT; } - + size_t plaintext_len = strlen(plaintext); if (plaintext_len > NOSTR_NIP44_MAX_PLAINTEXT_SIZE) { return NOSTR_ERROR_NIP44_BUFFER_TOO_SMALL; @@ -260,12 +261,12 @@ int nostr_nip44_encrypt_with_nonce(const unsigned char* sender_private_key, free(aad_data); return NOSTR_ERROR_MEMORY_FAILED; } - + payload[0] = 0x02; // NIP-44 version 2 memcpy(payload + 1, nonce_copy, 32); memcpy(payload + 33, ciphertext, padded_len); memcpy(payload + 33 + padded_len, mac, 32); - + // Base64 encode size_t b64_len = ((payload_len + 2) / 3) * 4 + 1; if (b64_len > output_size) { @@ -315,18 +316,18 @@ int nostr_nip44_encrypt_with_nonce(const unsigned char* sender_private_key, } int nostr_nip44_encrypt(const unsigned char* sender_private_key, - const unsigned char* recipient_public_key, - const char* plaintext, - char* output, - size_t output_size) { + const unsigned char* recipient_public_key, + const char* plaintext, + char* output, + size_t output_size) { // Generate random nonce and call the _with_nonce version unsigned char nonce[32]; if (nostr_secp256k1_get_random_bytes(nonce, 32) != 1) { return NOSTR_ERROR_CRYPTO_FAILED; } - - return nostr_nip44_encrypt_with_nonce(sender_private_key, recipient_public_key, - plaintext, nonce, output, output_size); + + return nostr_nip44_encrypt_with_nonce(sender_private_key, recipient_public_key, + plaintext, nonce, output, output_size); } int nostr_nip44_decrypt(const unsigned char* recipient_private_key, @@ -421,7 +422,6 @@ int nostr_nip44_decrypt(const unsigned char* recipient_private_key, return NOSTR_ERROR_CRYPTO_FAILED; } - // Constant-time MAC verification // Constant-time MAC verification if (!constant_time_compare(received_mac, computed_mac, 32)) { memory_clear(shared_secret, 32); @@ -446,7 +446,7 @@ int nostr_nip44_decrypt(const unsigned char* recipient_private_key, free(payload); return NOSTR_ERROR_MEMORY_FAILED; } - + if (chacha20_encrypt(chacha_key, 0, chacha_nonce, ciphertext, padded_plaintext, ciphertext_len) != 0) { memory_clear(shared_secret, 32); memory_clear(conversation_key, 32); @@ -459,7 +459,6 @@ int nostr_nip44_decrypt(const unsigned char* recipient_private_key, return NOSTR_ERROR_CRYPTO_FAILED; } - // Step 8: Remove padding according to NIP-44 spec // Step 8: Remove padding according to NIP-44 spec char* plaintext = unpad_plaintext(padded_plaintext, ciphertext_len); if (!plaintext) { diff --git a/nostr_core/nip044.h b/nostr_core/nip044.h index 09d60595..e8e3ec78 100644 --- a/nostr_core/nip044.h +++ b/nostr_core/nip044.h @@ -13,7 +13,7 @@ extern "C" { #endif // NIP-44 constants -// #define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 65535 +// #define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 1048576 /** * NIP-44: Encrypt a message using ECDH + ChaCha20 + HMAC diff --git a/nostr_core/nostr_common.h b/nostr_core/nostr_common.h index 9d52aca3..b8fda4b7 100644 --- a/nostr_core/nostr_common.h +++ b/nostr_core/nostr_common.h @@ -72,11 +72,11 @@ #define NIP05_DEFAULT_TIMEOUT 10 // NIP-04 Constants -#define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 16777216 // 16MB +#define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 1048576 // 1MB #define NOSTR_NIP04_MAX_ENCRYPTED_SIZE 22369621 // ~21.3MB (accounts for base64 overhead + IV) -// NIP-44 Constants -#define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 65536 // 64KB max plaintext (matches crypto header) +// NIP-44 Constants +#define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 1048576 // 1MB max plaintext (32-bit length field) // Forward declaration for cJSON (to avoid requiring cJSON.h in header) struct cJSON; diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index 907e1821..0a79808e 100644 --- a/nostr_core/nostr_core.h +++ b/nostr_core/nostr_core.h @@ -2,10 +2,10 @@ #define NOSTR_CORE_H // Version information (auto-updated by increment_and_push.sh) -#define VERSION "v0.4.6" +#define VERSION "v0.4.7" #define VERSION_MAJOR 0 #define VERSION_MINOR 4 -#define VERSION_PATCH 6 +#define VERSION_PATCH 7 /* * NOSTR Core Library - Complete API Reference diff --git a/tests/async_publish_test b/tests/async_publish_test index fca5e1ee..296826cd 100755 Binary files a/tests/async_publish_test and b/tests/async_publish_test differ diff --git a/tests/backward_compat_test b/tests/backward_compat_test new file mode 100755 index 00000000..6d7d6821 Binary files /dev/null and b/tests/backward_compat_test differ diff --git a/tests/nip04_test b/tests/nip04_test index 5205e55b..5c3b8244 100755 Binary files a/tests/nip04_test and b/tests/nip04_test differ diff --git a/tests/nip04_test.c b/tests/nip04_test.c index d13bcc1c..a034bd4c 100644 --- a/tests/nip04_test.c +++ b/tests/nip04_test.c @@ -671,8 +671,8 @@ int test_vector_7_10kb_payload(void) { printf("Last 80 chars: \"...%.80s\"\n", encrypted + encrypted_len - 80); printf("\n"); - // Test decryption with our ciphertext - char* decrypted = malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE); + // Test decryption with our ciphertext - allocate larger buffer for safety + char* decrypted = malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE + 1024); // 1MB + 1KB extra if (!decrypted) { printf("❌ MEMORY ALLOCATION FAILED for decrypted buffer\n"); free(large_plaintext); @@ -680,7 +680,7 @@ int test_vector_7_10kb_payload(void) { return 0; } printf("Testing decryption of 1MB ciphertext (Bob decrypts from Alice)...\n"); - result = nostr_nip04_decrypt(sk2, pk1, encrypted, decrypted, NOSTR_NIP04_MAX_PLAINTEXT_SIZE); + result = nostr_nip04_decrypt(sk2, pk1, encrypted, decrypted, NOSTR_NIP04_MAX_PLAINTEXT_SIZE + 1024); if (result != NOSTR_SUCCESS) { printf("❌ 1MB DECRYPTION FAILED: %s\n", nostr_strerror(result)); diff --git a/tests/nip17_test b/tests/nip17_test index 5cacb8d4..36f099ce 100755 Binary files a/tests/nip17_test and b/tests/nip17_test differ diff --git a/tests/nip44_test b/tests/nip44_test index 11dd2c1d..93d7dc9f 100755 Binary files a/tests/nip44_test and b/tests/nip44_test differ diff --git a/tests/nip44_test.c b/tests/nip44_test.c index 553cc8b9..59a00cd0 100644 --- a/tests/nip44_test.c +++ b/tests/nip44_test.c @@ -69,6 +69,13 @@ static nip44_test_vector_t test_vectors[] = { "4444444444444444444444444444444444444444444444444444444444444444", "", NULL + }, + { + "1MB payload test", + "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe", // Same keys as basic test + "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220", + NULL, // Will be generated dynamically + NULL } }; @@ -86,76 +93,144 @@ static int hex_to_bytes(const char* hex, unsigned char* bytes, size_t len) { static int test_nip44_round_trip(const nip44_test_vector_t* tv) { printf("Test: %s\n", tv->name); - + // Parse keys - both private keys unsigned char sender_private_key[32]; unsigned char recipient_private_key[32]; - + if (hex_to_bytes(tv->sender_private_key_hex, sender_private_key, 32) != 0) { printf(" FAIL: Failed to parse sender private key\n"); return -1; } - + if (hex_to_bytes(tv->recipient_private_key_hex, recipient_private_key, 32) != 0) { printf(" FAIL: Failed to parse recipient private key\n"); return -1; } - + // Generate the public keys from the private keys unsigned char sender_public_key[32]; unsigned char recipient_public_key[32]; - + if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) != 0) { printf(" FAIL: Failed to derive sender public key\n"); return -1; } - + if (nostr_ec_public_key_from_private_key(recipient_private_key, recipient_public_key) != 0) { printf(" FAIL: Failed to derive recipient public key\n"); return -1; } - - // Test encryption - char encrypted[8192]; - int encrypt_result = nostr_nip44_encrypt( - sender_private_key, - recipient_public_key, - tv->plaintext, - encrypted, - sizeof(encrypted) - ); - - if (encrypt_result != NOSTR_SUCCESS) { - printf(" FAIL: Encryption - Expected: %d, Actual: %d\n", NOSTR_SUCCESS, encrypt_result); + + // Special handling for large payload tests + char* test_plaintext; + if (strcmp(tv->name, "1MB payload test") == 0) { + // Generate exactly 1MB (1,048,576 bytes) of predictable content + const size_t payload_size = 1048576; + test_plaintext = malloc(payload_size + 1); + if (!test_plaintext) { + printf(" FAIL: Memory allocation failed for 1MB test payload\n"); + return -1; + } + + // Fill with a predictable pattern: "ABCDEFGH01234567" repeated + const char* pattern = "ABCDEFGH01234567"; // 16 bytes + const size_t pattern_len = 16; + + for (size_t i = 0; i < payload_size; i += pattern_len) { + size_t copy_len = (i + pattern_len <= payload_size) ? pattern_len : payload_size - i; + memcpy(test_plaintext + i, pattern, copy_len); + } + test_plaintext[payload_size] = '\0'; + + printf(" Generated 1MB test payload (%zu bytes)\n", payload_size); + printf(" Pattern: \"%s\" repeated\n", pattern); + printf(" First 64 chars: \"%.64s...\"\n", test_plaintext); + printf(" Last 64 chars: \"...%.64s\"\n", test_plaintext + payload_size - 64); + } else { + test_plaintext = (char*)tv->plaintext; + } + + // Debug: Check plaintext length + size_t plaintext_len = strlen(test_plaintext); + printf(" Plaintext length: %zu bytes\n", plaintext_len); + printf(" Output buffer size: %zu bytes\n", (size_t)10485760); + + // Test encryption - use larger buffer for 1MB+ payloads (10MB for NIP-44 overhead) + char* encrypted = malloc(10485760); // 10MB buffer for large payloads + if (!encrypted) { + printf(" FAIL: Memory allocation failed for encrypted buffer\n"); + if (strcmp(tv->name, "0.5MB payload test") == 0) free(test_plaintext); return -1; } - + + // For large payloads, use _with_nonce to avoid random generation issues + unsigned char fixed_nonce[32] = {0}; + int encrypt_result = nostr_nip44_encrypt_with_nonce( + sender_private_key, + recipient_public_key, + test_plaintext, + fixed_nonce, + encrypted, + 10485760 + ); + + if (encrypt_result != NOSTR_SUCCESS) { + printf(" FAIL: Encryption - Expected: %d, Actual: %d\n", NOSTR_SUCCESS, encrypt_result); + if (strcmp(tv->name, "1MB payload test") == 0) free(test_plaintext); + free(encrypted); + return -1; + } + // Test decryption - use recipient private key + sender public key - char decrypted[8192]; + char* decrypted = malloc(1048576 + 1); // 1MB + 1 for null terminator + if (!decrypted) { + printf(" FAIL: Memory allocation failed for decrypted buffer\n"); + if (strcmp(tv->name, "1MB payload test") == 0) free(test_plaintext); + free(encrypted); + return -1; + } int decrypt_result = nostr_nip44_decrypt( recipient_private_key, sender_public_key, encrypted, decrypted, - sizeof(decrypted) + 1048576 + 1 ); - + if (decrypt_result != NOSTR_SUCCESS) { printf(" FAIL: Decryption - Expected: %d, Actual: %d\n", NOSTR_SUCCESS, decrypt_result); + if (strcmp(tv->name, "1MB payload test") == 0) free(test_plaintext); + free(encrypted); + free(decrypted); return -1; } - + // Verify round-trip - if (strcmp(tv->plaintext, decrypted) != 0) { + if (strcmp(test_plaintext, decrypted) != 0) { printf(" FAIL: Round-trip mismatch\n"); - printf(" Expected: \"%s\"\n", tv->plaintext); + printf(" Expected: \"%s\"\n", test_plaintext); printf(" Actual: \"%s\"\n", decrypted); + if (strcmp(tv->name, "1MB payload test") == 0) free(test_plaintext); + free(encrypted); + free(decrypted); return -1; } - - printf(" PASS: Expected: \"%s\", Actual: \"%s\"\n", tv->plaintext, decrypted); - printf(" Encrypted output: %s\n", encrypted); - + + if (strcmp(tv->name, "1MB payload test") == 0) { + printf(" ✅ 1MB payload round-trip: PASS\n"); + printf(" ✅ Content verification: All %zu bytes match perfectly!\n", strlen(test_plaintext)); + printf(" Encrypted length: %zu bytes\n", strlen(encrypted)); + printf(" 🎉 1MB NIP-44 STRESS TEST COMPLETED SUCCESSFULLY! 🎉\n"); + } else { + printf(" PASS: Expected: \"%s\", Actual: \"%s\"\n", test_plaintext, decrypted); + printf(" Encrypted output: %s\n", encrypted); + } + + if (strcmp(tv->name, "1MB payload test") == 0) free(test_plaintext); + free(encrypted); + free(decrypted); + return 0; } @@ -241,13 +316,17 @@ static int test_nip44_decryption_vector(const nip44_test_vector_t* tv) { } // Test decryption of known vector - char decrypted[8192]; + char* decrypted = malloc(1048576 + 1); // 1MB + 1 for null terminator + if (!decrypted) { + printf(" FAIL: Memory allocation failed for decrypted buffer\n"); + return -1; + } int decrypt_result = nostr_nip44_decrypt( recipient_private_key, sender_public_key, tv->expected_encrypted, decrypted, - sizeof(decrypted) + 1048576 + 1 ); if (decrypt_result != NOSTR_SUCCESS) { @@ -265,7 +344,9 @@ static int test_nip44_decryption_vector(const nip44_test_vector_t* tv) { } printf(" PASS: Expected: \"%s\", Actual: \"%s\"\n", tv->plaintext, decrypted); - + + free(decrypted); + return 0; } @@ -287,11 +368,20 @@ static int test_nip44_encryption_variability() { } // Encrypt the same message multiple times - char encrypted1[8192], encrypted2[8192], encrypted3[8192]; + char* encrypted1 = malloc(2097152); // 2MB buffer + char* encrypted2 = malloc(2097152); + char* encrypted3 = malloc(2097152); + if (!encrypted1 || !encrypted2 || !encrypted3) { + printf(" FAIL: Memory allocation failed for encrypted buffers\n"); + free(encrypted1); + free(encrypted2); + free(encrypted3); + return -1; + } - int result1 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted1, sizeof(encrypted1)); - int result2 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted2, sizeof(encrypted2)); - int result3 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted3, sizeof(encrypted3)); + int result1 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted1, 2097152); + int result2 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted2, 2097152); + int result3 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted3, 2097152); if (result1 != NOSTR_SUCCESS || result2 != NOSTR_SUCCESS || result3 != NOSTR_SUCCESS) { printf(" FAIL: Encryption failed - Results: %d, %d, %d\n", result1, result2, result3); @@ -304,6 +394,9 @@ static int test_nip44_encryption_variability() { printf(" Encryption 1: %.50s...\n", encrypted1); printf(" Encryption 2: %.50s...\n", encrypted2); printf(" Encryption 3: %.50s...\n", encrypted3); + free(encrypted1); + free(encrypted2); + free(encrypted3); return -1; } @@ -314,11 +407,23 @@ static int test_nip44_encryption_variability() { return -1; } - char decrypted1[8192], decrypted2[8192], decrypted3[8192]; - - int decrypt1 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted1, decrypted1, sizeof(decrypted1)); - int decrypt2 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted2, decrypted2, sizeof(decrypted2)); - int decrypt3 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted3, decrypted3, sizeof(decrypted3)); + char* decrypted1 = malloc(1048576 + 1); + char* decrypted2 = malloc(1048576 + 1); + char* decrypted3 = malloc(1048576 + 1); + if (!decrypted1 || !decrypted2 || !decrypted3) { + printf(" FAIL: Memory allocation failed for decrypted buffers\n"); + free(encrypted1); + free(encrypted2); + free(encrypted3); + free(decrypted1); + free(decrypted2); + free(decrypted3); + return -1; + } + + int decrypt1 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted1, decrypted1, 1048576 + 1); + int decrypt2 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted2, decrypted2, 1048576 + 1); + int decrypt3 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted3, decrypted3, 1048576 + 1); if (decrypt1 != NOSTR_SUCCESS || decrypt2 != NOSTR_SUCCESS || decrypt3 != NOSTR_SUCCESS) { printf(" FAIL: Decryption failed - Results: %d, %d, %d\n", decrypt1, decrypt2, decrypt3); @@ -331,12 +436,25 @@ static int test_nip44_encryption_variability() { printf(" Decrypted1: \"%s\"\n", decrypted1); printf(" Decrypted2: \"%s\"\n", decrypted2); printf(" Decrypted3: \"%s\"\n", decrypted3); + free(encrypted1); + free(encrypted2); + free(encrypted3); + free(decrypted1); + free(decrypted2); + free(decrypted3); return -1; } - + printf(" PASS: All encryptions different, all decrypt to: \"%s\"\n", test_message); printf(" Sample ciphertext lengths: %zu, %zu, %zu bytes\n", strlen(encrypted1), strlen(encrypted2), strlen(encrypted3)); - + + free(encrypted1); + free(encrypted2); + free(encrypted3); + free(decrypted1); + free(decrypted2); + free(decrypted3); + return 0; } diff --git a/tests/relay_synchronous_test b/tests/relay_synchronous_test index 74453cce..ab8ad9a2 100755 Binary files a/tests/relay_synchronous_test and b/tests/relay_synchronous_test differ diff --git a/tests/simple_async_test b/tests/simple_async_test index c68b9c71..38ce667b 100755 Binary files a/tests/simple_async_test and b/tests/simple_async_test differ diff --git a/tests/simple_async_test.c b/tests/simple_async_test.c index 3d89fbf5..e5c5b4e3 100644 --- a/tests/simple_async_test.c +++ b/tests/simple_async_test.c @@ -9,10 +9,13 @@ // Test callback function static int callback_count = 0; -void test_callback(const char* relay_url, const char* event_id, +void test_callback(const char* relay_url, const char* event_id, int success, const char* message, void* user_data) { + (void)event_id; // Suppress unused parameter warning + (void)user_data; // Suppress unused parameter warning + callback_count++; - printf("📡 Callback %d: Relay %s, Success: %s\n", + printf("📡 Callback %d: Relay %s, Success: %s\n", callback_count, relay_url, success ? "YES" : "NO"); if (message) { printf(" Message: %s\n", message);