Fix NIP-03 timestamp upgrade: tree-walk pending attestations, correct PREPEND op composition, and verify Bitcoin attestation completion
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7
-43
@@ -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");
|
||||
|
||||
+446
-24
@@ -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/<commitment-hash>. 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/<commitment-hash> 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/<commitment-hash>
|
||||
* 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/<hash> 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/<hash> 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/<hash> */
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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/<commitment-hash>
|
||||
* 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
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user