@ar.io/sdk 3.11.0-alpha.8 → 3.11.0-beta.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/README.md +52 -0
- package/bundles/web.bundle.min.js +106 -106
- package/lib/cjs/cli/cli.js +137 -122
- package/lib/cjs/cli/commands/readCommands.js +6 -0
- package/lib/cjs/cli/utils.js +4 -1
- package/lib/cjs/common/ant.js +5 -5
- package/lib/cjs/common/contracts/ao-process.js +2 -1
- package/lib/cjs/common/io.js +37 -0
- package/lib/cjs/common/wayfinder/{gateways.js → gateways/network.js} +3 -41
- package/lib/cjs/common/wayfinder/gateways/simple-cache.js +35 -0
- package/lib/cjs/common/wayfinder/gateways/static.js +13 -0
- package/lib/cjs/common/wayfinder/index.js +11 -8
- package/lib/cjs/common/wayfinder/routing/strategies/ping.js +72 -0
- package/lib/cjs/common/wayfinder/routing/strategies/ping.test.js +156 -0
- package/lib/cjs/common/wayfinder/routing/strategies/random.js +13 -0
- package/lib/cjs/common/wayfinder/routing/strategies/random.test.js +68 -0
- package/lib/cjs/common/wayfinder/routing/strategies/round-robin.js +42 -0
- package/lib/cjs/common/wayfinder/routing/strategies/round-robin.test.js +78 -0
- package/lib/cjs/common/wayfinder/routing/strategies/static.js +29 -0
- package/lib/cjs/common/wayfinder/routing/strategies/static.test.js +40 -0
- package/lib/cjs/common/wayfinder/verification/{data-root-verifier.js → strategies/data-root-verifier.js} +4 -4
- package/lib/cjs/common/wayfinder/verification/{hash-verifier.js → strategies/hash-verifier.js} +4 -4
- package/lib/cjs/common/wayfinder/{gateways/trusted-gateways.js → verification/trusted.js} +1 -1
- package/lib/cjs/common/wayfinder/wayfinder.js +397 -257
- package/lib/cjs/common/wayfinder/wayfinder.test.js +227 -208
- package/lib/cjs/version.js +1 -1
- package/lib/esm/cli/cli.js +138 -123
- package/lib/esm/cli/commands/readCommands.js +5 -0
- package/lib/esm/cli/utils.js +4 -1
- package/lib/esm/common/ant.js +5 -5
- package/lib/esm/common/contracts/ao-process.js +2 -1
- package/lib/esm/common/io.js +37 -0
- package/lib/esm/common/wayfinder/{gateways.js → gateways/network.js} +2 -38
- package/lib/esm/common/wayfinder/gateways/simple-cache.js +31 -0
- package/lib/esm/common/wayfinder/gateways/static.js +9 -0
- package/lib/esm/common/wayfinder/index.js +11 -8
- package/lib/esm/common/wayfinder/routing/strategies/ping.js +68 -0
- package/lib/esm/common/wayfinder/routing/strategies/ping.test.js +151 -0
- package/lib/esm/common/wayfinder/routing/strategies/random.js +9 -0
- package/lib/esm/common/wayfinder/routing/strategies/random.test.js +63 -0
- package/lib/esm/common/wayfinder/routing/strategies/round-robin.js +38 -0
- package/lib/esm/common/wayfinder/routing/strategies/round-robin.test.js +73 -0
- package/lib/esm/common/wayfinder/routing/strategies/static.js +25 -0
- package/lib/esm/common/wayfinder/routing/strategies/static.test.js +35 -0
- package/lib/esm/common/wayfinder/verification/{data-root-verifier.js → strategies/data-root-verifier.js} +2 -2
- package/lib/esm/common/wayfinder/verification/{hash-verifier.js → strategies/hash-verifier.js} +2 -2
- package/lib/esm/common/wayfinder/{gateways/trusted-gateways.js → verification/trusted.js} +1 -1
- package/lib/esm/common/wayfinder/wayfinder.js +395 -255
- package/lib/esm/common/wayfinder/wayfinder.test.js +227 -208
- package/lib/esm/version.js +1 -1
- package/lib/types/cli/commands/readCommands.d.ts +1 -0
- package/lib/types/common/io.d.ts +5 -2
- package/lib/types/common/wayfinder/{gateways.d.ts → gateways/network.d.ts} +3 -23
- package/lib/types/common/wayfinder/{routers/random.d.ts → gateways/simple-cache.d.ts} +12 -8
- package/lib/types/common/wayfinder/{routers → gateways}/static.d.ts +6 -7
- package/lib/types/common/wayfinder/index.d.ts +10 -7
- package/lib/types/common/wayfinder/{routers/simple-cache.d.ts → routing/strategies/ping.d.ts} +10 -11
- package/lib/types/common/wayfinder/routing/strategies/random.d.ts +21 -0
- package/lib/types/common/wayfinder/routing/strategies/round-robin.d.ts +29 -0
- package/lib/types/common/wayfinder/routing/strategies/static.d.ts +29 -0
- package/lib/types/common/wayfinder/verification/{data-root-verifier.d.ts → strategies/data-root-verifier.d.ts} +2 -2
- package/lib/types/common/wayfinder/verification/{hash-verifier.d.ts → strategies/hash-verifier.d.ts} +2 -2
- package/lib/types/common/wayfinder/{gateways/trusted-gateways.d.ts → verification/trusted.d.ts} +1 -1
- package/lib/types/common/wayfinder/wayfinder.d.ts +111 -77
- package/lib/types/types/io.d.ts +16 -1
- package/lib/types/types/wayfinder.d.ts +8 -4
- package/lib/types/version.d.ts +1 -1
- package/package.json +1 -1
- package/lib/cjs/common/wayfinder/routers/priority.js +0 -29
- package/lib/cjs/common/wayfinder/routers/priority.test.js +0 -155
- package/lib/cjs/common/wayfinder/routers/random.js +0 -23
- package/lib/cjs/common/wayfinder/routers/random.test.js +0 -25
- package/lib/cjs/common/wayfinder/routers/simple-cache.js +0 -25
- package/lib/cjs/common/wayfinder/routers/simple-cache.test.js +0 -41
- package/lib/cjs/common/wayfinder/routers/static.js +0 -14
- package/lib/cjs/common/wayfinder/routers/static.test.js +0 -14
- package/lib/esm/common/wayfinder/routers/priority.js +0 -25
- package/lib/esm/common/wayfinder/routers/priority.test.js +0 -153
- package/lib/esm/common/wayfinder/routers/random.js +0 -19
- package/lib/esm/common/wayfinder/routers/random.test.js +0 -23
- package/lib/esm/common/wayfinder/routers/simple-cache.js +0 -21
- package/lib/esm/common/wayfinder/routers/simple-cache.test.js +0 -39
- package/lib/esm/common/wayfinder/routers/static.js +0 -10
- package/lib/esm/common/wayfinder/routers/static.test.js +0 -12
- package/lib/types/common/wayfinder/routers/priority.d.ts +0 -29
- /package/lib/types/common/wayfinder/{routers/priority.test.d.ts → routing/strategies/ping.test.d.ts} +0 -0
- /package/lib/types/common/wayfinder/{routers → routing/strategies}/random.test.d.ts +0 -0
- /package/lib/types/common/wayfinder/{routers/simple-cache.test.d.ts → routing/strategies/round-robin.test.d.ts} +0 -0
- /package/lib/types/common/wayfinder/{routers → routing/strategies}/static.test.d.ts +0 -0
|
@@ -17,50 +17,47 @@ import EventEmitter from 'node:events';
|
|
|
17
17
|
import { PassThrough, Readable } from 'node:stream';
|
|
18
18
|
import { ARIO } from '../io.js';
|
|
19
19
|
import { Logger } from '../logger.js';
|
|
20
|
-
import { NetworkGatewaysProvider
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
20
|
+
import { NetworkGatewaysProvider } from './gateways/network.js';
|
|
21
|
+
import { SimpleCacheGatewaysProvider } from './gateways/simple-cache.js';
|
|
22
|
+
import { StaticGatewaysProvider } from './gateways/static.js';
|
|
23
|
+
import { FastestPingRoutingStrategy } from './routing/strategies/ping.js';
|
|
24
|
+
import { HashVerificationStrategy } from './verification/strategies/hash-verifier.js';
|
|
25
|
+
import { TrustedGatewaysHashProvider } from './verification/trusted.js';
|
|
24
26
|
// known regexes for wayfinder urls
|
|
25
27
|
export const arnsRegex = /^[a-z0-9_-]{1,51}$/;
|
|
26
28
|
export const txIdRegex = /^[A-Za-z0-9_-]{43}$/;
|
|
27
29
|
/**
|
|
28
|
-
* Core function
|
|
30
|
+
* Core function that converts a wayfinder url to the proper ar-io gateway URL
|
|
29
31
|
* @param originalUrl - the wayfinder url to resolve
|
|
30
|
-
* @param
|
|
32
|
+
* @param selectedGateway - the target gateway to resolve the url against
|
|
31
33
|
* @returns the resolved url that can be used to make a request
|
|
32
34
|
*/
|
|
33
|
-
export const resolveWayfinderUrl =
|
|
35
|
+
export const resolveWayfinderUrl = ({ originalUrl, selectedGateway, logger, }) => {
|
|
34
36
|
if (originalUrl.toString().startsWith('ar://')) {
|
|
35
37
|
logger?.debug(`Applying wayfinder routing protocol to ${originalUrl}`, {
|
|
36
38
|
originalUrl,
|
|
37
39
|
});
|
|
38
|
-
const targetGatewayUrl = new URL(await targetGateway());
|
|
39
|
-
logger?.debug(`Selected target gateway: ${targetGatewayUrl}`, {
|
|
40
|
-
originalUrl,
|
|
41
|
-
targetGateway: targetGatewayUrl,
|
|
42
|
-
});
|
|
43
40
|
const [, path] = originalUrl.toString().split('ar://');
|
|
44
41
|
// e.g. ar:///info should route to the info endpoint of the target gateway
|
|
45
42
|
if (path.startsWith('/')) {
|
|
46
|
-
logger?.debug(`Routing to ${path.slice(1)} on ${
|
|
43
|
+
logger?.debug(`Routing to ${path.slice(1)} on ${selectedGateway}`, {
|
|
47
44
|
originalUrl,
|
|
48
|
-
|
|
45
|
+
selectedGateway,
|
|
49
46
|
});
|
|
50
|
-
return new URL(path.slice(1),
|
|
47
|
+
return new URL(path.slice(1), selectedGateway);
|
|
51
48
|
}
|
|
52
49
|
// TODO: this breaks 43 character named arns names - we should check a a local name cache list before resolving raw transaction ids
|
|
53
50
|
if (txIdRegex.test(path)) {
|
|
54
51
|
const [txId, ...rest] = path.split('/');
|
|
55
|
-
return new URL(`${txId}${rest.join('/')}`,
|
|
52
|
+
return new URL(`${txId}${rest.join('/')}`, selectedGateway);
|
|
56
53
|
}
|
|
57
54
|
if (arnsRegex.test(path)) {
|
|
58
55
|
// TODO: tests to ensure arns names support query params and paths
|
|
59
56
|
const [name, ...rest] = path.split('/');
|
|
60
|
-
const arnsUrl = `${
|
|
57
|
+
const arnsUrl = `${selectedGateway.protocol}//${name}.${selectedGateway.hostname}${selectedGateway.port ? `:${selectedGateway.port}` : ''}`;
|
|
61
58
|
logger?.debug(`Routing to ${path} on ${arnsUrl}`, {
|
|
62
59
|
originalUrl,
|
|
63
|
-
|
|
60
|
+
selectedGateway,
|
|
64
61
|
});
|
|
65
62
|
return new URL(rest.join('/'), arnsUrl);
|
|
66
63
|
}
|
|
@@ -79,7 +76,7 @@ export class WayfinderEmitter extends EventEmitter {
|
|
|
79
76
|
} = {}) {
|
|
80
77
|
super();
|
|
81
78
|
if (onVerificationPassed) {
|
|
82
|
-
this.on('verification-
|
|
79
|
+
this.on('verification-succeeded', onVerificationPassed);
|
|
83
80
|
}
|
|
84
81
|
if (onVerificationFailed) {
|
|
85
82
|
this.on('verification-failed', onVerificationFailed);
|
|
@@ -95,7 +92,7 @@ export class WayfinderEmitter extends EventEmitter {
|
|
|
95
92
|
return super.on(event, listener);
|
|
96
93
|
}
|
|
97
94
|
}
|
|
98
|
-
export function tapAndVerifyStream({ originalStream, contentLength, verifyData, txId, emitter, }) {
|
|
95
|
+
export function tapAndVerifyStream({ originalStream, contentLength, verifyData, txId, emitter, strict = false, }) {
|
|
99
96
|
// taps node streams
|
|
100
97
|
if (originalStream instanceof Readable &&
|
|
101
98
|
typeof originalStream.pipe === 'function') {
|
|
@@ -123,26 +120,39 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
|
|
|
123
120
|
});
|
|
124
121
|
originalStream.on('end', async () => {
|
|
125
122
|
streamToVerify.end(); // triggers verifier completion and completes the verification promise
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
123
|
+
if (strict) {
|
|
124
|
+
// in strict mode, we wait for verification to complete before ending the client stream
|
|
125
|
+
try {
|
|
126
|
+
await verificationPromise;
|
|
127
|
+
emitter?.emit('verification-succeeded', { txId });
|
|
128
|
+
tappedClientStream.end();
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
emitter?.emit('verification-failed', { error, txId });
|
|
132
|
+
// In strict mode, destroy the client stream with the error
|
|
133
|
+
tappedClientStream.destroy(new Error('Verification failed', { cause: error }));
|
|
134
|
+
}
|
|
132
135
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
else {
|
|
137
|
+
// in non-strict mode, we end the client stream immediately and handle verification asynchronously
|
|
138
|
+
tappedClientStream.end();
|
|
139
|
+
// trigger the verification promise and emit events for the result
|
|
140
|
+
verificationPromise
|
|
141
|
+
.then(() => {
|
|
142
|
+
emitter?.emit('verification-succeeded', { txId });
|
|
143
|
+
})
|
|
144
|
+
.catch((error) => {
|
|
145
|
+
emitter?.emit('verification-failed', { error, txId });
|
|
137
146
|
});
|
|
138
|
-
tappedClientStream.destroy(error);
|
|
139
147
|
}
|
|
140
148
|
});
|
|
141
149
|
originalStream.on('error', (err) => {
|
|
150
|
+
// emit the verification failed event
|
|
142
151
|
emitter?.emit('verification-failed', {
|
|
143
152
|
error: err,
|
|
144
153
|
txId,
|
|
145
154
|
});
|
|
155
|
+
// destroy both streams and propagate the original stream error
|
|
146
156
|
streamToVerify.destroy(err);
|
|
147
157
|
tappedClientStream.destroy(err);
|
|
148
158
|
});
|
|
@@ -164,20 +174,37 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
|
|
|
164
174
|
async pull(controller) {
|
|
165
175
|
const { done, value } = await reader.read();
|
|
166
176
|
if (done) {
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
txId
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
if (strict) {
|
|
178
|
+
// in strict mode, we wait for verification to complete before closing the controller
|
|
179
|
+
try {
|
|
180
|
+
await verificationPromise;
|
|
181
|
+
emitter?.emit('verification-succeeded', { txId });
|
|
182
|
+
controller.close();
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
emitter?.emit('verification-failed', {
|
|
186
|
+
txId,
|
|
187
|
+
error: err,
|
|
188
|
+
});
|
|
189
|
+
// In strict mode, we report the error to the client stream
|
|
190
|
+
controller.error(new Error('Verification failed', { cause: err }));
|
|
191
|
+
}
|
|
174
192
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
else {
|
|
194
|
+
// in non-strict mode, we close the controller immediately and handle verification asynchronously
|
|
195
|
+
controller.close();
|
|
196
|
+
// trigger the verification promise and emit events for the result
|
|
197
|
+
verificationPromise
|
|
198
|
+
.then(() => {
|
|
199
|
+
emitter?.emit('verification-succeeded', { txId });
|
|
200
|
+
})
|
|
201
|
+
.catch((err) => {
|
|
202
|
+
emitter?.emit('verification-failed', {
|
|
203
|
+
txId,
|
|
204
|
+
error: err,
|
|
205
|
+
});
|
|
206
|
+
// we don't call controller.error() to avoid breaking the client stream
|
|
179
207
|
});
|
|
180
|
-
controller.error(err);
|
|
181
208
|
}
|
|
182
209
|
}
|
|
183
210
|
else {
|
|
@@ -191,7 +218,9 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
|
|
|
191
218
|
}
|
|
192
219
|
},
|
|
193
220
|
cancel(reason) {
|
|
221
|
+
// cancel the reader regardless of verification status
|
|
194
222
|
reader.cancel(reason);
|
|
223
|
+
// emit the verification cancellation event
|
|
195
224
|
emitter?.emit('verification-failed', {
|
|
196
225
|
txId,
|
|
197
226
|
error: new Error('Verification cancelled', {
|
|
@@ -200,6 +229,8 @@ export function tapAndVerifyStream({ originalStream, contentLength, verifyData,
|
|
|
200
229
|
},
|
|
201
230
|
}),
|
|
202
231
|
});
|
|
232
|
+
// note: we don't block or throw errors here even in strict mode
|
|
233
|
+
// since the stream is already being cancelled by the client
|
|
203
234
|
},
|
|
204
235
|
});
|
|
205
236
|
return clientStreamWithVerification;
|
|
@@ -234,7 +265,7 @@ export function wrapVerifiedResponse(original, newBody, txId) {
|
|
|
234
265
|
* @param resolveUrl - the function to construct the redirect url for ar:// requests
|
|
235
266
|
* @returns a wrapped http client that supports ar:// protocol
|
|
236
267
|
*/
|
|
237
|
-
export const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, emitter = new WayfinderEmitter(), logger, }) => {
|
|
268
|
+
export const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, selectGateway, emitter = new WayfinderEmitter(), logger, strict = false, }) => {
|
|
238
269
|
const wayfinderRedirect = async (fn, rawArgs) => {
|
|
239
270
|
// TODO: handle if first arg is not a string (i.e. just return the result of the function call)
|
|
240
271
|
const [originalUrl, ...rest] = rawArgs;
|
|
@@ -242,157 +273,226 @@ export const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, emit
|
|
|
242
273
|
logger?.debug('Original URL is not a string, skipping routing', {
|
|
243
274
|
originalUrl,
|
|
244
275
|
});
|
|
276
|
+
emitter?.emit('routing-skipped', {
|
|
277
|
+
originalUrl: JSON.stringify(originalUrl),
|
|
278
|
+
});
|
|
245
279
|
return fn(...rawArgs);
|
|
246
280
|
}
|
|
247
281
|
emitter?.emit('routing-started', {
|
|
248
282
|
originalUrl: originalUrl.toString(),
|
|
249
283
|
});
|
|
250
|
-
//
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
});
|
|
259
|
-
logger?.debug(`Redirecting request to ${redirectUrl}`, {
|
|
260
|
-
originalUrl,
|
|
261
|
-
redirectUrl,
|
|
262
|
-
});
|
|
263
|
-
// make the request to the target gateway using the redirect url and http client
|
|
264
|
-
const response = await fn(redirectUrl.toString(), ...rest);
|
|
265
|
-
// TODO: trigger a routing event with the raw response object?
|
|
266
|
-
logger?.debug(`Successfully routed request to ${redirectUrl}`, {
|
|
267
|
-
redirectUrl,
|
|
268
|
-
originalUrl,
|
|
269
|
-
});
|
|
270
|
-
// only verify data if the redirect url is different from the original url
|
|
271
|
-
if (response && redirectUrl.toString() !== originalUrl.toString()) {
|
|
272
|
-
if (verifyData) {
|
|
273
|
-
// if the headers do not have .get on them, we need to parse the headers manually
|
|
274
|
-
const headers = new Headers();
|
|
275
|
-
let headersObject = response.headers ?? {};
|
|
276
|
-
if (typeof headersObject.get !== 'function') {
|
|
277
|
-
headersObject = Object.fromEntries(headersObject);
|
|
278
|
-
for (const [key, value] of Object.entries(headersObject)) {
|
|
279
|
-
headers.set(key, value);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
else {
|
|
283
|
-
for (const [key, value] of headersObject.entries()) {
|
|
284
|
-
headers.set(key, value);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
// transaction id is either in the response headers or the path of the request as the first parameter
|
|
288
|
-
// TODO: we may want to move this parsing to be returned by the resolveUrl function depending on the redirect URL we've constructed
|
|
289
|
-
const txId = headers.get('x-arns-resolved-id') ??
|
|
290
|
-
redirectUrl.pathname.split('/')[1];
|
|
291
|
-
// TODO: validate nodes return content length for all responses
|
|
292
|
-
const contentLength = +(headers.get('content-length') ?? 0);
|
|
293
|
-
if (!txIdRegex.test(txId)) {
|
|
294
|
-
// no transaction id found, skip verification
|
|
295
|
-
logger?.debug('No transaction id found, skipping verification', {
|
|
296
|
-
redirectUrl,
|
|
297
|
-
originalUrl,
|
|
298
|
-
});
|
|
299
|
-
emitter?.emit('verification-skipped', {
|
|
300
|
-
originalUrl,
|
|
301
|
-
});
|
|
302
|
-
return response;
|
|
303
|
-
}
|
|
304
|
-
emitter?.emit('identified-transaction-id', {
|
|
284
|
+
// TODO: by default we will retry 3 times but this should be configurable and moved to a routing strategy
|
|
285
|
+
const maxRetries = 3;
|
|
286
|
+
const retryDelay = 1000;
|
|
287
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
288
|
+
try {
|
|
289
|
+
// select the target gateway
|
|
290
|
+
const selectedGateway = await selectGateway();
|
|
291
|
+
logger?.debug('Selected gateway', {
|
|
305
292
|
originalUrl,
|
|
306
|
-
|
|
307
|
-
txId,
|
|
293
|
+
selectedGateway: selectedGateway.toString(),
|
|
308
294
|
});
|
|
309
|
-
//
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
295
|
+
// route the request to the target gateway
|
|
296
|
+
const redirectUrl = resolveUrl({
|
|
297
|
+
originalUrl,
|
|
298
|
+
selectedGateway,
|
|
299
|
+
logger,
|
|
300
|
+
});
|
|
301
|
+
emitter?.emit('routing-succeeded', {
|
|
302
|
+
originalUrl,
|
|
303
|
+
selectedGateway: selectedGateway.toString(),
|
|
304
|
+
redirectUrl: redirectUrl.toString(),
|
|
305
|
+
});
|
|
306
|
+
logger?.debug(`Redirecting request`, {
|
|
307
|
+
originalUrl,
|
|
308
|
+
redirectUrl: redirectUrl.toString(),
|
|
309
|
+
});
|
|
310
|
+
// make the request to the target gateway using the redirect url and http client
|
|
311
|
+
const response = await fn(redirectUrl.toString(), ...rest);
|
|
312
|
+
// TODO: trigger a routing event with the raw response object?
|
|
313
|
+
logger?.debug(`Successfully routed request to gateway`, {
|
|
314
|
+
redirectUrl: redirectUrl.toString(),
|
|
315
|
+
originalUrl: originalUrl.toString(),
|
|
316
|
+
});
|
|
317
|
+
// only verify data if the redirect url is different from the original url
|
|
318
|
+
if (response && redirectUrl.toString() !== originalUrl.toString()) {
|
|
319
|
+
if (verifyData) {
|
|
320
|
+
// if the headers do not have .get on them, we need to parse the headers manually
|
|
321
|
+
const headers = new Headers();
|
|
322
|
+
const headersObject = response.headers ?? {};
|
|
323
|
+
if (headersObject instanceof Map) {
|
|
324
|
+
for (const [key, value] of headersObject.entries()) {
|
|
325
|
+
headers.set(key, value);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else if (headersObject instanceof Headers) {
|
|
329
|
+
for (const [key, value] of headersObject.entries()) {
|
|
330
|
+
headers.set(key, value);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else if (headersObject !== undefined &&
|
|
334
|
+
typeof headersObject === 'object') {
|
|
335
|
+
for (const [key, value] of Object.entries(headersObject)) {
|
|
336
|
+
headers.set(key, value);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
throw new Error('Gateway did not return headers needed for verification', {
|
|
341
|
+
cause: {
|
|
342
|
+
redirectUrl: redirectUrl.toString(),
|
|
343
|
+
originalUrl: originalUrl.toString(),
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
// transaction id is either in the response headers or the path of the request as the first parameter
|
|
348
|
+
// TODO: we may want to move this parsing to be returned by the resolveUrl function depending on the redirect URL we've constructed
|
|
349
|
+
const txId = headers.get('x-arns-resolved-id') ??
|
|
350
|
+
redirectUrl.pathname.split('/')[1];
|
|
351
|
+
// TODO: validate nodes return content length for all responses
|
|
352
|
+
const contentLength = +(headers.get('content-length') ?? 0);
|
|
353
|
+
if (!txIdRegex.test(txId)) {
|
|
354
|
+
// no transaction id found, skip verification
|
|
355
|
+
logger?.debug('No transaction id found, skipping verification', {
|
|
356
|
+
redirectUrl: redirectUrl.toString(),
|
|
357
|
+
originalUrl,
|
|
358
|
+
});
|
|
359
|
+
emitter?.emit('verification-skipped', {
|
|
360
|
+
originalUrl,
|
|
361
|
+
});
|
|
362
|
+
return response;
|
|
363
|
+
}
|
|
364
|
+
emitter?.emit('identified-transaction-id', {
|
|
365
|
+
originalUrl,
|
|
366
|
+
selectedGateway: redirectUrl.toString(),
|
|
339
367
|
txId,
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
txId,
|
|
354
|
-
emitter,
|
|
355
|
-
});
|
|
356
|
-
if (responseBody instanceof ReadableStream) {
|
|
357
|
-
// specific to fetch
|
|
358
|
-
return wrapVerifiedResponse(response, newClientStream, txId);
|
|
359
|
-
}
|
|
360
|
-
else if (responseBody instanceof Readable) {
|
|
361
|
-
// overwrite the response body with the new client stream
|
|
362
|
-
response.txId = txId;
|
|
363
|
-
response.body = newClientStream;
|
|
364
|
-
return response;
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
// TODO: content-application/json and it's smaller than 10mb
|
|
368
|
-
// TODO: add tests and verify this works for all non-Readable/streamed responses
|
|
369
|
-
try {
|
|
370
|
-
// if strict set to true
|
|
371
|
-
await verifyData({
|
|
372
|
-
data: responseBody,
|
|
373
|
-
txId,
|
|
368
|
+
});
|
|
369
|
+
// parse out the key that contains the response body, we'll use it later when updating the response object
|
|
370
|
+
const responseDataKey = response.body
|
|
371
|
+
? 'body'
|
|
372
|
+
: response.data
|
|
373
|
+
? 'data'
|
|
374
|
+
: undefined;
|
|
375
|
+
if (responseDataKey === undefined) {
|
|
376
|
+
throw new Error('No data body or data provided, skipping verification', {
|
|
377
|
+
cause: {
|
|
378
|
+
redirectUrl: redirectUrl.toString(),
|
|
379
|
+
originalUrl: originalUrl.toString(),
|
|
380
|
+
},
|
|
374
381
|
});
|
|
375
|
-
|
|
376
|
-
|
|
382
|
+
}
|
|
383
|
+
const responseBody = response[responseDataKey];
|
|
384
|
+
// TODO: determine if it is data item or L1 transaction, and tell the verifier accordingly, just drop in hit to graphql now
|
|
385
|
+
if (txId === undefined) {
|
|
386
|
+
throw new Error('Failed to parse data hash from response headers', {
|
|
387
|
+
cause: {
|
|
388
|
+
redirectUrl: redirectUrl.toString(),
|
|
389
|
+
originalUrl: originalUrl.toString(),
|
|
390
|
+
txId,
|
|
391
|
+
},
|
|
377
392
|
});
|
|
378
393
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
394
|
+
else if (responseBody === undefined) {
|
|
395
|
+
throw new Error('No data body provided, skipping verification', {
|
|
396
|
+
cause: {
|
|
397
|
+
redirectUrl: redirectUrl.toString(),
|
|
398
|
+
originalUrl: originalUrl.toString(),
|
|
399
|
+
txId,
|
|
400
|
+
},
|
|
383
401
|
});
|
|
384
|
-
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
logger?.debug('Verifying data hash for txId', {
|
|
405
|
+
redirectUrl: redirectUrl.toString(),
|
|
406
|
+
originalUrl: originalUrl.toString(),
|
|
385
407
|
txId,
|
|
386
|
-
error,
|
|
387
408
|
});
|
|
409
|
+
if (responseBody instanceof ReadableStream ||
|
|
410
|
+
responseBody instanceof Readable) {
|
|
411
|
+
const newClientStream = tapAndVerifyStream({
|
|
412
|
+
originalStream: responseBody,
|
|
413
|
+
contentLength,
|
|
414
|
+
verifyData,
|
|
415
|
+
txId,
|
|
416
|
+
emitter,
|
|
417
|
+
strict,
|
|
418
|
+
});
|
|
419
|
+
if (response instanceof Response) {
|
|
420
|
+
// specific to fetch
|
|
421
|
+
return wrapVerifiedResponse(response, newClientStream, txId);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// overwrite the response body with the new client stream
|
|
425
|
+
response.txId = txId;
|
|
426
|
+
response.body = newClientStream;
|
|
427
|
+
return response;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
// TODO: content-application/json and it's smaller than 10mb
|
|
432
|
+
// TODO: add tests and verify this works for all non-Readable/streamed responses
|
|
433
|
+
if (strict) {
|
|
434
|
+
// In strict mode, wait for verification before returning response
|
|
435
|
+
try {
|
|
436
|
+
await verifyData({
|
|
437
|
+
data: responseBody,
|
|
438
|
+
txId,
|
|
439
|
+
});
|
|
440
|
+
emitter?.emit('verification-succeeded', { txId });
|
|
441
|
+
return response;
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
logger?.debug('Failed to verify data hash', {
|
|
445
|
+
error,
|
|
446
|
+
txId,
|
|
447
|
+
});
|
|
448
|
+
emitter?.emit('verification-failed', { txId, error });
|
|
449
|
+
throw new Error('Verification failed', { cause: error });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// In non-strict mode, perform verification in the background
|
|
454
|
+
verifyData({
|
|
455
|
+
data: responseBody,
|
|
456
|
+
txId,
|
|
457
|
+
})
|
|
458
|
+
.then(() => {
|
|
459
|
+
emitter?.emit('verification-succeeded', { txId });
|
|
460
|
+
})
|
|
461
|
+
.catch((error) => {
|
|
462
|
+
logger?.debug('Failed to verify data hash', {
|
|
463
|
+
error,
|
|
464
|
+
txId,
|
|
465
|
+
});
|
|
466
|
+
emitter?.emit('verification-failed', { txId, error });
|
|
467
|
+
});
|
|
468
|
+
return response;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
388
471
|
}
|
|
389
|
-
return response;
|
|
390
472
|
}
|
|
391
473
|
}
|
|
474
|
+
// TODO: if strict - wait for verification to finish and succeed before returning the response
|
|
475
|
+
return response;
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
logger?.debug('Failed to route request', {
|
|
479
|
+
error: error.message,
|
|
480
|
+
stack: error.stack,
|
|
481
|
+
originalUrl,
|
|
482
|
+
attempt: i + 1,
|
|
483
|
+
maxRetries,
|
|
484
|
+
});
|
|
485
|
+
if (i < maxRetries - 1) {
|
|
486
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
487
|
+
}
|
|
392
488
|
}
|
|
393
489
|
}
|
|
394
|
-
|
|
395
|
-
|
|
490
|
+
throw new Error('Failed to route request after max retries', {
|
|
491
|
+
cause: {
|
|
492
|
+
originalUrl,
|
|
493
|
+
maxRetries,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
396
496
|
};
|
|
397
497
|
return new Proxy(httpClient, {
|
|
398
498
|
// support direct calls: fetch('ar://…', options)
|
|
@@ -416,87 +516,116 @@ export const createWayfinderClient = ({ httpClient, resolveUrl, verifyData, emit
|
|
|
416
516
|
*/
|
|
417
517
|
export class Wayfinder {
|
|
418
518
|
/**
|
|
419
|
-
* The
|
|
519
|
+
* The native http client used by wayfinder. By default, the native fetch api is used.
|
|
420
520
|
*
|
|
421
521
|
* @example
|
|
422
522
|
* const wayfinder = new Wayfinder({
|
|
423
|
-
*
|
|
424
|
-
* gatewaysProvider: new SimpleCacheGatewaysProvider({
|
|
425
|
-
* gatewaysProvider: new NetworkGatewaysProvider({ ario: ARIO.mainnet() }),
|
|
426
|
-
* ttlSeconds: 60 * 60 * 24, // 1 day
|
|
427
|
-
* }),
|
|
428
|
-
* }),
|
|
523
|
+
* httpClient: axios,
|
|
429
524
|
* });
|
|
430
525
|
*
|
|
431
|
-
* // Returns a target gateway based on the routing strategy
|
|
432
|
-
* const targetGateway = await wayfinder.router.getTargetGateway();
|
|
433
526
|
*/
|
|
434
|
-
|
|
527
|
+
httpClient;
|
|
435
528
|
/**
|
|
436
|
-
* The
|
|
529
|
+
* The gateways provider is responsible for providing the list of gateways to use for routing requests.
|
|
437
530
|
*
|
|
438
531
|
* @example
|
|
439
532
|
* const wayfinder = new Wayfinder({
|
|
440
|
-
*
|
|
441
|
-
* gatewaysProvider: new
|
|
442
|
-
*
|
|
443
|
-
* ttlSeconds: 60 * 60 * 24, // 1 day
|
|
444
|
-
* }),
|
|
533
|
+
* gatewaysProvider: new SimpleCacheGatewaysProvider({
|
|
534
|
+
* gatewaysProvider: new NetworkGatewaysProvider({ ario: ARIO.mainnet() }),
|
|
535
|
+
* ttlSeconds: 60 * 60 * 24, // 1 day
|
|
445
536
|
* }),
|
|
446
|
-
* httpClient: axios,
|
|
447
537
|
* });
|
|
448
|
-
*
|
|
449
538
|
*/
|
|
450
|
-
|
|
539
|
+
gatewaysProvider;
|
|
451
540
|
/**
|
|
452
|
-
* The
|
|
541
|
+
* The routing strategy to use when routing requests.
|
|
453
542
|
*
|
|
454
543
|
* @example
|
|
455
544
|
* const wayfinder = new Wayfinder({
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
* gatewaysProvider: new NetworkGatewaysProvider({ ario: ARIO.mainnet() }),
|
|
459
|
-
* ttlSeconds: 60 * 60 * 24, // 1 day
|
|
460
|
-
* }),
|
|
545
|
+
* strategy: new FastestPingStrategy({
|
|
546
|
+
* timeoutMs: 1000,
|
|
461
547
|
* }),
|
|
462
|
-
* httpClient: axios,
|
|
463
548
|
* });
|
|
549
|
+
*/
|
|
550
|
+
routingStrategy;
|
|
551
|
+
/**
|
|
552
|
+
* A helper function that resolves the redirect url for ar:// requests to a target gateway.
|
|
553
|
+
*
|
|
554
|
+
* Note: no verification is done when resolving an ar://<path> url to a wayfinder route.
|
|
555
|
+
* In order to verify the data, you must use the `request` function or request the data and
|
|
556
|
+
* verify it yourself via the `verifyData` function.
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* const { resolveUrl } = new Wayfinder();
|
|
464
560
|
*
|
|
465
561
|
* // returns the redirected URL based on the routing strategy and the original url
|
|
466
|
-
* const redirectUrl = await
|
|
562
|
+
* const redirectUrl = await resolveUrl({ originalUrl: 'ar://example' });
|
|
563
|
+
*
|
|
564
|
+
* window.open(redirectUrl.toString(), '_blank');
|
|
467
565
|
*/
|
|
468
566
|
resolveUrl;
|
|
469
567
|
/**
|
|
470
|
-
*
|
|
568
|
+
*
|
|
569
|
+
* A wrapped http client that supports ar:// protocol. If a verification strategy is provided,
|
|
570
|
+
* the request will be verified and events will be emitted as the request is processed.
|
|
471
571
|
*
|
|
472
572
|
* @example
|
|
473
|
-
* const
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
* gatewaysProvider: new
|
|
477
|
-
*
|
|
573
|
+
* const wayfinder = new Wayfinder({
|
|
574
|
+
* verificationStrategy: new HashVerificationStrategy({
|
|
575
|
+
* trustedHashProvider: new TrustedGatewaysHashProvider({
|
|
576
|
+
* gatewaysProvider: new StaticGatewaysProvider({
|
|
577
|
+
* gateways: ['https://permagate.io'],
|
|
578
|
+
* }),
|
|
478
579
|
* }),
|
|
479
580
|
* }),
|
|
480
|
-
* httpClient: axios,
|
|
481
|
-
* });;
|
|
482
|
-
*
|
|
483
|
-
* const response = await wayfind('ar://example', {
|
|
484
|
-
* method: 'POST',
|
|
485
|
-
* data: {
|
|
486
|
-
* name: 'John Doe',
|
|
487
|
-
* },
|
|
488
581
|
* })
|
|
582
|
+
*
|
|
583
|
+
* // request an arns name
|
|
584
|
+
* const response = await wayfinder.request('ar://ardrive')
|
|
585
|
+
*
|
|
586
|
+
* // request a transaction id
|
|
587
|
+
* const response = await wayfinder.request('ar://1234567890')
|
|
588
|
+
*
|
|
589
|
+
* // request a transaction id with a custom http client
|
|
590
|
+
* const response = await wayfinder.request('ar://1234567890')
|
|
591
|
+
*
|
|
592
|
+
* // Set strict mode to true to make verification blocking
|
|
593
|
+
* const wayfinder = new Wayfinder({
|
|
594
|
+
* strict: true,
|
|
595
|
+
* });
|
|
596
|
+
*
|
|
597
|
+
* // This will throw an error if verification fails
|
|
598
|
+
* try {
|
|
599
|
+
* const response = await wayfinder.request('ar://1234567890');
|
|
600
|
+
* } catch (error) {
|
|
601
|
+
* console.error('Verification failed', error);
|
|
602
|
+
* }
|
|
489
603
|
*/
|
|
490
604
|
request;
|
|
491
|
-
|
|
492
|
-
|
|
605
|
+
/**
|
|
606
|
+
* The function that verifies the data hash for a given transaction id.
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* const wayfinder = new Wayfinder({
|
|
610
|
+
* verifyData: (data, txId) => {
|
|
611
|
+
* // some custom verification logic
|
|
612
|
+
* return true;
|
|
613
|
+
* },
|
|
614
|
+
* });
|
|
615
|
+
*/
|
|
493
616
|
verifyData;
|
|
617
|
+
/**
|
|
618
|
+
* Whether verification should be strict (blocking) or not.
|
|
619
|
+
* If true, verification failures will cause requests to fail.
|
|
620
|
+
* If false, verification will be performed asynchronously and failures will only emit events.
|
|
621
|
+
*/
|
|
622
|
+
strict;
|
|
494
623
|
/**
|
|
495
624
|
* The event emitter for wayfinder that emits verification events.
|
|
496
625
|
*
|
|
497
626
|
* const wayfinder = new Wayfinder()
|
|
498
627
|
*
|
|
499
|
-
* wayfinder.emitter.on('verification-
|
|
628
|
+
* wayfinder.emitter.on('verification-succeeded', (event) => {
|
|
500
629
|
* console.log('Verification passed!', event);
|
|
501
630
|
* })
|
|
502
631
|
*
|
|
@@ -522,63 +651,74 @@ export class Wayfinder {
|
|
|
522
651
|
*
|
|
523
652
|
* const response = await wayfind('ar://example');
|
|
524
653
|
*/
|
|
525
|
-
// TODO: consider changing this to events or event emitter
|
|
526
654
|
emitter;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
655
|
+
/**
|
|
656
|
+
* The constructor for the wayfinder
|
|
657
|
+
* @param httpClient - the http client to use for requests
|
|
658
|
+
* @param routingStrategy - the routing strategy to use for requests
|
|
659
|
+
* @param verificationStrategy - the verification strategy to use for requests
|
|
660
|
+
* @param gatewaysProvider - the gateways provider to use for routing requests
|
|
661
|
+
* @param logger - the logger to use for logging
|
|
662
|
+
* @param strict - if true, verification will be blocking and will fail requests if verification fails; if false, verification will be non-blocking
|
|
663
|
+
*/
|
|
664
|
+
constructor({ httpClient = fetch, logger = Logger.default, gatewaysProvider = new SimpleCacheGatewaysProvider({
|
|
665
|
+
gatewaysProvider: new NetworkGatewaysProvider({
|
|
666
|
+
ario: ARIO.mainnet(),
|
|
533
667
|
}),
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
668
|
+
ttlSeconds: 60 * 60, // 1 hour
|
|
669
|
+
}), routingStrategy = new FastestPingRoutingStrategy({
|
|
670
|
+
timeoutMs: 1000,
|
|
671
|
+
}), verificationStrategy = new HashVerificationStrategy({
|
|
537
672
|
trustedHashProvider: new TrustedGatewaysHashProvider({
|
|
538
673
|
gatewaysProvider: new StaticGatewaysProvider({
|
|
539
674
|
gateways: ['https://permagate.io'],
|
|
540
675
|
}),
|
|
541
676
|
}),
|
|
542
|
-
}), events
|
|
677
|
+
}), events = {
|
|
678
|
+
onVerificationPassed: (event) => {
|
|
679
|
+
logger.debug('Verification passed!', event);
|
|
680
|
+
},
|
|
681
|
+
onVerificationFailed: (event) => {
|
|
682
|
+
logger.error('Verification failed!', event);
|
|
683
|
+
},
|
|
684
|
+
onVerificationProgress: (event) => {
|
|
685
|
+
logger.debug('Verification progress!', event);
|
|
686
|
+
},
|
|
687
|
+
}, strict = false,
|
|
543
688
|
// TODO: stats provider
|
|
544
689
|
}) {
|
|
545
|
-
this.
|
|
690
|
+
this.routingStrategy = routingStrategy;
|
|
691
|
+
this.gatewaysProvider = gatewaysProvider;
|
|
546
692
|
this.httpClient = httpClient;
|
|
547
693
|
this.emitter = new WayfinderEmitter(events);
|
|
548
|
-
this.verifyData =
|
|
694
|
+
this.verifyData =
|
|
695
|
+
verificationStrategy.verifyData.bind(verificationStrategy);
|
|
696
|
+
this.strict = strict;
|
|
697
|
+
// top level function to easily resolve wayfinder urls using the routing strategy and gateways provider
|
|
549
698
|
this.resolveUrl = async ({ originalUrl, logger }) => {
|
|
699
|
+
const selectedGateway = await this.routingStrategy.selectGateway({
|
|
700
|
+
gateways: await this.gatewaysProvider.getGateways(),
|
|
701
|
+
});
|
|
550
702
|
return resolveWayfinderUrl({
|
|
551
703
|
originalUrl,
|
|
552
|
-
|
|
704
|
+
selectedGateway,
|
|
553
705
|
logger,
|
|
554
706
|
});
|
|
555
707
|
};
|
|
708
|
+
// create a wayfinder client with the routing strategy and gateways provider
|
|
556
709
|
this.request = createWayfinderClient({
|
|
557
710
|
httpClient,
|
|
558
|
-
|
|
711
|
+
selectGateway: async () => {
|
|
712
|
+
return this.routingStrategy.selectGateway({
|
|
713
|
+
gateways: await this.gatewaysProvider.getGateways(),
|
|
714
|
+
});
|
|
715
|
+
},
|
|
716
|
+
resolveUrl: resolveWayfinderUrl,
|
|
559
717
|
verifyData: this.verifyData,
|
|
560
718
|
emitter: this.emitter,
|
|
561
719
|
logger,
|
|
720
|
+
strict,
|
|
562
721
|
});
|
|
563
|
-
logger?.debug(`Wayfinder initialized with ${
|
|
722
|
+
logger?.debug(`Wayfinder initialized with ${routingStrategy.constructor.name} routing strategy`);
|
|
564
723
|
}
|
|
565
724
|
}
|
|
566
|
-
// TODO: add a chart for verification strategies and what they do
|
|
567
|
-
// include complexity, performance, and security
|
|
568
|
-
// explain use cases that each strategy is best for
|
|
569
|
-
// e.g.
|
|
570
|
-
/**
|
|
571
|
-
*
|
|
572
|
-
* type | complexity | performance | security
|
|
573
|
-
* ---------|------------|-------------|---------
|
|
574
|
-
* hash | low | high | low
|
|
575
|
-
* ---------|------------|-------------|---------
|
|
576
|
-
* data root | medium | medium | low | only L1
|
|
577
|
-
* ---------|------------|-------------|---------
|
|
578
|
-
* signature | medium | medium | medium
|
|
579
|
-
* ---------|------------|-------------|---------
|
|
580
|
-
* composite | high | low | high
|
|
581
|
-
* ---------|------------|-------------|---------
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*/
|