@ar.io/wayfinder-core 0.0.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/LICENSE +201 -0
- package/README.md +318 -0
- package/dist/gateways/network.d.ts +39 -0
- package/dist/gateways/network.d.ts.map +1 -0
- package/dist/gateways/network.js +62 -0
- package/dist/gateways/simple-cache.d.ts +51 -0
- package/dist/gateways/simple-cache.d.ts.map +1 -0
- package/dist/gateways/simple-cache.js +66 -0
- package/dist/gateways/static.d.ts +26 -0
- package/dist/gateways/static.d.ts.map +1 -0
- package/dist/gateways/static.js +9 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/routing/ping.d.ts +19 -0
- package/dist/routing/ping.d.ts.map +1 -0
- package/dist/routing/ping.js +77 -0
- package/dist/routing/preferred-with-fallback.d.ts +34 -0
- package/dist/routing/preferred-with-fallback.d.ts.map +1 -0
- package/dist/routing/preferred-with-fallback.js +41 -0
- package/dist/routing/random.d.ts +24 -0
- package/dist/routing/random.d.ts.map +1 -0
- package/dist/routing/random.js +9 -0
- package/dist/routing/round-robin.d.ts +50 -0
- package/dist/routing/round-robin.d.ts.map +1 -0
- package/dist/routing/round-robin.js +41 -0
- package/dist/routing/static.d.ts +49 -0
- package/dist/routing/static.d.ts.map +1 -0
- package/dist/routing/static.js +37 -0
- package/dist/utils/ario.d.ts +28 -0
- package/dist/utils/ario.d.ts.map +1 -0
- package/dist/utils/ario.js +27 -0
- package/dist/utils/base64.d.ts +21 -0
- package/dist/utils/base64.d.ts.map +1 -0
- package/dist/utils/base64.js +51 -0
- package/dist/utils/hash.d.ts +10 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +81 -0
- package/dist/utils/random.d.ts +25 -0
- package/dist/utils/random.d.ts.map +1 -0
- package/dist/utils/random.js +26 -0
- package/dist/verification/data-root-verifier.d.ts +28 -0
- package/dist/verification/data-root-verifier.d.ts.map +1 -0
- package/dist/verification/data-root-verifier.js +129 -0
- package/dist/verification/hash-verifier.d.ts +28 -0
- package/dist/verification/hash-verifier.d.ts.map +1 -0
- package/dist/verification/hash-verifier.js +108 -0
- package/dist/wayfinder.d.ts +301 -0
- package/dist/wayfinder.d.ts.map +1 -0
- package/dist/wayfinder.js +498 -0
- package/package.json +44 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fromB64Url(str: string): Uint8Array;
|
|
19
|
+
export declare function toB64Url(bytes: Uint8Array): string;
|
|
20
|
+
export declare function sha256B64Url(input: Uint8Array): string;
|
|
21
|
+
//# sourceMappingURL=base64.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base64.d.ts","sourceRoot":"","sources":["../../src/utils/base64.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAsBH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAIlD;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAGlD;AASD,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEtD"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from 'crypto';
|
|
19
|
+
// safely encodes and decodes base64url strings to and from buffers
|
|
20
|
+
const BASE64_CHAR_62 = '+';
|
|
21
|
+
const BASE64_CHAR_63 = '/';
|
|
22
|
+
const BASE64URL_CHAR_62 = '-';
|
|
23
|
+
const BASE64URL_CHAR_63 = '_';
|
|
24
|
+
const BASE64_PADDING = '=';
|
|
25
|
+
function base64urlToBase64(str) {
|
|
26
|
+
const padLength = str.length % 4;
|
|
27
|
+
if (padLength) {
|
|
28
|
+
str += BASE64_PADDING.repeat(4 - padLength);
|
|
29
|
+
}
|
|
30
|
+
return str
|
|
31
|
+
.replaceAll(BASE64URL_CHAR_62, BASE64_CHAR_62)
|
|
32
|
+
.replaceAll(BASE64URL_CHAR_63, BASE64_CHAR_63);
|
|
33
|
+
}
|
|
34
|
+
export function fromB64Url(str) {
|
|
35
|
+
const b64Str = base64urlToBase64(str);
|
|
36
|
+
const binaryStr = atob(b64Str);
|
|
37
|
+
return new Uint8Array([...binaryStr].map((c) => c.charCodeAt(0)));
|
|
38
|
+
}
|
|
39
|
+
export function toB64Url(bytes) {
|
|
40
|
+
const b64Str = btoa(String.fromCharCode(...bytes));
|
|
41
|
+
return base64urlFromBase64(b64Str);
|
|
42
|
+
}
|
|
43
|
+
function base64urlFromBase64(str) {
|
|
44
|
+
return str
|
|
45
|
+
.replaceAll(BASE64_CHAR_62, BASE64URL_CHAR_62)
|
|
46
|
+
.replaceAll(BASE64_CHAR_63, BASE64URL_CHAR_63)
|
|
47
|
+
.replaceAll(BASE64_PADDING, '');
|
|
48
|
+
}
|
|
49
|
+
export function sha256B64Url(input) {
|
|
50
|
+
return toB64Url(new Uint8Array(createHash('sha256').update(input).digest()));
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DataStream } from '../../types/wayfinder.js';
|
|
2
|
+
import { Logger } from '../wayfinder.js';
|
|
3
|
+
export declare function isAsyncIterable(obj: unknown): obj is AsyncIterable<Uint8Array>;
|
|
4
|
+
export declare function readableStreamToAsyncIterable(stream: ReadableStream<Uint8Array>): AsyncIterable<Uint8Array>;
|
|
5
|
+
export declare function hashDataStreamToB64Url({ stream, algorithm, logger, }: {
|
|
6
|
+
stream: DataStream;
|
|
7
|
+
algorithm?: string;
|
|
8
|
+
logger?: Logger;
|
|
9
|
+
}): Promise<string | undefined>;
|
|
10
|
+
//# sourceMappingURL=hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../src/utils/hash.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAC;AAGxD,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,GACX,GAAG,IAAI,aAAa,CAAC,UAAU,CAAC,CAQlC;AAED,wBAAuB,6BAA6B,CAClD,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GACjC,aAAa,CAAC,UAAU,CAAC,CAa3B;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,MAAM,EACN,SAAqB,EACrB,MAAsB,GACvB,EAAE;IACD,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA2C9B"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from 'crypto';
|
|
19
|
+
import { defaultLogger } from '../wayfinder.js';
|
|
20
|
+
import { toB64Url } from './base64.js';
|
|
21
|
+
export function isAsyncIterable(obj) {
|
|
22
|
+
return (obj !== null &&
|
|
23
|
+
typeof obj === 'object' &&
|
|
24
|
+
Symbol.asyncIterator in obj &&
|
|
25
|
+
typeof obj[Symbol.asyncIterator] ===
|
|
26
|
+
'function');
|
|
27
|
+
}
|
|
28
|
+
export async function* readableStreamToAsyncIterable(stream) {
|
|
29
|
+
const reader = stream.getReader();
|
|
30
|
+
try {
|
|
31
|
+
while (true) {
|
|
32
|
+
const { done, value } = await reader.read();
|
|
33
|
+
if (done) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
yield value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
reader.releaseLock();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function hashDataStreamToB64Url({ stream, algorithm = 'SHA-256', logger = defaultLogger, }) {
|
|
44
|
+
try {
|
|
45
|
+
logger.debug('Starting to hash data stream', {
|
|
46
|
+
algorithm,
|
|
47
|
+
streamType: isAsyncIterable(stream) ? 'AsyncIterable' : 'ReadableStream',
|
|
48
|
+
});
|
|
49
|
+
const asyncIterable = isAsyncIterable(stream)
|
|
50
|
+
? stream
|
|
51
|
+
: readableStreamToAsyncIterable(stream);
|
|
52
|
+
const hash = createHash(algorithm);
|
|
53
|
+
let bytesProcessed = 0;
|
|
54
|
+
for await (const chunk of asyncIterable) {
|
|
55
|
+
hash.update(chunk);
|
|
56
|
+
bytesProcessed += chunk.length;
|
|
57
|
+
// Log progress occasionally (every ~1MB)
|
|
58
|
+
if (bytesProcessed % (1024 * 1024) < chunk.length) {
|
|
59
|
+
logger.debug('Hashing progress', {
|
|
60
|
+
bytesProcessed,
|
|
61
|
+
algorithm,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const hashResult = toB64Url(new Uint8Array(hash.digest()));
|
|
66
|
+
logger.debug('Finished hashing data stream', {
|
|
67
|
+
bytesProcessed,
|
|
68
|
+
algorithm,
|
|
69
|
+
hashResult,
|
|
70
|
+
});
|
|
71
|
+
return hashResult;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.error('Error hashing data stream', {
|
|
75
|
+
error: error.message,
|
|
76
|
+
stack: error.stack,
|
|
77
|
+
algorithm,
|
|
78
|
+
});
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Returns a random integer between min (inclusive) and max (exclusive)
|
|
20
|
+
* @param min - The minimum value (inclusive)
|
|
21
|
+
* @param max - The maximum value (exclusive)
|
|
22
|
+
* @returns A random integer between min and max
|
|
23
|
+
*/
|
|
24
|
+
export declare function randomInt(min: number, max: number): number;
|
|
25
|
+
//# sourceMappingURL=random.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"random.d.ts","sourceRoot":"","sources":["../../src/utils/random.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Returns a random integer between min (inclusive) and max (exclusive)
|
|
20
|
+
* @param min - The minimum value (inclusive)
|
|
21
|
+
* @param max - The maximum value (exclusive)
|
|
22
|
+
* @returns A random integer between min and max
|
|
23
|
+
*/
|
|
24
|
+
export function randomInt(min, max) {
|
|
25
|
+
return Math.floor(Math.random() * (max - min)) + min;
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DataStream, VerificationStrategy } from '../../types/wayfinder.js';
|
|
2
|
+
import { Logger } from '../wayfinder.js';
|
|
3
|
+
export declare const convertDataStreamToDataRoot: ({ stream, }: {
|
|
4
|
+
stream: DataStream;
|
|
5
|
+
}) => Promise<string>;
|
|
6
|
+
export declare class DataRootVerificationStrategy implements VerificationStrategy {
|
|
7
|
+
private readonly trustedGateways;
|
|
8
|
+
private readonly maxConcurrency;
|
|
9
|
+
private readonly logger;
|
|
10
|
+
constructor({ trustedGateways, maxConcurrency, logger, }: {
|
|
11
|
+
trustedGateways: URL[];
|
|
12
|
+
maxConcurrency?: number;
|
|
13
|
+
logger?: Logger;
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Get the data root for a given txId from all trusted gateways and ensure they all match.
|
|
17
|
+
* @param txId - The txId to get the data root for.
|
|
18
|
+
* @returns The data root for the given txId.
|
|
19
|
+
*/
|
|
20
|
+
getDataRoot({ txId }: {
|
|
21
|
+
txId: string;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
verifyData({ data, txId, }: {
|
|
24
|
+
data: DataStream;
|
|
25
|
+
txId: string;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=data-root-verifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-root-verifier.d.ts","sourceRoot":"","sources":["../../src/verification/data-root-verifier.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAM5E,OAAO,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAC;AAExD,eAAO,MAAM,2BAA2B,GAAU,aAE/C;IACD,MAAM,EAAE,UAAU,CAAC;CACpB,KAAG,OAAO,CAAC,MAAM,CAwDjB,CAAC;AAEF,qBAAa,4BAA6B,YAAW,oBAAoB;IACvE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBACpB,EACV,eAAe,EACf,cAAkB,EAClB,MAAsB,GACvB,EAAE;QACD,eAAe,EAAE,GAAG,EAAE,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB;IAMD;;;;OAIG;IACG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAuCxD,UAAU,CAAC,EACf,IAAI,EACJ,IAAI,GACL,EAAE;QACD,IAAI,EAAE,UAAU,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,GAAG,OAAO,CAAC,IAAI,CAAC;CAelB"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import Arweave from 'arweave';
|
|
19
|
+
import { MAX_CHUNK_SIZE, MIN_CHUNK_SIZE, buildLayers, generateLeaves, } from 'arweave/node/lib/merkle.js';
|
|
20
|
+
import { pLimit } from 'plimit-lit';
|
|
21
|
+
import { toB64Url } from '../utils/base64.js';
|
|
22
|
+
import { isAsyncIterable, readableStreamToAsyncIterable, } from '../utils/hash.js';
|
|
23
|
+
import { defaultLogger } from '../wayfinder.js';
|
|
24
|
+
export const convertDataStreamToDataRoot = async ({ stream, }) => {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let leftover = new Uint8Array(0);
|
|
27
|
+
let cursor = 0;
|
|
28
|
+
const asyncIterable = isAsyncIterable(stream)
|
|
29
|
+
? stream
|
|
30
|
+
: readableStreamToAsyncIterable(stream);
|
|
31
|
+
for await (const data of asyncIterable) {
|
|
32
|
+
const inputChunk = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
33
|
+
const combined = new Uint8Array(leftover.length + inputChunk.length);
|
|
34
|
+
combined.set(leftover, 0);
|
|
35
|
+
combined.set(inputChunk, leftover.length);
|
|
36
|
+
let startIndex = 0;
|
|
37
|
+
while (combined.length - startIndex >= MAX_CHUNK_SIZE) {
|
|
38
|
+
let chunkSize = MAX_CHUNK_SIZE;
|
|
39
|
+
const remainderAfterThis = combined.length - startIndex - MAX_CHUNK_SIZE;
|
|
40
|
+
if (remainderAfterThis > 0 && remainderAfterThis < MIN_CHUNK_SIZE) {
|
|
41
|
+
chunkSize = Math.ceil((combined.length - startIndex) / 2);
|
|
42
|
+
}
|
|
43
|
+
const chunkData = combined.slice(startIndex, startIndex + chunkSize);
|
|
44
|
+
const dataHash = await Arweave.crypto.hash(chunkData);
|
|
45
|
+
chunks.push({
|
|
46
|
+
dataHash,
|
|
47
|
+
minByteRange: cursor,
|
|
48
|
+
maxByteRange: cursor + chunkSize,
|
|
49
|
+
});
|
|
50
|
+
cursor += chunkSize;
|
|
51
|
+
startIndex += chunkSize;
|
|
52
|
+
}
|
|
53
|
+
leftover = combined.slice(startIndex);
|
|
54
|
+
}
|
|
55
|
+
if (leftover.length > 0) {
|
|
56
|
+
// TODO: ensure a web friendly crypto hash function is used in web
|
|
57
|
+
const dataHash = await Arweave.crypto.hash(leftover);
|
|
58
|
+
chunks.push({
|
|
59
|
+
dataHash,
|
|
60
|
+
minByteRange: cursor,
|
|
61
|
+
maxByteRange: cursor + leftover.length,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const leaves = await generateLeaves(chunks);
|
|
65
|
+
const root = await buildLayers(leaves);
|
|
66
|
+
return toB64Url(new Uint8Array(root.id));
|
|
67
|
+
};
|
|
68
|
+
export class DataRootVerificationStrategy {
|
|
69
|
+
trustedGateways;
|
|
70
|
+
maxConcurrency;
|
|
71
|
+
logger;
|
|
72
|
+
constructor({ trustedGateways, maxConcurrency = 1, logger = defaultLogger, }) {
|
|
73
|
+
this.trustedGateways = trustedGateways;
|
|
74
|
+
this.maxConcurrency = maxConcurrency;
|
|
75
|
+
this.logger = logger;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the data root for a given txId from all trusted gateways and ensure they all match.
|
|
79
|
+
* @param txId - The txId to get the data root for.
|
|
80
|
+
* @returns The data root for the given txId.
|
|
81
|
+
*/
|
|
82
|
+
async getDataRoot({ txId }) {
|
|
83
|
+
this.logger.debug('Getting data root for txId', {
|
|
84
|
+
txId,
|
|
85
|
+
maxConcurrency: this.maxConcurrency,
|
|
86
|
+
trustedGateways: this.trustedGateways,
|
|
87
|
+
});
|
|
88
|
+
// TODO: shuffle gateways to avoid bias
|
|
89
|
+
const throttle = pLimit(this.maxConcurrency);
|
|
90
|
+
const dataRootPromises = this.trustedGateways.map(async (gateway) => {
|
|
91
|
+
return throttle(async () => {
|
|
92
|
+
const response = await fetch(`${gateway.toString()}tx/${txId}/data_root`);
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
// skip this gateway
|
|
95
|
+
throw new Error('Failed to fetch data root for txId', {
|
|
96
|
+
cause: {
|
|
97
|
+
txId,
|
|
98
|
+
gateway: gateway.toString(),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const dataRoot = await response.text();
|
|
103
|
+
return { dataRoot, gateway };
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
const { dataRoot, gateway } = await Promise.any(dataRootPromises);
|
|
107
|
+
this.logger.debug('Successfully fetched data root for txId', {
|
|
108
|
+
txId,
|
|
109
|
+
dataRoot,
|
|
110
|
+
gateway: gateway.toString(),
|
|
111
|
+
});
|
|
112
|
+
return dataRoot;
|
|
113
|
+
}
|
|
114
|
+
async verifyData({ data, txId, }) {
|
|
115
|
+
const [computedDataRoot, trustedDataRoot] = await Promise.all([
|
|
116
|
+
convertDataStreamToDataRoot({
|
|
117
|
+
stream: data,
|
|
118
|
+
}),
|
|
119
|
+
this.getDataRoot({
|
|
120
|
+
txId,
|
|
121
|
+
}),
|
|
122
|
+
]);
|
|
123
|
+
if (computedDataRoot !== trustedDataRoot) {
|
|
124
|
+
throw new Error('Data root does not match', {
|
|
125
|
+
cause: { computedDataRoot, trustedDataRoot },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DataStream, VerificationStrategy } from '../../types/wayfinder.js';
|
|
2
|
+
import { Logger } from '../wayfinder.js';
|
|
3
|
+
export declare class HashVerificationStrategy implements VerificationStrategy {
|
|
4
|
+
private readonly trustedGateways;
|
|
5
|
+
private readonly maxConcurrency;
|
|
6
|
+
private readonly logger;
|
|
7
|
+
constructor({ trustedGateways, maxConcurrency, logger, }: {
|
|
8
|
+
trustedGateways: URL[];
|
|
9
|
+
maxConcurrency?: number;
|
|
10
|
+
logger?: Logger;
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Gets the digest for a given txId from all trusted gateways and ensures they all match.
|
|
14
|
+
* @param txId - The txId to get the digest for.
|
|
15
|
+
* @returns The digest for the given txId.
|
|
16
|
+
*/
|
|
17
|
+
getDigest({ txId, }: {
|
|
18
|
+
txId: string;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
hash: string;
|
|
21
|
+
algorithm: 'sha256';
|
|
22
|
+
}>;
|
|
23
|
+
verifyData({ data, txId, }: {
|
|
24
|
+
data: DataStream;
|
|
25
|
+
txId: string;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=hash-verifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash-verifier.d.ts","sourceRoot":"","sources":["../../src/verification/hash-verifier.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAG5E,OAAO,EAAE,MAAM,EAAgC,MAAM,iBAAiB,CAAC;AAEvE,qBAAa,wBAAyB,YAAW,oBAAoB;IACnE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBACpB,EACV,eAAe,EACf,cAAkB,EAClB,MAAsB,GACvB,EAAE;QACD,eAAe,EAAE,GAAG,EAAE,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB;IAMD;;;;OAIG;IACG,SAAS,CAAC,EACd,IAAI,GACL,EAAE;QACD,IAAI,EAAE,MAAM,CAAC;KACd,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,QAAQ,CAAA;KAAE,CAAC;IAqE5C,UAAU,CAAC,EACf,IAAI,EACJ,IAAI,GACL,EAAE;QACD,IAAI,EAAE,UAAU,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,GAAG,OAAO,CAAC,IAAI,CAAC;CAgBlB"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful,
|
|
11
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
* GNU Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import { pLimit } from 'plimit-lit';
|
|
19
|
+
import { arioGatewayHeaders } from '../utils/ario.js';
|
|
20
|
+
import { hashDataStreamToB64Url } from '../utils/hash.js';
|
|
21
|
+
import { defaultLogger, sandboxFromId } from '../wayfinder.js';
|
|
22
|
+
export class HashVerificationStrategy {
|
|
23
|
+
trustedGateways;
|
|
24
|
+
maxConcurrency;
|
|
25
|
+
logger;
|
|
26
|
+
constructor({ trustedGateways, maxConcurrency = 1, logger = defaultLogger, }) {
|
|
27
|
+
this.trustedGateways = trustedGateways;
|
|
28
|
+
this.maxConcurrency = maxConcurrency;
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Gets the digest for a given txId from all trusted gateways and ensures they all match.
|
|
33
|
+
* @param txId - The txId to get the digest for.
|
|
34
|
+
* @returns The digest for the given txId.
|
|
35
|
+
*/
|
|
36
|
+
async getDigest({ txId, }) {
|
|
37
|
+
this.logger.debug('Getting digest for txId', {
|
|
38
|
+
txId,
|
|
39
|
+
maxConcurrency: this.maxConcurrency,
|
|
40
|
+
trustedGateways: this.trustedGateways,
|
|
41
|
+
});
|
|
42
|
+
// TODO: shuffle gateways to avoid bias
|
|
43
|
+
const throttle = pLimit(this.maxConcurrency);
|
|
44
|
+
const hashPromises = this.trustedGateways.map(async (gateway) => {
|
|
45
|
+
return throttle(async () => {
|
|
46
|
+
const sandbox = sandboxFromId(txId);
|
|
47
|
+
const urlWithSandbox = `${gateway.protocol}//${sandbox}.${gateway.hostname}/${txId}`;
|
|
48
|
+
/**
|
|
49
|
+
* This is a problem because we're not able to verify the hash of the data item if the gateway doesn't have the data in its cache. We start with a HEAD request, if it fails, we do a GET request to hydrate the cache and then a HEAD request again to get the cached digest.
|
|
50
|
+
*/
|
|
51
|
+
for (const method of ['HEAD', 'GET', 'HEAD']) {
|
|
52
|
+
const response = await fetch(urlWithSandbox, {
|
|
53
|
+
method,
|
|
54
|
+
redirect: 'follow',
|
|
55
|
+
mode: 'cors',
|
|
56
|
+
headers: {
|
|
57
|
+
'Cache-Control': 'no-cache',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
// skip if the request failed or the digest is not present
|
|
62
|
+
throw new Error('Failed to fetch digest for txId', {
|
|
63
|
+
cause: {
|
|
64
|
+
txId,
|
|
65
|
+
gateway: gateway.toString(),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const fetchedTxIdHash = response.headers.get(arioGatewayHeaders.digest);
|
|
70
|
+
if (fetchedTxIdHash) {
|
|
71
|
+
// avoid hitting other gateways if we've found the hash
|
|
72
|
+
throttle.clearQueue();
|
|
73
|
+
return { hash: fetchedTxIdHash, gateway };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw new Error('No hash found for txId', {
|
|
77
|
+
cause: {
|
|
78
|
+
txId,
|
|
79
|
+
gateway: gateway.toString(),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
const { hash, gateway } = await Promise.any(hashPromises);
|
|
85
|
+
this.logger.debug('Successfully fetched digest for txId from trusted gateway', {
|
|
86
|
+
txId,
|
|
87
|
+
hash,
|
|
88
|
+
gateway: gateway.toString(),
|
|
89
|
+
});
|
|
90
|
+
return { hash, algorithm: 'sha256' };
|
|
91
|
+
}
|
|
92
|
+
async verifyData({ data, txId, }) {
|
|
93
|
+
// kick off the hash computation, but don't wait for it until we compute our own hash
|
|
94
|
+
const [computedHash, fetchedHash] = await Promise.all([
|
|
95
|
+
hashDataStreamToB64Url({ stream: data }),
|
|
96
|
+
this.getDigest({ txId }),
|
|
97
|
+
]);
|
|
98
|
+
// await on the hash promise and compare to get a little concurrency when computing hashes over larger data
|
|
99
|
+
if (computedHash === undefined) {
|
|
100
|
+
throw new Error('Hash could not be computed');
|
|
101
|
+
}
|
|
102
|
+
if (computedHash !== fetchedHash.hash) {
|
|
103
|
+
throw new Error('Hash does not match', {
|
|
104
|
+
cause: { computedHash, trustedHash: fetchedHash },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|