/* * 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 Timestamp an existing file * ./ots_tool check Check if a proof is complete * ./ots_tool verify Verify a proof against a file * ./ots_tool upgrade 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 .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 Use a specific OTS calendar (default: https://alice.btc.calendar.opentimestamps.org) * --interval Poll interval in seconds (default: 60) * --max-wait 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 #include #include #include #include #include #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 Timestamp an existing file\n", prog); printf(" %s check Check if a proof is complete\n", prog); printf(" %s verify Verify a proof against a file\n", prog); printf(" %s upgrade [file] Upgrade a pending proof (optional: original file)\n", prog); printf("\n"); printf("Options:\n"); printf(" --calendar OTS calendar URL (default: %s)\n", DEFAULT_CALENDAR); printf(" --interval Poll interval in seconds (default: %d)\n", DEFAULT_INTERVAL_SEC); printf(" --max-wait 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, <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; } (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 \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; }