Fix NIP-03 OpenTimestamps parsing, multi-calendar proofs, upgrades, tests, and CLI tooling
This commit is contained in:
@@ -29,8 +29,14 @@ build/
|
||||
/examples/keypair_generation
|
||||
/examples/mnemonic_derivation
|
||||
/examples/mnemonic_generation
|
||||
/examples/ots_tool
|
||||
/examples/relay_pool
|
||||
/examples/send_nip17_dm
|
||||
/examples/simple_keygen
|
||||
/examples/timestamping
|
||||
/examples/utility_functions
|
||||
/examples/version_test
|
||||
|
||||
# Local OpenTimestamps CLI output
|
||||
/examples/timestamped_*
|
||||
/examples/*.ots
|
||||
|
||||
@@ -42,16 +42,37 @@ else()
|
||||
nostr_core/nostr_log.c
|
||||
nostr_core/request_validator.c
|
||||
nostr_core/nip001.c
|
||||
nostr_core/nip003.c
|
||||
nostr_core/nip004.c
|
||||
nostr_core/nip005.c
|
||||
nostr_core/nip006.c
|
||||
nostr_core/nip011.c
|
||||
nostr_core/nip013.c
|
||||
nostr_core/nip017.c
|
||||
nostr_core/nip019.c
|
||||
nostr_core/nip021.c
|
||||
nostr_core/nip042.c
|
||||
nostr_core/nip044.c
|
||||
nostr_core/nip046.c
|
||||
nostr_core/nip059.c
|
||||
nostr_core/nip060.c
|
||||
nostr_core/nip061.c
|
||||
nostr_core/nostr_signer.c
|
||||
nostr_core/nsigner_transport.c
|
||||
nostr_core/nsigner_client.c
|
||||
nostr_core/utils.c
|
||||
nostr_core/nostr_http.c
|
||||
nostr_core/core_relays.c
|
||||
nostr_core/core_relay_pool.c
|
||||
nostr_core/cashu_mint.c
|
||||
nostr_core/blossom_client.c
|
||||
nostr_core/crypto/nostr_secp256k1.c
|
||||
nostr_core/crypto/nostr_aes.c
|
||||
nostr_core/crypto/nostr_chacha20.c
|
||||
nostr_core/crypto/nostr_poly1305.c
|
||||
nostr_core/crypto/nostr_chacha20poly1305.c
|
||||
cjson/cJSON.c
|
||||
nostr_websocket/nostr_websocket_openssl.c
|
||||
platform/linux.c
|
||||
)
|
||||
|
||||
@@ -63,4 +84,36 @@ else()
|
||||
nostr_websocket
|
||||
)
|
||||
target_compile_definitions(nostr_core PRIVATE NOSTR_ENABLE_NSIGNER_CLIENT=1)
|
||||
target_compile_definitions(nostr_core PRIVATE ENABLE_FILE_LOGGING ENABLE_WEBSOCKET_LOGGING ENABLE_DEBUG_LOGGING)
|
||||
|
||||
# Link system dependencies
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PkgConfig_FOUND)
|
||||
pkg_check_modules(SECP256K1 QUIET libsecp256k1)
|
||||
pkg_check_modules(OPENSSL QUIET openssl)
|
||||
pkg_check_modules(CURL QUIET libcurl)
|
||||
endif()
|
||||
|
||||
if(SECP256K1_FOUND)
|
||||
target_include_directories(nostr_core PRIVATE ${SECP256K1_INCLUDE_DIRS})
|
||||
target_link_libraries(nostr_core PRIVATE ${SECP256K1_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(nostr_core PRIVATE secp256k1)
|
||||
endif()
|
||||
|
||||
if(OPENSSL_FOUND)
|
||||
target_include_directories(nostr_core PRIVATE ${OPENSSL_INCLUDE_DIRS})
|
||||
target_link_libraries(nostr_core PRIVATE ${OPENSSL_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(nostr_core PRIVATE ssl crypto)
|
||||
endif()
|
||||
|
||||
if(CURL_FOUND)
|
||||
target_include_directories(nostr_core PRIVATE ${CURL_INCLUDE_DIRS})
|
||||
target_link_libraries(nostr_core PRIVATE ${CURL_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(nostr_core PRIVATE curl)
|
||||
endif()
|
||||
|
||||
target_link_libraries(nostr_core PRIVATE z dl pthread m)
|
||||
endif()
|
||||
|
||||
@@ -434,6 +434,14 @@ Covered modules and functions:
|
||||
- NIP-01 (`nostr_core/nip001.h`)
|
||||
- `cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp);`
|
||||
|
||||
- NIP-03 OpenTimestamps (`nostr_core/nip003.h`)
|
||||
- `cJSON* nostr_nip03_create_proof_event_with_signer(const char* target_event_id, int target_event_kind, const char* ots_data_base64, const char* relay_url, nostr_signer_t* signer);`
|
||||
- `char* nostr_nip03_request_timestamp(const char* event_id_hex, const char* calendar_url, int timeout_seconds);`
|
||||
- `int nostr_nip03_is_proof_complete(const char* ots_data_base64);`
|
||||
- `int nostr_nip03_get_attestation_info(const char* ots_data_base64, int* has_bitcoin, int* has_litecoin, int* has_pending, uint64_t* bitcoin_height, uint64_t* litecoin_height, char** pending_uri_out);`
|
||||
- `char* nostr_nip03_upgrade_proof(const char* ots_data_base64, const char* calendar_url, int timeout_seconds);`
|
||||
- `int nostr_nip03_verify_proof(const char* ots_data_base64, const char* target_digest_hex, uint64_t* block_height_out, time_t* block_time_out);`
|
||||
|
||||
- NIP-17 (`nostr_core/nip017.h`)
|
||||
- `cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls, int num_relays, nostr_signer_t* signer);`
|
||||
- `int nostr_nip17_send_dm_with_signer(cJSON* dm_event, const char** recipient_pubkeys, int num_recipients, nostr_signer_t* signer, cJSON** gift_wraps_out, int max_gift_wraps, long max_delay_sec);`
|
||||
|
||||
@@ -0,0 +1,680 @@
|
||||
/*
|
||||
* OpenTimestamps Command-Line Tool
|
||||
*
|
||||
* A simple tool for timestamping files with OpenTimestamps.
|
||||
*
|
||||
* Usage:
|
||||
* ./ots_tool stamp Interactive: type text, save to file, submit to OTS
|
||||
* ./ots_tool stamp <file> Timestamp an existing file
|
||||
* ./ots_tool check <file.ots> Check if a proof is complete
|
||||
* ./ots_tool verify <file> <file.ots> Verify a proof against a file
|
||||
* ./ots_tool upgrade <file.ots> Upgrade a pending proof
|
||||
*
|
||||
* The "stamp" command:
|
||||
* 1. If no file argument, prompts for text input and saves it to a .txt file
|
||||
* 2. Computes the SHA-256 hash of the file
|
||||
* 3. Submits the hash to an OpenTimestamps calendar
|
||||
* 4. Saves the initial .ots proof to <file>.ots
|
||||
* 5. Polls every 60 seconds to check if the proof is complete
|
||||
* 6. When complete, saves the upgraded proof and verifies it
|
||||
*
|
||||
* Options:
|
||||
* --calendar <url> Use a specific OTS calendar (default: https://alice.btc.calendar.opentimestamps.org)
|
||||
* --interval <sec> Poll interval in seconds (default: 60)
|
||||
* --max-wait <min> Maximum wait time in minutes (default: 120 = 2 hours)
|
||||
*/
|
||||
|
||||
#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>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define DEFAULT_CALENDAR "https://alice.btc.calendar.opentimestamps.org"
|
||||
#define DEFAULT_INTERVAL_SEC 60
|
||||
#define DEFAULT_MAX_WAIT_MIN 120
|
||||
|
||||
/* Read an entire file into a malloc'd buffer. Returns NULL on error. */
|
||||
static unsigned char* read_file(const char* path, size_t* len_out) {
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
|
||||
if (size < 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
unsigned char* buf = (unsigned char*)malloc(size + 1);
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t nread = fread(buf, 1, size, f);
|
||||
fclose(f);
|
||||
|
||||
if (nread != (size_t)size) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buf[size] = '\0';
|
||||
*len_out = (size_t)size;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Write data to a file. Returns 0 on success, -1 on error. */
|
||||
static int write_file(const char* path, const unsigned char* data, size_t len) {
|
||||
FILE* f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
|
||||
size_t nwritten = fwrite(data, 1, len, f);
|
||||
fclose(f);
|
||||
|
||||
return (nwritten == len) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Compute SHA-256 of a file and return as hex string (64 chars + null). */
|
||||
static int compute_file_sha256_hex(const char* path, char* hex_out) {
|
||||
size_t file_len = 0;
|
||||
unsigned char* file_data = read_file(path, &file_len);
|
||||
if (!file_data) return -1;
|
||||
|
||||
unsigned char digest[32];
|
||||
int rc = nostr_sha256(file_data, file_len, digest);
|
||||
free(file_data);
|
||||
|
||||
if (rc != NOSTR_SUCCESS) return -1;
|
||||
|
||||
nostr_bytes_to_hex(digest, 32, hex_out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Convert base64 string to binary file and save. */
|
||||
static int save_base64_to_file(const char* path, const char* b64) {
|
||||
if (!b64) return -1;
|
||||
|
||||
size_t b64_len = strlen(b64);
|
||||
size_t max_decoded = (b64_len / 4) * 3 + 3;
|
||||
unsigned char* data = (unsigned char*)malloc(max_decoded);
|
||||
if (!data) return -1;
|
||||
|
||||
size_t data_len = base64_decode(b64, data);
|
||||
if (data_len == 0) {
|
||||
free(data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = write_file(path, data, data_len);
|
||||
free(data);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Read a binary file and convert to base64 string. Caller must free. */
|
||||
static char* read_file_as_base64(const char* path) {
|
||||
size_t file_len = 0;
|
||||
unsigned char* file_data = read_file(path, &file_len);
|
||||
if (!file_data) return NULL;
|
||||
|
||||
size_t b64_size = ((file_len + 2) / 3) * 4 + 1;
|
||||
char* b64 = (char*)malloc(b64_size);
|
||||
if (!b64) {
|
||||
free(file_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
base64_encode(file_data, file_len, b64, b64_size);
|
||||
free(file_data);
|
||||
return b64;
|
||||
}
|
||||
|
||||
static void print_usage(const char* prog) {
|
||||
printf("OpenTimestamps Command-Line Tool\n");
|
||||
printf("\n");
|
||||
printf("Usage:\n");
|
||||
printf(" %s stamp Interactive: type text, save, submit to OTS\n", prog);
|
||||
printf(" %s stamp <file> Timestamp an existing file\n", prog);
|
||||
printf(" %s check <file.ots> Check if a proof is complete\n", prog);
|
||||
printf(" %s verify <file> <file.ots> Verify a proof against a file\n", prog);
|
||||
printf(" %s upgrade <file.ots> [file] Upgrade a pending proof (optional: original file)\n", prog);
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" --calendar <url> OTS calendar URL (default: %s)\n", DEFAULT_CALENDAR);
|
||||
printf(" --interval <sec> Poll interval in seconds (default: %d)\n", DEFAULT_INTERVAL_SEC);
|
||||
printf(" --max-wait <min> Max wait time in minutes (default: %d)\n", DEFAULT_MAX_WAIT_MIN);
|
||||
printf("\n");
|
||||
printf("Examples:\n");
|
||||
printf(" %s stamp # Type text, timestamp it\n", prog);
|
||||
printf(" %s stamp mydocument.txt # Timestamp a file\n", prog);
|
||||
printf(" %s check mydocument.txt.ots # Check proof status\n", prog);
|
||||
printf(" %s verify mydocument.txt mydocument.txt.ots # Verify proof\n", prog);
|
||||
}
|
||||
|
||||
static int do_stamp(const char* file_path, const char* calendar_url,
|
||||
int interval_sec, int max_wait_min) {
|
||||
char auto_file[256] = {0};
|
||||
const char* target_file = file_path;
|
||||
|
||||
/* If no file specified, prompt for text input */
|
||||
if (!target_file) {
|
||||
printf("📝 Enter text to timestamp (press Enter twice to finish):\n\n");
|
||||
|
||||
/* Read multi-line input until empty line */
|
||||
char* content = NULL;
|
||||
size_t content_cap = 0;
|
||||
size_t content_len = 0;
|
||||
char line[1024];
|
||||
int empty_count = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), stdin)) {
|
||||
if (line[0] == '\n') {
|
||||
empty_count++;
|
||||
if (empty_count >= 1 && content_len > 0) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
empty_count = 0;
|
||||
}
|
||||
|
||||
size_t line_len = strlen(line);
|
||||
if (content_len + line_len + 1 > content_cap) {
|
||||
content_cap = (content_len + line_len + 1) * 2;
|
||||
char* new_content = (char*)realloc(content, content_cap);
|
||||
if (!new_content) {
|
||||
free(content);
|
||||
printf("❌ Out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
content = new_content;
|
||||
}
|
||||
memcpy(content + content_len, line, line_len);
|
||||
content_len += line_len;
|
||||
content[content_len] = '\0';
|
||||
}
|
||||
|
||||
if (!content || content_len == 0) {
|
||||
printf("❌ No text entered.\n");
|
||||
free(content);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Generate a filename based on timestamp */
|
||||
time_t now = time(NULL);
|
||||
struct tm* tm_info = localtime(&now);
|
||||
strftime(auto_file, sizeof(auto_file), "timestamped_%Y%m%d_%H%M%S.txt", tm_info);
|
||||
|
||||
/* Save the text to a file */
|
||||
if (write_file(auto_file, (const unsigned char*)content, content_len) != 0) {
|
||||
printf("❌ Failed to save text to file: %s\n", auto_file);
|
||||
free(content);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("\n💾 Saved text to: %s (%zu bytes)\n", auto_file, content_len);
|
||||
free(content);
|
||||
target_file = auto_file;
|
||||
}
|
||||
|
||||
/* Compute SHA-256 of the file */
|
||||
char digest_hex[65];
|
||||
if (compute_file_sha256_hex(target_file, digest_hex) != 0) {
|
||||
printf("❌ Failed to compute SHA-256 of file: %s\n", target_file);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("🔐 SHA-256: %s\n", digest_hex);
|
||||
|
||||
/* Submit to multiple OTS calendars for redundancy */
|
||||
const char* default_calendars[] = {
|
||||
"https://alice.btc.calendar.opentimestamps.org",
|
||||
"https://bob.btc.calendar.opentimestamps.org",
|
||||
"https://finney.calendar.eternitywall.com",
|
||||
"https://btc.calendar.catallaxy.com",
|
||||
};
|
||||
int num_calendars = 4;
|
||||
|
||||
/* If a specific calendar was specified, use only that one */
|
||||
const char** calendars_to_use;
|
||||
int num_to_use;
|
||||
if (strcmp(calendar_url, DEFAULT_CALENDAR) != 0) {
|
||||
calendars_to_use = &calendar_url;
|
||||
num_to_use = 1;
|
||||
} else {
|
||||
calendars_to_use = default_calendars;
|
||||
num_to_use = num_calendars;
|
||||
}
|
||||
|
||||
printf("\n📤 Submitting to %d OpenTimestamps calendar(s)...\n", num_to_use);
|
||||
|
||||
/* Submit to all calendars and create a combined DetachedTimestampFile */
|
||||
char* ots_b64 = nostr_nip03_stamp_with_multiple_calendars(
|
||||
digest_hex, calendars_to_use, num_to_use, 30);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to submit to any calendar.\n");
|
||||
printf(" Check your internet connection and try again.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Save the .ots proof */
|
||||
char ots_path[512];
|
||||
snprintf(ots_path, sizeof(ots_path), "%s.ots", target_file);
|
||||
|
||||
if (save_base64_to_file(ots_path, ots_b64) != 0) {
|
||||
printf("❌ Failed to save .ots proof to: %s\n", ots_path);
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ Proof saved to: %s\n", ots_path);
|
||||
|
||||
/* Show attestation info - list all pending calendars */
|
||||
char** pending_uris = NULL;
|
||||
int pending_count = nostr_nip03_get_pending_uris(ots_b64, &pending_uris, 16);
|
||||
if (pending_count > 0) {
|
||||
printf(" Pending at %d calendar(s):\n", pending_count);
|
||||
for (int i = 0; i < pending_count; i++) {
|
||||
printf(" • %s\n", pending_uris[i]);
|
||||
free(pending_uris[i]);
|
||||
}
|
||||
free(pending_uris);
|
||||
}
|
||||
|
||||
/* Check if already complete (unlikely but possible) */
|
||||
int complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
if (complete == 1) {
|
||||
printf("\n🎉 Proof is already complete!\n");
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Poll for completion */
|
||||
int max_attempts = (max_wait_min * 60) / interval_sec;
|
||||
printf("\n⏳ Waiting for Bitcoin attestation...\n");
|
||||
printf(" Polling every %d seconds (max %d minutes)\n", interval_sec, max_wait_min);
|
||||
printf(" Press Ctrl+C to stop. The .ots file can be checked later with 'check'.\n\n");
|
||||
|
||||
int attempts = 0;
|
||||
while (!complete && attempts < max_attempts) {
|
||||
attempts++;
|
||||
|
||||
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");
|
||||
fflush(stdout);
|
||||
|
||||
if (complete) break;
|
||||
|
||||
sleep(interval_sec);
|
||||
|
||||
/* Try to upgrade the proof by re-submitting to all calendars */
|
||||
char* upgraded = nostr_nip03_stamp_with_multiple_calendars(
|
||||
digest_hex, calendars_to_use, num_to_use, 30);
|
||||
if (upgraded) {
|
||||
free(ots_b64);
|
||||
ots_b64 = upgraded;
|
||||
|
||||
/* Save the upgraded proof */
|
||||
save_base64_to_file(ots_path, ots_b64);
|
||||
|
||||
/* Check if now complete */
|
||||
complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
}
|
||||
}
|
||||
|
||||
if (complete) {
|
||||
printf("\n🎉 SUCCESS! Proof is complete with a Bitcoin attestation!\n");
|
||||
|
||||
/* Save the final proof */
|
||||
save_base64_to_file(ots_path, ots_b64);
|
||||
printf(" Final proof saved to: %s\n", ots_path);
|
||||
|
||||
/* Show attestation details */
|
||||
int has_bitcoin = 0;
|
||||
uint64_t btc_height = 0;
|
||||
if (nostr_nip03_get_attestation_info(ots_b64, &has_bitcoin, NULL, NULL,
|
||||
&btc_height, NULL, NULL) == 0) {
|
||||
if (has_bitcoin) {
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
}
|
||||
|
||||
/* Verify the proof */
|
||||
uint64_t verify_height = 0;
|
||||
int verify_rc = nostr_nip03_verify_proof(ots_b64, digest_hex, &verify_height, NULL);
|
||||
if (verify_rc == 0) {
|
||||
printf(" ✅ Verified: file digest matches, Bitcoin height=%llu\n",
|
||||
(unsigned long long)verify_height);
|
||||
} else {
|
||||
printf(" ⚠️ Verification returned: %d\n", verify_rc);
|
||||
}
|
||||
} else {
|
||||
printf("\n⏳ Proof is still pending after %d minutes.\n", max_wait_min);
|
||||
printf(" The .ots file has been saved: %s\n", ots_path);
|
||||
printf(" Check again later with: ./ots_tool check %s\n", ots_path);
|
||||
printf(" Or upgrade with: ./ots_tool upgrade %s\n", ots_path);
|
||||
}
|
||||
|
||||
free(ots_b64);
|
||||
return complete ? 0 : 2;
|
||||
}
|
||||
|
||||
static int do_check(const char* ots_path) {
|
||||
printf("🔎 Checking proof: %s\n\n", ots_path);
|
||||
|
||||
char* ots_b64 = read_file_as_base64(ots_path);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to read .ots file: %s\n", ots_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
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 (invalid OTS file?)\n");
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Show detailed attestation info */
|
||||
int has_bitcoin = 0, has_litecoin = 0, has_pending = 0;
|
||||
uint64_t btc_height = 0, ltc_height = 0;
|
||||
|
||||
if (nostr_nip03_get_attestation_info(ots_b64, &has_bitcoin, &has_litecoin, &has_pending,
|
||||
&btc_height, <c_height, NULL) == 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Show all pending calendar URIs */
|
||||
if (has_pending) {
|
||||
char** pending_uris = NULL;
|
||||
int pending_count = nostr_nip03_get_pending_uris(ots_b64, &pending_uris, 16);
|
||||
if (pending_count > 0) {
|
||||
printf(" ⏳ Pending at %d calendar(s):\n", pending_count);
|
||||
for (int i = 0; i < pending_count; i++) {
|
||||
printf(" • %s\n", pending_uris[i]);
|
||||
free(pending_uris[i]);
|
||||
}
|
||||
free(pending_uris);
|
||||
}
|
||||
}
|
||||
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int do_verify(const char* file_path, const char* ots_path) {
|
||||
printf("🔍 Verifying proof...\n");
|
||||
printf(" File: %s\n", file_path);
|
||||
printf(" Proof: %s\n\n", ots_path);
|
||||
|
||||
/* Compute SHA-256 of the file */
|
||||
char digest_hex[65];
|
||||
if (compute_file_sha256_hex(file_path, digest_hex) != 0) {
|
||||
printf("❌ Failed to compute SHA-256 of file: %s\n", file_path);
|
||||
return 1;
|
||||
}
|
||||
printf(" File SHA-256: %s\n", digest_hex);
|
||||
|
||||
/* Read the .ots file */
|
||||
char* ots_b64 = read_file_as_base64(ots_path);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to read .ots file: %s\n", ots_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Verify the proof */
|
||||
uint64_t block_height = 0;
|
||||
time_t block_time = 0;
|
||||
int rc = nostr_nip03_verify_proof(ots_b64, digest_hex, &block_height, &block_time);
|
||||
|
||||
switch (rc) {
|
||||
case 0:
|
||||
printf("\n✅ Proof VERIFIED!\n");
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)block_height);
|
||||
printf(" The file's hash is committed in the Bitcoin blockchain.\n");
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
|
||||
case 1:
|
||||
printf("\n⏳ Proof is valid but still PENDING (no Bitcoin attestation yet).\n");
|
||||
printf(" The file digest matches the proof.\n");
|
||||
printf(" Wait for the calendar to commit to the blockchain, then run:\n");
|
||||
printf(" ./ots_tool upgrade %s\n", ots_path);
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
|
||||
case -1:
|
||||
printf("\n❌ Failed to parse OTS proof. The .ots file may be corrupted.\n");
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
|
||||
case -2:
|
||||
printf("\n❌ DIGEST MISMATCH! The proof does not match this file.\n");
|
||||
printf(" The file may have been modified after timestamping.\n");
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
|
||||
default:
|
||||
printf("\n❌ Unknown error: %d\n", rc);
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int do_upgrade(const char* ots_path, const char* file_path,
|
||||
const char* calendar_url) {
|
||||
printf("⬆️ Upgrading proof: %s\n\n", ots_path);
|
||||
|
||||
char* ots_b64 = read_file_as_base64(ots_path);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to read .ots file: %s\n", ots_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Check current status */
|
||||
int was_complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
if (was_complete == 1) {
|
||||
printf("✅ Proof is already complete! No upgrade needed.\n");
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Use multiple calendars for upgrade */
|
||||
const char* default_calendars[] = {
|
||||
"https://alice.btc.calendar.opentimestamps.org",
|
||||
"https://bob.btc.calendar.opentimestamps.org",
|
||||
"https://finney.calendar.eternitywall.com",
|
||||
"https://btc.calendar.catallaxy.com",
|
||||
};
|
||||
|
||||
/* Determine which calendars to use */
|
||||
const char** calendars_to_use;
|
||||
int num_to_use;
|
||||
if (strcmp(calendar_url, DEFAULT_CALENDAR) != 0) {
|
||||
calendars_to_use = &calendar_url;
|
||||
num_to_use = 1;
|
||||
} else {
|
||||
calendars_to_use = default_calendars;
|
||||
num_to_use = 4;
|
||||
}
|
||||
|
||||
printf("📤 Re-submitting to %d calendar(s)...\n", num_to_use);
|
||||
|
||||
/* Get the digest: try extracting from proof, or compute from file */
|
||||
char digest_hex[65] = {0};
|
||||
int have_digest = 0;
|
||||
|
||||
/* Try extracting from the proof (works for full DetachedTimestampFile) */
|
||||
/* The upgrade_proof function does this internally, but we need the digest
|
||||
* for the multi-calendar stamp function */
|
||||
if (file_path) {
|
||||
if (compute_file_sha256_hex(file_path, digest_hex) == 0) {
|
||||
have_digest = 1;
|
||||
}
|
||||
}
|
||||
|
||||
char* upgraded = NULL;
|
||||
if (have_digest) {
|
||||
printf(" Digest: %s\n", digest_hex);
|
||||
upgraded = nostr_nip03_stamp_with_multiple_calendars(
|
||||
digest_hex, calendars_to_use, num_to_use, 30);
|
||||
} else {
|
||||
/* Fall back to single-calendar upgrade via embedded digest */
|
||||
upgraded = nostr_nip03_upgrade_proof(ots_b64, calendar_url, 30);
|
||||
}
|
||||
|
||||
if (!upgraded) {
|
||||
printf("⏳ No upgrade available yet. The proof is still pending.\n");
|
||||
printf(" Bitcoin attestations typically take 1-12 hours.\n");
|
||||
if (!file_path) {
|
||||
printf(" Tip: provide the original file path to enable digest-based upgrade:\n");
|
||||
printf(" ./ots_tool upgrade %s <original-file>\n", ots_path);
|
||||
}
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Save the upgraded proof */
|
||||
if (save_base64_to_file(ots_path, upgraded) != 0) {
|
||||
printf("❌ Failed to save upgraded proof.\n");
|
||||
free(ots_b64);
|
||||
free(upgraded);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Check if now complete */
|
||||
int complete = nostr_nip03_is_proof_complete(upgraded);
|
||||
if (complete == 1) {
|
||||
printf("✅ Upgrade successful! Proof is now COMPLETE with a Bitcoin attestation.\n");
|
||||
|
||||
int has_bitcoin = 0;
|
||||
uint64_t btc_height = 0;
|
||||
if (nostr_nip03_get_attestation_info(upgraded, &has_bitcoin, NULL, NULL,
|
||||
&btc_height, NULL, NULL) == 0) {
|
||||
if (has_bitcoin) {
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printf("⏳ Upgrade received but proof is still pending.\n");
|
||||
printf(" The upgraded proof has been saved. Try again later.\n");
|
||||
}
|
||||
|
||||
printf(" Updated proof saved to: %s\n", ots_path);
|
||||
|
||||
free(ots_b64);
|
||||
free(upgraded);
|
||||
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;
|
||||
int interval_sec = DEFAULT_INTERVAL_SEC;
|
||||
int max_wait_min = DEFAULT_MAX_WAIT_MIN;
|
||||
|
||||
enum { MODE_NONE, MODE_STAMP, MODE_CHECK, MODE_VERIFY, MODE_UPGRADE } mode = MODE_NONE;
|
||||
const char* arg1 = NULL;
|
||||
const char* arg2 = NULL;
|
||||
|
||||
/* 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], "--calendar") == 0 && i + 1 < argc) {
|
||||
calendar_url = argv[++i];
|
||||
} else if (strcmp(argv[i], "--interval") == 0 && i + 1 < argc) {
|
||||
interval_sec = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--max-wait") == 0 && i + 1 < argc) {
|
||||
max_wait_min = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "stamp") == 0) {
|
||||
mode = MODE_STAMP;
|
||||
} else if (strcmp(argv[i], "check") == 0) {
|
||||
mode = MODE_CHECK;
|
||||
} else if (strcmp(argv[i], "verify") == 0) {
|
||||
mode = MODE_VERIFY;
|
||||
} else if (strcmp(argv[i], "upgrade") == 0) {
|
||||
mode = MODE_UPGRADE;
|
||||
} else if (argv[i][0] != '-') {
|
||||
if (!arg1) {
|
||||
arg1 = argv[i];
|
||||
} else if (!arg2) {
|
||||
arg2 = argv[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
|
||||
switch (mode) {
|
||||
case MODE_STAMP:
|
||||
rc = do_stamp(arg1, calendar_url, interval_sec, max_wait_min);
|
||||
break;
|
||||
|
||||
case MODE_CHECK:
|
||||
if (!arg1) {
|
||||
printf("❌ Missing .ots file path\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_check(arg1);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_VERIFY:
|
||||
if (!arg1 || !arg2) {
|
||||
printf("❌ Missing file or .ots path\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_verify(arg1, arg2);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_UPGRADE:
|
||||
if (!arg1) {
|
||||
printf("❌ Missing .ots file path\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_upgrade(arg1, arg2, calendar_url);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
+924
-30
File diff suppressed because it is too large
Load Diff
+172
-3
@@ -6,7 +6,9 @@
|
||||
#define NOSTR_NIP03_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "cjson/cJSON.h"
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -14,6 +16,10 @@ extern "C" {
|
||||
|
||||
#define NOSTR_KIND_OT_PROOF 1040
|
||||
|
||||
/* Default OTS calendars for multi-calendar stamping */
|
||||
#define NOSTR_OTS_DEFAULT_CALENDAR_COUNT 4
|
||||
extern const char* NOSTR_OTS_DEFAULT_CALENDARS[NOSTR_OTS_DEFAULT_CALENDAR_COUNT];
|
||||
|
||||
/**
|
||||
* Create a NIP-03 OpenTimestamps proof event (kind 1040)
|
||||
*
|
||||
@@ -30,6 +36,22 @@ cJSON* nostr_nip03_create_proof_event(const char* target_event_id,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key);
|
||||
|
||||
/**
|
||||
* Create a NIP-03 OpenTimestamps proof event using a signer (kind 1040)
|
||||
*
|
||||
* @param target_event_id The ID of the event being timestamped (hex string)
|
||||
* @param target_event_kind The kind of the event being timestamped
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param relay_url Optional relay URL where the target event can be found
|
||||
* @param signer Signer handle for signing the proof event
|
||||
* @return cJSON* The signed proof event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip03_create_proof_event_with_signer(const char* target_event_id,
|
||||
int target_event_kind,
|
||||
const char* ots_data_base64,
|
||||
const char* relay_url,
|
||||
nostr_signer_t* signer);
|
||||
|
||||
/**
|
||||
* Request a timestamp for an event ID from an OpenTimestamps calendar
|
||||
*
|
||||
@@ -46,25 +68,172 @@ char* nostr_nip03_request_timestamp(const char* event_id_hex,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Check if an OTS proof is complete (has a Bitcoin attestation)
|
||||
* Check if an OTS proof is complete (has a Bitcoin or Litecoin attestation)
|
||||
*
|
||||
* Parses the OTS binary file format and searches for a Bitcoin block header
|
||||
* attestation (tag 0588960d73d71901) or Litecoin attestation (tag 06856e0d73d71901).
|
||||
* A proof with only pending attestations is not yet complete.
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @return int 1 if complete, 0 if pending, -1 on error
|
||||
* @return int 1 if complete (has Bitcoin/Litecoin attestation), 0 if pending, -1 on error
|
||||
*/
|
||||
int nostr_nip03_is_proof_complete(const char* ots_data_base64);
|
||||
|
||||
/**
|
||||
* Get detailed attestation information from an OTS proof
|
||||
*
|
||||
* Parses the OTS file and extracts information about all attestations found.
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param has_bitcoin Output: 1 if a Bitcoin attestation is present (optional, NULL to skip)
|
||||
* @param has_litecoin Output: 1 if a Litecoin attestation is present (optional, NULL to skip)
|
||||
* @param has_pending Output: 1 if any pending attestations are present (optional, NULL to skip)
|
||||
* @param bitcoin_height Output: Bitcoin block height (optional, NULL to skip)
|
||||
* @param litecoin_height Output: Litecoin block height (optional, NULL to skip)
|
||||
* @param pending_uri_out Output: First pending calendar URI (optional, NULL to skip, caller must free)
|
||||
* @return int 0 on success, -1 on parse error
|
||||
*/
|
||||
int nostr_nip03_get_attestation_info(const char* ots_data_base64,
|
||||
int* has_bitcoin,
|
||||
int* has_litecoin,
|
||||
int* has_pending,
|
||||
uint64_t* bitcoin_height,
|
||||
uint64_t* litecoin_height,
|
||||
char** pending_uri_out);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by fetching missing attestations from a calendar
|
||||
*
|
||||
* Sends the current proof to the calendar's /upgrade endpoint and returns
|
||||
* the updated proof. The calendar may return the same proof if no new
|
||||
* attestations are available, or an upgraded proof with Bitcoin/Litecoin
|
||||
* attestations.
|
||||
*
|
||||
* @param ots_data_base64 Current base64 encoded .ots file content
|
||||
* @param calendar_url The OTS calendar URL
|
||||
* @param timeout_seconds Request timeout
|
||||
* @return char* New base64 encoded .ots data, or NULL on error/no change (caller must free)
|
||||
* @return char* New base64 encoded .ots data, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof(const char* ots_data_base64,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by re-submitting the digest to a calendar
|
||||
*
|
||||
* This is the preferred upgrade method for raw timestamp proofs (returned by
|
||||
* the calendar /digest endpoint) which don't contain the file digest.
|
||||
* Re-submits the digest to the calendar, which returns the current proof
|
||||
* that may include new Bitcoin attestations.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest of the timestamped data
|
||||
* @param calendar_url The OTS calendar URL
|
||||
* @param timeout_seconds Request timeout
|
||||
* @return char* New base64 encoded .ots data, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof_with_digest(const char* digest_hex,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Get all pending calendar URIs from an OTS proof
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param uris_out Array of char* pointers (caller must free each string and the array)
|
||||
* @param max_uris Maximum number of URIs to return
|
||||
* @return int Number of pending URIs found, or -1 on error
|
||||
*/
|
||||
int nostr_nip03_get_pending_uris(const char* ots_data_base64,
|
||||
char*** uris_out,
|
||||
int max_uris);
|
||||
|
||||
/**
|
||||
* Create a full DetachedTimestampFile (.ots) from a digest
|
||||
*
|
||||
* Wraps a SHA-256 digest in the OTS DetachedTimestampFile format with the
|
||||
* proper header magic, version, hash op, and digest. This is the standard
|
||||
* .ots file format that can be verified by the `ots` command-line tool.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest
|
||||
* @return char* Base64 encoded DetachedTimestampFile, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_create_ots_file(const char* digest_hex);
|
||||
|
||||
/**
|
||||
* Submit a digest to multiple calendars and create a combined proof
|
||||
*
|
||||
* Submits the digest to each calendar, collects all the returned timestamps,
|
||||
* and combines them into a single DetachedTimestampFile with a fork operation.
|
||||
* This is the recommended way to timestamp data — using multiple calendars
|
||||
* provides redundancy.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest
|
||||
* @param calendar_urls Array of calendar URLs
|
||||
* @param calendar_count Number of calendars
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* Base64 encoded DetachedTimestampFile with all calendar proofs, or NULL on error
|
||||
*/
|
||||
char* nostr_nip03_stamp_with_multiple_calendars(const char* digest_hex,
|
||||
const char** calendar_urls,
|
||||
int calendar_count,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Request a timestamp for an event ID from multiple OTS calendars
|
||||
*
|
||||
* Submits the event ID to multiple calendars and combines the results into a
|
||||
* single DetachedTimestampFile. Uses the default calendars if none specified.
|
||||
*
|
||||
* @param event_id_hex The event ID to timestamp (64-char hex string)
|
||||
* @param calendar_urls Array of calendar URLs (NULL to use defaults)
|
||||
* @param calendar_count Number of calendars (0 to use defaults)
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* Base64 encoded DetachedTimestampFile, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_request_timestamp_multi(const char* event_id_hex,
|
||||
const char** calendar_urls,
|
||||
int calendar_count,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by re-submitting to multiple calendars
|
||||
*
|
||||
* Re-submits the digest to all specified calendars (or defaults) and returns
|
||||
* a new combined proof. The proof may now contain Bitcoin attestations if
|
||||
* any calendar has committed since the original submission.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest
|
||||
* @param calendar_urls Array of calendar URLs (NULL to use defaults)
|
||||
* @param calendar_count Number of calendars (0 to use defaults)
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* New base64 encoded DetachedTimestampFile, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof_multi(const char* digest_hex,
|
||||
const char** calendar_urls,
|
||||
int calendar_count,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Verify an OTS proof structurally
|
||||
*
|
||||
* Parses the OTS file, optionally verifies the file digest matches the
|
||||
* expected target, and checks for a Bitcoin block header attestation.
|
||||
*
|
||||
* Note: Full on-chain verification (checking the Bitcoin block header's
|
||||
* merkle root) requires fetching the block header from a blockchain API.
|
||||
* This function performs structural verification only.
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param target_digest_hex Expected SHA256 digest of the timestamped data (64-char hex, or NULL to skip)
|
||||
* @param block_height_out Output: Bitcoin block height (optional, NULL to skip)
|
||||
* @param block_time_out Output: Block time (optional, NULL to skip, always 0 for structural verification)
|
||||
* @return int 0 on success (verified), 1 if no Bitcoin attestation found, -1 on parse error, -2 on digest mismatch
|
||||
*/
|
||||
int nostr_nip03_verify_proof(const char* ots_data_base64,
|
||||
const char* target_digest_hex,
|
||||
uint64_t* block_height_out,
|
||||
time_t* block_time_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
#define NOSTR_CORE_H
|
||||
|
||||
// Version information (auto-updated by increment_and_push.sh)
|
||||
#define VERSION "v0.6.6"
|
||||
#define VERSION "v0.6.7"
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 6
|
||||
#define VERSION_PATCH 6
|
||||
#define VERSION_PATCH 7
|
||||
|
||||
/*
|
||||
* NOSTR Core Library - Complete API Reference
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# NIP-03 OpenTimestamps Fix Plan
|
||||
|
||||
## Problem Analysis
|
||||
|
||||
The NIP-03 timestamping implementation in `nostr_core/nip003.c` was never working properly due to several critical bugs in the OTS proof parsing and upgrade logic.
|
||||
|
||||
## OTS Binary Format (Verified against real .ots file)
|
||||
|
||||
### File Structure
|
||||
1. **Header magic** (31 bytes): `\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94`
|
||||
2. **Major version** (LEB128 varuint): typically `0x01`
|
||||
3. **File hash op** (1 byte): `0x08` = SHA256
|
||||
4. **File digest** (32 bytes for SHA256)
|
||||
5. **Timestamp tree** (recursive structure)
|
||||
|
||||
### Timestamp Tree Format
|
||||
- Read a tag byte
|
||||
- `0xff` = separator (read next tag, process it, continue looping)
|
||||
- `0x00` = attestation follows
|
||||
- Any other byte = op code (process op, then recursively parse sub-timestamp)
|
||||
- The last tag (not preceded by `0xff`) is the final item
|
||||
|
||||
### Attestation Format (after `0x00` tag)
|
||||
- 8-byte attestation tag:
|
||||
- `83dfe30d2ef90c8e` = PendingAttestation
|
||||
- `0588960d73d71901` = BitcoinBlockHeaderAttestation
|
||||
- `06856e0d73d71901` = LitecoinBlockHeaderAttestation
|
||||
- varuint payload length
|
||||
- payload bytes:
|
||||
- Pending: varbytes (URI string like "https://alice.btc.calendar.opentimestamps.org")
|
||||
- Bitcoin: varuint (block height)
|
||||
- Litecoin: varuint (block height)
|
||||
|
||||
### Op Codes
|
||||
- `0x02` = SHA1 (CryptOp, 20-byte digest)
|
||||
- `0x03` = RIPEMD160 (CryptOp, 20-byte digest)
|
||||
- `0x08` = SHA256 (CryptOp, 32-byte digest)
|
||||
- `0x67` = KECCAK256 (CryptOp, 32-byte digest)
|
||||
- `0xf0` = Append (BinaryOp: varbytes argument)
|
||||
- `0xf1` = Prepend (BinaryOp: varbytes argument)
|
||||
- `0xf2` = Reverse (UnaryOp)
|
||||
- `0xf3` = Hexlify (UnaryOp)
|
||||
|
||||
### LEB128 Varuint Format
|
||||
- Read bytes, each contributes 7 bits (low bits)
|
||||
- High bit (0x80) = continue reading
|
||||
- Low bit = final byte
|
||||
|
||||
### Varbytes Format
|
||||
- varuint length prefix
|
||||
- followed by that many bytes
|
||||
|
||||
## Bugs Found
|
||||
|
||||
### Bug 1: Broken `is_proof_complete` (nip003.c:128)
|
||||
```c
|
||||
if (len > 150) {
|
||||
complete = 1;
|
||||
}
|
||||
```
|
||||
Uses a size heuristic instead of parsing the OTS format. A pending proof can be >150 bytes, and a complete proof might be smaller. Should parse the binary format and look for Bitcoin/Litecoin attestation tags.
|
||||
|
||||
### Bug 2: Flawed `upgrade_proof` (nip003.c:184)
|
||||
```c
|
||||
if (resp.body_len <= len) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
Rejects any response that isn't strictly larger. A calendar may return the same proof if nothing changed, or a differently-serialized proof. Should compare content, not just length.
|
||||
|
||||
### Bug 3: CMakeLists.txt missing NIP-03
|
||||
The CMake build only includes a subset of NIPs. `nip003.c` and many other NIP source files are missing from the library build target.
|
||||
|
||||
### Bug 4: No proof verification function
|
||||
No way to verify a proof is cryptographically valid against the Bitcoin blockchain.
|
||||
|
||||
### Bug 5: No signer variant
|
||||
All other NIPs have `_with_signer` variants but NIP-03 only has raw private key version.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Add OTS parser to nip003.c
|
||||
Add static helper functions:
|
||||
- `ots_read_varuint(data, pos)` - read LEB128 varuint
|
||||
- `ots_read_varbytes(data, pos)` - read varbytes
|
||||
- `ots_parse_timestamp(data, pos, len, results)` - recursively parse timestamp tree
|
||||
- `ots_find_attestations(data, len)` - top-level parser that finds all attestations
|
||||
|
||||
### Step 2: Fix `nostr_nip03_is_proof_complete`
|
||||
Replace the size heuristic with proper parsing:
|
||||
- Parse the OTS file format
|
||||
- Walk the timestamp tree
|
||||
- Return 1 if any Bitcoin (`0588960d73d71901`) or Litecoin (`06856e0d73d71901`) attestation is found
|
||||
- Return 0 if only pending attestations found
|
||||
- Return -1 on parse error
|
||||
|
||||
### Step 3: Fix `nostr_nip03_upgrade_proof`
|
||||
- Remove the `resp.body_len <= len` check
|
||||
- Accept any valid 200 response with non-empty body
|
||||
- Return the base64-encoded response
|
||||
|
||||
### Step 4: Add `nostr_nip03_verify_proof`
|
||||
New function that:
|
||||
- Parses the OTS proof
|
||||
- Extracts Bitcoin attestation(s) with block height(s)
|
||||
- Optionally fetches the Bitcoin block header from a blockchain API
|
||||
- Verifies the merkle root matches the computed digest
|
||||
- Returns the timestamp (block time) on success
|
||||
|
||||
### Step 5: Add `nostr_nip03_create_proof_event_with_signer`
|
||||
Mirror the pattern used by other NIPs for signer support.
|
||||
|
||||
### Step 6: Fix CMakeLists.txt
|
||||
Add all missing NIP source files to the library build target.
|
||||
|
||||
### Step 7: Add example program
|
||||
Create `examples/timestamping.c` demonstrating the full workflow:
|
||||
- Create a Nostr event
|
||||
- Submit to OTS calendar
|
||||
- Poll for completion
|
||||
- Create proof event
|
||||
- Verify proof
|
||||
|
||||
### Step 8: Fix live test
|
||||
Fix the broken curl debug command in `tests/nip03_live_test.c`.
|
||||
|
||||
### Step 9: Add unit tests
|
||||
Add tests for:
|
||||
- OTS parsing with known test vectors
|
||||
- Proof completeness detection (pending vs complete)
|
||||
- Varuint/varbytes encoding/decoding
|
||||
|
||||
### Step 10: Update documentation
|
||||
Update `README.md` and `nostr_core.h` with NIP-03 function documentation.
|
||||
+53
-7
@@ -2,6 +2,8 @@
|
||||
* NOSTR Core Library - NIP-03 Live Network Test
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "nostr_core/nostr_core.h"
|
||||
#include "nostr_core/nip003.h"
|
||||
#include <stdio.h>
|
||||
@@ -51,11 +53,17 @@ int main() {
|
||||
char* ots_b64 = nostr_nip03_request_timestamp(event_id, CALENDAR_URL, 15);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to submit to calendar. Trying with curl to debug...\n");
|
||||
/* Use a temp file for the binary digest to avoid shell escaping issues */
|
||||
char cmd[1024];
|
||||
snprintf(cmd, sizeof(cmd), "curl -s -X POST --data-binary @- %s/digest <<EOF\n$(echo \"%s\" | xxd -r -p)\nEOF\n", CALENDAR_URL, event_id);
|
||||
printf("Running: %s\n", cmd);
|
||||
system(cmd);
|
||||
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"echo '%s' | xxd -r -p > /tmp/ots_digest.bin && "
|
||||
"curl -s -X POST --data-binary @/tmp/ots_digest.bin -H 'Content-Type: application/octet-stream' %s/digest | xxd | head -20",
|
||||
event_id, CALENDAR_URL);
|
||||
printf("Running debug command...\n");
|
||||
int rc = system(cmd);
|
||||
(void)rc;
|
||||
printf("\n");
|
||||
|
||||
cJSON_Delete(event);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
@@ -86,16 +94,54 @@ int main() {
|
||||
|
||||
if (complete) {
|
||||
printf("\n🎉 SUCCESS! The proof is now complete with a Bitcoin attestation.\n");
|
||||
|
||||
|
||||
// Get attestation details
|
||||
int has_bitcoin = 0, has_pending = 0;
|
||||
uint64_t btc_height = 0;
|
||||
char* pending_uri = NULL;
|
||||
if (nostr_nip03_get_attestation_info(ots_b64, &has_bitcoin, NULL, &has_pending,
|
||||
&btc_height, NULL, &pending_uri) == 0) {
|
||||
if (has_bitcoin) {
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
if (has_pending && pending_uri) {
|
||||
printf(" Pending calendar: %s\n", pending_uri);
|
||||
free(pending_uri);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the proof structurally
|
||||
uint64_t verify_height = 0;
|
||||
int verify_rc = nostr_nip03_verify_proof(ots_b64, event_id, &verify_height, NULL);
|
||||
if (verify_rc == 0) {
|
||||
printf(" ✅ Proof verified: digest matches, Bitcoin height=%llu\n",
|
||||
(unsigned long long)verify_height);
|
||||
} else if (verify_rc == -2) {
|
||||
printf(" ⚠️ Proof verification: digest mismatch\n");
|
||||
} else {
|
||||
printf(" ⚠️ Proof verification returned: %d\n", verify_rc);
|
||||
}
|
||||
|
||||
// Create the final Nostr event
|
||||
cJSON* proof_event = nostr_nip03_create_proof_event(event_id, 1, ots_b64, NULL, private_key);
|
||||
char* json = cJSON_Print(proof_event);
|
||||
printf("\nFinal NIP-03 Event:\n%s\n", json);
|
||||
|
||||
|
||||
free(json);
|
||||
cJSON_Delete(proof_event);
|
||||
} else {
|
||||
printf("\n❌ Test timed out after %d minutes. The proof is still pending.\n", attempts);
|
||||
printf("\n❌ Test timed out after %d attempts. The proof is still pending.\n", attempts);
|
||||
|
||||
// Show pending attestation info
|
||||
int has_pending = 0;
|
||||
char* pending_uri = NULL;
|
||||
if (nostr_nip03_get_attestation_info(ots_b64, NULL, NULL, &has_pending,
|
||||
NULL, NULL, &pending_uri) == 0) {
|
||||
if (has_pending && pending_uri) {
|
||||
printf(" Pending at: %s\n", pending_uri);
|
||||
free(pending_uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(ots_b64);
|
||||
|
||||
@@ -49,6 +49,174 @@ void test_nip03_create_proof_event() {
|
||||
cJSON_Delete(event);
|
||||
}
|
||||
|
||||
void test_nip03_create_proof_event_with_signer() {
|
||||
printf("🧪 Testing NIP-03 proof event creation with signer...\n");
|
||||
|
||||
unsigned char private_key[32];
|
||||
unsigned char public_key[32];
|
||||
nostr_generate_keypair(private_key, public_key);
|
||||
|
||||
nostr_signer_t* signer = nostr_signer_local(private_key);
|
||||
assert(signer != NULL);
|
||||
|
||||
const char* target_id = "e71c6ea722987debdb60f81f9ea4f604b5ac0664120dd64fb9d23abc4ec7c323";
|
||||
int target_kind = 1;
|
||||
const char* ots_b64 = "base64encodedotsdata";
|
||||
|
||||
cJSON* event = nostr_nip03_create_proof_event_with_signer(target_id, target_kind, ots_b64, NULL, signer);
|
||||
assert(event != NULL);
|
||||
|
||||
// Verify kind
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
assert(kind != NULL && kind->valueint == 1040);
|
||||
|
||||
// Verify content
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
assert(content != NULL && strcmp(content->valuestring, ots_b64) == 0);
|
||||
|
||||
// Verify tags (no relay URL, so e_tag has only 2 elements)
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
assert(tags != NULL && cJSON_GetArraySize(tags) == 2);
|
||||
|
||||
cJSON* e_tag = cJSON_GetArrayItem(tags, 0);
|
||||
assert(strcmp(cJSON_GetArrayItem(e_tag, 0)->valuestring, "e") == 0);
|
||||
assert(strcmp(cJSON_GetArrayItem(e_tag, 1)->valuestring, target_id) == 0);
|
||||
assert(cJSON_GetArraySize(e_tag) == 2); /* no relay URL */
|
||||
|
||||
printf("✅ NIP-03 proof event creation with signer test passed!\n");
|
||||
cJSON_Delete(event);
|
||||
nostr_signer_free(signer);
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a real pending OTS proof (base64 encoded) from the OpenTimestamps
|
||||
* calendar. It contains only pending attestations (no Bitcoin attestation),
|
||||
* so is_proof_complete should return 0.
|
||||
*
|
||||
* The proof was generated for a test file and submitted to 3 calendars:
|
||||
* - alice.btc.calendar.opentimestamps.org
|
||||
* - bob.btc.calendar.opentimestamps.org
|
||||
* - finney.calendar.eternitywall.com
|
||||
*
|
||||
* File digest (SHA256): dd4861b2521f83430a93696ca83b085b7fb8fd17ed6a2cee9496ec87d9fd0d6c
|
||||
*/
|
||||
static const char* PENDING_OTS_B64 =
|
||||
"AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEI3UhhslIfg0MKk2lsqDsIW3+4/Rftaizu"
|
||||
"lJbsh9n9DWzwEPH1H7z7SBTEU40qNFhcPKcI//AIxd2zXDTCN5cI8BCIgv8EsqQXO7DY4MNvo50e"
|
||||
"CPEgksMhj36iAZdVcrXi4Q+Y/opid1wVrerjQ+gjGP5DB3wI8QRqVlUo8Ai72PBveP4l/QCD3+MN"
|
||||
"LvkMji4taHR0cHM6Ly9hbGljZS5idGMuY2FsZW5kYXIub3BlbnRpbWVzdGFtcHMub3Jn//AIb923"
|
||||
"oTIXfHkI8BDEnrX0tZZwN/drZnEOh+sACPEgcsd1lsM45YKtSytPZWr+Wkn3MMffWkFzj0qK7ehe"
|
||||
"90wI8QRqVlUo8AjeBSDPqcrx9QCD3+MNLvkMjiwraHR0cHM6Ly9ib2IuYnRjLmNhbGVuZGFyLm9w"
|
||||
"ZW50aW1lc3RhbXBzLm9yZ/AQjt5nZXTx0ZchzqvQ2HoNoAjxIKzXgsLhE8UXDdGZSuVT9f5FvggB"
|
||||
"2nwYP7dmO/NjZwO1CPEEalZVKfAIeB6WGBjHG7gAg9/jDS75DI4pKGh0dHBzOi8vZmlubmV5LmNh"
|
||||
"bGVuZGFyLmV0ZXJuaXR5d2FsbC5jb20=";
|
||||
|
||||
void test_nip03_is_proof_complete_pending() {
|
||||
printf("🧪 Testing NIP-03 is_proof_complete with pending proof...\n");
|
||||
|
||||
int result = nostr_nip03_is_proof_complete(PENDING_OTS_B64);
|
||||
printf(" Result: %d (expected 0 = pending)\n", result);
|
||||
assert(result == 0);
|
||||
|
||||
printf("✅ NIP-03 is_proof_complete (pending) test passed!\n");
|
||||
}
|
||||
|
||||
void test_nip03_get_attestation_info_pending() {
|
||||
printf("🧪 Testing NIP-03 get_attestation_info with pending proof...\n");
|
||||
|
||||
int has_bitcoin = 0, has_litecoin = 0, has_pending = 0;
|
||||
uint64_t btc_height = 0, ltc_height = 0;
|
||||
char* pending_uri = NULL;
|
||||
|
||||
int rc = nostr_nip03_get_attestation_info(PENDING_OTS_B64,
|
||||
&has_bitcoin, &has_litecoin, &has_pending,
|
||||
&btc_height, <c_height, &pending_uri);
|
||||
assert(rc == 0);
|
||||
assert(has_bitcoin == 0);
|
||||
assert(has_litecoin == 0);
|
||||
assert(has_pending == 1);
|
||||
assert(pending_uri != NULL);
|
||||
|
||||
printf(" Pending URI: %s\n", pending_uri);
|
||||
/* Should be one of the known calendar URLs */
|
||||
assert(strstr(pending_uri, "opentimestamps.org") != NULL ||
|
||||
strstr(pending_uri, "eternitywall.com") != NULL);
|
||||
|
||||
free(pending_uri);
|
||||
|
||||
printf("✅ NIP-03 get_attestation_info (pending) test passed!\n");
|
||||
}
|
||||
|
||||
void test_nip03_is_proof_complete_invalid() {
|
||||
printf("🧪 Testing NIP-03 is_proof_complete with invalid data...\n");
|
||||
|
||||
/* Invalid base64 / not an OTS file */
|
||||
int result = nostr_nip03_is_proof_complete("notavalidotsfile");
|
||||
printf(" Result: %d (expected -1 = error)\n", result);
|
||||
assert(result == -1);
|
||||
|
||||
/* NULL input */
|
||||
result = nostr_nip03_is_proof_complete(NULL);
|
||||
assert(result == -1);
|
||||
|
||||
printf("✅ NIP-03 is_proof_complete (invalid) test passed!\n");
|
||||
}
|
||||
|
||||
void test_nip03_verify_proof_pending() {
|
||||
printf("🧪 Testing NIP-03 verify_proof with pending proof...\n");
|
||||
|
||||
/* The file digest for this proof */
|
||||
const char* expected_digest = "dd4861b2521f83430a93696ca83b085b7fb8fd17ed6a2cee9496ec87d9fd0d6c";
|
||||
|
||||
uint64_t block_height = 0;
|
||||
time_t block_time = 0;
|
||||
|
||||
int rc = nostr_nip03_verify_proof(PENDING_OTS_B64, expected_digest,
|
||||
&block_height, &block_time);
|
||||
printf(" Result: %d (expected 1 = no bitcoin attestation)\n", rc);
|
||||
/* Should return 1 (no Bitcoin attestation found, but digest matches) */
|
||||
assert(rc == 1);
|
||||
|
||||
/* Test with wrong digest */
|
||||
rc = nostr_nip03_verify_proof(PENDING_OTS_B64,
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
&block_height, &block_time);
|
||||
printf(" Result with wrong digest: %d (expected -2 = digest mismatch)\n", rc);
|
||||
assert(rc == -2);
|
||||
|
||||
printf("✅ NIP-03 verify_proof (pending) test passed!\n");
|
||||
}
|
||||
|
||||
void test_nip03_create_and_verify_roundtrip() {
|
||||
printf("🧪 Testing NIP-03 create proof event + verify roundtrip...\n");
|
||||
|
||||
unsigned char private_key[32];
|
||||
unsigned char public_key[32];
|
||||
nostr_generate_keypair(private_key, public_key);
|
||||
|
||||
/* Create a proof event with the pending OTS data */
|
||||
const char* target_id = "dd4861b2521f83430a93696ca83b085b7fb8fd17ed6a2cee9496ec87d9fd0d6c";
|
||||
cJSON* proof_event = nostr_nip03_create_proof_event(target_id, 1, PENDING_OTS_B64, NULL, private_key);
|
||||
assert(proof_event != NULL);
|
||||
|
||||
/* Verify the event is valid */
|
||||
int validate_rc = nostr_validate_event(proof_event);
|
||||
assert(validate_rc == NOSTR_SUCCESS);
|
||||
|
||||
/* Extract the OTS data from the event content and check completeness */
|
||||
cJSON* content = cJSON_GetObjectItem(proof_event, "content");
|
||||
assert(content != NULL && cJSON_IsString(content));
|
||||
int complete = nostr_nip03_is_proof_complete(content->valuestring);
|
||||
assert(complete == 0); /* pending */
|
||||
|
||||
char* json = cJSON_Print(proof_event);
|
||||
printf(" Created proof event (kind 1040), %zu bytes\n", strlen(json));
|
||||
free(json);
|
||||
|
||||
cJSON_Delete(proof_event);
|
||||
printf("✅ NIP-03 create + verify roundtrip test passed!\n");
|
||||
}
|
||||
|
||||
int main() {
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize NOSTR library\n");
|
||||
@@ -56,9 +224,16 @@ int main() {
|
||||
}
|
||||
|
||||
test_nip03_create_proof_event();
|
||||
test_nip03_create_proof_event_with_signer();
|
||||
test_nip03_is_proof_complete_pending();
|
||||
test_nip03_get_attestation_info_pending();
|
||||
test_nip03_is_proof_complete_invalid();
|
||||
test_nip03_verify_proof_pending();
|
||||
test_nip03_create_and_verify_roundtrip();
|
||||
|
||||
// Note: We don't test nostr_nip03_request_timestamp here as it requires a live OTS calendar
|
||||
// and network access, which might be flaky in a test environment.
|
||||
// See tests/nip03_live_test.c for live network tests.
|
||||
|
||||
nostr_cleanup();
|
||||
printf("\n🎉 All NIP-03 tests passed!\n");
|
||||
|
||||
Reference in New Issue
Block a user