@orangecheck/stamp-ots 0.1.1 → 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 +66 -4
- package/dist/index.d.ts +66 -4
- package/dist/index.js +373 -105
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +363 -106
- package/dist/index.mjs.map +1 -1
- package/package.json +60 -64
- package/src/anchor.test.ts +72 -0
- package/src/anchor.ts +78 -135
- 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/anchor.ts
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
// Anchor
|
|
1
|
+
// Anchor verification. See SPEC.md §6.3.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// 1.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// A confirmed anchor holds when all three are true:
|
|
4
|
+
// 1. the proof commits to the envelope id and walks to a Bitcoin
|
|
5
|
+
// attestation at the declared height;
|
|
6
|
+
// 2. the message at that attestation equals the Merkle root in the block
|
|
7
|
+
// header at that height;
|
|
8
|
+
// 3. that header double-SHA256es to the declared block hash.
|
|
8
9
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// block header.
|
|
14
|
-
//
|
|
15
|
-
// B) `makeDefaultAnchorVerifier({ headerSource })` — dynamic-imports the
|
|
16
|
-
// `opentimestamps` npm package (optional peer dependency) and wires
|
|
17
|
-
// it into (A). One import, full verification.
|
|
18
|
-
//
|
|
19
|
-
// In both cases the resulting function is what `@orangecheck/stamp-core`
|
|
20
|
-
// expects at the `verifyOtsAnchor` input of `verify()` — no further adaptation
|
|
21
|
-
// needed. The envelope id is passed directly into the hook by stamp-core; we
|
|
22
|
-
// drop the old broken `adaptAnchorVerifier` that tried to derive the digest
|
|
23
|
-
// from the block hash.
|
|
10
|
+
// The resulting function is what `@orangecheck/stamp-core` expects at the
|
|
11
|
+
// `verifyOtsAnchor` input of `verify()`.
|
|
12
|
+
|
|
13
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
24
14
|
|
|
25
15
|
import { base64Decode, hexDecode, hexEncode } from './base64.js';
|
|
16
|
+
import { bitcoinAnchors, bytesEqual, parseProof } from './ots.js';
|
|
26
17
|
import type {
|
|
27
18
|
AnchorVerificationInput,
|
|
28
19
|
AnchorVerificationResult,
|
|
@@ -33,39 +24,53 @@ import type {
|
|
|
33
24
|
export interface AnchorVerifierConfig {
|
|
34
25
|
/**
|
|
35
26
|
* Parse the proof bytes and return the Merkle root the proof walks to,
|
|
36
|
-
* plus
|
|
37
|
-
*
|
|
27
|
+
* plus its block height. Optional: the built-in OTS parser is used when
|
|
28
|
+
* omitted. `blockHeight` is the declared height, for proofs that carry
|
|
29
|
+
* attestations at more than one height.
|
|
38
30
|
*/
|
|
39
|
-
walkProof
|
|
31
|
+
walkProof?: (input: {
|
|
40
32
|
proofBytes: Uint8Array;
|
|
41
33
|
digest: Uint8Array;
|
|
34
|
+
blockHeight?: number;
|
|
42
35
|
}) => Promise<{ merkleRoot: Uint8Array; blockHeight: number } | null>;
|
|
43
36
|
/**
|
|
44
|
-
* Source of Bitcoin block headers
|
|
45
|
-
*
|
|
37
|
+
* Source of Bitcoin block headers: a full node, SPV, a headers snapshot,
|
|
38
|
+
* or `mempoolHeaderSource()`. This is the verifier's trust anchor.
|
|
46
39
|
*/
|
|
47
40
|
headerSource: BlockHeaderSource;
|
|
48
41
|
}
|
|
49
42
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
43
|
+
/** Built-in walker: parse the OTS proof and pick the Bitcoin attestation. */
|
|
44
|
+
export async function walkOtsProof(input: {
|
|
45
|
+
proofBytes: Uint8Array;
|
|
46
|
+
digest: Uint8Array;
|
|
47
|
+
blockHeight?: number;
|
|
48
|
+
}): Promise<{ merkleRoot: Uint8Array; blockHeight: number } | null> {
|
|
49
|
+
try {
|
|
50
|
+
const anchors = bitcoinAnchors(parseProof(input.proofBytes, input.digest));
|
|
51
|
+
return anchors.find((a) => a.blockHeight === input.blockHeight) ?? anchors[0] ?? null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Block hash (display hex) of an 80-byte header. */
|
|
58
|
+
export function blockHashOf(header: Uint8Array): string {
|
|
59
|
+
return hexEncode(sha256(sha256(header)).reverse());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
62
|
export function makeAnchorVerifier(
|
|
63
63
|
cfg: AnchorVerifierConfig
|
|
64
64
|
): (proofB64: string, blockHeight: number, blockHash: string, envelopeId: string) => Promise<boolean> {
|
|
65
|
+
const walkProof = cfg.walkProof ?? walkOtsProof;
|
|
65
66
|
const verifier: AnchorVerifier = async (
|
|
66
67
|
input: AnchorVerificationInput
|
|
67
68
|
): Promise<AnchorVerificationResult> => {
|
|
68
|
-
const walked = await
|
|
69
|
+
const walked = await walkProof({
|
|
70
|
+
proofBytes: input.proofBytes,
|
|
71
|
+
digest: input.digest,
|
|
72
|
+
blockHeight: input.blockHeight,
|
|
73
|
+
});
|
|
69
74
|
if (!walked) {
|
|
70
75
|
return { ok: false, reason: 'proof could not be walked to a Bitcoin commitment' };
|
|
71
76
|
}
|
|
@@ -76,23 +81,18 @@ export function makeAnchorVerifier(
|
|
|
76
81
|
};
|
|
77
82
|
}
|
|
78
83
|
const header = await cfg.headerSource.getHeaderAt(input.blockHeight);
|
|
79
|
-
if (!header) {
|
|
84
|
+
if (!header || header.byteLength !== 80) {
|
|
80
85
|
return { ok: false, reason: `no block header available at height ${input.blockHeight}` };
|
|
81
86
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// endiannesses to stay compatible across implementations.
|
|
85
|
-
if (header.byteLength < 80) {
|
|
86
|
-
return { ok: false, reason: 'malformed Bitcoin header (too short)' };
|
|
87
|
+
if (blockHashOf(header) !== input.blockHash.toLowerCase()) {
|
|
88
|
+
return { ok: false, reason: 'block header does not hash to the declared block hash' };
|
|
87
89
|
}
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
const matchesBE = bytesEqual(headerMerkleBE, walked.merkleRoot);
|
|
92
|
-
if (!matchesLE && !matchesBE) {
|
|
90
|
+
// OTS attests to the Merkle root in header byte order (offset 36).
|
|
91
|
+
const headerMerkle = header.subarray(36, 68);
|
|
92
|
+
if (!bytesEqual(headerMerkle, walked.merkleRoot)) {
|
|
93
93
|
return { ok: false, reason: 'block header Merkle root does not match walked proof root' };
|
|
94
94
|
}
|
|
95
|
-
return { ok: true, merkleRoot: hexEncode(
|
|
95
|
+
return { ok: true, merkleRoot: hexEncode(headerMerkle) };
|
|
96
96
|
};
|
|
97
97
|
|
|
98
98
|
return async (proofB64, blockHeight, blockHash, envelopeId) => {
|
|
@@ -107,98 +107,41 @@ export function makeAnchorVerifier(
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* consumers who only need calendar submission don't pay the dep cost.
|
|
113
|
-
*
|
|
114
|
-
* Install `opentimestamps` alongside `@orangecheck/stamp-ots` to use this:
|
|
115
|
-
*
|
|
116
|
-
* npm i @orangecheck/stamp-ots opentimestamps
|
|
117
|
-
*
|
|
118
|
-
* Then:
|
|
119
|
-
*
|
|
120
|
-
* const verifyOtsAnchor = await makeDefaultAnchorVerifier({ headerSource });
|
|
121
|
-
* await verify({ envelope, verifyOtsAnchor });
|
|
122
|
-
*
|
|
123
|
-
* Throws if `opentimestamps` isn't resolvable. That's intentional — callers
|
|
124
|
-
* who haven't installed it should use `makeAnchorVerifier` with their own
|
|
125
|
-
* walker instead, so the failure surfaces immediately rather than silently
|
|
126
|
-
* reporting every stamp as unverified.
|
|
110
|
+
* Anchor verifier using the built-in OTS parser. Kept async for callers of the
|
|
111
|
+
* 0.1 API, which lazily loaded an optional parser here.
|
|
127
112
|
*/
|
|
128
113
|
export async function makeDefaultAnchorVerifier(opts: {
|
|
129
114
|
headerSource: BlockHeaderSource;
|
|
130
115
|
}): Promise<
|
|
131
116
|
(proofB64: string, blockHeight: number, blockHash: string, envelopeId: string) => Promise<boolean>
|
|
132
117
|
> {
|
|
133
|
-
|
|
134
|
-
return makeAnchorVerifier({ walkProof, headerSource: opts.headerSource });
|
|
118
|
+
return makeAnchorVerifier({ headerSource: opts.headerSource });
|
|
135
119
|
}
|
|
136
120
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// the error is explicit and actionable.
|
|
142
|
-
let ots: unknown;
|
|
143
|
-
try {
|
|
144
|
-
ots = await import(/* webpackIgnore: true */ 'opentimestamps' as string);
|
|
145
|
-
} catch (e) {
|
|
146
|
-
throw new Error(
|
|
147
|
-
'@orangecheck/stamp-ots: makeDefaultAnchorVerifier requires the optional peer dependency `opentimestamps`. ' +
|
|
148
|
-
`Install it with \`npm i opentimestamps\` or supply your own walkProof via makeAnchorVerifier. ` +
|
|
149
|
-
`Underlying error: ${e instanceof Error ? e.message : String(e)}`
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Normalize the module shape. `opentimestamps` publishes both CJS-default
|
|
154
|
-
// and named exports across its versions.
|
|
155
|
-
const mod = (ots as { default?: unknown }).default ?? ots;
|
|
156
|
-
const modAny = mod as Record<string, unknown>;
|
|
157
|
-
|
|
158
|
-
type DetachedLike = { deserialize(bytes: Uint8Array): unknown; timestamp: unknown };
|
|
159
|
-
type TimestampLike = {
|
|
160
|
-
verify(): Promise<Map<string, { height: number; timestamp?: unknown }>>;
|
|
161
|
-
msg: Uint8Array;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const DetachedCtor =
|
|
165
|
-
(modAny.DetachedTimestampFile as { deserialize(b: Uint8Array): DetachedLike } | undefined) ??
|
|
166
|
-
(modAny.default as Record<string, unknown> | undefined)?.DetachedTimestampFile as
|
|
167
|
-
| { deserialize(b: Uint8Array): DetachedLike }
|
|
168
|
-
| undefined;
|
|
169
|
-
|
|
170
|
-
if (!DetachedCtor || typeof DetachedCtor.deserialize !== 'function') {
|
|
171
|
-
throw new Error(
|
|
172
|
-
'@orangecheck/stamp-ots: unexpected shape from `opentimestamps` package — ' +
|
|
173
|
-
'expected DetachedTimestampFile.deserialize. If you are using a non-standard build, ' +
|
|
174
|
-
'pass your own walkProof via makeAnchorVerifier instead.'
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return async ({ proofBytes }) => {
|
|
179
|
-
try {
|
|
180
|
-
const detached = DetachedCtor.deserialize(proofBytes);
|
|
181
|
-
const ts = detached.timestamp as TimestampLike;
|
|
182
|
-
const attestations = await ts.verify();
|
|
183
|
-
const first = attestations.values().next();
|
|
184
|
-
if (first.done) return null;
|
|
185
|
-
const { height, timestamp } = first.value;
|
|
186
|
-
if (typeof height !== 'number' || !timestamp) return null;
|
|
187
|
-
// The attestation provides the block height; the merkle root the
|
|
188
|
-
// walker produces is the timestamp.msg bytes the walker committed
|
|
189
|
-
// against. opentimestamps resolves that through its internal
|
|
190
|
-
// proof chain; reading ts.msg gives the final digest it committed.
|
|
191
|
-
const merkleRoot = (timestamp as { msg?: Uint8Array }).msg;
|
|
192
|
-
if (!merkleRoot) return null;
|
|
193
|
-
return { merkleRoot, blockHeight: height };
|
|
194
|
-
} catch {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
};
|
|
121
|
+
export interface MempoolHeaderSourceOptions {
|
|
122
|
+
/** Esplora-compatible API base. Default https://mempool.space/api. */
|
|
123
|
+
baseUrl?: string;
|
|
124
|
+
fetch?: typeof fetch;
|
|
198
125
|
}
|
|
199
126
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Block headers from an Esplora-compatible API. The verifier checks each
|
|
129
|
+
* header against the declared block hash, so this source is trusted only to
|
|
130
|
+
* report which block is at a height.
|
|
131
|
+
*/
|
|
132
|
+
export function mempoolHeaderSource(opts: MempoolHeaderSourceOptions = {}): BlockHeaderSource {
|
|
133
|
+
const base = (opts.baseUrl ?? 'https://mempool.space/api').replace(/\/+$/, '');
|
|
134
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
135
|
+
return {
|
|
136
|
+
async getHeaderAt(height) {
|
|
137
|
+
const h = await fetchImpl(`${base}/block-height/${height}`);
|
|
138
|
+
if (!h.ok) return null;
|
|
139
|
+
const hash = (await h.text()).trim();
|
|
140
|
+
if (!/^[0-9a-f]{64}$/.test(hash)) return null;
|
|
141
|
+
const r = await fetchImpl(`${base}/block/${hash}/header`);
|
|
142
|
+
if (!r.ok) return null;
|
|
143
|
+
const hex = (await r.text()).trim();
|
|
144
|
+
return /^[0-9a-f]{160}$/.test(hex) ? hexDecode(hex) : null;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
204
147
|
}
|
package/src/calendar.ts
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
// of the calendar protocol that an OC Stamp implementation needs:
|
|
3
3
|
//
|
|
4
4
|
// POST <url>/digest body = raw digest bytes -> pending proof bytes
|
|
5
|
-
// GET <url>/timestamp/<hex>
|
|
5
|
+
// GET <url>/timestamp/<hex(commitment)> -> continuation, or 404
|
|
6
6
|
//
|
|
7
|
-
// The client
|
|
8
|
-
// separate concern addressed by whichever OTS proof library the consumer
|
|
9
|
-
// plugs in (see verifyOtsAnchor in ./anchor.ts).
|
|
7
|
+
// The client moves bytes only; ./ots.ts parses them.
|
|
10
8
|
|
|
11
9
|
import type { CalendarClient } from './types.js';
|
|
12
10
|
import { hexEncode } from './base64.js';
|
|
@@ -62,12 +60,12 @@ export function createCalendarClient(url: string, opts: HttpCalendarOptions = {}
|
|
|
62
60
|
const buf = await resp.arrayBuffer();
|
|
63
61
|
return new Uint8Array(buf);
|
|
64
62
|
},
|
|
65
|
-
async fetchProof(
|
|
66
|
-
if (
|
|
67
|
-
throw new Error('OTS:
|
|
63
|
+
async fetchProof(commitment, signal) {
|
|
64
|
+
if (commitment.byteLength === 0 || commitment.byteLength > 64) {
|
|
65
|
+
throw new Error('OTS: commitment must be 1-64 bytes');
|
|
68
66
|
}
|
|
69
67
|
const ac = withTimeout(signal, timeoutMs);
|
|
70
|
-
const resp = await fetchImpl(`${base}/timestamp/${hexEncode(
|
|
68
|
+
const resp = await fetchImpl(`${base}/timestamp/${hexEncode(commitment)}`, {
|
|
71
69
|
method: 'GET',
|
|
72
70
|
headers: { Accept: 'application/vnd.opentimestamps.v1' },
|
|
73
71
|
signal: ac.signal,
|
package/src/index.ts
CHANGED
|
@@ -6,5 +6,22 @@ export { submitToCalendars } from './submit.js';
|
|
|
6
6
|
export type { SubmitOptions } from './submit.js';
|
|
7
7
|
export { upgradeProof } from './upgrade.js';
|
|
8
8
|
export type { UpgradeOptions } from './upgrade.js';
|
|
9
|
-
export {
|
|
10
|
-
|
|
9
|
+
export {
|
|
10
|
+
blockHashOf,
|
|
11
|
+
makeAnchorVerifier,
|
|
12
|
+
makeDefaultAnchorVerifier,
|
|
13
|
+
mempoolHeaderSource,
|
|
14
|
+
walkOtsProof,
|
|
15
|
+
} from './anchor.js';
|
|
16
|
+
export type { AnchorVerifierConfig, MempoolHeaderSourceOptions } from './anchor.js';
|
|
17
|
+
export {
|
|
18
|
+
attestations,
|
|
19
|
+
bitcoinAnchors,
|
|
20
|
+
mergeAt,
|
|
21
|
+
parseDetached,
|
|
22
|
+
parseProof,
|
|
23
|
+
parseTimestamp,
|
|
24
|
+
pendingCommitments,
|
|
25
|
+
serializeTimestamp,
|
|
26
|
+
} from './ots.js';
|
|
27
|
+
export type { OtsAttestation, OtsOp, OtsTimestamp } from './ots.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// A real OpenTimestamps proof: opentimestamps-client examples/hello-world.txt.ots,
|
|
2
|
+
// committing to sha256("Hello World!\n") and anchored in Bitcoin block 358391.
|
|
3
|
+
|
|
4
|
+
import { base64Decode } from './base64.js';
|
|
5
|
+
|
|
6
|
+
export const HELLO_DIGEST = '03ba204e50d126e4674c005e04d82e84c21366780af1f43bd54a37816b6ab340';
|
|
7
|
+
|
|
8
|
+
export const HELLO_OTS_FILE = base64Decode(
|
|
9
|
+
'AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/ieLohOiSlAEIA7ogTlDRJuRnTABeBNguhMITZngK8fQ71Uo3gWtqs0AD' +
|
|
10
|
+
'8cgBAQAAAAHkgvnTLsw7ple2nYmAEIV7VEV6kEl5gv9W+XxOxY5vmAEAAABrSDBFAiEAslOt0dHPkIRDOKR1oE/x' +
|
|
11
|
+
'P8nnvSQrB3Yt6gf1YIst42cCIACyaMqcM0KzdpzdBiiRMXzc74eqwxC2hV6dk4mOu+jsASECDY5NEH0rM5sAUO/d' +
|
|
12
|
+
'S0oJJFqgVgSPElOWN06moqsHCcb/////AmUz5gUAAAAAGXapFAvwV9QPu6Z0SGJRX1tVojEN5XcviKyghgEAAAAA' +
|
|
13
|
+
'ABl2qRTwBoisAAAAAAgI8SCph/cWxTORPDFMeONdNYhMrJQ/pCysSdKyxp9AA/hfiAgI8SDexVs0h+Hj9yKkm1Wn' +
|
|
14
|
+
'eDIVhieF9KOss5KEYBn3HcZKnQgI8SCyyhj0heCAR44CXas9RktBbA4ey2Ypya786MghTQQkMggI8CARsOkGYRlv' +
|
|
15
|
+
'9LCBPD7aFBurXpFgSDe996DJ3zfbDjoRmAgI8CDDS8GkoQk//RSMAWseZkdCkU6Tnvq+TT01ZRWRSybZ4ggI8CDD' +
|
|
16
|
+
'5ufDjGn2ryTCvjTrrEglft5h7AohuVNeREMne+MGRggI8SAHmL+GBuAAJOXV1UvwyWD2Kd+52taRV0VbbyZSwOje' +
|
|
17
|
+
'gQgI8CA/mtptYLqiRABrsKrVFEitL6+51LZIegmZz/JrkfD1NggI8SDHAwGelZqN0/rvdIm7MoukhVdHWOcJHwFG' +
|
|
18
|
+
'TrZYcsl1yAgI8CDL/v/1E/+EuRXj/tb515lnZjD4Nk6ipsdVf62UpbXXiAgI8SAL4jcJhZkTur1EYLvd+O0hPnyH' +
|
|
19
|
+
'c6Sx+s4w+Kz98JO3BQgIAAWIlg1z1xkBA/fvFQ==');
|
|
20
|
+
|
|
21
|
+
export const BLOCK_HEIGHT = 358391;
|
|
22
|
+
export const BLOCK_HASH = '000000000000000003e892881a8cdcdc117c06d444057c98b6f04a9ee75a2319';
|
|
23
|
+
export const BLOCK_HEADER_HEX =
|
|
24
|
+
'02000000b96394585a281b7e5f438fd1c9ed492645a1fd61cb3802040000000000000000007ee445d23ad061af4a36b809501fab1ac4f2d7e7a739817dd0cbb7ec661b8a1e376755f58616186272def6';
|
package/src/ots.test.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { hexDecode, hexEncode } from './base64.js';
|
|
4
|
+
import { bitcoinAnchors, parseDetached, parseProof, serializeTimestamp } from './ots.js';
|
|
5
|
+
import { BLOCK_HEADER_HEX, BLOCK_HEIGHT, HELLO_DIGEST, HELLO_OTS_FILE } from './ots-fixture.test-helper.js';
|
|
6
|
+
|
|
7
|
+
describe('OTS proof parsing', () => {
|
|
8
|
+
it('walks a detached proof to the Merkle root in the attested block header', () => {
|
|
9
|
+
const { digest, timestamp } = parseDetached(HELLO_OTS_FILE);
|
|
10
|
+
expect(hexEncode(digest)).toBe(HELLO_DIGEST);
|
|
11
|
+
const [anchor] = bitcoinAnchors(timestamp);
|
|
12
|
+
expect(anchor?.blockHeight).toBe(BLOCK_HEIGHT);
|
|
13
|
+
expect(hexEncode(anchor!.merkleRoot)).toBe(BLOCK_HEADER_HEX.slice(72, 136));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('serializes a parsed timestamp back to the same bytes', () => {
|
|
17
|
+
const { timestamp } = parseDetached(HELLO_OTS_FILE);
|
|
18
|
+
const bare = serializeTimestamp(timestamp);
|
|
19
|
+
expect(hexEncode(bare)).toBe(hexEncode(HELLO_OTS_FILE.subarray(HELLO_OTS_FILE.length - bare.length)));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('accepts the bare timestamp encoding rooted at the digest', () => {
|
|
23
|
+
const bare = serializeTimestamp(parseDetached(HELLO_OTS_FILE).timestamp);
|
|
24
|
+
expect(bitcoinAnchors(parseProof(bare, hexDecode(HELLO_DIGEST)))[0]?.blockHeight).toBe(BLOCK_HEIGHT);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('refuses a detached proof for a different digest', () => {
|
|
28
|
+
expect(() => parseProof(HELLO_OTS_FILE, hexDecode('00'.repeat(32)))).toThrow(/different digest/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('rejects truncated proofs', () => {
|
|
32
|
+
expect(() => parseDetached(HELLO_OTS_FILE.subarray(0, HELLO_OTS_FILE.length - 5))).toThrow();
|
|
33
|
+
});
|
|
34
|
+
});
|