@oxy.so/protocol 1.2.2 → 1.3.1
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/dist/cjs/chain/engine.js +1 -1
- package/dist/cjs/chain/recordStore.js +3 -3
- package/dist/cjs/chain/verify.js +8 -4
- package/dist/cjs/envelope/signingInput.js +12 -1
- package/dist/cjs/identity/resolver.js +2 -1
- package/dist/cjs/node/didWebResolver.js +1 -3
- package/dist/cjs/node/nodeApp.js +9 -3
- package/dist/cjs/node/nodeClient.js +7 -2
- package/dist/cjs/platform/platform.js +1 -1
- package/dist/cjs/secp256k1.js +25 -26
- package/dist/cjs/sign.js +17 -0
- package/dist/esm/chain/engine.js +1 -1
- package/dist/esm/chain/recordStore.js +3 -3
- package/dist/esm/chain/verify.js +8 -4
- package/dist/esm/envelope/sign.js +1 -1
- package/dist/esm/envelope/signingInput.js +12 -1
- package/dist/esm/identity/resolver.js +2 -1
- package/dist/esm/node/didWebResolver.js +1 -3
- package/dist/esm/node/nodeApp.js +9 -3
- package/dist/esm/node/nodeClient.js +8 -3
- package/dist/esm/platform/platform.js +1 -1
- package/dist/esm/secp256k1.js +26 -27
- package/dist/esm/sign.js +10 -0
- package/dist/types/chain/engine.d.ts +1 -1
- package/dist/types/chain/recordStore.d.ts +3 -3
- package/dist/types/chain/verify.d.ts +3 -2
- package/dist/types/sign.d.ts +10 -0
- package/package.json +21 -22
- package/src/__tests__/canonicalJson.test.ts +2 -6
- package/src/__tests__/chain.test.ts +42 -11
- package/src/__tests__/commonsIdentityBridge.test.ts +7 -2
- package/src/__tests__/didWebResolver.test.ts +36 -7
- package/src/__tests__/nodeApp.test.ts +76 -21
- package/src/__tests__/nodeClient.test.ts +30 -7
- package/src/__tests__/nodeHarness.ts +7 -3
- package/src/__tests__/optionalNativePeers.test.ts +8 -6
- package/src/__tests__/rateLimit.test.ts +2 -1
- package/src/__tests__/registrationPow.test.ts +35 -37
- package/src/__tests__/secp256k1.test.ts +41 -70
- package/src/__tests__/transparency.test.ts +4 -1
- package/src/auth/registrationPow.ts +5 -1
- package/src/chain/engine.ts +1 -1
- package/src/chain/recordStore.ts +3 -3
- package/src/chain/verify.ts +11 -4
- package/src/envelope/recordId.ts +1 -4
- package/src/envelope/sign.ts +6 -6
- package/src/envelope/signingInput.ts +12 -1
- package/src/identity/resolver.ts +9 -2
- package/src/node/didWebResolver.ts +2 -7
- package/src/node/nodeApp.ts +85 -67
- package/src/node/nodeClient.ts +35 -16
- package/src/platform/crypto.native.ts +9 -2
- package/src/platform/platform.ts +3 -1
- package/src/secp256k1.ts +33 -58
- package/src/sign.ts +16 -0
- package/dist/cjs/.tsbuildinfo +0 -1
- package/dist/esm/.tsbuildinfo +0 -1
- package/dist/types/.tsbuildinfo +0 -1
|
@@ -91,7 +91,11 @@ describe('createNodeApp', () => {
|
|
|
91
91
|
const empty = await request(app).get('/oxy/head');
|
|
92
92
|
expect(empty.body).toEqual({ seq: null, headRecordId: null, recordCount: 0 });
|
|
93
93
|
|
|
94
|
-
const envelope = await buildSignedEnvelope({
|
|
94
|
+
const envelope = await buildSignedEnvelope({
|
|
95
|
+
privateKey: owner.privateKey,
|
|
96
|
+
seq: 0,
|
|
97
|
+
prev: null,
|
|
98
|
+
});
|
|
95
99
|
const recordId = await computeRecordId(envelope);
|
|
96
100
|
const write = await request(app).post('/records').send(envelope);
|
|
97
101
|
expect(write.status).toBe(201);
|
|
@@ -107,7 +111,12 @@ describe('createNodeApp', () => {
|
|
|
107
111
|
const genesisId = await computeRecordId(genesis);
|
|
108
112
|
await request(app).post('/records').send(genesis).expect(201);
|
|
109
113
|
|
|
110
|
-
const second = await buildSignedEnvelope({
|
|
114
|
+
const second = await buildSignedEnvelope({
|
|
115
|
+
privateKey: owner.privateKey,
|
|
116
|
+
seq: 1,
|
|
117
|
+
prev: genesisId,
|
|
118
|
+
record: { step: 2 },
|
|
119
|
+
});
|
|
111
120
|
const secondId = await computeRecordId(second);
|
|
112
121
|
const res = await request(app).post('/records').send(second);
|
|
113
122
|
expect(res.status).toBe(201);
|
|
@@ -117,7 +126,11 @@ describe('createNodeApp', () => {
|
|
|
117
126
|
it('POST /records rejects a record signed by a NON-owner key (403 not_owner)', async () => {
|
|
118
127
|
const { app } = buildApp(owner);
|
|
119
128
|
const attacker = generateKeyPair();
|
|
120
|
-
const envelope = await buildSignedEnvelope({
|
|
129
|
+
const envelope = await buildSignedEnvelope({
|
|
130
|
+
privateKey: attacker.privateKey,
|
|
131
|
+
seq: 0,
|
|
132
|
+
prev: null,
|
|
133
|
+
});
|
|
121
134
|
const res = await request(app).post('/records').send(envelope);
|
|
122
135
|
expect(res.status).toBe(403);
|
|
123
136
|
expect(res.body).toEqual({ error: 'not_owner' });
|
|
@@ -132,7 +145,11 @@ describe('createNodeApp', () => {
|
|
|
132
145
|
|
|
133
146
|
it('POST /records rejects a chain gap (422 chain_gap)', async () => {
|
|
134
147
|
const { app } = buildApp(owner);
|
|
135
|
-
const envelope = await buildSignedEnvelope({
|
|
148
|
+
const envelope = await buildSignedEnvelope({
|
|
149
|
+
privateKey: owner.privateKey,
|
|
150
|
+
seq: 3,
|
|
151
|
+
prev: null,
|
|
152
|
+
});
|
|
136
153
|
const res = await request(app).post('/records').send(envelope);
|
|
137
154
|
expect(res.status).toBe(422);
|
|
138
155
|
expect(res.body).toEqual({ error: 'chain_gap' });
|
|
@@ -162,8 +179,14 @@ describe('createNodeApp', () => {
|
|
|
162
179
|
it('POST /sync/push verifies + appends a batch in order', async () => {
|
|
163
180
|
const { app } = buildApp(owner);
|
|
164
181
|
const genesis = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 0, prev: null });
|
|
165
|
-
const second = await buildSignedEnvelope({
|
|
166
|
-
|
|
182
|
+
const second = await buildSignedEnvelope({
|
|
183
|
+
privateKey: owner.privateKey,
|
|
184
|
+
seq: 1,
|
|
185
|
+
prev: await computeRecordId(genesis),
|
|
186
|
+
});
|
|
187
|
+
const res = await request(app)
|
|
188
|
+
.post('/sync/push')
|
|
189
|
+
.send({ records: [genesis, second] });
|
|
167
190
|
expect(res.status).toBe(200);
|
|
168
191
|
expect(res.body.accepted).toBe(2);
|
|
169
192
|
expect(res.body.results.every((r: { ok: boolean }) => r.ok)).toBe(true);
|
|
@@ -174,7 +197,12 @@ describe('createNodeApp', () => {
|
|
|
174
197
|
let prev: string | null = null;
|
|
175
198
|
const ids: string[] = [];
|
|
176
199
|
for (let seq = 0; seq < 4; seq += 1) {
|
|
177
|
-
const envelope = await buildSignedEnvelope({
|
|
200
|
+
const envelope = await buildSignedEnvelope({
|
|
201
|
+
privateKey: owner.privateKey,
|
|
202
|
+
seq,
|
|
203
|
+
prev,
|
|
204
|
+
record: { seq },
|
|
205
|
+
});
|
|
178
206
|
const id = await computeRecordId(envelope);
|
|
179
207
|
await request(app).post('/records').send(envelope).expect(201);
|
|
180
208
|
ids.push(id);
|
|
@@ -193,7 +221,9 @@ describe('createNodeApp', () => {
|
|
|
193
221
|
const sinceCursor = await request(app).get('/oxy/log').query({ since: ids[1] });
|
|
194
222
|
expect(sinceCursor.body.records.map((entry: { seq: number }) => entry.seq)).toEqual([2, 3]);
|
|
195
223
|
|
|
196
|
-
const unknownCursor = await request(app)
|
|
224
|
+
const unknownCursor = await request(app)
|
|
225
|
+
.get('/oxy/log')
|
|
226
|
+
.query({ since: 'f'.repeat(64) });
|
|
197
227
|
expect(unknownCursor.body.records).toEqual([]);
|
|
198
228
|
});
|
|
199
229
|
|
|
@@ -215,11 +245,14 @@ describe('createNodeApp', () => {
|
|
|
215
245
|
expect(put.status).toBe(201);
|
|
216
246
|
expect(put.body).toEqual({ hash, size: bytes.length });
|
|
217
247
|
|
|
218
|
-
const get = await request(app)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
248
|
+
const get = await request(app)
|
|
249
|
+
.get(`/blobs/${hash}`)
|
|
250
|
+
.buffer(true)
|
|
251
|
+
.parse((res, cb) => {
|
|
252
|
+
const chunks: Buffer[] = [];
|
|
253
|
+
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
254
|
+
res.on('end', () => cb(null, Buffer.concat(chunks)));
|
|
255
|
+
});
|
|
223
256
|
expect(get.status).toBe(200);
|
|
224
257
|
expect(Buffer.isBuffer(get.body) ? get.body : Buffer.from(get.body)).toEqual(bytes);
|
|
225
258
|
});
|
|
@@ -269,7 +302,9 @@ describe('createNodeApp', () => {
|
|
|
269
302
|
const auth = await signBlobPin(hash, owner);
|
|
270
303
|
|
|
271
304
|
const server = createServer(app);
|
|
272
|
-
await new Promise<void>((resolve) => {
|
|
305
|
+
await new Promise<void>((resolve) => {
|
|
306
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
307
|
+
});
|
|
273
308
|
const { port } = server.address() as AddressInfo;
|
|
274
309
|
|
|
275
310
|
const raw = await new Promise<string>((resolve, reject) => {
|
|
@@ -284,12 +319,14 @@ describe('createNodeApp', () => {
|
|
|
284
319
|
'Connection: close',
|
|
285
320
|
'',
|
|
286
321
|
'',
|
|
287
|
-
].join('\r\n')
|
|
322
|
+
].join('\r\n'),
|
|
288
323
|
);
|
|
289
324
|
});
|
|
290
325
|
let received = '';
|
|
291
326
|
socket.setEncoding('utf8');
|
|
292
|
-
socket.on('data', (chunk: string) => {
|
|
327
|
+
socket.on('data', (chunk: string) => {
|
|
328
|
+
received += chunk;
|
|
329
|
+
});
|
|
293
330
|
socket.on('error', reject);
|
|
294
331
|
socket.on('end', () => resolve(received));
|
|
295
332
|
});
|
|
@@ -360,7 +397,11 @@ describe('createNodeApp', () => {
|
|
|
360
397
|
// within the window is rejected 429 BEFORE the handler runs, regardless of
|
|
361
398
|
// each request's chain outcome.
|
|
362
399
|
const { app } = buildApp(owner, [], { windowMs: 60_000, max: 2 });
|
|
363
|
-
const envelope = await buildSignedEnvelope({
|
|
400
|
+
const envelope = await buildSignedEnvelope({
|
|
401
|
+
privateKey: owner.privateKey,
|
|
402
|
+
seq: 0,
|
|
403
|
+
prev: null,
|
|
404
|
+
});
|
|
364
405
|
|
|
365
406
|
const first = await request(app).post('/records').send(envelope);
|
|
366
407
|
expect(first.status).toBe(201);
|
|
@@ -379,7 +420,12 @@ describe('createNodeApp', () => {
|
|
|
379
420
|
let prev: string | null = null;
|
|
380
421
|
const ids: string[] = [];
|
|
381
422
|
for (let seq = 0; seq < 3; seq += 1) {
|
|
382
|
-
const envelope = await buildSignedEnvelope({
|
|
423
|
+
const envelope = await buildSignedEnvelope({
|
|
424
|
+
privateKey: owner.privateKey,
|
|
425
|
+
seq,
|
|
426
|
+
prev,
|
|
427
|
+
record: { seq },
|
|
428
|
+
});
|
|
383
429
|
const id = await computeRecordId(envelope);
|
|
384
430
|
await request(app).post('/records').send(envelope).expect(201);
|
|
385
431
|
ids.push(id);
|
|
@@ -388,7 +434,9 @@ describe('createNodeApp', () => {
|
|
|
388
434
|
|
|
389
435
|
// `?since[]=<id1>&since[]=<id2>` arrives as a string[] — the handler must
|
|
390
436
|
// take the FIRST value (a tampered array can never cause type confusion).
|
|
391
|
-
const res = await request(app)
|
|
437
|
+
const res = await request(app)
|
|
438
|
+
.get('/oxy/log')
|
|
439
|
+
.query({ 'since[]': [ids[0], ids[2]] });
|
|
392
440
|
expect(res.status).toBe(200);
|
|
393
441
|
// Cursor resolves off ids[0] → records strictly after seq 0 → [1, 2].
|
|
394
442
|
expect(res.body.records.map((entry: { seq: number }) => entry.seq)).toEqual([1, 2]);
|
|
@@ -398,12 +446,19 @@ describe('createNodeApp', () => {
|
|
|
398
446
|
const { app } = buildApp(owner);
|
|
399
447
|
let prev: string | null = null;
|
|
400
448
|
for (let seq = 0; seq < 4; seq += 1) {
|
|
401
|
-
const envelope = await buildSignedEnvelope({
|
|
449
|
+
const envelope = await buildSignedEnvelope({
|
|
450
|
+
privateKey: owner.privateKey,
|
|
451
|
+
seq,
|
|
452
|
+
prev,
|
|
453
|
+
record: { seq },
|
|
454
|
+
});
|
|
402
455
|
await request(app).post('/records').send(envelope).expect(201);
|
|
403
456
|
prev = await computeRecordId(envelope);
|
|
404
457
|
}
|
|
405
458
|
|
|
406
|
-
const res = await request(app)
|
|
459
|
+
const res = await request(app)
|
|
460
|
+
.get('/oxy/log')
|
|
461
|
+
.query({ 'limit[]': ['2', '99'] });
|
|
407
462
|
expect(res.status).toBe(200);
|
|
408
463
|
expect(res.body.records.map((entry: { seq: number }) => entry.seq)).toEqual([0, 1]);
|
|
409
464
|
});
|
|
@@ -71,7 +71,9 @@ describe('trimTrailingSlashes', () => {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
it('preserves interior slashes and the empty string', () => {
|
|
74
|
-
expect(trimTrailingSlashes('https://node.example/oxy/log')).toBe(
|
|
74
|
+
expect(trimTrailingSlashes('https://node.example/oxy/log')).toBe(
|
|
75
|
+
'https://node.example/oxy/log',
|
|
76
|
+
);
|
|
75
77
|
expect(trimTrailingSlashes('')).toBe('');
|
|
76
78
|
expect(trimTrailingSlashes('///')).toBe('');
|
|
77
79
|
});
|
|
@@ -127,7 +129,11 @@ describe('NodeClient (end-to-end against createNodeApp)', () => {
|
|
|
127
129
|
const genesisId = await computeRecordId(genesis);
|
|
128
130
|
await client.writeRecord(genesis);
|
|
129
131
|
|
|
130
|
-
const second = await buildSignedEnvelope({
|
|
132
|
+
const second = await buildSignedEnvelope({
|
|
133
|
+
privateKey: owner.privateKey,
|
|
134
|
+
seq: 1,
|
|
135
|
+
prev: genesisId,
|
|
136
|
+
});
|
|
131
137
|
const secondId = await computeRecordId(second);
|
|
132
138
|
expect(await client.writeRecord(second)).toEqual({ recordId: secondId, seq: 1 });
|
|
133
139
|
|
|
@@ -143,7 +149,10 @@ describe('NodeClient (end-to-end against createNodeApp)', () => {
|
|
|
143
149
|
|
|
144
150
|
it('writeRecord surfaces a chain gap as a NodeClientError (422 chain_gap)', async () => {
|
|
145
151
|
const gap = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 3, prev: null });
|
|
146
|
-
await expect(client.writeRecord(gap)).rejects.toMatchObject({
|
|
152
|
+
await expect(client.writeRecord(gap)).rejects.toMatchObject({
|
|
153
|
+
status: 422,
|
|
154
|
+
reason: 'chain_gap',
|
|
155
|
+
});
|
|
147
156
|
await expect(client.writeRecord(gap)).rejects.toBeInstanceOf(NodeClientError);
|
|
148
157
|
});
|
|
149
158
|
|
|
@@ -151,14 +160,28 @@ describe('NodeClient (end-to-end against createNodeApp)', () => {
|
|
|
151
160
|
const genesis = await buildSignedEnvelope({ privateKey: owner.privateKey, seq: 0, prev: null });
|
|
152
161
|
await client.writeRecord(genesis);
|
|
153
162
|
|
|
154
|
-
const fork = await buildSignedEnvelope({
|
|
155
|
-
|
|
163
|
+
const fork = await buildSignedEnvelope({
|
|
164
|
+
privateKey: owner.privateKey,
|
|
165
|
+
seq: 1,
|
|
166
|
+
prev: 'f'.repeat(64),
|
|
167
|
+
});
|
|
168
|
+
await expect(client.writeRecord(fork)).rejects.toMatchObject({
|
|
169
|
+
status: 422,
|
|
170
|
+
reason: 'chain_fork',
|
|
171
|
+
});
|
|
156
172
|
});
|
|
157
173
|
|
|
158
174
|
it('writeRecord surfaces a non-owner write as a NodeClientError (403 not_owner)', async () => {
|
|
159
175
|
const attacker = generateKeyPair();
|
|
160
|
-
const envelope = await buildSignedEnvelope({
|
|
161
|
-
|
|
176
|
+
const envelope = await buildSignedEnvelope({
|
|
177
|
+
privateKey: attacker.privateKey,
|
|
178
|
+
seq: 0,
|
|
179
|
+
prev: null,
|
|
180
|
+
});
|
|
181
|
+
await expect(client.writeRecord(envelope)).rejects.toMatchObject({
|
|
182
|
+
status: 403,
|
|
183
|
+
reason: 'not_owner',
|
|
184
|
+
});
|
|
162
185
|
});
|
|
163
186
|
|
|
164
187
|
it('putBlob then getBlob round-trips; getBlob returns null when absent', async () => {
|
|
@@ -95,14 +95,18 @@ export function createInMemoryNodeStore(): RecordStore & BlobStore {
|
|
|
95
95
|
},
|
|
96
96
|
async resolveCursorSeq(_subject, recordId): Promise<number | null> {
|
|
97
97
|
const found = records.find((r) => r.recordId === recordId);
|
|
98
|
-
return found ? found.env.seq ?? -1 : null;
|
|
98
|
+
return found ? (found.env.seq ?? -1) : null;
|
|
99
99
|
},
|
|
100
100
|
async materializeCurrent(_subject, collection, rkey): Promise<SignedRecordEnvelope | null> {
|
|
101
|
-
const matching = records.filter(
|
|
101
|
+
const matching = records.filter(
|
|
102
|
+
(r) => r.env.collection === collection && r.env.rkey === rkey,
|
|
103
|
+
);
|
|
102
104
|
return matching.length ? matching[matching.length - 1].env : null;
|
|
103
105
|
},
|
|
104
106
|
async latestIssuedAtForKey(_subject, env): Promise<number | null> {
|
|
105
|
-
const matching = records.filter(
|
|
107
|
+
const matching = records.filter(
|
|
108
|
+
(r) => r.env.collection === env.collection && r.env.rkey === env.rkey,
|
|
109
|
+
);
|
|
106
110
|
return matching.length ? matching[matching.length - 1].env.issuedAt : null;
|
|
107
111
|
},
|
|
108
112
|
async putBlob(hash, bytes): Promise<void> {
|
|
@@ -26,9 +26,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
26
26
|
const SRC_DIR = resolve(__dirname, '..');
|
|
27
27
|
const ROOT_ENTRY = join(SRC_DIR, 'index.ts');
|
|
28
28
|
|
|
29
|
-
const packageJson = JSON.parse(
|
|
30
|
-
readFileSync(resolve(SRC_DIR, '..', 'package.json'), 'utf8'),
|
|
31
|
-
) as {
|
|
29
|
+
const packageJson = JSON.parse(readFileSync(resolve(SRC_DIR, '..', 'package.json'), 'utf8')) as {
|
|
32
30
|
peerDependenciesMeta?: Record<string, { optional?: boolean }>;
|
|
33
31
|
};
|
|
34
32
|
|
|
@@ -218,9 +216,13 @@ describe('crypto.native optional-peer degradation', () => {
|
|
|
218
216
|
});
|
|
219
217
|
jest.doMock('expo-crypto', () => fakeCrypto, { virtual: true });
|
|
220
218
|
jest.doMock('expo-secure-store', () => fakeSecureStore, { virtual: true });
|
|
221
|
-
jest.doMock(
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
jest.doMock(
|
|
220
|
+
'@react-native-async-storage/async-storage',
|
|
221
|
+
() => ({ default: fakeAsyncStorage }),
|
|
222
|
+
{
|
|
223
|
+
virtual: true,
|
|
224
|
+
},
|
|
225
|
+
);
|
|
224
226
|
|
|
225
227
|
const mod = require(MODULE_PATH) as typeof import('../platform/crypto.native');
|
|
226
228
|
|
|
@@ -209,7 +209,8 @@ describe('createRateLimiter', () => {
|
|
|
209
209
|
await jest.isolateModulesAsync(async () => {
|
|
210
210
|
const reloaded = await import('../node/rateLimit');
|
|
211
211
|
afterRestart = reloaded.clientRateLimitKey({ ip: ADDRESS } as Request);
|
|
212
|
-
stableAfterRestart =
|
|
212
|
+
stableAfterRestart =
|
|
213
|
+
reloaded.clientRateLimitKey({ ip: ADDRESS } as Request) === afterRestart;
|
|
213
214
|
});
|
|
214
215
|
|
|
215
216
|
expect(afterRestart).not.toBe(before);
|
|
@@ -3,70 +3,68 @@ import {
|
|
|
3
3
|
meetsRegistrationPowDifficulty,
|
|
4
4
|
registrationPowMessage,
|
|
5
5
|
REGISTRATION_POW_DIFFICULTY_BITS,
|
|
6
|
-
} from
|
|
6
|
+
} from '../auth/registrationPow';
|
|
7
7
|
|
|
8
|
-
describe(
|
|
9
|
-
test(
|
|
10
|
-
expect(registrationPowMessage(
|
|
11
|
-
|
|
8
|
+
describe('registrationPowMessage', () => {
|
|
9
|
+
test('binds publicKey, timestamp and nonce, distinct from the registration signature message', () => {
|
|
10
|
+
expect(registrationPowMessage('pk', 1700000000000, '42')).toBe(
|
|
11
|
+
'oxy:register-pow:pk:1700000000000:42',
|
|
12
12
|
);
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
test(
|
|
16
|
-
expect(registrationPowMessage(
|
|
17
|
-
registrationPowMessage("pk", 1, "b"),
|
|
18
|
-
);
|
|
15
|
+
test('a different nonce is a different message', () => {
|
|
16
|
+
expect(registrationPowMessage('pk', 1, 'a')).not.toBe(registrationPowMessage('pk', 1, 'b'));
|
|
19
17
|
});
|
|
20
18
|
});
|
|
21
19
|
|
|
22
|
-
describe(
|
|
20
|
+
describe('countLeadingZeroBits', () => {
|
|
23
21
|
test.each([
|
|
24
|
-
[
|
|
25
|
-
[
|
|
26
|
-
[
|
|
27
|
-
[
|
|
28
|
-
[
|
|
29
|
-
[
|
|
30
|
-
[
|
|
31
|
-
[
|
|
32
|
-
[
|
|
33
|
-
[
|
|
34
|
-
[
|
|
22
|
+
['', 0],
|
|
23
|
+
['f', 0],
|
|
24
|
+
['8000', 0],
|
|
25
|
+
['0', 4],
|
|
26
|
+
['00', 8],
|
|
27
|
+
['01', 7],
|
|
28
|
+
['0f', 4],
|
|
29
|
+
['1f', 3],
|
|
30
|
+
['2f', 2],
|
|
31
|
+
['4f', 1],
|
|
32
|
+
['0000f', 16],
|
|
35
33
|
// Case-insensitive: the digest producers this compares against are
|
|
36
34
|
// lowercase, but the counter must not silently under-count an uppercase one.
|
|
37
|
-
[
|
|
38
|
-
])(
|
|
35
|
+
['00F', 8],
|
|
36
|
+
])('counts %s as %i leading zero bits', (hex, expected) => {
|
|
39
37
|
expect(countLeadingZeroBits(hex)).toBe(expected);
|
|
40
38
|
});
|
|
41
39
|
|
|
42
|
-
test(
|
|
43
|
-
expect(countLeadingZeroBits(`00f${
|
|
40
|
+
test('stops at the first non-zero nibble rather than scanning the whole digest', () => {
|
|
41
|
+
expect(countLeadingZeroBits(`00f${'0'.repeat(60)}`)).toBe(8);
|
|
44
42
|
});
|
|
45
43
|
|
|
46
|
-
test(
|
|
47
|
-
expect(countLeadingZeroBits(
|
|
48
|
-
expect(() => countLeadingZeroBits(
|
|
44
|
+
test('a non-hex character ends the count instead of throwing', () => {
|
|
45
|
+
expect(countLeadingZeroBits('0z')).toBe(4);
|
|
46
|
+
expect(() => countLeadingZeroBits('not-hex-at-all')).not.toThrow();
|
|
49
47
|
});
|
|
50
48
|
});
|
|
51
49
|
|
|
52
|
-
describe(
|
|
53
|
-
test(
|
|
54
|
-
expect(meetsRegistrationPowDifficulty(
|
|
55
|
-
expect(meetsRegistrationPowDifficulty(
|
|
50
|
+
describe('meetsRegistrationPowDifficulty', () => {
|
|
51
|
+
test('passes a digest that clears the bound', () => {
|
|
52
|
+
expect(meetsRegistrationPowDifficulty('00f0', 8)).toBe(true);
|
|
53
|
+
expect(meetsRegistrationPowDifficulty('00f0', 9)).toBe(false);
|
|
56
54
|
});
|
|
57
55
|
|
|
58
|
-
test(
|
|
59
|
-
expect(meetsRegistrationPowDifficulty(
|
|
56
|
+
test('an exact match at the bound passes', () => {
|
|
57
|
+
expect(meetsRegistrationPowDifficulty('00f0', 8)).toBe(true);
|
|
60
58
|
});
|
|
61
59
|
|
|
62
|
-
test(
|
|
60
|
+
test('the exported difficulty constant is a defensible, positive bound', () => {
|
|
63
61
|
// Pinned as a regression check on the constant itself: a change here is a
|
|
64
62
|
// deliberate retune, not an accidental one, and shows up in review.
|
|
65
63
|
expect(REGISTRATION_POW_DIFFICULTY_BITS).toBe(16);
|
|
66
|
-
expect(meetsRegistrationPowDifficulty(
|
|
64
|
+
expect(meetsRegistrationPowDifficulty('0'.repeat(64), REGISTRATION_POW_DIFFICULTY_BITS)).toBe(
|
|
67
65
|
true,
|
|
68
66
|
);
|
|
69
|
-
expect(meetsRegistrationPowDifficulty(
|
|
67
|
+
expect(meetsRegistrationPowDifficulty('f'.repeat(64), REGISTRATION_POW_DIFFICULTY_BITS)).toBe(
|
|
70
68
|
false,
|
|
71
69
|
);
|
|
72
70
|
});
|
|
@@ -8,111 +8,82 @@ import {
|
|
|
8
8
|
normalizeSecp256k1PublicKey,
|
|
9
9
|
signSecp256k1Digest,
|
|
10
10
|
verifySecp256k1Digest,
|
|
11
|
-
} from
|
|
11
|
+
} from '../secp256k1';
|
|
12
12
|
|
|
13
|
-
const PRIVATE_KEY_ONE = `${
|
|
14
|
-
const PRIVATE_KEY_TWO = `${
|
|
15
|
-
const DIGEST_ONE = `${
|
|
13
|
+
const PRIVATE_KEY_ONE = `${'0'.repeat(63)}1`;
|
|
14
|
+
const PRIVATE_KEY_TWO = `${'0'.repeat(63)}2`;
|
|
15
|
+
const DIGEST_ONE = `${'0'.repeat(63)}1`;
|
|
16
16
|
|
|
17
17
|
const PUBLIC_KEY_ONE =
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
'0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798' +
|
|
19
|
+
'483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8';
|
|
20
20
|
const COMPRESSED_PUBLIC_KEY_ONE =
|
|
21
|
-
|
|
21
|
+
'0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798';
|
|
22
22
|
const PUBLIC_KEY_TWO =
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
'04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5' +
|
|
24
|
+
'1ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a';
|
|
25
25
|
|
|
26
26
|
// Exact DER output produced by elliptic 6.6.1 for this RFC 6979 vector.
|
|
27
27
|
const HISTORICAL_HIGH_S_SIGNATURE =
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
'304502206673ffad2147741f04772b6f921f0ba6af0c1e77fc439e65c36dedf4092e8898' +
|
|
29
|
+
'022100b3e568e9ad1f52577fedf107fda18f5ebb8e5c220badf23532bf6fbcc67fb4b8';
|
|
30
30
|
const LOW_S_SIGNATURE =
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
'304402206673ffad2147741f04772b6f921f0ba6af0c1e77fc439e65c36dedf4092e8898' +
|
|
32
|
+
'02204c1a971652e0ada880120ef8025e709fff2080c4a39aae068d12eed009b68c89';
|
|
33
33
|
|
|
34
|
-
describe(
|
|
35
|
-
test(
|
|
36
|
-
expect(normalizeSecp256k1PrivateKey(
|
|
34
|
+
describe('secp256k1 primitives', () => {
|
|
35
|
+
test('preserves the canonical private/public key formats', () => {
|
|
36
|
+
expect(normalizeSecp256k1PrivateKey('1')).toBe(PRIVATE_KEY_ONE);
|
|
37
37
|
expect(deriveSecp256k1PublicKey(PRIVATE_KEY_ONE)).toBe(PUBLIC_KEY_ONE);
|
|
38
|
-
expect(deriveSecp256k1PublicKey(PRIVATE_KEY_ONE, true)).toBe(
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
expect(normalizeSecp256k1PublicKey(COMPRESSED_PUBLIC_KEY_ONE)).toBe(
|
|
42
|
-
PUBLIC_KEY_ONE,
|
|
43
|
-
);
|
|
44
|
-
expect(normalizeSecp256k1PublicKey(PUBLIC_KEY_ONE, true)).toBe(
|
|
45
|
-
COMPRESSED_PUBLIC_KEY_ONE,
|
|
46
|
-
);
|
|
38
|
+
expect(deriveSecp256k1PublicKey(PRIVATE_KEY_ONE, true)).toBe(COMPRESSED_PUBLIC_KEY_ONE);
|
|
39
|
+
expect(normalizeSecp256k1PublicKey(COMPRESSED_PUBLIC_KEY_ONE)).toBe(PUBLIC_KEY_ONE);
|
|
40
|
+
expect(normalizeSecp256k1PublicKey(PUBLIC_KEY_ONE, true)).toBe(COMPRESSED_PUBLIC_KEY_ONE);
|
|
47
41
|
});
|
|
48
42
|
|
|
49
|
-
test(
|
|
43
|
+
test('matches the historical deterministic DER wire format', () => {
|
|
50
44
|
const signature = signSecp256k1Digest(PRIVATE_KEY_ONE, DIGEST_ONE);
|
|
51
45
|
expect(signature).toBe(HISTORICAL_HIGH_S_SIGNATURE);
|
|
52
|
-
expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
expect(
|
|
56
|
-
verifySecp256k1Digest(COMPRESSED_PUBLIC_KEY_ONE, DIGEST_ONE, signature),
|
|
57
|
-
).toBe(true);
|
|
46
|
+
expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(true);
|
|
47
|
+
expect(verifySecp256k1Digest(COMPRESSED_PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(true);
|
|
58
48
|
});
|
|
59
49
|
|
|
60
|
-
test(
|
|
50
|
+
test('supports explicit low-S signatures while accepting both historical forms', () => {
|
|
61
51
|
const signature = signSecp256k1Digest(PRIVATE_KEY_ONE, DIGEST_ONE, {
|
|
62
52
|
lowS: true,
|
|
63
53
|
});
|
|
64
54
|
expect(signature).toBe(LOW_S_SIGNATURE);
|
|
65
|
-
expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(
|
|
55
|
+
expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, signature)).toBe(true);
|
|
56
|
+
expect(verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, HISTORICAL_HIGH_S_SIGNATURE)).toBe(
|
|
66
57
|
true,
|
|
67
58
|
);
|
|
68
|
-
expect(
|
|
69
|
-
verifySecp256k1Digest(
|
|
70
|
-
PUBLIC_KEY_ONE,
|
|
71
|
-
DIGEST_ONE,
|
|
72
|
-
HISTORICAL_HIGH_S_SIGNATURE,
|
|
73
|
-
),
|
|
74
|
-
).toBe(true);
|
|
75
59
|
});
|
|
76
60
|
|
|
77
|
-
test(
|
|
78
|
-
const expected =
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
PRIVATE_KEY_ONE,
|
|
82
|
-
PUBLIC_KEY_TWO,
|
|
83
|
-
);
|
|
84
|
-
const twoToOne = deriveSecp256k1SharedSecret(
|
|
85
|
-
PRIVATE_KEY_TWO,
|
|
86
|
-
PUBLIC_KEY_ONE,
|
|
87
|
-
);
|
|
61
|
+
test('derives the fixed-width ECDH x-coordinate symmetrically', () => {
|
|
62
|
+
const expected = 'c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5';
|
|
63
|
+
const oneToTwo = deriveSecp256k1SharedSecret(PRIVATE_KEY_ONE, PUBLIC_KEY_TWO);
|
|
64
|
+
const twoToOne = deriveSecp256k1SharedSecret(PRIVATE_KEY_TWO, PUBLIC_KEY_ONE);
|
|
88
65
|
|
|
89
|
-
expect(Buffer.from(oneToTwo).toString(
|
|
66
|
+
expect(Buffer.from(oneToTwo).toString('hex')).toBe(expected);
|
|
90
67
|
expect(oneToTwo).toEqual(twoToOne);
|
|
91
68
|
expect(oneToTwo).toHaveLength(32);
|
|
92
69
|
});
|
|
93
70
|
|
|
94
|
-
test(
|
|
71
|
+
test('generates canonical key pairs that round-trip', () => {
|
|
95
72
|
const keyPair = generateSecp256k1KeyPair();
|
|
96
73
|
expect(keyPair.privateKey).toMatch(/^[0-9a-f]{64}$/);
|
|
97
74
|
expect(keyPair.publicKey).toMatch(/^04[0-9a-f]{128}$/);
|
|
98
|
-
expect(deriveSecp256k1PublicKey(keyPair.privateKey)).toBe(
|
|
99
|
-
keyPair.publicKey,
|
|
100
|
-
);
|
|
75
|
+
expect(deriveSecp256k1PublicKey(keyPair.privateKey)).toBe(keyPair.publicKey);
|
|
101
76
|
});
|
|
102
77
|
|
|
103
|
-
test(
|
|
104
|
-
expect(isValidSecp256k1PrivateKey(
|
|
105
|
-
expect(isValidSecp256k1PrivateKey(
|
|
106
|
-
expect(isValidSecp256k1PrivateKey(
|
|
107
|
-
expect(isValidSecp256k1PublicKey(`04${
|
|
108
|
-
expect(isValidSecp256k1PublicKey(`05${PUBLIC_KEY_ONE.slice(2)}`)).toBe(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
expect(() => signSecp256k1Digest(PRIVATE_KEY_ONE, "00")).toThrow(
|
|
112
|
-
"digest must be exactly 32 bytes",
|
|
78
|
+
test('strictly rejects malformed scalars, points, digests, and DER', () => {
|
|
79
|
+
expect(isValidSecp256k1PrivateKey('0')).toBe(false);
|
|
80
|
+
expect(isValidSecp256k1PrivateKey('zz')).toBe(false);
|
|
81
|
+
expect(isValidSecp256k1PrivateKey('f'.repeat(66))).toBe(false);
|
|
82
|
+
expect(isValidSecp256k1PublicKey(`04${'0'.repeat(128)}`)).toBe(false);
|
|
83
|
+
expect(isValidSecp256k1PublicKey(`05${PUBLIC_KEY_ONE.slice(2)}`)).toBe(false);
|
|
84
|
+
expect(() => signSecp256k1Digest(PRIVATE_KEY_ONE, '00')).toThrow(
|
|
85
|
+
'digest must be exactly 32 bytes',
|
|
113
86
|
);
|
|
114
|
-
expect(() =>
|
|
115
|
-
verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, "00"),
|
|
116
|
-
).toThrow();
|
|
87
|
+
expect(() => verifySecp256k1Digest(PUBLIC_KEY_ONE, DIGEST_ONE, '00')).toThrow();
|
|
117
88
|
});
|
|
118
89
|
});
|
|
@@ -216,7 +216,10 @@ describe('verifyInclusionProof', () => {
|
|
|
216
216
|
const entries = heads(5);
|
|
217
217
|
const tree = await buildTransparencyTreeFromHeads(entries);
|
|
218
218
|
const proof = inclusionProof(tree, 2);
|
|
219
|
-
const tamperedLeaf = await transparencyLeafHash({
|
|
219
|
+
const tamperedLeaf = await transparencyLeafHash({
|
|
220
|
+
...entries[2],
|
|
221
|
+
headRecordId: 'f'.repeat(64),
|
|
222
|
+
});
|
|
220
223
|
await expect(
|
|
221
224
|
verifyInclusionProof({ leaf: tamperedLeaf, index: 2, treeSize: 5, proof, root: tree.root }),
|
|
222
225
|
).resolves.toBe(false);
|
|
@@ -89,7 +89,11 @@ const NIBBLE_LEADING_ZERO_BITS: Readonly<Record<string, number>> = {
|
|
|
89
89
|
* vice versa — the two are unrelated preimages even for the same
|
|
90
90
|
* `publicKey`/`timestamp` pair.
|
|
91
91
|
*/
|
|
92
|
-
export function registrationPowMessage(
|
|
92
|
+
export function registrationPowMessage(
|
|
93
|
+
publicKey: string,
|
|
94
|
+
timestamp: number,
|
|
95
|
+
nonce: string,
|
|
96
|
+
): string {
|
|
93
97
|
return `oxy:register-pow:${publicKey}:${timestamp}:${nonce}`;
|
|
94
98
|
}
|
|
95
99
|
|
package/src/chain/engine.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* engine owns the verification + ordering policy.
|
|
10
10
|
*
|
|
11
11
|
* Storage and identity are both injected, so the engine has zero knowledge of
|
|
12
|
-
*
|
|
12
|
+
* the database, Oxy DIDs, or any app's lexicon — exactly what makes it reusable.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { SignedRecordEnvelope } from '@oxy.so/contracts';
|
package/src/chain/recordStore.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* The engine ({@link ./verify}, {@link ./engine}) is storage-agnostic: it owns
|
|
5
5
|
* the verification state machine and continuity logic, and delegates EVERY read
|
|
6
6
|
* and write to an injected {@link RecordStore}. An app supplies a store over its
|
|
7
|
-
* own backend (oxy-api over
|
|
8
|
-
* Mention over its own
|
|
7
|
+
* own backend (oxy-api over Postgres `signed_records`/`repo_heads`, a node over
|
|
8
|
+
* SQLite, Mention over its own database) without the engine knowing anything Oxy- or
|
|
9
9
|
* app-specific.
|
|
10
10
|
*
|
|
11
11
|
* All methods are **subject-keyed**: `subject` is the chain's subject DID
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* `append` MUST be atomic (record insert + head advance in one unit) and MUST
|
|
18
18
|
* translate a duplicate-key collision on the unique `(subject, seq)` /
|
|
19
19
|
* `recordId` index — i.e. a concurrent writer that already took this `seq` — into
|
|
20
|
-
* `{ ok: false, reason: 'chain_conflict' }` (
|
|
20
|
+
* `{ ok: false, reason: 'chain_conflict' }` (Postgres `23505` / SQLite
|
|
21
21
|
* `SQLITE_CONSTRAINT`). That is the real multi-writer race guard; the engine's
|
|
22
22
|
* pre-append continuity check is only the fast-path rejection.
|
|
23
23
|
*/
|