@orangecheck/stamp-ots 0.1.0 → 0.2.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/README.md +17 -19
- package/dist/index.d.mts +70 -6
- package/dist/index.d.ts +70 -6
- package/dist/index.js +380 -75
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +369 -75
- package/dist/index.mjs.map +1 -1
- package/package.json +60 -56
- package/src/anchor.test.ts +72 -0
- package/src/anchor.ts +100 -56
- package/src/calendar.ts +6 -8
- package/src/index.ts +19 -2
- package/src/ots-fixture.test-helper.ts +24 -0
- package/src/ots.test.ts +34 -0
- package/src/ots.ts +320 -0
- package/src/submit.test.ts +0 -17
- package/src/types.ts +7 -6
- package/src/upgrade.test.ts +110 -0
- package/src/upgrade.ts +72 -67
package/src/upgrade.ts
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
// Upgrade a pending OTS proof to a confirmed one. See SPEC.md §6.2.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// proof
|
|
8
|
-
//
|
|
9
|
-
// This package does NOT parse the proof bytes to extract block_height /
|
|
10
|
-
// block_hash — that requires a full OTS proof parser. We provide an anchor
|
|
11
|
-
// verifier callback pattern instead (see AnchorVerifier in ./types.ts).
|
|
3
|
+
// A pending proof ends in calendar attestations, each naming a calendar and
|
|
4
|
+
// the commitment (the message at that leaf) the calendar will later anchor.
|
|
5
|
+
// Calendars index upgrades by that commitment, not by the submitted digest.
|
|
6
|
+
// For each pending leaf we ask its calendar for the continuation, graft it on,
|
|
7
|
+
// and report the proof as confirmed once it reaches a Bitcoin attestation
|
|
8
|
+
// whose block header checks out.
|
|
12
9
|
|
|
10
|
+
import { blockHashOf } from './anchor.js';
|
|
13
11
|
import { base64Decode, base64Encode, hexDecode } from './base64.js';
|
|
14
12
|
import { createCalendarClient } from './calendar.js';
|
|
15
|
-
import
|
|
13
|
+
import {
|
|
14
|
+
bitcoinAnchors,
|
|
15
|
+
bytesEqual,
|
|
16
|
+
mergeAt,
|
|
17
|
+
parseProof,
|
|
18
|
+
parseTimestamp,
|
|
19
|
+
pendingCommitments,
|
|
20
|
+
serializeTimestamp,
|
|
21
|
+
} from './ots.js';
|
|
22
|
+
import type { BlockHeaderSource, OtsProof } from './types.js';
|
|
16
23
|
|
|
17
24
|
export interface UpgradeOptions {
|
|
18
25
|
/**
|
|
19
|
-
*
|
|
20
|
-
* the proof
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
* Header source used to resolve the anchor's block hash and to check that
|
|
27
|
+
* the proof's Merkle root is in that block.
|
|
28
|
+
*/
|
|
29
|
+
headerSource?: BlockHeaderSource;
|
|
30
|
+
/**
|
|
31
|
+
* Alternative to `headerSource`: given the merged proof bytes, return the
|
|
32
|
+
* anchor if the proof is confirmed, else null.
|
|
24
33
|
*/
|
|
25
|
-
parseAnchor
|
|
34
|
+
parseAnchor?: (proofBytes: Uint8Array) => Promise<{ blockHeight: number; blockHash: string } | null>;
|
|
26
35
|
/** Optional custom fetch passed through to calendar clients. */
|
|
27
36
|
fetch?: typeof fetch;
|
|
28
37
|
/** Request timeout per calendar. Default 30_000ms. */
|
|
@@ -30,73 +39,69 @@ export interface UpgradeOptions {
|
|
|
30
39
|
signal?: AbortSignal;
|
|
31
40
|
}
|
|
32
41
|
|
|
42
|
+
const norm = (u: string) => u.replace(/\/+$/, '');
|
|
43
|
+
|
|
33
44
|
export async function upgradeProof(
|
|
34
45
|
current: OtsProof,
|
|
35
46
|
idHex: string,
|
|
36
47
|
opts: UpgradeOptions
|
|
37
48
|
): Promise<OtsProof> {
|
|
38
49
|
if (current.status === 'confirmed') return current; // idempotent
|
|
50
|
+
if (!opts.headerSource && !opts.parseAnchor) {
|
|
51
|
+
throw new Error('upgradeProof: pass headerSource or parseAnchor');
|
|
52
|
+
}
|
|
39
53
|
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
const before = base64Decode(current.proof);
|
|
55
|
+
const tree = parseProof(before, hexDecode(idHex));
|
|
56
|
+
|
|
57
|
+
// Only ask calendars this proof was submitted to.
|
|
58
|
+
const listed = new Set(current.calendars.map(norm));
|
|
59
|
+
const pending = pendingCommitments(tree).filter((p) => listed.has(norm(p.uri)));
|
|
60
|
+
if (pending.length === 0 && bitcoinAnchors(tree).length === 0) {
|
|
61
|
+
throw new Error('upgradeProof: proof has no pending attestation from a listed calendar');
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
64
|
+
const answered: string[] = [];
|
|
65
|
+
await Promise.allSettled(
|
|
66
|
+
pending.map(async ({ uri, commitment }) => {
|
|
67
|
+
const client = createCalendarClient(uri, { fetch: opts.fetch, timeoutMs: opts.timeoutMs });
|
|
68
|
+
const bytes = await client.fetchProof(commitment, opts.signal);
|
|
69
|
+
if (!bytes) return;
|
|
70
|
+
if (mergeAt(tree, parseTimestamp(bytes, commitment))) answered.push(norm(uri));
|
|
54
71
|
})
|
|
55
72
|
);
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const { url, proof, anchor } = r.value;
|
|
61
|
-
if (anchor) {
|
|
62
|
-
return {
|
|
63
|
-
status: 'confirmed',
|
|
64
|
-
proof: base64Encode(proof),
|
|
65
|
-
calendars: uniqueFirst(current.calendars, url),
|
|
66
|
-
blockHeight: anchor.blockHeight,
|
|
67
|
-
blockHash: anchor.blockHash,
|
|
68
|
-
upgradedAt: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
74
|
+
const merged = serializeTimestamp(tree);
|
|
75
|
+
const anchor = await resolveAnchor(merged, tree, opts);
|
|
76
|
+
const calendars = [...answered, ...current.calendars.filter((c) => !answered.includes(norm(c)))];
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
if (anchor) {
|
|
79
|
+
return {
|
|
80
|
+
status: 'confirmed',
|
|
81
|
+
proof: base64Encode(merged),
|
|
82
|
+
calendars,
|
|
83
|
+
blockHeight: anchor.blockHeight,
|
|
84
|
+
blockHash: anchor.blockHash,
|
|
85
|
+
upgradedAt: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
86
|
+
};
|
|
81
87
|
}
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
if (longest.proof.byteLength > currentBytes.byteLength) {
|
|
85
|
-
return {
|
|
86
|
-
status: 'pending',
|
|
87
|
-
proof: base64Encode(longest.proof),
|
|
88
|
-
calendars: uniqueFirst(current.calendars, longest.url),
|
|
89
|
-
blockHeight: null,
|
|
90
|
-
blockHash: null,
|
|
91
|
-
upgradedAt: null,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
88
|
+
if (!bytesEqual(merged, before)) {
|
|
89
|
+
return { ...current, proof: base64Encode(merged), calendars };
|
|
94
90
|
}
|
|
95
91
|
return current;
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
async function resolveAnchor(
|
|
95
|
+
merged: Uint8Array,
|
|
96
|
+
tree: ReturnType<typeof parseTimestamp>,
|
|
97
|
+
opts: UpgradeOptions
|
|
98
|
+
): Promise<{ blockHeight: number; blockHash: string } | null> {
|
|
99
|
+
if (opts.parseAnchor) return opts.parseAnchor(merged);
|
|
100
|
+
for (const a of bitcoinAnchors(tree)) {
|
|
101
|
+
const header = await opts.headerSource!.getHeaderAt(a.blockHeight);
|
|
102
|
+
if (header && header.byteLength === 80 && bytesEqual(header.subarray(36, 68), a.merkleRoot)) {
|
|
103
|
+
return { blockHeight: a.blockHeight, blockHash: blockHashOf(header) };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
102
107
|
}
|