329 lines
12 KiB
C
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, <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;
|
|
}
|