Files

329 lines
12 KiB
C

/*
* 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 <event_id> # Timestamp an existing event ID
* ./timestamping --verify <ots_b64> # 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#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 <event_id> Timestamp an existing event ID (64-char hex)\n", prog_name);
printf(" %s --verify <ots_b64> Verify a base64-encoded OTS proof\n", prog_name);
printf(" %s --check <ots_b64> Check if a proof is complete\n", prog_name);
printf("\n");
printf("Options:\n");
printf(" --calendar <url> Use a specific OTS calendar URL\n");
printf(" --interval <seconds> Poll interval in seconds (default: %d)\n", POLL_INTERVAL_SEC);
printf(" --max-attempts <n> 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, &ltc_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;
}