@orangecheck/stamp-ots 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/index.d.mts +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +326 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +313 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
- package/src/anchor.ts +103 -0
- package/src/base64.test.ts +45 -0
- package/src/base64.ts +81 -0
- package/src/calendar.ts +101 -0
- package/src/index.ts +10 -0
- package/src/submit.test.ts +108 -0
- package/src/submit.ts +89 -0
- package/src/types.test.ts +37 -0
- package/src/types.ts +101 -0
- package/src/upgrade.ts +102 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
function toStampOts(p) {
|
|
3
|
+
return {
|
|
4
|
+
status: p.status,
|
|
5
|
+
proof: p.proof,
|
|
6
|
+
calendars: p.calendars,
|
|
7
|
+
block_height: p.blockHeight,
|
|
8
|
+
block_hash: p.blockHash,
|
|
9
|
+
upgraded_at: p.upgradedAt
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function fromStampOts(s) {
|
|
13
|
+
return {
|
|
14
|
+
status: s.status,
|
|
15
|
+
proof: s.proof,
|
|
16
|
+
calendars: s.calendars,
|
|
17
|
+
blockHeight: s.block_height,
|
|
18
|
+
blockHash: s.block_hash,
|
|
19
|
+
upgradedAt: s.upgraded_at
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/base64.ts
|
|
24
|
+
var B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
25
|
+
var B64_DECODE_TABLE = (() => {
|
|
26
|
+
const t = new Int16Array(128).fill(-1);
|
|
27
|
+
for (let i = 0; i < B64_CHARS.length; i++) t[B64_CHARS.charCodeAt(i)] = i;
|
|
28
|
+
t["=".charCodeAt(0)] = -2;
|
|
29
|
+
return t;
|
|
30
|
+
})();
|
|
31
|
+
function base64Encode(bytes) {
|
|
32
|
+
let out = "";
|
|
33
|
+
let i = 0;
|
|
34
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
35
|
+
const a = bytes[i];
|
|
36
|
+
const b = bytes[i + 1];
|
|
37
|
+
const c = bytes[i + 2];
|
|
38
|
+
out += B64_CHARS[a >> 2] + B64_CHARS[(a & 3) << 4 | b >> 4] + B64_CHARS[(b & 15) << 2 | c >> 6] + B64_CHARS[c & 63];
|
|
39
|
+
}
|
|
40
|
+
if (i < bytes.length) {
|
|
41
|
+
const a = bytes[i];
|
|
42
|
+
const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
|
|
43
|
+
out += B64_CHARS[a >> 2];
|
|
44
|
+
out += B64_CHARS[(a & 3) << 4 | b >> 4];
|
|
45
|
+
out += i + 1 < bytes.length ? B64_CHARS[(b & 15) << 2] : "=";
|
|
46
|
+
out += "=";
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
function base64Decode(s) {
|
|
51
|
+
const str = s.replace(/\s+/g, "");
|
|
52
|
+
const len = str.length;
|
|
53
|
+
if (len === 0) return new Uint8Array(0);
|
|
54
|
+
if (len % 4 !== 0) throw new Error("base64: bad length");
|
|
55
|
+
let padding = 0;
|
|
56
|
+
if (str[len - 1] === "=") padding++;
|
|
57
|
+
if (str[len - 2] === "=") padding++;
|
|
58
|
+
const outLen = len / 4 * 3 - padding;
|
|
59
|
+
const out = new Uint8Array(outLen);
|
|
60
|
+
let o = 0;
|
|
61
|
+
for (let i = 0; i < len; i += 4) {
|
|
62
|
+
const c0 = B64_DECODE_TABLE[str.charCodeAt(i)];
|
|
63
|
+
const c1 = B64_DECODE_TABLE[str.charCodeAt(i + 1)];
|
|
64
|
+
const c2 = B64_DECODE_TABLE[str.charCodeAt(i + 2)];
|
|
65
|
+
const c3 = B64_DECODE_TABLE[str.charCodeAt(i + 3)];
|
|
66
|
+
if (c0 < 0 || c1 < 0 || c2 < -2 || c3 < -2) throw new Error("base64: bad char");
|
|
67
|
+
out[o++] = c0 << 2 | c1 >> 4;
|
|
68
|
+
if (c2 >= 0) out[o++] = (c1 & 15) << 4 | c2 >> 2;
|
|
69
|
+
if (c3 >= 0) out[o++] = (c2 & 3) << 6 | c3;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function hexDecode(s) {
|
|
74
|
+
if (s.length % 2 !== 0) throw new Error("hex: odd length");
|
|
75
|
+
const out = new Uint8Array(s.length / 2);
|
|
76
|
+
for (let i = 0; i < out.length; i++) {
|
|
77
|
+
const hi = parseInt(s[i * 2], 16);
|
|
78
|
+
const lo = parseInt(s[i * 2 + 1], 16);
|
|
79
|
+
if (Number.isNaN(hi) || Number.isNaN(lo)) throw new Error("hex: bad char");
|
|
80
|
+
out[i] = hi << 4 | lo;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function hexEncode(bytes) {
|
|
85
|
+
const HEX = "0123456789abcdef";
|
|
86
|
+
let out = "";
|
|
87
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
88
|
+
const b = bytes[i];
|
|
89
|
+
out += HEX[b >> 4 & 15] + HEX[b & 15];
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/calendar.ts
|
|
95
|
+
var DEFAULT_CALENDARS = [
|
|
96
|
+
"https://alice.btc.calendar.opentimestamps.org",
|
|
97
|
+
"https://bob.btc.calendar.opentimestamps.org",
|
|
98
|
+
"https://finney.calendar.eternitywall.com"
|
|
99
|
+
];
|
|
100
|
+
function createCalendarClient(url, opts = {}) {
|
|
101
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
102
|
+
if (typeof fetchImpl !== "function") {
|
|
103
|
+
throw new Error(
|
|
104
|
+
"@orangecheck/stamp-ots: no fetch implementation available. Pass one via { fetch }."
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
108
|
+
const base = url.replace(/\/$/, "");
|
|
109
|
+
return {
|
|
110
|
+
url: base,
|
|
111
|
+
async submit(digest, signal) {
|
|
112
|
+
if (digest.byteLength !== 32) {
|
|
113
|
+
throw new Error("OTS: digest must be exactly 32 bytes");
|
|
114
|
+
}
|
|
115
|
+
const ac = withTimeout(signal, timeoutMs);
|
|
116
|
+
const resp = await fetchImpl(`${base}/digest`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/vnd.opentimestamps.v1" },
|
|
119
|
+
body: digest,
|
|
120
|
+
signal: ac.signal
|
|
121
|
+
});
|
|
122
|
+
ac.clear();
|
|
123
|
+
if (!resp.ok) {
|
|
124
|
+
throw new Error(`OTS calendar ${base} submit failed: HTTP ${resp.status}`);
|
|
125
|
+
}
|
|
126
|
+
const buf = await resp.arrayBuffer();
|
|
127
|
+
return new Uint8Array(buf);
|
|
128
|
+
},
|
|
129
|
+
async fetchProof(digest, signal) {
|
|
130
|
+
if (digest.byteLength !== 32) {
|
|
131
|
+
throw new Error("OTS: digest must be exactly 32 bytes");
|
|
132
|
+
}
|
|
133
|
+
const ac = withTimeout(signal, timeoutMs);
|
|
134
|
+
const resp = await fetchImpl(`${base}/timestamp/${hexEncode(digest)}`, {
|
|
135
|
+
method: "GET",
|
|
136
|
+
headers: { Accept: "application/vnd.opentimestamps.v1" },
|
|
137
|
+
signal: ac.signal
|
|
138
|
+
});
|
|
139
|
+
ac.clear();
|
|
140
|
+
if (resp.status === 404) return null;
|
|
141
|
+
if (!resp.ok) {
|
|
142
|
+
throw new Error(`OTS calendar ${base} fetchProof failed: HTTP ${resp.status}`);
|
|
143
|
+
}
|
|
144
|
+
const buf = await resp.arrayBuffer();
|
|
145
|
+
return new Uint8Array(buf);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function withTimeout(parent, ms) {
|
|
150
|
+
const ac = new AbortController();
|
|
151
|
+
const onParent = () => ac.abort(parent.reason);
|
|
152
|
+
if (parent) {
|
|
153
|
+
if (parent.aborted) ac.abort(parent.reason);
|
|
154
|
+
else parent.addEventListener("abort", onParent, { once: true });
|
|
155
|
+
}
|
|
156
|
+
const t = setTimeout(() => ac.abort(new Error(`OTS request timed out after ${ms}ms`)), ms);
|
|
157
|
+
return {
|
|
158
|
+
signal: ac.signal,
|
|
159
|
+
clear: () => {
|
|
160
|
+
clearTimeout(t);
|
|
161
|
+
if (parent) parent.removeEventListener("abort", onParent);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/submit.ts
|
|
167
|
+
async function submitToCalendars(idHex, opts = {}) {
|
|
168
|
+
const digest = hexDecode(idHex);
|
|
169
|
+
if (digest.byteLength !== 32) {
|
|
170
|
+
throw new Error("submitToCalendars: idHex must be 32 bytes (64 hex chars)");
|
|
171
|
+
}
|
|
172
|
+
const clients = (opts.calendars ?? DEFAULT_CALENDARS).map(
|
|
173
|
+
(c) => typeof c === "string" ? createCalendarClient(c, { fetch: opts.fetch, timeoutMs: opts.timeoutMs }) : c
|
|
174
|
+
);
|
|
175
|
+
if (clients.length === 0) {
|
|
176
|
+
throw new Error("submitToCalendars: at least one calendar is required");
|
|
177
|
+
}
|
|
178
|
+
const minSuccesses = Math.max(1, opts.minSuccesses ?? 1);
|
|
179
|
+
const settled = await Promise.allSettled(
|
|
180
|
+
clients.map(async (c) => {
|
|
181
|
+
const proof = await c.submit(digest, opts.signal);
|
|
182
|
+
return { url: c.url, proof };
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
const successes = settled.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
186
|
+
const failures = settled.filter((r) => r.status === "rejected").map((r) => r.reason instanceof Error ? r.reason.message : String(r.reason));
|
|
187
|
+
if (successes.length < minSuccesses) {
|
|
188
|
+
const msg = `OTS submission did not reach ${minSuccesses} calendar(s). Errors: ${failures.join("; ")}`;
|
|
189
|
+
const err = new Error(msg);
|
|
190
|
+
err.errors = failures;
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
const first = successes[0];
|
|
194
|
+
return {
|
|
195
|
+
status: "pending",
|
|
196
|
+
proof: base64Encode(first.proof),
|
|
197
|
+
calendars: successes.map((s) => s.url),
|
|
198
|
+
blockHeight: null,
|
|
199
|
+
blockHash: null,
|
|
200
|
+
upgradedAt: null
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/upgrade.ts
|
|
205
|
+
async function upgradeProof(current, idHex, opts) {
|
|
206
|
+
if (current.status === "confirmed") return current;
|
|
207
|
+
const digest = hexDecode(idHex);
|
|
208
|
+
const clients = current.calendars.map(
|
|
209
|
+
(url) => createCalendarClient(url, { fetch: opts.fetch, timeoutMs: opts.timeoutMs })
|
|
210
|
+
);
|
|
211
|
+
if (clients.length === 0) {
|
|
212
|
+
throw new Error("upgradeProof: no calendars listed in current proof");
|
|
213
|
+
}
|
|
214
|
+
const attempts = await Promise.allSettled(
|
|
215
|
+
clients.map(async (c) => {
|
|
216
|
+
const proof = await c.fetchProof(digest, opts.signal);
|
|
217
|
+
if (!proof) return null;
|
|
218
|
+
const anchor = await opts.parseAnchor(proof);
|
|
219
|
+
return { url: c.url, proof, anchor };
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
for (const r of attempts) {
|
|
223
|
+
if (r.status !== "fulfilled" || r.value === null) continue;
|
|
224
|
+
const { url, proof, anchor } = r.value;
|
|
225
|
+
if (anchor) {
|
|
226
|
+
return {
|
|
227
|
+
status: "confirmed",
|
|
228
|
+
proof: base64Encode(proof),
|
|
229
|
+
calendars: uniqueFirst(current.calendars, url),
|
|
230
|
+
blockHeight: anchor.blockHeight,
|
|
231
|
+
blockHash: anchor.blockHash,
|
|
232
|
+
upgradedAt: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z")
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
let longest = null;
|
|
237
|
+
for (const r of attempts) {
|
|
238
|
+
if (r.status !== "fulfilled" || r.value === null) continue;
|
|
239
|
+
if (!longest || r.value.proof.byteLength > longest.proof.byteLength) {
|
|
240
|
+
longest = { url: r.value.url, proof: r.value.proof };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (longest) {
|
|
244
|
+
const currentBytes = base64Decode(current.proof);
|
|
245
|
+
if (longest.proof.byteLength > currentBytes.byteLength) {
|
|
246
|
+
return {
|
|
247
|
+
status: "pending",
|
|
248
|
+
proof: base64Encode(longest.proof),
|
|
249
|
+
calendars: uniqueFirst(current.calendars, longest.url),
|
|
250
|
+
blockHeight: null,
|
|
251
|
+
blockHash: null,
|
|
252
|
+
upgradedAt: null
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return current;
|
|
257
|
+
}
|
|
258
|
+
function uniqueFirst(arr, first) {
|
|
259
|
+
const out = [first];
|
|
260
|
+
for (const x of arr) if (x !== first && !out.includes(x)) out.push(x);
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/anchor.ts
|
|
265
|
+
function makeAnchorVerifier(cfg) {
|
|
266
|
+
return async (input) => {
|
|
267
|
+
const walked = await cfg.walkProof({ proofBytes: input.proofBytes, digest: input.digest });
|
|
268
|
+
if (!walked) {
|
|
269
|
+
return { ok: false, reason: "proof could not be walked to a Bitcoin commitment" };
|
|
270
|
+
}
|
|
271
|
+
if (walked.blockHeight !== input.blockHeight) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
reason: `proof anchors at block ${walked.blockHeight}, envelope declares ${input.blockHeight}`
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const header = await cfg.headerSource.getHeaderAt(input.blockHeight);
|
|
278
|
+
if (!header) {
|
|
279
|
+
return { ok: false, reason: `no block header available at height ${input.blockHeight}` };
|
|
280
|
+
}
|
|
281
|
+
if (header.byteLength < 80) {
|
|
282
|
+
return { ok: false, reason: "malformed Bitcoin header (too short)" };
|
|
283
|
+
}
|
|
284
|
+
const headerMerkleLE = header.subarray(36, 68);
|
|
285
|
+
const headerMerkleBE = new Uint8Array(headerMerkleLE).reverse();
|
|
286
|
+
const matchesLE = bytesEqual(headerMerkleLE, walked.merkleRoot);
|
|
287
|
+
const matchesBE = bytesEqual(headerMerkleBE, walked.merkleRoot);
|
|
288
|
+
if (!matchesLE && !matchesBE) {
|
|
289
|
+
return { ok: false, reason: "block header Merkle root does not match walked proof root" };
|
|
290
|
+
}
|
|
291
|
+
return { ok: true, merkleRoot: hexEncode(matchesBE ? headerMerkleBE : headerMerkleLE) };
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function bytesEqual(a, b) {
|
|
295
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
296
|
+
for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
function adaptAnchorVerifier(verifier, digestLookup) {
|
|
300
|
+
return async (proofB64, blockHeight, blockHash) => {
|
|
301
|
+
const r = await verifier({
|
|
302
|
+
proofBytes: base64Decode(proofB64),
|
|
303
|
+
digest: digestLookup(blockHash),
|
|
304
|
+
blockHeight,
|
|
305
|
+
blockHash
|
|
306
|
+
});
|
|
307
|
+
return r.ok;
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export { DEFAULT_CALENDARS, adaptAnchorVerifier, base64Decode, base64Encode, createCalendarClient, fromStampOts, hexDecode, hexEncode, makeAnchorVerifier, submitToCalendars, toStampOts, upgradeProof };
|
|
312
|
+
//# sourceMappingURL=index.mjs.map
|
|
313
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/base64.ts","../src/calendar.ts","../src/submit.ts","../src/upgrade.ts","../src/anchor.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// Types for @orangecheck/stamp-ots.\n\nimport type { StampOts } from '@orangecheck/stamp-core';\n\nexport type OtsProofStatus = 'pending' | 'confirmed';\n\nexport interface OtsProof {\n status: OtsProofStatus;\n /** Raw proof bytes as base64. Opaque at this layer; parsers plug in. */\n proof: string;\n /** Calendar URLs that contain this proof, in submission order. */\n calendars: string[];\n /** Bitcoin block height at which this proof is anchored (null if pending). */\n blockHeight: number | null;\n /** Bitcoin block hash (hex, lowercase) at the anchor block (null if pending). */\n blockHash: string | null;\n /** ISO 8601 UTC of the upgrade (null if pending). */\n upgradedAt: string | null;\n}\n\nexport interface CalendarClient {\n /** Base URL of the calendar, e.g. \"https://alice.btc.calendar.opentimestamps.org\". */\n readonly url: string;\n\n /**\n * Submit a 32-byte digest to the calendar for aggregation. Returns the raw\n * pending proof bytes.\n *\n * OTS calendar HTTP API: POST <url>/digest with the raw bytes as the body,\n * Content-Type: application/vnd.opentimestamps.v1, returns the proof bytes\n * as the response body.\n */\n submit(digest: Uint8Array, signal?: AbortSignal): Promise<Uint8Array>;\n\n /**\n * Fetch the current (possibly upgraded) proof for a previously-submitted\n * digest. Returns null if the calendar has no proof for this digest yet.\n *\n * OTS calendar HTTP API: GET <url>/timestamp/<hex(digest)>, returns the\n * proof bytes or 404.\n */\n fetchProof(digest: Uint8Array, signal?: AbortSignal): Promise<Uint8Array | null>;\n}\n\nexport interface AnchorVerificationInput {\n /** Raw OTS proof bytes (base64-decoded by the caller). */\n proofBytes: Uint8Array;\n /** 32-byte digest that the proof commits to. */\n digest: Uint8Array;\n /** Declared block height from the envelope. */\n blockHeight: number;\n /** Declared block hash (hex) from the envelope. */\n blockHash: string;\n /**\n * Optional block header source. If provided, the verifier uses this to\n * fetch the header at `blockHeight` and compare its Merkle root to the\n * one derived from walking the proof.\n *\n * If omitted, the verifier MAY still parse the proof and report\n * structural validity but CANNOT report a bound anchor.\n */\n headerSource?: BlockHeaderSource;\n}\n\nexport interface BlockHeaderSource {\n /** Return the 80-byte raw block header at the given height, or null if unknown. */\n getHeaderAt(height: number): Promise<Uint8Array | null>;\n}\n\nexport type AnchorVerifier = (input: AnchorVerificationInput) => Promise<AnchorVerificationResult>;\n\nexport type AnchorVerificationResult =\n | { ok: true; merkleRoot: string }\n | { ok: false; reason: string };\n\n/**\n * Convert an OtsProof (library type) to the StampOts field shape that the\n * envelope carries. The two are isomorphic; this function exists to keep\n * stamp-core's type surface narrow.\n */\nexport function toStampOts(p: OtsProof): StampOts {\n return {\n status: p.status,\n proof: p.proof,\n calendars: p.calendars,\n block_height: p.blockHeight,\n block_hash: p.blockHash,\n upgraded_at: p.upgradedAt,\n };\n}\n\nexport function fromStampOts(s: StampOts): OtsProof {\n return {\n status: s.status,\n proof: s.proof,\n calendars: s.calendars,\n blockHeight: s.block_height,\n blockHash: s.block_hash,\n upgradedAt: s.upgraded_at,\n };\n}\n","// Minimal, dependency-free base64 <-> Uint8Array helpers. OTS proof bytes\n// are carried as base64 inside OC Stamp envelopes so they survive any text\n// transport (URL fragment, JSON, Nostr event content).\n\nconst B64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst B64_DECODE_TABLE = (() => {\n const t = new Int16Array(128).fill(-1);\n for (let i = 0; i < B64_CHARS.length; i++) t[B64_CHARS.charCodeAt(i)] = i;\n t['='.charCodeAt(0)] = -2;\n return t;\n})();\n\nexport function base64Encode(bytes: Uint8Array): string {\n let out = '';\n let i = 0;\n for (; i + 2 < bytes.length; i += 3) {\n const a = bytes[i]!;\n const b = bytes[i + 1]!;\n const c = bytes[i + 2]!;\n out +=\n B64_CHARS[a >> 2]! +\n B64_CHARS[((a & 0x03) << 4) | (b >> 4)]! +\n B64_CHARS[((b & 0x0f) << 2) | (c >> 6)]! +\n B64_CHARS[c & 0x3f]!;\n }\n if (i < bytes.length) {\n const a = bytes[i]!;\n const b = i + 1 < bytes.length ? bytes[i + 1]! : 0;\n out += B64_CHARS[a >> 2]!;\n out += B64_CHARS[((a & 0x03) << 4) | (b >> 4)]!;\n out += i + 1 < bytes.length ? B64_CHARS[(b & 0x0f) << 2]! : '=';\n out += '=';\n }\n return out;\n}\n\nexport function base64Decode(s: string): Uint8Array {\n const str = s.replace(/\\s+/g, '');\n const len = str.length;\n if (len === 0) return new Uint8Array(0);\n if (len % 4 !== 0) throw new Error('base64: bad length');\n let padding = 0;\n if (str[len - 1] === '=') padding++;\n if (str[len - 2] === '=') padding++;\n const outLen = (len / 4) * 3 - padding;\n const out = new Uint8Array(outLen);\n let o = 0;\n for (let i = 0; i < len; i += 4) {\n const c0 = B64_DECODE_TABLE[str.charCodeAt(i)]!;\n const c1 = B64_DECODE_TABLE[str.charCodeAt(i + 1)]!;\n const c2 = B64_DECODE_TABLE[str.charCodeAt(i + 2)]!;\n const c3 = B64_DECODE_TABLE[str.charCodeAt(i + 3)]!;\n if (c0 < 0 || c1 < 0 || c2 < -2 || c3 < -2) throw new Error('base64: bad char');\n out[o++] = (c0 << 2) | (c1 >> 4);\n if (c2 >= 0) out[o++] = ((c1 & 0x0f) << 4) | (c2 >> 2);\n if (c3 >= 0) out[o++] = ((c2 & 0x03) << 6) | c3;\n }\n return out;\n}\n\nexport function hexDecode(s: string): Uint8Array {\n if (s.length % 2 !== 0) throw new Error('hex: odd length');\n const out = new Uint8Array(s.length / 2);\n for (let i = 0; i < out.length; i++) {\n const hi = parseInt(s[i * 2]!, 16);\n const lo = parseInt(s[i * 2 + 1]!, 16);\n if (Number.isNaN(hi) || Number.isNaN(lo)) throw new Error('hex: bad char');\n out[i] = (hi << 4) | lo;\n }\n return out;\n}\n\nexport function hexEncode(bytes: Uint8Array): string {\n const HEX = '0123456789abcdef';\n let out = '';\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i]!;\n out += HEX[(b >> 4) & 0x0f]! + HEX[b & 0x0f]!;\n }\n return out;\n}\n","// HTTP client for an OpenTimestamps calendar. Implements the minimal subset\n// of the calendar protocol that an OC Stamp implementation needs:\n//\n// POST <url>/digest body = raw digest bytes -> pending proof bytes\n// GET <url>/timestamp/<hex> -> upgraded proof bytes, or 404\n//\n// The client is thin by design. It does not parse proof bytes; that is a\n// separate concern addressed by whichever OTS proof library the consumer\n// plugs in (see verifyOtsAnchor in ./anchor.ts).\n\nimport type { CalendarClient } from './types.js';\nimport { hexEncode } from './base64.js';\n\nexport const DEFAULT_CALENDARS: readonly string[] = [\n 'https://alice.btc.calendar.opentimestamps.org',\n 'https://bob.btc.calendar.opentimestamps.org',\n 'https://finney.calendar.eternitywall.com',\n];\n\nexport interface HttpCalendarOptions {\n /**\n * Custom fetch implementation. Defaults to global `fetch`. Useful in\n * environments that need a specific adapter (Node < 18, Cloudflare\n * Workers, proxies).\n */\n fetch?: typeof fetch;\n /** Request timeout in milliseconds. Default 30_000. */\n timeoutMs?: number;\n}\n\nexport function createCalendarClient(url: string, opts: HttpCalendarOptions = {}): CalendarClient {\n const fetchImpl = opts.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new Error(\n '@orangecheck/stamp-ots: no fetch implementation available. Pass one via { fetch }.'\n );\n }\n const timeoutMs = opts.timeoutMs ?? 30_000;\n const base = url.replace(/\\/$/, '');\n\n return {\n url: base,\n async submit(digest, signal) {\n if (digest.byteLength !== 32) {\n throw new Error('OTS: digest must be exactly 32 bytes');\n }\n const ac = withTimeout(signal, timeoutMs);\n // Uint8Array is a valid fetch body at runtime in Node 18+ and all\n // modern browsers, but TypeScript's DOM typings don't accept it\n // directly under `strict` + `noUncheckedIndexedAccess`. The cast is\n // narrow and intentional.\n const resp = await fetchImpl(`${base}/digest`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/vnd.opentimestamps.v1' },\n body: digest as unknown as BodyInit,\n signal: ac.signal,\n });\n ac.clear();\n if (!resp.ok) {\n throw new Error(`OTS calendar ${base} submit failed: HTTP ${resp.status}`);\n }\n const buf = await resp.arrayBuffer();\n return new Uint8Array(buf);\n },\n async fetchProof(digest, signal) {\n if (digest.byteLength !== 32) {\n throw new Error('OTS: digest must be exactly 32 bytes');\n }\n const ac = withTimeout(signal, timeoutMs);\n const resp = await fetchImpl(`${base}/timestamp/${hexEncode(digest)}`, {\n method: 'GET',\n headers: { Accept: 'application/vnd.opentimestamps.v1' },\n signal: ac.signal,\n });\n ac.clear();\n if (resp.status === 404) return null;\n if (!resp.ok) {\n throw new Error(`OTS calendar ${base} fetchProof failed: HTTP ${resp.status}`);\n }\n const buf = await resp.arrayBuffer();\n return new Uint8Array(buf);\n },\n };\n}\n\nfunction withTimeout(parent: AbortSignal | undefined, ms: number): { signal: AbortSignal; clear: () => void } {\n const ac = new AbortController();\n const onParent = () => ac.abort((parent as AbortSignal & { reason?: unknown }).reason);\n if (parent) {\n if (parent.aborted) ac.abort((parent as AbortSignal & { reason?: unknown }).reason);\n else parent.addEventListener('abort', onParent, { once: true });\n }\n const t = setTimeout(() => ac.abort(new Error(`OTS request timed out after ${ms}ms`)), ms);\n return {\n signal: ac.signal,\n clear: () => {\n clearTimeout(t);\n if (parent) parent.removeEventListener('abort', onParent);\n },\n };\n}\n","// Submit an envelope id to one or more OTS calendars. Returns a pending proof.\n// See SPEC.md §6.1.\n//\n// The submission strategy: try each calendar in parallel, take the first\n// success, report which calendar produced it. A stamp that only reaches one\n// calendar is a single point of failure for upgrade; the reference app\n// submits to at least two calendars for redundancy.\n\nimport { base64Encode, hexDecode } from './base64.js';\nimport { createCalendarClient, DEFAULT_CALENDARS } from './calendar.js';\nimport type { CalendarClient, OtsProof } from './types.js';\n\nexport interface SubmitOptions {\n /**\n * Calendars to submit to. Defaults to DEFAULT_CALENDARS. Can be strings\n * (URLs) or pre-built CalendarClient instances.\n */\n calendars?: readonly (string | CalendarClient)[];\n /**\n * Minimum number of calendars that must succeed. Defaults to 1.\n * If fewer than `minSuccesses` calendars respond with a proof,\n * submit() throws an aggregate error.\n */\n minSuccesses?: number;\n /** Optional custom fetch passed through to calendar clients. */\n fetch?: typeof fetch;\n /** Request timeout per calendar. Default 30_000ms. */\n timeoutMs?: number;\n signal?: AbortSignal;\n}\n\n/**\n * Submit the envelope `id` (64-hex) to OTS calendars and return an OtsProof in\n * pending state. The caller merges this into the envelope's `ots` field.\n *\n * Implementation note: the raw proof bytes returned by OTS calendars are not\n * a superset of the digest alone — they encode the calendar's commitment.\n * Because different calendars return different pending-proof bytes for the\n * same digest, we carry the FIRST successful calendar's bytes in the\n * envelope. Upgrade (see upgrade.ts) can fetch richer proofs from any of the\n * submitted-to calendars later.\n */\nexport async function submitToCalendars(idHex: string, opts: SubmitOptions = {}): Promise<OtsProof> {\n const digest = hexDecode(idHex);\n if (digest.byteLength !== 32) {\n throw new Error('submitToCalendars: idHex must be 32 bytes (64 hex chars)');\n }\n\n const clients = (opts.calendars ?? DEFAULT_CALENDARS).map((c) =>\n typeof c === 'string'\n ? createCalendarClient(c, { fetch: opts.fetch, timeoutMs: opts.timeoutMs })\n : c\n );\n if (clients.length === 0) {\n throw new Error('submitToCalendars: at least one calendar is required');\n }\n const minSuccesses = Math.max(1, opts.minSuccesses ?? 1);\n\n const settled = await Promise.allSettled(\n clients.map(async (c) => {\n const proof = await c.submit(digest, opts.signal);\n return { url: c.url, proof };\n })\n );\n const successes = settled\n .filter((r): r is PromiseFulfilledResult<{ url: string; proof: Uint8Array }> => r.status === 'fulfilled')\n .map((r) => r.value);\n const failures = settled\n .filter((r): r is PromiseRejectedResult => r.status === 'rejected')\n .map((r) => (r.reason instanceof Error ? r.reason.message : String(r.reason)));\n\n if (successes.length < minSuccesses) {\n // AggregateError would be nicer but targets ES2021; Error keeps us at ES2020.\n const msg = `OTS submission did not reach ${minSuccesses} calendar(s). Errors: ${failures.join('; ')}`;\n const err = new Error(msg) as Error & { errors: string[] };\n err.errors = failures;\n throw err;\n }\n\n const first = successes[0]!;\n return {\n status: 'pending',\n proof: base64Encode(first.proof),\n calendars: successes.map((s) => s.url),\n blockHeight: null,\n blockHash: null,\n upgradedAt: null,\n };\n}\n","// Upgrade a pending OTS proof to a confirmed one. See SPEC.md §6.2.\n//\n// Upgrade strategy: for each listed calendar, ask whether it has an upgraded\n// proof for this digest. If at least one calendar returns a proof longer than\n// the current pending one (heuristic for \"more commitments included\"),\n// return an upgraded OtsProof. The caller is responsible for verifying the\n// proof chains to a real Bitcoin block header (see anchor.ts).\n//\n// This package does NOT parse the proof bytes to extract block_height /\n// block_hash — that requires a full OTS proof parser. We provide an anchor\n// verifier callback pattern instead (see AnchorVerifier in ./types.ts).\n\nimport { base64Decode, base64Encode, hexDecode } from './base64.js';\nimport { createCalendarClient } from './calendar.js';\nimport type { CalendarClient, OtsProof } from './types.js';\n\nexport interface UpgradeOptions {\n /**\n * A parser that, given proof bytes, returns `{blockHeight, blockHash}` if\n * the proof is fully anchored, or null if it's still pending.\n *\n * Callers plug in their own parser (e.g., via the `opentimestamps` npm\n * package). This keeps @orangecheck/stamp-ots free of heavy parser deps.\n */\n parseAnchor: (proofBytes: Uint8Array) => Promise<{ blockHeight: number; blockHash: string } | null>;\n /** Optional custom fetch passed through to calendar clients. */\n fetch?: typeof fetch;\n /** Request timeout per calendar. Default 30_000ms. */\n timeoutMs?: number;\n signal?: AbortSignal;\n}\n\nexport async function upgradeProof(\n current: OtsProof,\n idHex: string,\n opts: UpgradeOptions\n): Promise<OtsProof> {\n if (current.status === 'confirmed') return current; // idempotent\n\n const digest = hexDecode(idHex);\n const clients: CalendarClient[] = current.calendars.map((url) =>\n createCalendarClient(url, { fetch: opts.fetch, timeoutMs: opts.timeoutMs })\n );\n if (clients.length === 0) {\n throw new Error('upgradeProof: no calendars listed in current proof');\n }\n\n const attempts = await Promise.allSettled(\n clients.map(async (c) => {\n const proof = await c.fetchProof(digest, opts.signal);\n if (!proof) return null;\n const anchor = await opts.parseAnchor(proof);\n return { url: c.url, proof, anchor };\n })\n );\n\n // Prefer a fully-confirmed proof from any calendar.\n for (const r of attempts) {\n if (r.status !== 'fulfilled' || r.value === null) continue;\n const { url, proof, anchor } = r.value;\n if (anchor) {\n return {\n status: 'confirmed',\n proof: base64Encode(proof),\n calendars: uniqueFirst(current.calendars, url),\n blockHeight: anchor.blockHeight,\n blockHash: anchor.blockHash,\n upgradedAt: new Date().toISOString().replace(/\\.\\d+Z$/, 'Z'),\n };\n }\n }\n\n // No confirmation yet. Some calendars may still have returned updated\n // pending bytes (more commitments included). Prefer the longest.\n let longest: { url: string; proof: Uint8Array } | null = null;\n for (const r of attempts) {\n if (r.status !== 'fulfilled' || r.value === null) continue;\n if (!longest || r.value.proof.byteLength > longest.proof.byteLength) {\n longest = { url: r.value.url, proof: r.value.proof };\n }\n }\n if (longest) {\n const currentBytes = base64Decode(current.proof);\n if (longest.proof.byteLength > currentBytes.byteLength) {\n return {\n status: 'pending',\n proof: base64Encode(longest.proof),\n calendars: uniqueFirst(current.calendars, longest.url),\n blockHeight: null,\n blockHash: null,\n upgradedAt: null,\n };\n }\n }\n return current;\n}\n\nfunction uniqueFirst(arr: readonly string[], first: string): string[] {\n const out = [first];\n for (const x of arr) if (x !== first && !out.includes(x)) out.push(x);\n return out;\n}\n","// Anchor-verification helper. See SPEC.md §6.3.\n//\n// Full OTS proof verification requires:\n// 1. Parsing the proof's Merkle path from digest to calendar root.\n// 2. Walking that path to the declared Bitcoin Merkle root.\n// 3. Fetching the Bitcoin block header at the declared height and comparing\n// its Merkle root.\n//\n// Step 1 needs a maintained OTS proof parser. We do not ship one here; this\n// file exposes a small helper that composes a parsed-anchor result with a\n// block-header source into the `verifyOtsAnchor` callback expected by\n// @orangecheck/stamp-core's verify().\n\nimport { base64Decode, hexEncode } from './base64.js';\nimport type {\n AnchorVerificationInput,\n AnchorVerificationResult,\n AnchorVerifier,\n BlockHeaderSource,\n} from './types.js';\n\nexport interface AnchorVerifierConfig {\n /**\n * Parse the proof bytes and return the Merkle root the proof walks to,\n * plus the declared block height. Returns null if parsing failed or the\n * proof isn't anchored.\n */\n walkProof: (input: {\n proofBytes: Uint8Array;\n digest: Uint8Array;\n }) => Promise<{ merkleRoot: Uint8Array; blockHeight: number } | null>;\n /**\n * Source of Bitcoin block headers. Can be a full node, SPV, or a\n * pre-computed headers snapshot.\n */\n headerSource: BlockHeaderSource;\n}\n\n/**\n * Build a concrete anchor verifier from a proof walker plus a header source.\n *\n * Callers wire this into `verify({ verifyOtsAnchor: makeAnchorVerifier(...) })`.\n * If either the proof walker returns null or the header's Merkle root does\n * not match, the verifier returns `{ ok: false, ... }`.\n */\nexport function makeAnchorVerifier(cfg: AnchorVerifierConfig): AnchorVerifier {\n return async (input: AnchorVerificationInput): Promise<AnchorVerificationResult> => {\n const walked = await cfg.walkProof({ proofBytes: input.proofBytes, digest: input.digest });\n if (!walked) {\n return { ok: false, reason: 'proof could not be walked to a Bitcoin commitment' };\n }\n if (walked.blockHeight !== input.blockHeight) {\n return {\n ok: false,\n reason: `proof anchors at block ${walked.blockHeight}, envelope declares ${input.blockHeight}`,\n };\n }\n const header = await cfg.headerSource.getHeaderAt(input.blockHeight);\n if (!header) {\n return { ok: false, reason: `no block header available at height ${input.blockHeight}` };\n }\n // Bitcoin block header: 80 bytes. Merkle root is at offset 36, 32 bytes,\n // stored little-endian. Compare to the proof-walked root, which OTS\n // reports in big-endian conventionally.\n if (header.byteLength < 80) {\n return { ok: false, reason: 'malformed Bitcoin header (too short)' };\n }\n const headerMerkleLE = header.subarray(36, 68);\n const headerMerkleBE = new Uint8Array(headerMerkleLE).reverse();\n const matchesLE = bytesEqual(headerMerkleLE, walked.merkleRoot);\n const matchesBE = bytesEqual(headerMerkleBE, walked.merkleRoot);\n if (!matchesLE && !matchesBE) {\n return { ok: false, reason: 'block header Merkle root does not match walked proof root' };\n }\n return { ok: true, merkleRoot: hexEncode(matchesBE ? headerMerkleBE : headerMerkleLE) };\n };\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.byteLength !== b.byteLength) return false;\n for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/**\n * Adapt an AnchorVerifier into the signature that\n * @orangecheck/stamp-core#verify expects: a function taking (proofB64,\n * blockHeight, blockHash) that returns boolean.\n */\nexport function adaptAnchorVerifier(\n verifier: AnchorVerifier,\n digestLookup: (blockHash: string) => Uint8Array\n): (proofB64: string, blockHeight: number, blockHash: string) => Promise<boolean> {\n return async (proofB64, blockHeight, blockHash) => {\n const r = await verifier({\n proofBytes: base64Decode(proofB64),\n digest: digestLookup(blockHash),\n blockHeight,\n blockHash,\n });\n return r.ok;\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orangecheck/stamp-ots",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenTimestamps calendar client, proof helpers, and anchor-verification hooks for OC Stamp envelopes. See https://github.com/orangecheck/oc-stamp-protocol.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bitcoin",
|
|
7
|
+
"opentimestamps",
|
|
8
|
+
"ots",
|
|
9
|
+
"timestamp",
|
|
10
|
+
"provenance",
|
|
11
|
+
"oc-stamp",
|
|
12
|
+
"orangecheck"
|
|
13
|
+
],
|
|
14
|
+
"author": "OrangeCheck",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/orangecheck/oc-packages.git",
|
|
19
|
+
"directory": "stamp-ots"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/orangecheck/oc-stamp-protocol",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/orangecheck/oc-stamp-protocol/issues"
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.mjs",
|
|
32
|
+
"require": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"dev": "tsup --watch",
|
|
44
|
+
"type-check": "tsc --noEmit",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"clean": "rm -rf dist",
|
|
48
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@orangecheck/stamp-core": "^0.1.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^22.10.2",
|
|
55
|
+
"tsup": "^8.3.5",
|
|
56
|
+
"typescript": "^5.7.2",
|
|
57
|
+
"vitest": "^3.2.4"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/anchor.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Anchor-verification helper. See SPEC.md §6.3.
|
|
2
|
+
//
|
|
3
|
+
// Full OTS proof verification requires:
|
|
4
|
+
// 1. Parsing the proof's Merkle path from digest to calendar root.
|
|
5
|
+
// 2. Walking that path to the declared Bitcoin Merkle root.
|
|
6
|
+
// 3. Fetching the Bitcoin block header at the declared height and comparing
|
|
7
|
+
// its Merkle root.
|
|
8
|
+
//
|
|
9
|
+
// Step 1 needs a maintained OTS proof parser. We do not ship one here; this
|
|
10
|
+
// file exposes a small helper that composes a parsed-anchor result with a
|
|
11
|
+
// block-header source into the `verifyOtsAnchor` callback expected by
|
|
12
|
+
// @orangecheck/stamp-core's verify().
|
|
13
|
+
|
|
14
|
+
import { base64Decode, hexEncode } from './base64.js';
|
|
15
|
+
import type {
|
|
16
|
+
AnchorVerificationInput,
|
|
17
|
+
AnchorVerificationResult,
|
|
18
|
+
AnchorVerifier,
|
|
19
|
+
BlockHeaderSource,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
|
|
22
|
+
export interface AnchorVerifierConfig {
|
|
23
|
+
/**
|
|
24
|
+
* Parse the proof bytes and return the Merkle root the proof walks to,
|
|
25
|
+
* plus the declared block height. Returns null if parsing failed or the
|
|
26
|
+
* proof isn't anchored.
|
|
27
|
+
*/
|
|
28
|
+
walkProof: (input: {
|
|
29
|
+
proofBytes: Uint8Array;
|
|
30
|
+
digest: Uint8Array;
|
|
31
|
+
}) => Promise<{ merkleRoot: Uint8Array; blockHeight: number } | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Source of Bitcoin block headers. Can be a full node, SPV, or a
|
|
34
|
+
* pre-computed headers snapshot.
|
|
35
|
+
*/
|
|
36
|
+
headerSource: BlockHeaderSource;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a concrete anchor verifier from a proof walker plus a header source.
|
|
41
|
+
*
|
|
42
|
+
* Callers wire this into `verify({ verifyOtsAnchor: makeAnchorVerifier(...) })`.
|
|
43
|
+
* If either the proof walker returns null or the header's Merkle root does
|
|
44
|
+
* not match, the verifier returns `{ ok: false, ... }`.
|
|
45
|
+
*/
|
|
46
|
+
export function makeAnchorVerifier(cfg: AnchorVerifierConfig): AnchorVerifier {
|
|
47
|
+
return async (input: AnchorVerificationInput): Promise<AnchorVerificationResult> => {
|
|
48
|
+
const walked = await cfg.walkProof({ proofBytes: input.proofBytes, digest: input.digest });
|
|
49
|
+
if (!walked) {
|
|
50
|
+
return { ok: false, reason: 'proof could not be walked to a Bitcoin commitment' };
|
|
51
|
+
}
|
|
52
|
+
if (walked.blockHeight !== input.blockHeight) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
reason: `proof anchors at block ${walked.blockHeight}, envelope declares ${input.blockHeight}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const header = await cfg.headerSource.getHeaderAt(input.blockHeight);
|
|
59
|
+
if (!header) {
|
|
60
|
+
return { ok: false, reason: `no block header available at height ${input.blockHeight}` };
|
|
61
|
+
}
|
|
62
|
+
// Bitcoin block header: 80 bytes. Merkle root is at offset 36, 32 bytes,
|
|
63
|
+
// stored little-endian. Compare to the proof-walked root, which OTS
|
|
64
|
+
// reports in big-endian conventionally.
|
|
65
|
+
if (header.byteLength < 80) {
|
|
66
|
+
return { ok: false, reason: 'malformed Bitcoin header (too short)' };
|
|
67
|
+
}
|
|
68
|
+
const headerMerkleLE = header.subarray(36, 68);
|
|
69
|
+
const headerMerkleBE = new Uint8Array(headerMerkleLE).reverse();
|
|
70
|
+
const matchesLE = bytesEqual(headerMerkleLE, walked.merkleRoot);
|
|
71
|
+
const matchesBE = bytesEqual(headerMerkleBE, walked.merkleRoot);
|
|
72
|
+
if (!matchesLE && !matchesBE) {
|
|
73
|
+
return { ok: false, reason: 'block header Merkle root does not match walked proof root' };
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, merkleRoot: hexEncode(matchesBE ? headerMerkleBE : headerMerkleLE) };
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
80
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
81
|
+
for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Adapt an AnchorVerifier into the signature that
|
|
87
|
+
* @orangecheck/stamp-core#verify expects: a function taking (proofB64,
|
|
88
|
+
* blockHeight, blockHash) that returns boolean.
|
|
89
|
+
*/
|
|
90
|
+
export function adaptAnchorVerifier(
|
|
91
|
+
verifier: AnchorVerifier,
|
|
92
|
+
digestLookup: (blockHash: string) => Uint8Array
|
|
93
|
+
): (proofB64: string, blockHeight: number, blockHash: string) => Promise<boolean> {
|
|
94
|
+
return async (proofB64, blockHeight, blockHash) => {
|
|
95
|
+
const r = await verifier({
|
|
96
|
+
proofBytes: base64Decode(proofB64),
|
|
97
|
+
digest: digestLookup(blockHash),
|
|
98
|
+
blockHeight,
|
|
99
|
+
blockHash,
|
|
100
|
+
});
|
|
101
|
+
return r.ok;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { base64Decode, base64Encode, hexDecode, hexEncode } from './base64.js';
|
|
4
|
+
|
|
5
|
+
describe('base64', () => {
|
|
6
|
+
it('encode → decode round-trips', () => {
|
|
7
|
+
const bytes = new Uint8Array([0, 1, 2, 3, 4, 5, 255, 254, 128]);
|
|
8
|
+
const s = base64Encode(bytes);
|
|
9
|
+
expect(base64Decode(s)).toEqual(bytes);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('empty bytes round-trip', () => {
|
|
13
|
+
const empty = new Uint8Array(0);
|
|
14
|
+
expect(base64Encode(empty)).toBe('');
|
|
15
|
+
expect(base64Decode('').byteLength).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('canonical RFC 4648 vectors', () => {
|
|
19
|
+
// Testing "f", "fo", "foo" from the RFC.
|
|
20
|
+
expect(base64Encode(new TextEncoder().encode('f'))).toBe('Zg==');
|
|
21
|
+
expect(base64Encode(new TextEncoder().encode('fo'))).toBe('Zm8=');
|
|
22
|
+
expect(base64Encode(new TextEncoder().encode('foo'))).toBe('Zm9v');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('rejects garbage input', () => {
|
|
26
|
+
expect(() => base64Decode('!!!')).toThrow();
|
|
27
|
+
expect(() => base64Decode('abc')).toThrow(); // length not multiple of 4
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('hex', () => {
|
|
32
|
+
it('encode → decode round-trips', () => {
|
|
33
|
+
const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
|
|
34
|
+
expect(hexEncode(bytes)).toBe('deadbeef');
|
|
35
|
+
expect(hexDecode('deadbeef')).toEqual(bytes);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('rejects odd-length input', () => {
|
|
39
|
+
expect(() => hexDecode('abc')).toThrow();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('rejects non-hex chars', () => {
|
|
43
|
+
expect(() => hexDecode('xx')).toThrow();
|
|
44
|
+
});
|
|
45
|
+
});
|