Files
nostr_core_lib/examples/ots_tool.c
T

645 lines
21 KiB
C

/*
* 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 walking the tree and fetching upgraded sub-proofs */
char* upgraded = nostr_nip03_upgrade_proof_tree(ots_b64, 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, &ltc_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;
}
(void)calendar_url; /* upgrade now uses pending-attestation URIs from proof */
(void)file_path;
printf("📤 Checking pending attestations and fetching upgrades...\n");
char* upgraded = NULL;
/* Use the tree-walking upgrade which correctly queries commitment hashes */
upgraded = nostr_nip03_upgrade_proof_tree(ots_b64, 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;
}