diff --git a/VERSION b/VERSION index 2228cad4..fae59cac 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.7 +0.6.8 diff --git a/examples/cashu_wallet b/examples/cashu_wallet index 34111e2a..79566e21 100755 Binary files a/examples/cashu_wallet and b/examples/cashu_wallet differ diff --git a/examples/nip46_remote_signer b/examples/nip46_remote_signer index 9dc4c897..1288ff46 100755 Binary files a/examples/nip46_remote_signer and b/examples/nip46_remote_signer differ diff --git a/examples/note_poster b/examples/note_poster index 38c1d193..132ad0e8 100755 Binary files a/examples/note_poster and b/examples/note_poster differ diff --git a/examples/ots_tool.c b/examples/ots_tool.c index 601ab5b7..6cd7fb7c 100644 --- a/examples/ots_tool.c +++ b/examples/ots_tool.c @@ -319,9 +319,8 @@ static int do_stamp(const char* file_path, const char* calendar_url, 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); + /* 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; @@ -501,49 +500,14 @@ static int do_upgrade(const char* ots_path, const char* file_path, 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", - }; + (void)calendar_url; /* upgrade now uses pending-attestation URIs from proof */ + (void)file_path; - /* 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; - } - } + printf("📤 Checking pending attestations and fetching upgrades...\n"); 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); - } + /* 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"); diff --git a/nostr_core/nip003.c b/nostr_core/nip003.c index c7888042..d0234abd 100644 --- a/nostr_core/nip003.c +++ b/nostr_core/nip003.c @@ -375,6 +375,361 @@ static int ots_parse_file(const unsigned char* data, size_t data_len, return ots_parse_timestamp(data, data_len, &pos, list, 0); } +/* ========================================================================== */ +/* Tree-Walking Upgrade Logic */ +/* ========================================================================== */ + +/* + * Walk the proof tree and find all pending attestations with their + * commitment hashes. For each, fetch the upgraded proof from the calendar + * via GET /timestamp/. If an upgraded proof is found, + * replace the pending attestation in the original proof bytes. + * + * This implements the same logic as the reference otsclient upgrade_timestamp(): + * - Walk the tree, computing intermediate hashes by applying ops + * - For each PendingAttestation, get the commitment hash (sub_stamp.msg) + * - Call GET /timestamp/ on the calendar + * - If the response contains a Bitcoin attestation, splice it in + */ + +/* Track a pending attestation location in the proof */ +typedef struct { + unsigned char commitment[128]; /* The commitment hash at this point (can be >32 bytes) */ + size_t commitment_len; /* Length of the commitment */ + char uri[256]; /* Calendar URI (null-terminated) */ + size_t att_start; /* Byte offset of the 0x00 attestation tag */ + size_t att_end; /* Byte offset after the attestation */ +} ots_pending_location_t; + +#define OTS_MAX_PENDING_LOCATIONS 32 + +typedef struct { + ots_pending_location_t locations[OTS_MAX_PENDING_LOCATIONS]; + int count; +} ots_pending_location_list_t; + +/* Compute SHA-256 of data */ +static int compute_sha256(const unsigned char* data, size_t len, unsigned char* out) { + return nostr_sha256(data, len, out); +} + +/* Apply an op to a message and return the result */ +static int apply_op(unsigned char op, const unsigned char* arg, size_t arg_len, + const unsigned char* msg, size_t msg_len, + unsigned char* result, size_t* result_len) { + if (op == OTS_OP_APPEND) { + /* result = msg + arg */ + if (msg_len + arg_len > 4096) return -1; + memcpy(result, msg, msg_len); + memcpy(result + msg_len, arg, arg_len); + *result_len = msg_len + arg_len; + } else if (op == OTS_OP_PREPEND) { + /* result = arg + msg */ + if (msg_len + arg_len > 4096) return -1; + memcpy(result, arg, arg_len); + memcpy(result + arg_len, msg, msg_len); + *result_len = msg_len + arg_len; + } else if (op == OTS_OP_SHA256) { + /* result = SHA256(msg) */ + compute_sha256(msg, msg_len, result); + *result_len = 32; + } else if (op == OTS_OP_RIPEMD160) { + /* Not commonly used in OTS proofs, skip */ + return -1; + } else if (op == OTS_OP_REVERSE) { + /* result = reverse(msg) */ + if (msg_len > 4096) return -1; + for (size_t i = 0; i < msg_len; i++) { + result[i] = msg[msg_len - 1 - i]; + } + *result_len = msg_len; + } else if (op == OTS_OP_HEXLIFY) { + /* result = hex(msg) */ + if (msg_len * 2 > 4096) return -1; + nostr_bytes_to_hex(msg, msg_len, (char*)result); + *result_len = msg_len * 2; + } else { + return -1; + } + return 0; +} + +/* + * Walk the proof tree tracking commitment hashes and finding pending attestations. + * Records the byte positions of each pending attestation for later replacement. + */ +static int walk_tree_for_pending(const unsigned char* data, size_t data_len, + size_t* pos, const unsigned char* current_msg, + size_t msg_len, + ots_pending_location_list_t* locations, int depth) { + if (depth > 256 || *pos >= data_len) return -1; + + unsigned char tag = data[*pos]; + (*pos)++; + + /* Handle 0xff separators */ + while (tag == OTS_TAG_SEPARATOR) { + if (*pos >= data_len) return -1; + unsigned char sep_tag = data[*pos]; + (*pos)++; + + if (sep_tag == OTS_TAG_ATTESTATION) { + /* Attestation after separator - check if pending */ + size_t att_start = *pos - 1; /* Position of the 0x00 tag */ + ots_attestation_t att; + if (ots_parse_attestation(data, data_len, pos, &att) != 0) return -1; + size_t att_end = *pos; + + if (att.type == OTS_ATTESTATION_PENDING && locations->count < OTS_MAX_PENDING_LOCATIONS) { + ots_pending_location_t* loc = &locations->locations[locations->count]; + size_t copy_len = msg_len < sizeof(loc->commitment) ? msg_len : sizeof(loc->commitment); + memcpy(loc->commitment, current_msg, copy_len); + loc->commitment_len = copy_len; + size_t uri_copy_len = att.uri_len < sizeof(loc->uri) - 1 ? att.uri_len : sizeof(loc->uri) - 1; + memcpy(loc->uri, att.uri, uri_copy_len); + loc->uri[uri_copy_len] = '\0'; + loc->att_start = att_start; + loc->att_end = att_end; + locations->count++; + } + } else { + /* Op after separator - process op and recurse */ + unsigned char result[4096]; + size_t result_len = 0; + + if (sep_tag == OTS_OP_APPEND || sep_tag == OTS_OP_PREPEND) { + const unsigned char* arg; + size_t arg_len; + if (ots_read_varbytes(data, data_len, pos, &arg, &arg_len) != 0) return -1; + if (apply_op(sep_tag, arg, arg_len, current_msg, msg_len, result, &result_len) != 0) return -1; + } else { + if (apply_op(sep_tag, NULL, 0, current_msg, msg_len, result, &result_len) != 0) return -1; + } + + if (walk_tree_for_pending(data, data_len, pos, result, result_len, locations, depth + 1) != 0) + return -1; + } + + if (*pos >= data_len) return -1; + tag = data[*pos]; + (*pos)++; + } + + /* Process the final tag */ + if (tag == OTS_TAG_ATTESTATION) { + size_t att_start = *pos - 1; + ots_attestation_t att; + if (ots_parse_attestation(data, data_len, pos, &att) != 0) return -1; + size_t att_end = *pos; + + if (att.type == OTS_ATTESTATION_PENDING && locations->count < OTS_MAX_PENDING_LOCATIONS) { + ots_pending_location_t* loc = &locations->locations[locations->count]; + size_t copy_len = msg_len < sizeof(loc->commitment) ? msg_len : sizeof(loc->commitment); + memcpy(loc->commitment, current_msg, copy_len); + loc->commitment_len = copy_len; + size_t uri_copy_len = att.uri_len < sizeof(loc->uri) - 1 ? att.uri_len : sizeof(loc->uri) - 1; + memcpy(loc->uri, att.uri, uri_copy_len); + loc->uri[uri_copy_len] = '\0'; + loc->att_start = att_start; + loc->att_end = att_end; + + locations->count++; + } + } else { + /* Op - process and recurse */ + unsigned char result[4096]; + size_t result_len = 0; + + if (tag == OTS_OP_APPEND || tag == OTS_OP_PREPEND) { + const unsigned char* arg; + size_t arg_len; + if (ots_read_varbytes(data, data_len, pos, &arg, &arg_len) != 0) return -1; + if (apply_op(tag, arg, arg_len, current_msg, msg_len, result, &result_len) != 0) return -1; + } else { + if (apply_op(tag, NULL, 0, current_msg, msg_len, result, &result_len) != 0) return -1; + } + + if (walk_tree_for_pending(data, data_len, pos, result, result_len, locations, depth + 1) != 0) + return -1; + } + + return 0; +} + +/* + * Fetch an upgraded proof from a calendar via GET /timestamp/ + * Returns the raw proof bytes (caller must free), or NULL on error/404. + */ +static unsigned char* fetch_upgraded_proof(const char* calendar_uri, + const unsigned char* commitment, size_t commitment_len, + int timeout_seconds, + size_t* out_len) { + char commitment_hex[65]; + nostr_bytes_to_hex(commitment, commitment_len, commitment_hex); + + char full_url[512]; + snprintf(full_url, sizeof(full_url), "%s/timestamp/%s", calendar_uri, commitment_hex); + + nostr_http_request_t req = {0}; + req.method = "GET"; + req.url = full_url; + req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10; + req.follow_redirects = 1; + req.max_redirects = 3; + + const char* headers[] = { "Accept: application/octet-stream", NULL }; + req.headers = headers; + + nostr_http_response_t resp = {0}; + if (nostr_http_request(&req, &resp) != NOSTR_SUCCESS || + resp.status_code != 200 || !resp.body || resp.body_len == 0) { + nostr_http_response_free(&resp); + return NULL; + } + + /* Copy the response body */ + unsigned char* proof = (unsigned char*)malloc(resp.body_len); + if (proof) { + memcpy(proof, resp.body, resp.body_len); + *out_len = resp.body_len; + } + + nostr_http_response_free(&resp); + return proof; +} + +/* + * Check if a raw timestamp proof contains a Bitcoin attestation. + */ +static int has_bitcoin_attestation(const unsigned char* data, size_t data_len) { + ots_attestation_list_t list; + if (ots_parse_file(data, data_len, &list) != 0) return 0; + for (int i = 0; i < list.count; i++) { + if (list.attestations[i].type == OTS_ATTESTATION_BITCOIN || + list.attestations[i].type == OTS_ATTESTATION_LITECOIN) { + return 1; + } + } + return 0; +} + +/* + * Upgrade a proof by walking the tree, finding pending attestations, + * fetching upgraded proofs from calendars, and splicing them in. + * + * Returns a new base64-encoded proof (caller must free), or NULL on error. + */ +char* nostr_nip03_upgrade_proof_tree(const char* ots_data_base64, + int timeout_seconds) { + if (!ots_data_base64) return NULL; + + size_t input_len = strlen(ots_data_base64); + size_t max_decoded_len = (input_len / 4) * 3 + 3; + unsigned char* data = (unsigned char*)malloc(max_decoded_len); + if (!data) return NULL; + + size_t data_len = base64_decode(ots_data_base64, data); + if (data_len == 0) { + free(data); + return NULL; + } + + /* Find the start of the timestamp tree and the file digest */ + size_t pos = 0; + unsigned char file_digest[32]; + size_t digest_len = 32; + + if (data_len >= OTS_HEADER_MAGIC_LEN && + memcmp(data, OTS_HEADER_MAGIC, OTS_HEADER_MAGIC_LEN) == 0) { + pos = OTS_HEADER_MAGIC_LEN; + uint64_t version; + if (ots_read_varuint(data, data_len, &pos, &version) != 0) { free(data); return NULL; } + if (pos >= data_len) { free(data); return NULL; } + unsigned char hash_op = data[pos++]; + digest_len = ots_digest_length(hash_op); + if (digest_len == 0 || pos + digest_len > data_len) { free(data); return NULL; } + memcpy(file_digest, data + pos, digest_len); + pos += digest_len; + } else { + /* Raw timestamp - can't upgrade without knowing the digest */ + free(data); + return NULL; + } + + /* Walk the tree to find all pending attestations */ + ots_pending_location_list_t locations; + memset(&locations, 0, sizeof(locations)); + + size_t tree_pos = pos; + if (walk_tree_for_pending(data, data_len, &tree_pos, file_digest, digest_len, + &locations, 0) != 0) { + free(data); + return NULL; + } + + if (locations.count == 0) { + /* No pending attestations - nothing to upgrade */ + free(data); + return NULL; + } + + /* For each pending attestation, try to fetch an upgraded proof */ + int upgraded = 0; + for (int i = 0; i < locations.count; i++) { + ots_pending_location_t* loc = &locations.locations[i]; + + size_t upgraded_len = 0; + unsigned char* upgraded_proof = fetch_upgraded_proof( + loc->uri, loc->commitment, loc->commitment_len, timeout_seconds, &upgraded_len); + + if (!upgraded_proof) { + continue; + } + if (!has_bitcoin_attestation(upgraded_proof, upgraded_len)) { + free(upgraded_proof); + continue; + } + + /* We have an upgraded proof with a Bitcoin attestation! + * Replace the pending attestation bytes [att_start, att_end) with + * the upgraded proof bytes. */ + size_t old_att_len = loc->att_end - loc->att_start; + size_t new_data_len = data_len - old_att_len + upgraded_len; + unsigned char* new_data = (unsigned char*)malloc(new_data_len); + if (!new_data) { free(upgraded_proof); continue; } + + /* Copy: [0..att_start) + upgraded_proof + [att_end..data_len) */ + memcpy(new_data, data, loc->att_start); + memcpy(new_data + loc->att_start, upgraded_proof, upgraded_len); + memcpy(new_data + loc->att_start + upgraded_len, data + loc->att_end, + data_len - loc->att_end); + + free(upgraded_proof); + free(data); + data = new_data; + data_len = new_data_len; + upgraded = 1; + + /* Only need one Bitcoin attestation */ + break; + } + + if (!upgraded) { + free(data); + return NULL; + } + + /* Base64 encode the upgraded proof */ + size_t b64_size = ((data_len + 2) / 3) * 4 + 1; + char* b64 = (char*)malloc(b64_size); + if (b64) { + base64_encode(data, data_len, b64, b64_size); + } + + free(data); + return b64; +} + /* ========================================================================== */ /* NIP-03 Public API */ /* ========================================================================== */ @@ -700,24 +1055,25 @@ char* nostr_nip03_upgrade_proof(const char* ots_data_base64, return NULL; } - /* Re-submit the digest to the calendar's /digest endpoint. - * The calendar will return the current proof, which may include - * new Bitcoin attestations if the digest has been committed since - * the original submission. */ + /* Fetch the upgraded proof from the calendar's /timestamp/ endpoint. + * This is the correct API for retrieving upgraded proofs — it returns the + * current proof which may include Bitcoin attestations if the digest has + * been committed since the original submission. + * (POST /digest always returns a fresh pending proof, not the upgraded one.) */ + char digest_hex_str[65]; + nostr_bytes_to_hex(digest, 32, digest_hex_str); + char full_url[512]; - snprintf(full_url, sizeof(full_url), "%s/digest", calendar_url); + snprintf(full_url, sizeof(full_url), "%s/timestamp/%s", calendar_url, digest_hex_str); nostr_http_request_t req = {0}; - req.method = "POST"; + req.method = "GET"; req.url = full_url; - req.body = digest; - req.body_len = 32; req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10; req.follow_redirects = 1; req.max_redirects = 3; const char* headers[] = { - "Content-Type: application/octet-stream", "Accept: application/octet-stream", NULL }; @@ -748,27 +1104,19 @@ char* nostr_nip03_upgrade_proof_with_digest(const char* digest_hex, int timeout_seconds) { if (!digest_hex || !calendar_url) return NULL; - /* Convert hex digest to binary */ - unsigned char digest[32]; - if (nostr_hex_to_bytes(digest_hex, digest, 32) != 0) { - return NULL; - } - - /* Re-submit the digest to the calendar's /digest endpoint */ + /* Fetch the upgraded proof from the calendar's /timestamp/ endpoint. + * This returns the current proof which may include Bitcoin attestations. */ char full_url[512]; - snprintf(full_url, sizeof(full_url), "%s/digest", calendar_url); + snprintf(full_url, sizeof(full_url), "%s/timestamp/%s", calendar_url, digest_hex); nostr_http_request_t req = {0}; - req.method = "POST"; + req.method = "GET"; req.url = full_url; - req.body = digest; - req.body_len = 32; req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10; req.follow_redirects = 1; req.max_redirects = 3; const char* headers[] = { - "Content-Type: application/octet-stream", "Accept: application/octet-stream", NULL }; @@ -1085,7 +1433,81 @@ char* nostr_nip03_upgrade_proof_multi(const char* digest_hex, calendar_count = NOSTR_OTS_DEFAULT_CALENDAR_COUNT; } - /* Re-submit to all calendars to get current proofs */ - return nostr_nip03_stamp_with_multiple_calendars(digest_hex, calendar_urls, - calendar_count, timeout_seconds); + /* Convert hex digest to binary */ + unsigned char digest[32]; + if (nostr_hex_to_bytes(digest_hex, digest, 32) != 0) { + return NULL; + } + + /* Fetch upgraded proof from each calendar via GET /timestamp/ */ + const unsigned char* raw_proofs[16]; + size_t raw_proof_lens[16]; + unsigned char* proof_buffers[16]; + int success_count = 0; + + for (int i = 0; i < calendar_count && i < 16; i++) { + if (!calendar_urls[i]) continue; + + char full_url[512]; + snprintf(full_url, sizeof(full_url), "%s/timestamp/%s", calendar_urls[i], digest_hex); + + nostr_http_request_t req = {0}; + req.method = "GET"; + req.url = full_url; + req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10; + req.follow_redirects = 1; + req.max_redirects = 3; + + const char* headers[] = { + "Accept: application/octet-stream", + NULL + }; + req.headers = headers; + + nostr_http_response_t resp = {0}; + if (nostr_http_request(&req, &resp) != NOSTR_SUCCESS || + resp.status_code != 200 || !resp.body || resp.body_len == 0) { + nostr_http_response_free(&resp); + continue; /* Calendar may not have this digest (404) */ + } + + /* Copy the raw proof data */ + unsigned char* proof_copy = (unsigned char*)malloc(resp.body_len); + if (proof_copy) { + memcpy(proof_copy, resp.body, resp.body_len); + raw_proofs[success_count] = proof_copy; + raw_proof_lens[success_count] = resp.body_len; + proof_buffers[success_count] = proof_copy; + success_count++; + } + + nostr_http_response_free(&resp); + } + + if (success_count == 0) { + return NULL; /* No calendar had an upgraded proof */ + } + + /* Build a combined DetachedTimestampFile */ + size_t out_len = 0; + unsigned char* ots_data = build_dts_file(digest, 32, + raw_proofs, raw_proof_lens, + success_count, &out_len); + + /* Free the individual proof buffers */ + for (int i = 0; i < success_count; i++) { + free(proof_buffers[i]); + } + + if (!ots_data) return NULL; + + /* Base64 encode */ + size_t b64_size = ((out_len + 2) / 3) * 4 + 1; + char* b64 = (char*)malloc(b64_size); + if (b64) { + base64_encode(ots_data, out_len, b64, b64_size); + } + + free(ots_data); + return b64; } diff --git a/nostr_core/nip003.h b/nostr_core/nip003.h index c91baae7..763c88e2 100644 --- a/nostr_core/nip003.h +++ b/nostr_core/nip003.h @@ -135,6 +135,22 @@ char* nostr_nip03_upgrade_proof_with_digest(const char* digest_hex, const char* calendar_url, int timeout_seconds); +/** + * Upgrade an OTS proof by walking the proof tree and fetching upgraded sub-proofs + * + * This is the correct upgrade mechanism, matching the reference ots client: + * 1. Parses the proof tree + * 2. Walks the tree computing intermediate commitment hashes + * 3. For each PendingAttestation, calls GET /timestamp/ + * 4. If the calendar returns a proof with a Bitcoin attestation, splices it in + * + * @param ots_data_base64 Base64 encoded DetachedTimestampFile + * @param timeout_seconds Per-request timeout + * @return char* New base64 encoded upgraded proof, or NULL if no upgrade available (caller must free) + */ +char* nostr_nip03_upgrade_proof_tree(const char* ots_data_base64, + int timeout_seconds); + /** * Get all pending calendar URIs from an OTS proof * diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index 71bc20de..3d655016 100644 --- a/nostr_core/nostr_core.h +++ b/nostr_core/nostr_core.h @@ -2,10 +2,10 @@ #define NOSTR_CORE_H // Version information (auto-updated by increment_and_push.sh) -#define VERSION "v0.6.7" +#define VERSION "v0.6.8" #define VERSION_MAJOR 0 #define VERSION_MINOR 6 -#define VERSION_PATCH 7 +#define VERSION_PATCH 8 /* * NOSTR Core Library - Complete API Reference