@oxy.so/protocol 1.0.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 +202 -0
- package/NOTICE +16 -0
- package/dist/cjs/.tsbuildinfo +1 -0
- package/dist/cjs/chain/continuity.js +54 -0
- package/dist/cjs/chain/engine.js +34 -0
- package/dist/cjs/chain/recordStore.js +25 -0
- package/dist/cjs/chain/types.js +22 -0
- package/dist/cjs/chain/verify.js +82 -0
- package/dist/cjs/envelope/canonicalJson.js +107 -0
- package/dist/cjs/envelope/recordId.js +60 -0
- package/dist/cjs/envelope/sign.js +75 -0
- package/dist/cjs/envelope/signingInput.js +32 -0
- package/dist/cjs/identity/resolver.js +50 -0
- package/dist/cjs/index.js +71 -0
- package/dist/cjs/node/constants.js +85 -0
- package/dist/cjs/node/didWebResolver.js +126 -0
- package/dist/cjs/node/httpFetch.js +61 -0
- package/dist/cjs/node/index.js +71 -0
- package/dist/cjs/node/nodeApp.js +344 -0
- package/dist/cjs/node/nodeClient.js +204 -0
- package/dist/cjs/node/rateLimit.js +187 -0
- package/dist/cjs/node/verifyRecord.js +51 -0
- package/dist/cjs/platform/crypto.js +186 -0
- package/dist/cjs/platform/crypto.native.js +204 -0
- package/dist/cjs/platform/expoTypes.js +24 -0
- package/dist/cjs/platform/platform.js +33 -0
- package/dist/cjs/secp256k1.js +148 -0
- package/dist/cjs/transparency/checkpoint.js +79 -0
- package/dist/cjs/transparency/tree.js +197 -0
- package/dist/esm/.tsbuildinfo +1 -0
- package/dist/esm/chain/continuity.js +51 -0
- package/dist/esm/chain/engine.js +31 -0
- package/dist/esm/chain/recordStore.js +24 -0
- package/dist/esm/chain/types.js +19 -0
- package/dist/esm/chain/verify.js +78 -0
- package/dist/esm/envelope/canonicalJson.js +104 -0
- package/dist/esm/envelope/recordId.js +56 -0
- package/dist/esm/envelope/sign.js +69 -0
- package/dist/esm/envelope/signingInput.js +29 -0
- package/dist/esm/identity/resolver.js +47 -0
- package/dist/esm/index.js +36 -0
- package/dist/esm/node/constants.js +82 -0
- package/dist/esm/node/didWebResolver.js +122 -0
- package/dist/esm/node/httpFetch.js +55 -0
- package/dist/esm/node/index.js +28 -0
- package/dist/esm/node/nodeApp.js +336 -0
- package/dist/esm/node/nodeClient.js +198 -0
- package/dist/esm/node/rateLimit.js +182 -0
- package/dist/esm/node/verifyRecord.js +48 -0
- package/dist/esm/platform/crypto.js +145 -0
- package/dist/esm/platform/crypto.native.js +196 -0
- package/dist/esm/platform/expoTypes.js +23 -0
- package/dist/esm/platform/platform.js +29 -0
- package/dist/esm/secp256k1.js +137 -0
- package/dist/esm/transparency/checkpoint.js +73 -0
- package/dist/esm/transparency/tree.js +189 -0
- package/dist/types/.tsbuildinfo +1 -0
- package/dist/types/chain/continuity.d.ts +28 -0
- package/dist/types/chain/engine.d.ts +27 -0
- package/dist/types/chain/recordStore.d.ts +85 -0
- package/dist/types/chain/types.d.ts +79 -0
- package/dist/types/chain/verify.d.ts +45 -0
- package/dist/types/envelope/canonicalJson.d.ts +44 -0
- package/dist/types/envelope/recordId.d.ts +30 -0
- package/dist/types/envelope/sign.d.ts +47 -0
- package/dist/types/envelope/signingInput.d.ts +33 -0
- package/dist/types/identity/resolver.d.ts +67 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/node/constants.d.ts +80 -0
- package/dist/types/node/didWebResolver.d.ts +47 -0
- package/dist/types/node/httpFetch.d.ts +60 -0
- package/dist/types/node/index.d.ts +28 -0
- package/dist/types/node/nodeApp.d.ts +120 -0
- package/dist/types/node/nodeClient.d.ts +135 -0
- package/dist/types/node/rateLimit.d.ts +95 -0
- package/dist/types/node/verifyRecord.d.ts +41 -0
- package/dist/types/platform/crypto.d.ts +93 -0
- package/dist/types/platform/crypto.native.d.ts +77 -0
- package/dist/types/platform/expoTypes.d.ts +99 -0
- package/dist/types/platform/platform.d.ts +25 -0
- package/dist/types/secp256k1.d.ts +45 -0
- package/dist/types/transparency/checkpoint.d.ts +71 -0
- package/dist/types/transparency/tree.d.ts +135 -0
- package/package.json +157 -0
- package/src/__tests__/canonicalJson.test.ts +116 -0
- package/src/__tests__/chain.test.ts +279 -0
- package/src/__tests__/didWebResolver.test.ts +132 -0
- package/src/__tests__/envelope.test.ts +267 -0
- package/src/__tests__/nodeApp.test.ts +410 -0
- package/src/__tests__/nodeClient.test.ts +177 -0
- package/src/__tests__/nodeHarness.ts +151 -0
- package/src/__tests__/optionalNativePeers.test.ts +233 -0
- package/src/__tests__/rateLimit.test.ts +268 -0
- package/src/__tests__/runnerGuard.test.ts +85 -0
- package/src/__tests__/secp256k1.test.ts +118 -0
- package/src/__tests__/transparency.test.ts +353 -0
- package/src/chain/continuity.ts +59 -0
- package/src/chain/engine.ts +43 -0
- package/src/chain/recordStore.ts +98 -0
- package/src/chain/types.ts +85 -0
- package/src/chain/verify.ts +102 -0
- package/src/envelope/canonicalJson.ts +120 -0
- package/src/envelope/recordId.ts +63 -0
- package/src/envelope/sign.ts +86 -0
- package/src/envelope/signingInput.ts +48 -0
- package/src/identity/resolver.ts +90 -0
- package/src/index.ts +101 -0
- package/src/node/constants.ts +105 -0
- package/src/node/didWebResolver.ts +162 -0
- package/src/node/httpFetch.ts +88 -0
- package/src/node/index.ts +87 -0
- package/src/node/nodeApp.ts +471 -0
- package/src/node/nodeClient.ts +322 -0
- package/src/node/rateLimit.ts +233 -0
- package/src/node/verifyRecord.ts +60 -0
- package/src/platform/crypto.native.ts +251 -0
- package/src/platform/crypto.ts +172 -0
- package/src/platform/expoTypes.ts +99 -0
- package/src/platform/platform.ts +31 -0
- package/src/secp256k1.ts +207 -0
- package/src/transparency/checkpoint.ts +109 -0
- package/src/transparency/tree.ts +258 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Crypto / Storage — React Native Variant
|
|
3
|
+
*
|
|
4
|
+
* Companion to `./crypto.ts`. See the doc-comment at the top of that file for
|
|
5
|
+
* the full design.
|
|
6
|
+
*
|
|
7
|
+
* Metro auto-selects this file in any non-web build (`preferNativePlatform`
|
|
8
|
+
* is `true` for iOS / Android, so `*.native.js` shadows `*.js` during
|
|
9
|
+
* source-extension resolution inside `node_modules/@oxy.so/protocol/dist/`). On
|
|
10
|
+
* iOS / Android `<base>.ios.js` / `<base>.android.js` would shadow this file
|
|
11
|
+
* if they existed, but they don't — `.native.js` is the shared RN variant.
|
|
12
|
+
*
|
|
13
|
+
* - The default variant references Node's `'crypto'` and would crash Metro
|
|
14
|
+
* if bundled into an RN app.
|
|
15
|
+
* - This variant references the RN-only modules (`expo-crypto`,
|
|
16
|
+
* `expo-secure-store`, `@react-native-async-storage/async-storage`),
|
|
17
|
+
* each behind Metro's optional-dependency mechanism (see below).
|
|
18
|
+
*
|
|
19
|
+
* Both variants expose the same surface; importers don't care which one
|
|
20
|
+
* they got.
|
|
21
|
+
*
|
|
22
|
+
* # Why `try { require('literal') } catch` and not a static import?
|
|
23
|
+
*
|
|
24
|
+
* Those three RN modules are declared OPTIONAL peer dependencies in
|
|
25
|
+
* `package.json`. A static `import` contradicts that: an optional peer that is
|
|
26
|
+
* omitted does not degrade, it fails to RESOLVE, and Metro aborts the whole
|
|
27
|
+
* bundle. Because `@oxy.so/core`'s `crypto/polyfill` imports `@oxy.so/protocol`
|
|
28
|
+
* from its root entry, this file is in the eager graph of EVERY React Native
|
|
29
|
+
* app on `@oxy.so/core` — so a single undeclared optional peer broke the native
|
|
30
|
+
* bundle of every app that did not happen to install it, with a resolution
|
|
31
|
+
* error pointing at a dependency the app never mentions.
|
|
32
|
+
*
|
|
33
|
+
* Metro treats a `require()` of a STRING LITERAL that sits inside a `try`
|
|
34
|
+
* block as an optional dependency: it resolves it when present, and when
|
|
35
|
+
* absent emits a stub that throws on evaluation instead of failing the build.
|
|
36
|
+
* The `catch` turns that into a `null` module handle, and the loader below
|
|
37
|
+
* throws an actionable error naming the missing package the first time the
|
|
38
|
+
* capability is actually used. Bundle-time hard failure becomes a
|
|
39
|
+
* capability-scoped runtime failure — which is exactly what "optional peer"
|
|
40
|
+
* is supposed to mean.
|
|
41
|
+
*
|
|
42
|
+
* Two constraints this shape has to respect, both learned the hard way:
|
|
43
|
+
*
|
|
44
|
+
* - The specifier MUST be a literal. A runtime-computed `require(variable)`
|
|
45
|
+
* is unresolvable for Metro (that is the bug the shared-identity bridge
|
|
46
|
+
* below documents) and silently yields nothing in a consuming repo.
|
|
47
|
+
* - The load MUST stay synchronous. `getRandomBytesRN` backs
|
|
48
|
+
* `globalThis.crypto.getRandomValues` in `@oxy.so/core`'s polyfill, which
|
|
49
|
+
* cannot await anything.
|
|
50
|
+
*
|
|
51
|
+
* `expo-modules-core` is a NON-optional peer (every RN app has it via `expo`),
|
|
52
|
+
* so it stays a plain static import.
|
|
53
|
+
*/
|
|
54
|
+
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
55
|
+
let expoCryptoModule = null;
|
|
56
|
+
let expoCryptoError;
|
|
57
|
+
try {
|
|
58
|
+
expoCryptoModule = require('expo-crypto');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
expoCryptoError = error;
|
|
62
|
+
}
|
|
63
|
+
let secureStoreModule = null;
|
|
64
|
+
let secureStoreError;
|
|
65
|
+
try {
|
|
66
|
+
secureStoreModule = require('expo-secure-store');
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
secureStoreError = error;
|
|
70
|
+
}
|
|
71
|
+
let asyncStorageModule = null;
|
|
72
|
+
let asyncStorageError;
|
|
73
|
+
try {
|
|
74
|
+
// Babel's default-import interop unwraps `.default` for us on a static
|
|
75
|
+
// import; a raw `require` has to do it by hand. The `?? namespace` fallback
|
|
76
|
+
// covers a host that hands back a real ESM namespace with no `default`.
|
|
77
|
+
const namespace = require('@react-native-async-storage/async-storage');
|
|
78
|
+
asyncStorageModule = namespace.default ?? namespace;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
asyncStorageError = error;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Actionable error for a missing optional peer. Carries the underlying Metro
|
|
85
|
+
* resolution message so the failure is never silent — the `catch` above only
|
|
86
|
+
* defers the report to the point where the capability is actually needed.
|
|
87
|
+
*/
|
|
88
|
+
function missingOptionalPeerError(packageName, capability, cause) {
|
|
89
|
+
const sentences = [
|
|
90
|
+
`[oxy.protocol.crypto] '${packageName}' is not installed, so ${capability} is unavailable in this app.`,
|
|
91
|
+
'It is an optional peer dependency of @oxy.so/protocol that the React Native runtime needs —',
|
|
92
|
+
`install it with \`npx expo install ${packageName}\`.`,
|
|
93
|
+
];
|
|
94
|
+
if (cause instanceof Error) {
|
|
95
|
+
sentences.push(`Underlying error: ${cause.message}`);
|
|
96
|
+
}
|
|
97
|
+
return new Error(sentences.join(' '));
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Node `crypto` — never available in RN.
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
export async function loadNodeCrypto() {
|
|
103
|
+
// Unreachable in practice: every caller gates with `isNodeJS()` before
|
|
104
|
+
// invoking this. If it somehow does fire, throw immediately with a clear
|
|
105
|
+
// diagnostic rather than letting Metro / Hermes attempt to find a
|
|
106
|
+
// non-existent module at runtime.
|
|
107
|
+
throw new Error("[oxy.protocol.crypto] Node's built-in 'crypto' module is not available " +
|
|
108
|
+
'in a React Native runtime. Use the RN-specific helpers ' +
|
|
109
|
+
'(loadExpoCrypto, getRandomBytesRN) or the Web Crypto API (`globalThis.crypto`).');
|
|
110
|
+
}
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// expo-crypto — RN cryptographic primitives.
|
|
113
|
+
//
|
|
114
|
+
// The real module satisfies `ExpoCryptoLike` structurally; the structural
|
|
115
|
+
// interface narrows the surface so consumers never pull expo's own types into
|
|
116
|
+
// their compilation (see expoTypes.ts).
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
export async function loadExpoCrypto() {
|
|
119
|
+
if (!expoCryptoModule) {
|
|
120
|
+
throw missingOptionalPeerError('expo-crypto', 'React Native cryptography', expoCryptoError);
|
|
121
|
+
}
|
|
122
|
+
return expoCryptoModule;
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// expo-secure-store — RN keychain / keystore.
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
export async function loadSecureStore() {
|
|
128
|
+
if (!secureStoreModule) {
|
|
129
|
+
throw missingOptionalPeerError('expo-secure-store', 'on-device identity storage', secureStoreError);
|
|
130
|
+
}
|
|
131
|
+
return secureStoreModule;
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// @react-native-async-storage/async-storage — RN persistent KV storage.
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
export async function loadAsyncStorage() {
|
|
137
|
+
if (!asyncStorageModule) {
|
|
138
|
+
throw missingOptionalPeerError('@react-native-async-storage/async-storage', 'device/session persistence', asyncStorageError);
|
|
139
|
+
}
|
|
140
|
+
// Mirror the shape callers historically used (`module.default.<method>`)
|
|
141
|
+
// so the call sites don't have to know whether the underlying module
|
|
142
|
+
// ships ESM or CJS-with-default.
|
|
143
|
+
return { default: asyncStorageModule };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Synchronous random-bytes via `expo-crypto.getRandomBytes`.
|
|
147
|
+
*
|
|
148
|
+
* Synchronous by contract: `@oxy.so/core`'s crypto polyfill uses this to back
|
|
149
|
+
* `globalThis.crypto.getRandomValues`, which cannot await. That is why
|
|
150
|
+
* `expo-crypto` is resolved with a synchronous `require` at module scope rather
|
|
151
|
+
* than a dynamic `import()`.
|
|
152
|
+
*/
|
|
153
|
+
export function getRandomBytesRN(byteCount) {
|
|
154
|
+
if (!expoCryptoModule) {
|
|
155
|
+
throw missingOptionalPeerError('expo-crypto', 'the React Native CSPRNG (crypto.getRandomValues)', expoCryptoError);
|
|
156
|
+
}
|
|
157
|
+
return expoCryptoModule.getRandomBytes(byteCount);
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Shared identity bridge — `@oxy.so/expo-oxy-identity` (native-only, OPTIONAL).
|
|
161
|
+
//
|
|
162
|
+
// `@oxy.so/expo-oxy-identity` is the in-repo Expo module autolinked into the
|
|
163
|
+
// identity apps (Commons + the reader RPs). We resolve its NATIVE module
|
|
164
|
+
// directly via expo-modules-core's `requireOptionalNativeModule('OxyIdentity')`
|
|
165
|
+
// — a static import Metro always resolves — instead of dynamically importing the
|
|
166
|
+
// module's JS wrapper. A runtime-computed `import(moduleName)` compiled to a
|
|
167
|
+
// `require(variable)` in the CJS build, which Metro cannot resolve in a consuming
|
|
168
|
+
// repo (the bridge silently resolved `null` there — the cross-app SSO bug). Since
|
|
169
|
+
// the native module is what actually holds the shared identity, going through the
|
|
170
|
+
// native registry is both correct and Metro-safe. `requireOptionalNativeModule`
|
|
171
|
+
// returns `null` (never throws) when the module is not autolinked (web, or apps
|
|
172
|
+
// that don't ship it), so `@oxy.so/core`'s `KeyManager` cleanly falls back to its
|
|
173
|
+
// package-private store.
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
let sharedIdentityBridgePromise = null;
|
|
176
|
+
export function loadSharedIdentityBridge() {
|
|
177
|
+
if (!sharedIdentityBridgePromise) {
|
|
178
|
+
sharedIdentityBridgePromise = Promise.resolve().then(() => {
|
|
179
|
+
const native = requireOptionalNativeModule('OxyIdentity');
|
|
180
|
+
if (native &&
|
|
181
|
+
typeof native.getShared === 'function' &&
|
|
182
|
+
typeof native.putShared === 'function' &&
|
|
183
|
+
typeof native.hasShared === 'function' &&
|
|
184
|
+
typeof native.clearShared === 'function') {
|
|
185
|
+
return {
|
|
186
|
+
getShared: native.getShared.bind(native),
|
|
187
|
+
putShared: native.putShared.bind(native),
|
|
188
|
+
hasShared: native.hasShared.bind(native),
|
|
189
|
+
clearShared: native.clearShared.bind(native),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return sharedIdentityBridgePromise;
|
|
196
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural interfaces for Expo platform modules.
|
|
3
|
+
*
|
|
4
|
+
* These replace `typeof import('expo-crypto')` and
|
|
5
|
+
* `typeof import('expo-secure-store')` in the built declaration files of
|
|
6
|
+
* `@oxy.so/protocol` and `@oxy.so/core`.
|
|
7
|
+
*
|
|
8
|
+
* ## Why structural interfaces instead of `typeof import('expo-*')`?
|
|
9
|
+
*
|
|
10
|
+
* Under NodeNext module resolution (used by `@oxy.so/api` and `@oxy.so/node`),
|
|
11
|
+
* `expo-crypto` ships with `"exports": {}` (empty exports map). TypeScript
|
|
12
|
+
* traverses into the package anyway via the `types` field, which transitively
|
|
13
|
+
* loads `expo-modules-core`. That pollution makes `setInterval`/`setTimeout`
|
|
14
|
+
* resolve to DOM's `number` return type rather than Node's `NodeJS.Timeout`,
|
|
15
|
+
* producing ~10 spurious `TS2322` / `TS2339` errors in every consumer that
|
|
16
|
+
* uses Node timer APIs — none of which reference protocol types at all.
|
|
17
|
+
*
|
|
18
|
+
* Structural interfaces break the transitive expo-modules-core dependency
|
|
19
|
+
* entirely: consumers that don't have Expo installed see clean types, and
|
|
20
|
+
* the actual RN runtime (which DOES have Expo installed) still works because
|
|
21
|
+
* the real modules satisfy these interfaces structurally.
|
|
22
|
+
*/
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Detection — runtime predicates.
|
|
3
|
+
*
|
|
4
|
+
* Detects the host runtime WITHOUT importing from 'react-native', so the
|
|
5
|
+
* protocol's crypto modules can be used in web / Node.js / React Native
|
|
6
|
+
* environments without bundlers failing on react-native imports.
|
|
7
|
+
*
|
|
8
|
+
* Only the two predicates the protocol's platform-crypto loaders need live
|
|
9
|
+
* here. Richer platform detection (`getPlatformOS`, `isWeb`, `isNative`, …)
|
|
10
|
+
* is an SDK concern and stays in `@oxy.so/core`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Check if running in React Native.
|
|
14
|
+
*
|
|
15
|
+
* Selects the React Native crypto variant (`expo-crypto` /
|
|
16
|
+
* `expo-secure-store` / async-storage) over the Node/web variant.
|
|
17
|
+
*/
|
|
18
|
+
export function isReactNative() {
|
|
19
|
+
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Check if running in Node.js.
|
|
23
|
+
*
|
|
24
|
+
* Gates use of Node's built-in `crypto` (the synchronous SHA-256 path and
|
|
25
|
+
* `randomBytes`) and the `await import('node:crypto')` loader.
|
|
26
|
+
*/
|
|
27
|
+
export function isNodeJS() {
|
|
28
|
+
return typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* secp256k1 primitives shared by every Oxy runtime.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the curve binding and the wire formats used across Oxy:
|
|
5
|
+
* 32-byte private keys, compressed or uncompressed SEC1 public keys,
|
|
6
|
+
* RFC 6979 deterministic ECDSA signatures encoded as DER, and the 32-byte
|
|
7
|
+
* ECDH x-coordinate. Callers never receive a library-specific key object.
|
|
8
|
+
*/
|
|
9
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
10
|
+
const HEX = /^[0-9a-fA-F]+$/;
|
|
11
|
+
const COMPRESSED_PUBLIC_KEY = /^(02|03)[0-9a-fA-F]{64}$/;
|
|
12
|
+
const UNCOMPRESSED_PUBLIC_KEY = /^04[0-9a-fA-F]{128}$/;
|
|
13
|
+
function bytesToHex(bytes) {
|
|
14
|
+
let result = "";
|
|
15
|
+
for (const byte of bytes) {
|
|
16
|
+
result += byte.toString(16).padStart(2, "0");
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function hexToBytes(value, label) {
|
|
21
|
+
if (value.length === 0 || value.length % 2 !== 0 || !HEX.test(value)) {
|
|
22
|
+
throw new Error(`${label} must be an even-length hexadecimal string`);
|
|
23
|
+
}
|
|
24
|
+
const bytes = new Uint8Array(value.length / 2);
|
|
25
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
26
|
+
bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
|
|
27
|
+
}
|
|
28
|
+
return bytes;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Normalize a legacy short/cased scalar to canonical 32-byte lowercase hex.
|
|
32
|
+
* The scalar must be within the secp256k1 order; zero and overflow are rejected.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeSecp256k1PrivateKey(privateKeyHex) {
|
|
35
|
+
if (typeof privateKeyHex !== "string" ||
|
|
36
|
+
privateKeyHex.length === 0 ||
|
|
37
|
+
privateKeyHex.length > 64 ||
|
|
38
|
+
!HEX.test(privateKeyHex)) {
|
|
39
|
+
throw new Error("secp256k1 private key must contain 1 to 64 hexadecimal characters");
|
|
40
|
+
}
|
|
41
|
+
const normalized = privateKeyHex.toLowerCase().padStart(64, "0");
|
|
42
|
+
const privateKey = hexToBytes(normalized, "secp256k1 private key");
|
|
43
|
+
if (!secp256k1.utils.isValidSecretKey(privateKey)) {
|
|
44
|
+
throw new Error("secp256k1 private key is outside the valid scalar range");
|
|
45
|
+
}
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
/** True when a value is a valid secp256k1 scalar (legacy short hex accepted). */
|
|
49
|
+
export function isValidSecp256k1PrivateKey(privateKeyHex) {
|
|
50
|
+
try {
|
|
51
|
+
normalizeSecp256k1PrivateKey(privateKeyHex);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function parsePrivateKey(privateKeyHex) {
|
|
59
|
+
return hexToBytes(normalizeSecp256k1PrivateKey(privateKeyHex), "secp256k1 private key");
|
|
60
|
+
}
|
|
61
|
+
function parsePublicKey(publicKeyHex) {
|
|
62
|
+
if (typeof publicKeyHex !== "string" ||
|
|
63
|
+
(!COMPRESSED_PUBLIC_KEY.test(publicKeyHex) &&
|
|
64
|
+
!UNCOMPRESSED_PUBLIC_KEY.test(publicKeyHex))) {
|
|
65
|
+
throw new Error("secp256k1 public key must be a compressed or uncompressed SEC1 hex key");
|
|
66
|
+
}
|
|
67
|
+
const publicKey = hexToBytes(publicKeyHex, "secp256k1 public key");
|
|
68
|
+
// Parsing validates the SEC1 prefix, coordinate range, and curve equation.
|
|
69
|
+
secp256k1.Point.fromBytes(publicKey);
|
|
70
|
+
return publicKey;
|
|
71
|
+
}
|
|
72
|
+
function parseDigest(digestHex) {
|
|
73
|
+
if (typeof digestHex !== "string" || digestHex.length !== 64) {
|
|
74
|
+
throw new Error("secp256k1 digest must be exactly 32 bytes of hexadecimal data");
|
|
75
|
+
}
|
|
76
|
+
return hexToBytes(digestHex, "secp256k1 digest");
|
|
77
|
+
}
|
|
78
|
+
/** True for a valid compressed or uncompressed SEC1 secp256k1 public key. */
|
|
79
|
+
export function isValidSecp256k1PublicKey(publicKeyHex) {
|
|
80
|
+
try {
|
|
81
|
+
parsePublicKey(publicKeyHex);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Generate a canonical private key and its uncompressed public key. */
|
|
89
|
+
export function generateSecp256k1KeyPair() {
|
|
90
|
+
const privateKey = secp256k1.utils.randomSecretKey();
|
|
91
|
+
return {
|
|
92
|
+
privateKey: bytesToHex(privateKey),
|
|
93
|
+
publicKey: bytesToHex(secp256k1.getPublicKey(privateKey, false)),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Derive a compressed or uncompressed SEC1 public key from a private scalar. */
|
|
97
|
+
export function deriveSecp256k1PublicKey(privateKeyHex, compressed = false) {
|
|
98
|
+
return bytesToHex(secp256k1.getPublicKey(parsePrivateKey(privateKeyHex), compressed));
|
|
99
|
+
}
|
|
100
|
+
/** Parse and re-encode a public key in canonical lowercase SEC1 form. */
|
|
101
|
+
export function normalizeSecp256k1PublicKey(publicKeyHex, compressed = false) {
|
|
102
|
+
const point = secp256k1.Point.fromBytes(parsePublicKey(publicKeyHex));
|
|
103
|
+
return point.toHex(compressed);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Sign a 32-byte digest with deterministic RFC 6979 ECDSA and return DER hex.
|
|
107
|
+
*
|
|
108
|
+
* `lowS` defaults to false because historical Oxy signatures used elliptic's
|
|
109
|
+
* default and therefore may occupy either half of the curve order. Verification
|
|
110
|
+
* accepts both forms; callers that require low-S normalization opt in explicitly.
|
|
111
|
+
*/
|
|
112
|
+
export function signSecp256k1Digest(privateKeyHex, digestHex, options = {}) {
|
|
113
|
+
const signature = secp256k1.sign(parseDigest(digestHex), parsePrivateKey(privateKeyHex), {
|
|
114
|
+
lowS: options.lowS ?? false,
|
|
115
|
+
});
|
|
116
|
+
return signature.toHex("der");
|
|
117
|
+
}
|
|
118
|
+
/** Verify a DER-encoded ECDSA signature, accepting historical high-S signatures. */
|
|
119
|
+
export function verifySecp256k1Digest(publicKeyHex, digestHex, signatureDerHex) {
|
|
120
|
+
const publicKey = parsePublicKey(publicKeyHex);
|
|
121
|
+
const digest = parseDigest(digestHex);
|
|
122
|
+
const signature = hexToBytes(signatureDerHex, "secp256k1 DER signature");
|
|
123
|
+
// Parse once up front so malformed or non-DER input is rejected explicitly.
|
|
124
|
+
secp256k1.Signature.fromBytes(signature, "der");
|
|
125
|
+
return secp256k1.verify(signature, digest, publicKey, {
|
|
126
|
+
format: "der",
|
|
127
|
+
lowS: false,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** Derive the fixed-width 32-byte ECDH x-coordinate shared secret. */
|
|
131
|
+
export function deriveSecp256k1SharedSecret(privateKeyHex, publicKeyHex) {
|
|
132
|
+
const encodedPoint = secp256k1.getSharedSecret(parsePrivateKey(privateKeyHex), parsePublicKey(publicKeyHex), true);
|
|
133
|
+
if (encodedPoint.length !== 33) {
|
|
134
|
+
throw new Error("secp256k1 ECDH returned an unexpected point encoding");
|
|
135
|
+
}
|
|
136
|
+
return encodedPoint.slice(1);
|
|
137
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transparency checkpoint — the signed, hash-linked commitment to a tree root.
|
|
3
|
+
*
|
|
4
|
+
* A checkpoint is what the operator PUBLISHES: "at `periodEnd` I committed to
|
|
5
|
+
* `root` over `treeSize` subjects, and the previous checkpoint hashed to
|
|
6
|
+
* `prevCheckpointHash`". The `prevCheckpointHash` link makes the checkpoint
|
|
7
|
+
* sequence itself append-only: once any checkpoint is anchored publicly, none of
|
|
8
|
+
* its ancestors can be rewritten without breaking the chain of hashes.
|
|
9
|
+
*
|
|
10
|
+
* ## Why the signature covers ONLY the five fields
|
|
11
|
+
*
|
|
12
|
+
* The signing input is derived from exactly `{index, periodEnd, treeSize, root,
|
|
13
|
+
* prevCheckpointHash}` — never from the surrounding storage document, and never
|
|
14
|
+
* from other signatures. That is what makes a checkpoint CO-SIGNABLE: the
|
|
15
|
+
* operator and any number of independent witnesses (e.g. user-run
|
|
16
|
+
* `@oxy.so/node` deployments) each sign the identical bytes with their own key,
|
|
17
|
+
* with zero coordination and in any order. Two conflicting roots for one
|
|
18
|
+
* `index`, each carrying valid signatures, is then transferable proof of
|
|
19
|
+
* equivocation that needs no cooperation from the operator to demonstrate.
|
|
20
|
+
*
|
|
21
|
+
* Extra fields on the object passed in are ignored by design, so handing this a
|
|
22
|
+
* database document cannot change what was signed.
|
|
23
|
+
*/
|
|
24
|
+
import { canonicalize } from '../envelope/canonicalJson.js';
|
|
25
|
+
import { sha256 } from '../envelope/recordId.js';
|
|
26
|
+
import { signMessage, verifySignature } from '../envelope/sign.js';
|
|
27
|
+
import { deriveSecp256k1PublicKey } from '../secp256k1.js';
|
|
28
|
+
/** Domain prefix for checkpoint signing bytes. */
|
|
29
|
+
const CHECKPOINT_PREFIX = 'oxy.transparency.checkpoint.v1:';
|
|
30
|
+
/** The one signature algorithm the protocol emits. */
|
|
31
|
+
const ALG = 'ES256K-DER-SHA256';
|
|
32
|
+
/**
|
|
33
|
+
* The exact bytes every co-signer signs: the canonical JSON of the five signed
|
|
34
|
+
* fields under the checkpoint domain prefix. Key order in the input object is
|
|
35
|
+
* irrelevant; extra properties are dropped.
|
|
36
|
+
*/
|
|
37
|
+
export function checkpointSigningInput(fields) {
|
|
38
|
+
return `${CHECKPOINT_PREFIX}${canonicalize({
|
|
39
|
+
index: fields.index,
|
|
40
|
+
periodEnd: fields.periodEnd,
|
|
41
|
+
treeSize: fields.treeSize,
|
|
42
|
+
root: fields.root,
|
|
43
|
+
prevCheckpointHash: fields.prevCheckpointHash,
|
|
44
|
+
})}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The content address of a checkpoint — what the NEXT checkpoint's
|
|
48
|
+
* `prevCheckpointHash` references.
|
|
49
|
+
*/
|
|
50
|
+
export async function checkpointHash(fields) {
|
|
51
|
+
return sha256(checkpointSigningInput(fields));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Sign a checkpoint with an explicit private key. Used by the operator and,
|
|
55
|
+
* identically, by every witness that co-signs the same checkpoint.
|
|
56
|
+
*/
|
|
57
|
+
export async function signCheckpoint(fields, privateKeyHex) {
|
|
58
|
+
return {
|
|
59
|
+
publicKey: deriveSecp256k1PublicKey(privateKeyHex),
|
|
60
|
+
alg: ALG,
|
|
61
|
+
signature: await signMessage(checkpointSigningInput(fields), privateKeyHex),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Verify one signature over a checkpoint's signed fields.
|
|
66
|
+
*
|
|
67
|
+
* Confirms the signature matches the embedded `publicKey` for exactly these
|
|
68
|
+
* fields. It does NOT establish that the key belongs to a trusted operator or an
|
|
69
|
+
* accepted witness — that policy lives with the verifier's key list.
|
|
70
|
+
*/
|
|
71
|
+
export async function verifyCheckpointSignature(fields, signature) {
|
|
72
|
+
return verifySignature(checkpointSigningInput(fields), signature.signature, signature.publicKey);
|
|
73
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transparency Merkle tree — leaf hashing, root computation, and inclusion
|
|
3
|
+
* proofs over a snapshot of chain heads.
|
|
4
|
+
*
|
|
5
|
+
* ## What this is for
|
|
6
|
+
*
|
|
7
|
+
* A per-subject hash chain proves nobody edited a record. It does NOT prove the
|
|
8
|
+
* SERVER didn't serve two different histories to two parties (equivocation), or
|
|
9
|
+
* quietly drop a record. That gap is closed by committing to every subject's
|
|
10
|
+
* chain head at once, publishing the commitment, and letting anyone verify that
|
|
11
|
+
* their own head is inside it — which is exactly what this tree does. The
|
|
12
|
+
* commitment (the root) is what gets signed into a checkpoint (`./checkpoint`)
|
|
13
|
+
* and anchored on a public chain.
|
|
14
|
+
*
|
|
15
|
+
* ## Shape
|
|
16
|
+
*
|
|
17
|
+
* The tree follows RFC 6962 (Certificate Transparency) so the algorithms are
|
|
18
|
+
* standard and independently reimplementable: leaves and interior nodes are
|
|
19
|
+
* hashed under DISTINCT domain prefixes (a leaf hash can never be reinterpreted
|
|
20
|
+
* as an interior node), an odd level splits so the LEFT subtree is the largest
|
|
21
|
+
* power of two below the size, and a single-leaf tree's root is the leaf itself.
|
|
22
|
+
*
|
|
23
|
+
* A built tree keeps every LEVEL, not just its leaves, because that is what
|
|
24
|
+
* makes serving proofs cheap: {@link inclusionProof} reads siblings straight out
|
|
25
|
+
* of the levels instead of re-hashing subtrees, so one build amortizes over all
|
|
26
|
+
* the proofs cut from it (a checkpoint is built once and proved thousands of
|
|
27
|
+
* times, once per subject that audits it).
|
|
28
|
+
*
|
|
29
|
+
* Crucially, {@link verifyInclusionProof} needs only the verifier's OWN leaf,
|
|
30
|
+
* its index, the tree size, the audit path, and the signed root — never the
|
|
31
|
+
* other leaves. So a device or node can audit its own history against a
|
|
32
|
+
* published checkpoint without downloading anyone else's data.
|
|
33
|
+
*
|
|
34
|
+
* The leaf order is fixed by {@link buildTransparencyTreeFromHeads}: ascending
|
|
35
|
+
* by `subjectDid` in UTF-16 code-unit order. Order is part of the commitment, so
|
|
36
|
+
* every implementation MUST sort identically or the roots diverge.
|
|
37
|
+
*/
|
|
38
|
+
import { sha256 } from '../envelope/recordId.js';
|
|
39
|
+
import { canonicalize } from '../envelope/canonicalJson.js';
|
|
40
|
+
/** Domain prefix for a leaf hash. Distinct from {@link NODE_PREFIX}. */
|
|
41
|
+
const LEAF_PREFIX = 'oxy.transparency.leaf.v1:';
|
|
42
|
+
/** Domain prefix for an interior node hash. Distinct from {@link LEAF_PREFIX}. */
|
|
43
|
+
const NODE_PREFIX = 'oxy.transparency.node.v1:';
|
|
44
|
+
/** The pre-image of {@link EMPTY_TRANSPARENCY_ROOT}. */
|
|
45
|
+
const EMPTY_PREIMAGE = 'oxy.transparency.empty.v1';
|
|
46
|
+
/**
|
|
47
|
+
* The root of a tree with zero leaves: `sha256("oxy.transparency.empty.v1")`.
|
|
48
|
+
*
|
|
49
|
+
* Hard-coded because hashing is async and this is needed as a value; a Jest
|
|
50
|
+
* regression test pins it against the live hash of {@link EMPTY_PREIMAGE}.
|
|
51
|
+
*/
|
|
52
|
+
export const EMPTY_TRANSPARENCY_ROOT = '315338df4bc34de7d057b583a082268016888323c79c0376586406a64f441b1e';
|
|
53
|
+
/**
|
|
54
|
+
* Hash one subject's head into a leaf.
|
|
55
|
+
*
|
|
56
|
+
* The pre-image is the canonical JSON of the three committed fields under the
|
|
57
|
+
* leaf domain prefix — so a DID containing a delimiter-like character cannot
|
|
58
|
+
* forge another subject's leaf (JSON escaping makes the encoding unambiguous),
|
|
59
|
+
* and a leaf can never collide with an interior node.
|
|
60
|
+
*/
|
|
61
|
+
export async function transparencyLeafHash(entry) {
|
|
62
|
+
const preimage = canonicalize({
|
|
63
|
+
subjectDid: entry.subjectDid,
|
|
64
|
+
seq: entry.seq,
|
|
65
|
+
headRecordId: entry.headRecordId,
|
|
66
|
+
});
|
|
67
|
+
return sha256(`${LEAF_PREFIX}${preimage}`);
|
|
68
|
+
}
|
|
69
|
+
/** Hash an interior node over its ordered children. */
|
|
70
|
+
async function nodeHash(left, right) {
|
|
71
|
+
return sha256(`${NODE_PREFIX}${left}${right}`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build a tree over already-hashed leaves, in the given order.
|
|
75
|
+
*
|
|
76
|
+
* Levels are built bottom-up, each one pairing the level below and carrying a
|
|
77
|
+
* trailing odd node up unchanged — which yields exactly RFC 6962's tree, whose
|
|
78
|
+
* recursive definition splits at the largest power of two below the size.
|
|
79
|
+
* `transparency.test.ts` pins that equivalence against a direct transcription of
|
|
80
|
+
* the RFC's MTH for every size up to 40, so the shape can never silently drift.
|
|
81
|
+
*
|
|
82
|
+
* The order IS part of the commitment — prefer
|
|
83
|
+
* {@link buildTransparencyTreeFromHeads}, which owns the canonical ordering.
|
|
84
|
+
*/
|
|
85
|
+
export async function buildTransparencyTree(leaves) {
|
|
86
|
+
const levels = [[...leaves]];
|
|
87
|
+
let current = levels[0];
|
|
88
|
+
while (current.length > 1) {
|
|
89
|
+
const pairCount = Math.floor(current.length / 2);
|
|
90
|
+
const parents = await Promise.all(Array.from({ length: pairCount }, (_unused, pair) => nodeHash(current[pair * 2], current[pair * 2 + 1])));
|
|
91
|
+
if (current.length % 2 === 1) {
|
|
92
|
+
parents.push(current[current.length - 1]);
|
|
93
|
+
}
|
|
94
|
+
levels.push(parents);
|
|
95
|
+
current = parents;
|
|
96
|
+
}
|
|
97
|
+
const top = levels[levels.length - 1];
|
|
98
|
+
return {
|
|
99
|
+
root: top.length === 1 ? top[0] : EMPTY_TRANSPARENCY_ROOT,
|
|
100
|
+
treeSize: leaves.length,
|
|
101
|
+
levels,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build the canonical tree for a snapshot of chain heads.
|
|
106
|
+
*
|
|
107
|
+
* Sorts ascending by `subjectDid` in UTF-16 code-unit order — NEVER
|
|
108
|
+
* `localeCompare`, whose order is locale-dependent and would make two
|
|
109
|
+
* verifiers compute different roots from identical data.
|
|
110
|
+
*
|
|
111
|
+
* Throws on a duplicate `subjectDid`: a snapshot must commit to exactly one head
|
|
112
|
+
* per subject, and silently keeping one of two would hide the other's history.
|
|
113
|
+
*/
|
|
114
|
+
export async function buildTransparencyTreeFromHeads(entries) {
|
|
115
|
+
const ordered = [...entries].sort((a, b) => a.subjectDid < b.subjectDid ? -1 : a.subjectDid > b.subjectDid ? 1 : 0);
|
|
116
|
+
const indexBySubject = {};
|
|
117
|
+
ordered.forEach((entry, index) => {
|
|
118
|
+
if (indexBySubject[entry.subjectDid] !== undefined) {
|
|
119
|
+
throw new Error(`Duplicate subject in transparency snapshot: ${entry.subjectDid}`);
|
|
120
|
+
}
|
|
121
|
+
indexBySubject[entry.subjectDid] = index;
|
|
122
|
+
});
|
|
123
|
+
const leaves = await Promise.all(ordered.map(transparencyLeafHash));
|
|
124
|
+
return { ...(await buildTransparencyTree(leaves)), indexBySubject };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The audit path proving `index` is committed in `tree` (RFC 6962 PATH): each
|
|
128
|
+
* step is the sibling subtree hash, leaf-adjacent first.
|
|
129
|
+
*
|
|
130
|
+
* Pure index arithmetic over the tree's levels — no hashing, so cutting a proof
|
|
131
|
+
* costs O(log n) array reads however large the checkpoint is.
|
|
132
|
+
*/
|
|
133
|
+
export function inclusionProof(tree, index) {
|
|
134
|
+
if (!Number.isInteger(index) || index < 0 || index >= tree.treeSize) {
|
|
135
|
+
throw new Error(`Leaf index ${index} is outside a tree of ${tree.treeSize} leaves`);
|
|
136
|
+
}
|
|
137
|
+
const proof = [];
|
|
138
|
+
let position = index;
|
|
139
|
+
for (let level = 0; level < tree.levels.length - 1; level += 1) {
|
|
140
|
+
const nodes = tree.levels[level];
|
|
141
|
+
const sibling = position % 2 === 0 ? position + 1 : position - 1;
|
|
142
|
+
// A trailing odd node is carried up unpaired, so it contributes no step.
|
|
143
|
+
if (sibling < nodes.length) {
|
|
144
|
+
proof.push(nodes[sibling]);
|
|
145
|
+
}
|
|
146
|
+
position = Math.floor(position / 2);
|
|
147
|
+
}
|
|
148
|
+
return proof;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Verify an audit path against a committed root (RFC 6962 §2.1.1).
|
|
152
|
+
*
|
|
153
|
+
* Recomputes the root from the leaf upward using only the path, so the verifier
|
|
154
|
+
* never needs another subject's data. Returns `false` for every failure mode —
|
|
155
|
+
* tampered leaf, replayed index, truncated path, foreign root — rather than
|
|
156
|
+
* throwing, so callers treat auditing as a boolean.
|
|
157
|
+
*/
|
|
158
|
+
export async function verifyInclusionProof(check) {
|
|
159
|
+
const { leaf, index, treeSize, proof, root } = check;
|
|
160
|
+
if (!Number.isInteger(index) || !Number.isInteger(treeSize)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (index < 0 || treeSize <= 0 || index >= treeSize) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
let fn = index;
|
|
167
|
+
let sn = treeSize - 1;
|
|
168
|
+
let computed = leaf;
|
|
169
|
+
for (const sibling of proof) {
|
|
170
|
+
if (sn === 0) {
|
|
171
|
+
// The path claims more levels than the tree has.
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (fn % 2 === 1 || fn === sn) {
|
|
175
|
+
computed = await nodeHash(sibling, computed);
|
|
176
|
+
while (fn % 2 === 0 && fn !== 0) {
|
|
177
|
+
fn = Math.floor(fn / 2);
|
|
178
|
+
sn = Math.floor(sn / 2);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
computed = await nodeHash(computed, sibling);
|
|
183
|
+
}
|
|
184
|
+
fn = Math.floor(fn / 2);
|
|
185
|
+
sn = Math.floor(sn / 2);
|
|
186
|
+
}
|
|
187
|
+
// `sn > 0` means the path was truncated before reaching the root.
|
|
188
|
+
return sn === 0 && computed === root;
|
|
189
|
+
}
|