Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf6333917d | ||
|
|
122d51903d | ||
|
|
3d3d5d7dd7 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "post_quantum_nostr",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.10",
|
||||
"description": "A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
+50
-5
@@ -497,9 +497,6 @@
|
||||
<div class="pq-button-row">
|
||||
<button class="pq-button pq-button-secondary pq-hidden" id="pqOtsContinueBtn">Continue to OpenTimestamps</button>
|
||||
</div>
|
||||
<div class="pq-button-row">
|
||||
<button class="pq-button pq-button-secondary" id="pqDoneBtn">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 6: OPENTIMESTAMPS -->
|
||||
@@ -737,6 +734,26 @@
|
||||
e.preventDefault();
|
||||
for (let i = 1; i <= 5; i++) setStepDone(i);
|
||||
setStepActive(6);
|
||||
// Fetch the full kind 11112 event from relays so that pqEvent has
|
||||
// the tags and content needed for buildUpgradedEvent() when the
|
||||
// Bitcoin attestation is confirmed and we republish.
|
||||
if (!pqEvent.tags || !pqEvent.content) {
|
||||
otsLog('Resuming OTS workflow: fetching full event from relays...');
|
||||
const relayUrls = getRelayUrls();
|
||||
let fetched = null;
|
||||
await Promise.all(relayUrls.map(async (url) => {
|
||||
try {
|
||||
const ev = await queryRelayForKind11112(currentPubkey, url);
|
||||
if (ev && (!fetched || ev.created_at > fetched.created_at)) fetched = ev;
|
||||
} catch (e) { /* ignore */ }
|
||||
}));
|
||||
if (fetched) {
|
||||
pqEvent = fetched;
|
||||
otsLog(`Resuming OTS workflow: fetched event ${pqEvent.id.substring(0, 16)}... from relays.`);
|
||||
} else {
|
||||
otsLog('WARNING: Could not fetch the full event from relays. Republishing may fail.');
|
||||
}
|
||||
}
|
||||
await startOtsFlow();
|
||||
});
|
||||
}
|
||||
@@ -1149,6 +1166,36 @@
|
||||
}));
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
RELAY QUERY (fetch kind 11112 event for OTS resume)
|
||||
================================================================ */
|
||||
function queryRelayForKind11112(pubkeyHex, relayUrl) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const ws = new WebSocket(relayUrl);
|
||||
let resolved = false;
|
||||
const timeout = setTimeout(() => {
|
||||
if (!resolved) { resolved = true; try { ws.close(); } catch (e) {} reject(new Error('timeout')); }
|
||||
}, 15000);
|
||||
ws.onopen = () => {
|
||||
const subId = 'pq_resume_' + Math.random().toString(36).substring(7);
|
||||
ws.send(JSON.stringify(['REQ', subId, { kinds: [NIP_QR_KIND], authors: [pubkeyHex], limit: 1 }]));
|
||||
};
|
||||
ws.onmessage = (msg) => {
|
||||
try {
|
||||
const data = JSON.parse(msg.data);
|
||||
if (data[0] === 'EVENT') {
|
||||
if (!resolved) { resolved = true; clearTimeout(timeout); try { ws.close(); } catch (e) {} resolve(data[2]); }
|
||||
} else if (data[0] === 'EOSE') {
|
||||
if (!resolved) { resolved = true; clearTimeout(timeout); try { ws.close(); } catch (e) {} resolve(null); }
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
ws.onerror = () => { if (!resolved) { resolved = true; clearTimeout(timeout); reject(new Error('connection error')); } };
|
||||
} catch (e) { reject(e); }
|
||||
});
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
RELAY LIST MANAGEMENT
|
||||
================================================================ */
|
||||
@@ -1359,8 +1406,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pqDoneBtn').addEventListener('click', () => { showAuthedView(); });
|
||||
|
||||
// Continue to OpenTimestamps step (shown after successful publish)
|
||||
document.getElementById('pqOtsContinueBtn').addEventListener('click', () => { prepareOtsStep(); });
|
||||
|
||||
|
||||
+101
-31
@@ -769,9 +769,18 @@ export const PQ_KEY_INFO = {
|
||||
// OPENTIMESTAMPS (NIP-03)
|
||||
// ============================================================================
|
||||
|
||||
// Calendar servers to submit to. We submit to ALL of them in parallel and
|
||||
// merge the returned fragments into a single .ots proof with multiple
|
||||
// attestation branches. This provides redundancy (if one calendar goes
|
||||
// offline, others still provide a path to Bitcoin confirmation) and faster
|
||||
// confirmation (whichever calendar gets the hash into a Bitcoin block first
|
||||
// wins). Servers are ordered by observed uptime/reliability.
|
||||
const OTS_CALENDAR_SERVERS = [
|
||||
'https://alice.btc.calendar.opentimestamps.org',
|
||||
'https://bob.btc.calendar.opentimestamps.org',
|
||||
'https://a.pool.opentimestamps.org',
|
||||
'https://b.pool.opentimestamps.org',
|
||||
'https://ots.btc.catallaxy.com',
|
||||
];
|
||||
|
||||
// Detached .ots file prefix:
|
||||
@@ -807,48 +816,109 @@ export function isDetachedOtsFile(otsBytes) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a hash to OpenTimestamps for timestamping.
|
||||
* Returns a pending .ots file (binary).
|
||||
* Submit a hash to a single OpenTimestamps calendar server.
|
||||
* Returns the raw timestamp fragment bytes (NOT a full .ots file).
|
||||
*
|
||||
* The API endpoint is POST {server}/digest with the raw 32-byte hash as the body.
|
||||
* @param {string} server - Calendar server base URL
|
||||
* @param {Uint8Array} hashBytes - 32-byte SHA-256 hash to timestamp
|
||||
* @returns {Promise<Uint8Array>} timestamp fragment bytes
|
||||
*/
|
||||
async function submitToCalendar(server, hashBytes) {
|
||||
const response = await fetch(`${server}/digest`, {
|
||||
method: 'POST',
|
||||
body: hashBytes,
|
||||
headers: {
|
||||
'Accept': 'application/vnd.opentimestamps.v1',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Calendar ${server} returned HTTP ${response.status}`);
|
||||
}
|
||||
const fragmentBuffer = await response.arrayBuffer();
|
||||
return new Uint8Array(fragmentBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a hash to OpenTimestamps for timestamping.
|
||||
*
|
||||
* Submits to ALL configured calendar servers in parallel and merges the
|
||||
* returned fragments into a single detached .ots file with multiple
|
||||
* attestation branches. This provides:
|
||||
* - Redundancy: if one calendar goes offline, others still provide a path
|
||||
* to Bitcoin confirmation.
|
||||
* - Faster confirmation: whichever calendar gets the hash into a Bitcoin
|
||||
* block first wins.
|
||||
* - Stronger proof: multiple attestations are harder to forge.
|
||||
*
|
||||
* The .ots timestamp tree format supports multiple parallel attestations via
|
||||
* the 0xff continuation marker. Each calendar's fragment is a self-contained
|
||||
* timestamp subtree (ops + pending attestation). We join them at the top
|
||||
* level: [0xff][fragment1][0xff][fragment2]...[fragmentN].
|
||||
*
|
||||
* @param {string} eventIdHex - The Nostr event id (hex string, 32 bytes)
|
||||
* @returns {Promise<Uint8Array>} pending .ots file bytes
|
||||
* @returns {Promise<Uint8Array>} pending .ots file bytes with multiple calendar attestations
|
||||
*/
|
||||
export async function timestampEvent(eventIdHex) {
|
||||
const hashBytes = hexToBytes(eventIdHex);
|
||||
|
||||
// Try each calendar server until one succeeds
|
||||
for (const server of OTS_CALENDAR_SERVERS) {
|
||||
try {
|
||||
const response = await fetch(`${server}/digest`, {
|
||||
method: 'POST',
|
||||
body: hashBytes,
|
||||
headers: {
|
||||
'Accept': 'application/vnd.opentimestamps.v1',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
// Submit to all calendar servers in parallel
|
||||
const results = await Promise.allSettled(
|
||||
OTS_CALENDAR_SERVERS.map(server => submitToCalendar(server, hashBytes))
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`[ots] Calendar ${server} returned ${response.status}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calendar response is a serialized timestamp fragment, not a full
|
||||
// detached .ots file. Wrap it with the OTS header, SHA-256 op, and
|
||||
// the original 32-byte event digest.
|
||||
const fragmentBuffer = await response.arrayBuffer();
|
||||
const fragmentBytes = new Uint8Array(fragmentBuffer);
|
||||
const otsBytes = concatBytes(OTS_DETACHED_PREFIX, hashBytes, fragmentBytes);
|
||||
console.log(`[ots] Received calendar fragment (${fragmentBytes.length} bytes); built detached .ots file (${otsBytes.length} bytes) from ${server}`);
|
||||
return otsBytes;
|
||||
} catch (e) {
|
||||
console.warn(`[ots] Calendar ${server} failed:`, e.message);
|
||||
// Collect successful fragments
|
||||
const fragments = [];
|
||||
const succeeded = [];
|
||||
const failed = [];
|
||||
results.forEach((result, i) => {
|
||||
const server = OTS_CALENDAR_SERVERS[i];
|
||||
if (result.status === 'fulfilled' && result.value && result.value.length > 0) {
|
||||
fragments.push(result.value);
|
||||
succeeded.push(server);
|
||||
console.log(`[ots] Calendar ${server} returned fragment (${result.value.length} bytes)`);
|
||||
} else {
|
||||
const reason = result.status === 'rejected' ? result.reason.message : 'empty response';
|
||||
failed.push(`${server}: ${reason}`);
|
||||
console.warn(`[ots] Calendar ${server} failed: ${reason}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (fragments.length === 0) {
|
||||
throw new Error(`All OpenTimestamps calendar servers failed (${failed.join('; ')})`);
|
||||
}
|
||||
|
||||
throw new Error('All OpenTimestamps calendar servers failed');
|
||||
// Merge fragments into a single .ots file.
|
||||
//
|
||||
// The detached .ots file format is:
|
||||
// [magic header][version][hash op tag][32-byte digest][timestamp tree]
|
||||
//
|
||||
// The timestamp tree for a single attestation is just the fragment bytes.
|
||||
// For multiple attestations, we use 0xff continuation markers to join them:
|
||||
// [0xff][fragment1][0xff][fragment2]...[fragmentN]
|
||||
//
|
||||
// The 0xff byte means "another attestation/op follows for this timestamp
|
||||
// node." The last fragment has no 0xff prefix. This is the same format the
|
||||
// reference `ots-cli stamp` tool produces when stamping with multiple
|
||||
// calendars.
|
||||
const FF = new Uint8Array([0xff]);
|
||||
const treeParts = [];
|
||||
for (let i = 0; i < fragments.length; i++) {
|
||||
if (i < fragments.length - 1) {
|
||||
treeParts.push(FF, fragments[i]);
|
||||
} else {
|
||||
treeParts.push(fragments[i]);
|
||||
}
|
||||
}
|
||||
const timestampTree = concatBytes(...treeParts);
|
||||
|
||||
const otsBytes = concatBytes(OTS_DETACHED_PREFIX, hashBytes, timestampTree);
|
||||
console.log(`[ots] Built merged .ots file (${otsBytes.length} bytes) from ${fragments.length}/${OTS_CALENDAR_SERVERS.length} calendars (${succeeded.join(', ')})`);
|
||||
if (failed.length > 0) {
|
||||
console.warn(`[ots] ${failed.length} calendar(s) failed: ${failed.join('; ')}`);
|
||||
}
|
||||
|
||||
return otsBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.0.7",
|
||||
"VERSION_NUMBER": "0.0.7",
|
||||
"BUILD_DATE": "2026-07-19T11:33:54.796Z"
|
||||
"VERSION": "v0.0.10",
|
||||
"VERSION_NUMBER": "0.0.10",
|
||||
"BUILD_DATE": "2026-07-19T12:34:09.698Z"
|
||||
}
|
||||
|
||||
+54
-23
@@ -9993,7 +9993,10 @@ var PQ_KEY_INFO = {
|
||||
};
|
||||
var OTS_CALENDAR_SERVERS = [
|
||||
"https://alice.btc.calendar.opentimestamps.org",
|
||||
"https://bob.btc.calendar.opentimestamps.org"
|
||||
"https://bob.btc.calendar.opentimestamps.org",
|
||||
"https://a.pool.opentimestamps.org",
|
||||
"https://b.pool.opentimestamps.org",
|
||||
"https://ots.btc.catallaxy.com"
|
||||
];
|
||||
var OTS_DETACHED_PREFIX = hexToBytes3(
|
||||
"004f70656e54696d657374616d7073000050726f6f6600bf89e2e884e892940108"
|
||||
@@ -10015,32 +10018,60 @@ function isDetachedOtsFile(otsBytes) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
async function submitToCalendar(server, hashBytes) {
|
||||
const response = await fetch(`${server}/digest`, {
|
||||
method: "POST",
|
||||
body: hashBytes,
|
||||
headers: {
|
||||
"Accept": "application/vnd.opentimestamps.v1",
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Calendar ${server} returned HTTP ${response.status}`);
|
||||
}
|
||||
const fragmentBuffer = await response.arrayBuffer();
|
||||
return new Uint8Array(fragmentBuffer);
|
||||
}
|
||||
async function timestampEvent(eventIdHex) {
|
||||
const hashBytes = hexToBytes3(eventIdHex);
|
||||
for (const server of OTS_CALENDAR_SERVERS) {
|
||||
try {
|
||||
const response = await fetch(`${server}/digest`, {
|
||||
method: "POST",
|
||||
body: hashBytes,
|
||||
headers: {
|
||||
"Accept": "application/vnd.opentimestamps.v1",
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.warn(`[ots] Calendar ${server} returned ${response.status}`);
|
||||
continue;
|
||||
}
|
||||
const fragmentBuffer = await response.arrayBuffer();
|
||||
const fragmentBytes = new Uint8Array(fragmentBuffer);
|
||||
const otsBytes = concatBytes4(OTS_DETACHED_PREFIX, hashBytes, fragmentBytes);
|
||||
console.log(`[ots] Received calendar fragment (${fragmentBytes.length} bytes); built detached .ots file (${otsBytes.length} bytes) from ${server}`);
|
||||
return otsBytes;
|
||||
} catch (e) {
|
||||
console.warn(`[ots] Calendar ${server} failed:`, e.message);
|
||||
const results = await Promise.allSettled(
|
||||
OTS_CALENDAR_SERVERS.map((server) => submitToCalendar(server, hashBytes))
|
||||
);
|
||||
const fragments = [];
|
||||
const succeeded = [];
|
||||
const failed = [];
|
||||
results.forEach((result, i) => {
|
||||
const server = OTS_CALENDAR_SERVERS[i];
|
||||
if (result.status === "fulfilled" && result.value && result.value.length > 0) {
|
||||
fragments.push(result.value);
|
||||
succeeded.push(server);
|
||||
console.log(`[ots] Calendar ${server} returned fragment (${result.value.length} bytes)`);
|
||||
} else {
|
||||
const reason = result.status === "rejected" ? result.reason.message : "empty response";
|
||||
failed.push(`${server}: ${reason}`);
|
||||
console.warn(`[ots] Calendar ${server} failed: ${reason}`);
|
||||
}
|
||||
});
|
||||
if (fragments.length === 0) {
|
||||
throw new Error(`All OpenTimestamps calendar servers failed (${failed.join("; ")})`);
|
||||
}
|
||||
const FF = new Uint8Array([255]);
|
||||
const treeParts = [];
|
||||
for (let i = 0; i < fragments.length; i++) {
|
||||
if (i < fragments.length - 1) {
|
||||
treeParts.push(FF, fragments[i]);
|
||||
} else {
|
||||
treeParts.push(fragments[i]);
|
||||
}
|
||||
}
|
||||
throw new Error("All OpenTimestamps calendar servers failed");
|
||||
const timestampTree = concatBytes4(...treeParts);
|
||||
const otsBytes = concatBytes4(OTS_DETACHED_PREFIX, hashBytes, timestampTree);
|
||||
console.log(`[ots] Built merged .ots file (${otsBytes.length} bytes) from ${fragments.length}/${OTS_CALENDAR_SERVERS.length} calendars (${succeeded.join(", ")})`);
|
||||
if (failed.length > 0) {
|
||||
console.warn(`[ots] ${failed.length} calendar(s) failed: ${failed.join("; ")}`);
|
||||
}
|
||||
return otsBytes;
|
||||
}
|
||||
async function upgradeOts(otsBytes) {
|
||||
const upgradeUrl = typeof window !== "undefined" && window.location ? `${window.location.origin}/ots-upgrade` : "https://laantungir.net/ots-upgrade";
|
||||
|
||||
Reference in New Issue
Block a user