Skip to content

Commit da849da

Browse files
author
dtfiedler
committed
feat(api): add peer list endpoints
These will include gateways, and node lists. We will look to add peer weights based on retrival/network metrcis
1 parent a9d608a commit da849da

File tree

10 files changed

+193
-60
lines changed

10 files changed

+193
-60
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126
"test": "node --import ./register.js --test --test-concurrency 1 src/**/*.test.ts",
127127
"test:ci": "npx c8 -r lcov node --import ./register.js --test --test-concurrency 1 --test-reporter=spec --test-reporter-destination=stdout src/**/*.test.ts",
128128
"test:coverage": "npx c8 -r text -r html node --import ./register.js --test --test-concurrency 1 src/**/*.test.ts",
129-
"test:e2e": "node --import ./register.js --test --test-concurrency 1 --test-reporter=spec --test-reporter-destination=stdout test/**/*.test.ts",
129+
"test:e2e": "node --import ./register.js --test --test-concurrency 1 --test-reporter=spec --test-reporter-destination=stdout test/**/data.test.ts",
130130
"lint:check": "eslint src test",
131131
"lint:fix": "eslint --fix src test"
132132
}

src/arweave/composite-client.ts

+4
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ export class ArweaveCompositeClient
379379
}
380380
}
381381

382+
getPeers(): Record<string, Peer> {
383+
return this.peers;
384+
}
385+
382386
selectPeers(peerCount: number, peerListName: WeightedPeerListName): string[] {
383387
const log = this.log.child({ method: 'selectPeers', peerListName });
384388

src/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export const headerNames = {
2626
cache: 'X-Cache',
2727
rootTransactionId: 'X-AR-IO-Root-Transaction-Id',
2828
dataItemDataOffset: 'X-AR-IO-Data-Item-Data-Offset',
29+
dataItemOwnerOffset: 'X-AR-IO-Data-Item-Owner-Offset',
30+
dataItemSignatureOffset: 'X-AR-IO-Data-Item-Signature-Offset',
2931
arnsTtlSeconds: 'X-ArNS-TTL-Seconds',
3032
arnsResolvedId: 'X-ArNS-Resolved-Id',
3133
arnsProcessId: 'X-ArNS-Process-Id',

src/data/ar-io-data-source.ts

+5
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ export class ArIODataSource implements ContiguousDataSource {
136136
}
137137
}
138138

139+
getPeers(): Record<string, string> {
140+
// TODO: any other data we want to return
141+
return this.peers;
142+
}
143+
139144
async updatePeerList() {
140145
const log = this.log.child({ method: 'updatePeerList' });
141146
log.info('Fetching AR.IO network peer list');

src/routes/ar-io.ts

+7
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ export const arIoInfoHandler = (_req: Request, res: Response) => {
103103
};
104104
arIoRouter.get('/ar-io/info', arIoInfoHandler);
105105

106+
// peer list
107+
arIoRouter.get('/ar-io/peers', async (_req, res) => {
108+
const gateways = await system.arIODataSource.peers;
109+
const nodes = await system.arweaveClient.getPeers();
110+
res.json({ gateways, nodes });
111+
});
112+
106113
// Only allow access to admin routes if the bearer token matches the admin api key
107114
arIoRouter.use('/ar-io/admin', (req, res, next) => {
108115
if (req.headers.authorization === `Bearer ${config.ADMIN_API_KEY}`) {

src/routes/data/handlers.ts

+22
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,28 @@ const setDataHeaders = ({
134134
);
135135
}
136136

137+
if (
138+
dataAttributes?.rootParentOffset !== undefined &&
139+
dataAttributes?.ownerOffset !== undefined
140+
) {
141+
res.header(
142+
headerNames.dataItemOwnerOffset,
143+
(dataAttributes.rootParentOffset + dataAttributes.ownerOffset).toString(),
144+
);
145+
}
146+
147+
if (
148+
dataAttributes?.rootParentOffset !== undefined &&
149+
dataAttributes?.signatureOffset !== undefined
150+
) {
151+
res.header(
152+
headerNames.dataItemSignatureOffset,
153+
(
154+
dataAttributes.rootParentOffset + dataAttributes.signatureOffset
155+
).toString(),
156+
);
157+
}
158+
137159
setDigestStableVerifiedHeaders({ res, dataAttributes, data });
138160
};
139161

src/system.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ const gatewaysDataSource = new GatewaysDataSource({
422422
trustedGatewaysUrls: config.TRUSTED_GATEWAYS_URLS,
423423
});
424424

425-
const arIODataSource = new ArIODataSource({
425+
export const arIODataSource = new ArIODataSource({
426426
log,
427427
networkProcess,
428428
nodeWallet: config.AR_IO_WALLET,

src/types.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,8 @@ export interface ContiguousDataAttributes {
546546
size: number;
547547
contentEncoding?: string;
548548
contentType?: string;
549+
ownerOffset?: number;
550+
signatureOffset?: number;
549551
rootTransactionId?: string;
550552
rootParentOffset?: number;
551553
dataOffset?: number;

test/end-to-end/ar-io.test.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* AR.IO Gateway
3+
* Copyright (C) 2022-2023 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 { strict as assert } from 'node:assert';
19+
import { after, before, describe, it } from 'node:test';
20+
import { StartedDockerComposeEnvironment } from 'testcontainers';
21+
import axios from 'axios';
22+
import { cleanDb, composeUp } from './utils.js';
23+
24+
let compose: StartedDockerComposeEnvironment;
25+
26+
before(async function () {
27+
await cleanDb();
28+
29+
compose = await composeUp({
30+
START_WRITERS: 'false',
31+
});
32+
});
33+
34+
after(async function () {
35+
await compose.down();
36+
});
37+
38+
describe('ArIO', function () {
39+
it('should return the network contract info on the /info endpoint', async function () {
40+
const res = await axios.get('http://localhost:4000/ar-io/info');
41+
assert.ok(res.data.wallet);
42+
assert.ok(res.data.processId);
43+
assert.ok(res.data.supportedManifestVersions);
44+
assert.ok(res.data.release);
45+
});
46+
47+
it('should return a list of peers', async function () {
48+
const res = await axios.get('http://localhost:4000/ar-io/peers');
49+
assert.ok(res.data.length > 0);
50+
});
51+
});

test/end-to-end/data.test.ts

+98-58
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const tx3 = 'lbeIMUvoEqR2q-pKsT4Y5tz6mm9ppemReyLnQ8P7XpM';
5252
// manifest with paths without trailing slash
5353
const tx4 = 'sYaO7sklQ8FyObQNLy7kDbEvwUNKKes7mUnv-_Ri9bE';
5454

55+
// bundle with data item
5556
const bundle1 = '73QwVewKc0hXmuiaahtGJqHEY5pb85SoqCC33VE0Teg';
5657

5758
describe('Data', function () {
@@ -272,7 +273,7 @@ describe('X-Cache header', { skip: isTestFiltered(['flaky']) }, function () {
272273
});
273274
});
274275

275-
describe('X-AR-IO-Root-Transaction-Id header', function () {
276+
describe('Data item headers', function () {
276277
let compose: StartedDockerComposeEnvironment;
277278

278279
before(async function () {
@@ -302,87 +303,126 @@ describe('X-AR-IO-Root-Transaction-Id header', function () {
302303
await compose.down();
303304
});
304305

305-
it('Verifying header for trascation', async function () {
306-
const bundleRes = await axios.head(`http://localhost:4000/raw/${bundle1}`);
306+
describe('X-AR-IO-Root-Transaction-Id header', function () {
307+
it('Verifying header for trascation', async function () {
308+
const bundleRes = await axios.head(
309+
`http://localhost:4000/raw/${bundle1}`,
310+
);
307311

308-
assert.equal(bundleRes.headers['x-ar-io-root-transaction-id'], undefined);
309-
});
312+
assert.equal(bundleRes.headers['x-ar-io-root-transaction-id'], undefined);
313+
});
310314

311-
it('Verifying header for data item', async function () {
312-
const datasItemRes = await axios.head(`http://localhost:4000/raw/${tx1}`);
315+
it('Verifying header for data item', async function () {
316+
const datasItemRes = await axios.head(`http://localhost:4000/raw/${tx1}`);
313317

314-
assert.equal(datasItemRes.headers['x-ar-io-root-transaction-id'], bundle1);
318+
assert.equal(
319+
datasItemRes.headers['x-ar-io-root-transaction-id'],
320+
bundle1,
321+
);
322+
});
315323
});
316-
});
317324

318-
describe('X-AR-IO-Data-Item-Data-Offset header', function () {
319-
let compose: StartedDockerComposeEnvironment;
320-
const bundle = '-H3KW7RKTXMg5Miq2jHx36OHSVsXBSYuE2kxgsFj6OQ';
321-
const bdi = 'fLxHz2WbpNFL7x1HrOyUlsAVHYaKSyj6IqgCJlFuv9g';
322-
const di = 'Dc-q5iChuRWcsjVBFstEqmLTx4SWkGZxcVO9OTEGjkQ';
325+
describe('X-AR-IO-Data-Item-Data-Offset header', function () {
326+
it('Verifying header for L1 bundle', async function () {
327+
const res = await axios.head(`http://localhost:4000/raw/${bundle1}`);
323328

324-
before(async function () {
325-
await cleanDb();
326-
327-
compose = await composeUp({
328-
START_WRITERS: 'false',
329-
GET_DATA_CIRCUIT_BREAKER_TIMEOUT_MS: '100000',
329+
assert.equal(res.headers['x-ar-io-data-item-data-offset'], undefined);
330330
});
331331

332-
await axios.post(
333-
'http://localhost:4000/ar-io/admin/queue-bundle',
334-
{ id: bundle },
335-
{
336-
headers: {
337-
'Content-Type': 'application/json',
338-
Authorization: 'Bearer secret',
339-
},
340-
},
341-
);
332+
it('Verifying header for bundle data item', async function () {
333+
const res = await axios.head(`http://localhost:4000/raw/${tx1}`);
342334

343-
await waitForDataItemToBeIndexed({ id: di });
335+
assert.equal(res.headers['x-ar-io-data-item-data-offset'], '53293');
336+
});
344337
});
345338

346-
after(async function () {
347-
await compose.down();
348-
});
339+
describe('X-AR-IO-Data-Item-Owner-Offset header', function () {
340+
it('Verifying header for L1 bundle', async function () {
341+
const res = await axios.head(`http://localhost:4000/raw/${bundle1}`);
349342

350-
it('Verifying header for L1 bundle', async function () {
351-
const res = await axios.head(`http://localhost:4000/raw/${bundle}`);
343+
assert.equal(res.headers['x-ar-io-data-item-owner-offset'], undefined);
344+
});
345+
346+
it('Verifying header for bundle data item', async function () {
347+
const res = await axios.head(`http://localhost:4000/raw/${tx1}`);
352348

353-
assert.equal(res.headers['x-ar-io-data-item-data-offset'], undefined);
349+
assert.equal(res.headers['x-ar-io-data-item-owner-offset'], '3072');
350+
});
354351
});
355352

356-
it('Verifying header for bundle data item', async function () {
357-
const res = await axios.head(`http://localhost:4000/raw/${bdi}`);
353+
describe('X-AR-IO-Data-Item-Signature-Offset header', function () {
354+
it('Verifying header for L1 bundle', async function () {
355+
const res = await axios.head(`http://localhost:4000/raw/${bundle1}`);
358356

359-
assert.equal(res.headers['x-ar-io-data-item-data-offset'], '3072');
360-
});
357+
assert.equal(
358+
res.headers['x-ar-io-data-item-signature-offset'],
359+
undefined,
360+
);
361+
});
361362

362-
it('Verifying header for data item inside bundle data item', async function () {
363-
const res = await axios.head(`http://localhost:4000/raw/${di}`);
363+
it('Verifying header for bundle data item', async function () {
364+
const res = await axios.head(`http://localhost:4000/raw/${tx1}`);
364365

365-
assert.equal(res.headers['x-ar-io-data-item-data-offset'], '5783');
366+
assert.equal(res.headers['x-ar-io-data-item-signature-offset'], '3072');
367+
});
366368
});
367369

368-
it('Comparing data downloaded through L1 tx using offsets and data item data', async function () {
369-
const dataItem = await axios.get(`http://localhost:4000/${di}`, {
370-
responseType: 'arraybuffer',
370+
describe('Nested data item headers', function () {
371+
const bundle = '-H3KW7RKTXMg5Miq2jHx36OHSVsXBSYuE2kxgsFj6OQ';
372+
const bdi = 'fLxHz2WbpNFL7x1HrOyUlsAVHYaKSyj6IqgCJlFuv9g';
373+
const di = 'Dc-q5iChuRWcsjVBFstEqmLTx4SWkGZxcVO9OTEGjkQ';
374+
before(async function () {
375+
await axios.post(
376+
'http://localhost:4000/ar-io/admin/queue-bundle',
377+
{ id: bundle },
378+
{
379+
headers: {
380+
'Content-Type': 'application/json',
381+
Authorization: 'Bearer secret',
382+
},
383+
},
384+
);
385+
386+
await waitForDataItemToBeIndexed({ id: di });
371387
});
372-
const dataItemOffset = parseInt(
373-
dataItem.headers['x-ar-io-data-item-data-offset'],
374-
10,
375-
);
376-
const dataItemSize = parseInt(dataItem.headers['content-length'], 10);
377388

378-
const rangeRequest = await axios.get(`http://localhost:4000/${bundle}`, {
379-
headers: {
380-
Range: `bytes=${dataItemOffset}-${dataItemOffset + dataItemSize - 1}`,
381-
},
382-
responseType: 'arraybuffer',
389+
it('Verifying header for L1 bundle', async function () {
390+
const res = await axios.head(`http://localhost:4000/raw/${bundle}`);
391+
392+
assert.equal(res.headers['x-ar-io-data-item-data-offset'], undefined);
383393
});
384394

385-
assert.deepEqual(rangeRequest.data, dataItem.data);
395+
it('Verifying header for bundle data item', async function () {
396+
const res = await axios.head(`http://localhost:4000/raw/${bdi}`);
397+
398+
assert.equal(res.headers['x-ar-io-data-item-data-offset'], '3072');
399+
});
400+
401+
it('Verifying header for data item inside bundle data item', async function () {
402+
const res = await axios.head(`http://localhost:4000/raw/${di}`);
403+
404+
assert.equal(res.headers['x-ar-io-data-item-data-offset'], '5783');
405+
});
406+
407+
it('Comparing data downloaded through L1 tx using offsets and data item data', async function () {
408+
const dataItem = await axios.get(`http://localhost:4000/${di}`, {
409+
responseType: 'arraybuffer',
410+
});
411+
const dataItemOffset = parseInt(
412+
dataItem.headers['x-ar-io-data-item-data-offset'],
413+
10,
414+
);
415+
const dataItemSize = parseInt(dataItem.headers['content-length'], 10);
416+
417+
const rangeRequest = await axios.get(`http://localhost:4000/${bundle}`, {
418+
headers: {
419+
Range: `bytes=${dataItemOffset}-${dataItemOffset + dataItemSize - 1}`,
420+
},
421+
responseType: 'arraybuffer',
422+
});
423+
424+
assert.deepEqual(rangeRequest.data, dataItem.data);
425+
});
386426
});
387427
});
388428

0 commit comments

Comments
 (0)