/* * NIP-03 OpenTimestamps Example * * This example demonstrates the full NIP-03 timestamping workflow: * 1. Create a Nostr event * 2. Submit the event ID to an OpenTimestamps calendar * 3. Poll for proof completion (Bitcoin attestation) * 4. Create a NIP-03 proof event (kind 1040) * 5. Verify the proof * * Usage: * ./timestamping # Create and timestamp a test event * ./timestamping # Timestamp an existing event ID * ./timestamping --verify # Verify a proof * * Note: Bitcoin attestations typically take 1-12 hours to appear. * The OpenTimestamps calendars commit to the Bitcoin blockchain * periodically, so you may need to wait and poll for completion. */ #define _POSIX_C_SOURCE 200809L #include "nostr_core/nostr_core.h" #include "nostr_core/nip003.h" #include #include #include #include #include #define DEFAULT_CALENDAR_URL "https://alice.btc.calendar.opentimestamps.org" #define POLL_INTERVAL_SEC 300 /* 5 minutes */ #define MAX_POLL_ATTEMPTS 144 /* 12 hours total */ static void print_usage(const char* prog_name) { printf("NIP-03 OpenTimestamps Example\n"); printf("\n"); printf("Usage:\n"); printf(" %s Create and timestamp a test event\n", prog_name); printf(" %s Timestamp an existing event ID (64-char hex)\n", prog_name); printf(" %s --verify Verify a base64-encoded OTS proof\n", prog_name); printf(" %s --check Check if a proof is complete\n", prog_name); printf("\n"); printf("Options:\n"); printf(" --calendar Use a specific OTS calendar URL\n"); printf(" --interval Poll interval in seconds (default: %d)\n", POLL_INTERVAL_SEC); printf(" --max-attempts Max poll attempts (default: %d)\n", MAX_POLL_ATTEMPTS); printf("\n"); printf("Common calendar URLs:\n"); printf(" https://alice.btc.calendar.opentimestamps.org\n"); printf(" https://bob.btc.calendar.opentimestamps.org\n"); printf(" https://finney.calendar.eternitywall.com\n"); } static void print_attestation_info(const char* ots_b64) { int has_bitcoin = 0, has_litecoin = 0, has_pending = 0; uint64_t btc_height = 0, ltc_height = 0; char* pending_uri = NULL; if (nostr_nip03_get_attestation_info(ots_b64, &has_bitcoin, &has_litecoin, &has_pending, &btc_height, <c_height, &pending_uri) != 0) { printf(" ⚠️ Failed to parse attestation info\n"); return; } if (has_bitcoin) { printf(" ✅ Bitcoin attestation: block height %llu\n", (unsigned long long)btc_height); } if (has_litecoin) { printf(" ✅ Litecoin attestation: block height %llu\n", (unsigned long long)ltc_height); } if (has_pending) { if (pending_uri) { printf(" ⏳ Pending at: %s\n", pending_uri); free(pending_uri); } } if (!has_bitcoin && !has_litecoin && !has_pending) { printf(" ⚠️ No attestations found\n"); } } static int do_timestamp(const char* event_id_hex, const char* calendar_url, int poll_interval, int max_attempts) { printf("📅 NIP-03 Timestamping\n"); printf(" Event ID: %s\n", event_id_hex); printf(" Calendar: %s\n", calendar_url); printf("\n"); /* Step 1: Submit to calendar */ printf("📤 Submitting to OpenTimestamps calendar...\n"); char* ots_b64 = nostr_nip03_request_timestamp(event_id_hex, calendar_url, 30); if (!ots_b64) { printf("❌ Failed to submit to calendar.\n"); printf(" Check your internet connection and the calendar URL.\n"); return 1; } printf("✅ Initial proof received.\n"); print_attestation_info(ots_b64); printf("\n"); /* Step 2: Poll for completion */ printf("⏳ Polling for Bitcoin attestation (every %d seconds, max %d attempts)...\n", poll_interval, max_attempts); int complete = 0; int attempts = 0; while (!complete && attempts < max_attempts) { attempts++; complete = nostr_nip03_is_proof_complete(ots_b64); time_t now = time(NULL); char time_str[26]; ctime_r(&now, time_str); time_str[24] = '\0'; printf("[%s] Attempt %d/%d: %s\n", time_str, attempts, max_attempts, complete ? "✅ COMPLETE" : "⏳ PENDING"); if (!complete) { sleep(poll_interval); /* Try to upgrade the proof */ char* upgraded = nostr_nip03_upgrade_proof(ots_b64, calendar_url, 30); if (upgraded) { free(ots_b64); ots_b64 = upgraded; } } } if (complete) { printf("\n🎉 SUCCESS! The proof is now complete with a Bitcoin attestation.\n"); print_attestation_info(ots_b64); /* Verify the proof */ uint64_t block_height = 0; int verify_rc = nostr_nip03_verify_proof(ots_b64, event_id_hex, &block_height, NULL); if (verify_rc == 0) { printf(" ✅ Proof verified: digest matches, Bitcoin height=%llu\n", (unsigned long long)block_height); } else { printf(" ⚠️ Proof verification returned: %d\n", verify_rc); } /* Create the NIP-03 proof event */ unsigned char private_key[32]; unsigned char public_key[32]; nostr_generate_keypair(private_key, public_key); cJSON* proof_event = nostr_nip03_create_proof_event(event_id_hex, 1, ots_b64, NULL, private_key); if (proof_event) { char* json = cJSON_Print(proof_event); printf("\n📄 NIP-03 Proof Event (kind 1040):\n%s\n", json); free(json); cJSON_Delete(proof_event); } /* Print the base64 OTS data for storage */ printf("\n📦 OTS proof (base64, save this):\n%s\n", ots_b64); } else { printf("\n⏳ Proof is still pending after %d attempts.\n", attempts); printf(" Bitcoin attestations can take several hours.\n"); printf(" Save the OTS proof and check again later:\n%s\n", ots_b64); } free(ots_b64); return complete ? 0 : 2; } static int do_verify(const char* ots_b64, const char* target_digest_hex) { printf("🔍 Verifying OTS proof...\n\n"); uint64_t block_height = 0; time_t block_time = 0; int rc = nostr_nip03_verify_proof(ots_b64, target_digest_hex, &block_height, &block_time); switch (rc) { case 0: printf("✅ Proof verified!\n"); printf(" Bitcoin block height: %llu\n", (unsigned long long)block_height); print_attestation_info(ots_b64); return 0; case 1: printf("⏳ Proof is valid but has no Bitcoin attestation (still pending).\n"); print_attestation_info(ots_b64); return 1; case -1: printf("❌ Failed to parse OTS proof.\n"); return 1; case -2: printf("❌ Digest mismatch! The proof does not match the expected digest.\n"); return 1; default: printf("❌ Unknown error: %d\n", rc); return 1; } } static int do_check(const char* ots_b64) { printf("🔎 Checking OTS proof status...\n\n"); int complete = nostr_nip03_is_proof_complete(ots_b64); if (complete == 1) { printf("✅ Proof is COMPLETE (has Bitcoin/Litecoin attestation)\n"); } else if (complete == 0) { printf("⏳ Proof is PENDING (no Bitcoin attestation yet)\n"); } else { printf("❌ Failed to parse proof (error)\n"); return 1; } print_attestation_info(ots_b64); return 0; } int main(int argc, char* argv[]) { if (nostr_init() != NOSTR_SUCCESS) { fprintf(stderr, "Failed to initialize NOSTR library\n"); return 1; } const char* calendar_url = DEFAULT_CALENDAR_URL; int poll_interval = POLL_INTERVAL_SEC; int max_attempts = MAX_POLL_ATTEMPTS; const char* event_id = NULL; const char* ots_b64 = NULL; const char* verify_digest = NULL; enum { MODE_TIMESTAMP, MODE_VERIFY, MODE_CHECK } mode = MODE_TIMESTAMP; /* Parse arguments */ for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { print_usage(argv[0]); nostr_cleanup(); return 0; } else if (strcmp(argv[i], "--verify") == 0) { mode = MODE_VERIFY; /* OTS data is provided as a positional argument */ } else if (strcmp(argv[i], "--check") == 0) { mode = MODE_CHECK; /* OTS data is provided as a positional argument */ } else if (strcmp(argv[i], "--calendar") == 0 && i + 1 < argc) { calendar_url = argv[++i]; } else if (strcmp(argv[i], "--interval") == 0 && i + 1 < argc) { poll_interval = atoi(argv[++i]); } else if (strcmp(argv[i], "--max-attempts") == 0 && i + 1 < argc) { max_attempts = atoi(argv[++i]); } else if (strcmp(argv[i], "--digest") == 0 && i + 1 < argc) { verify_digest = argv[++i]; } else if (argv[i][0] != '-' && !event_id && !ots_b64) { if (mode == MODE_VERIFY || mode == MODE_CHECK) { if (!ots_b64) ots_b64 = argv[i]; } else { event_id = argv[i]; } } } int rc = 0; switch (mode) { case MODE_TIMESTAMP: if (!event_id) { /* Create a test event */ printf("🧪 Creating a test event to timestamp...\n"); unsigned char private_key[32]; unsigned char public_key[32]; nostr_generate_keypair(private_key, public_key); char content[128]; snprintf(content, sizeof(content), "NIP-03 timestamp test at %ld", time(NULL)); cJSON* event = nostr_create_and_sign_event(1, content, NULL, private_key, 0); if (!event) { printf("❌ Failed to create test event\n"); nostr_cleanup(); return 1; } cJSON* id_item = cJSON_GetObjectItem(event, "id"); if (!id_item || !cJSON_IsString(id_item)) { printf("❌ Failed to get event ID\n"); cJSON_Delete(event); nostr_cleanup(); return 1; } char* event_json = cJSON_Print(event); printf("📄 Test event:\n%s\n\n", event_json); free(event_json); rc = do_timestamp(id_item->valuestring, calendar_url, poll_interval, max_attempts); cJSON_Delete(event); } else { rc = do_timestamp(event_id, calendar_url, poll_interval, max_attempts); } break; case MODE_VERIFY: if (!ots_b64) { printf("❌ Missing OTS proof (base64)\n"); print_usage(argv[0]); rc = 1; } else { rc = do_verify(ots_b64, verify_digest); } break; case MODE_CHECK: if (!ots_b64) { printf("❌ Missing OTS proof (base64)\n"); print_usage(argv[0]); rc = 1; } else { rc = do_check(ots_b64); } break; } nostr_cleanup(); return rc; }