diff --git a/packages/cli/scripts/seeder.ts b/packages/cli/scripts/seeder.ts index 059c31835..c2b577738 100644 --- a/packages/cli/scripts/seeder.ts +++ b/packages/cli/scripts/seeder.ts @@ -171,16 +171,20 @@ const main = async () => { [ clientInfo.peer1.enr, getContentKey(HistoryNetworkContentType.BlockHeader, fromHexString(block[0])), - toHexString( - Block.fromRLPSerializedBlock(hexToBytes((block[1] as any).rlp), { - setHardfork: true, - }).header.serialize(), - ), - toHexString( - Block.fromRLPSerializedBlock(hexToBytes((block[1] as any).rlp), { - setHardfork: true, - }).header.serialize(), - ), + [ + [ + toHexString( + Block.fromRLPSerializedBlock(hexToBytes((block[1] as any).rlp), { + setHardfork: true, + }).header.serialize(), + ), + toHexString( + Block.fromRLPSerializedBlock(hexToBytes((block[1] as any).rlp), { + setHardfork: true, + }).header.serialize(), + ), + ], + ], ], ]) } diff --git a/packages/cli/scripts/sendOffer.ts b/packages/cli/scripts/sendOffer.ts index caf162710..96caca3a1 100644 --- a/packages/cli/scripts/sendOffer.ts +++ b/packages/cli/scripts/sendOffer.ts @@ -46,8 +46,7 @@ const main = async () => { const offer = await nodeA.request('portal_historyOffer', [ nodeBEnr.result.enr, - blockBodyContent_key, - blockBodyContent_value, + [[blockBodyContent_key, blockBodyContent_value]], ]) console.log(offer) diff --git a/packages/cli/src/rpc/modules/portal.ts b/packages/cli/src/rpc/modules/portal.ts index b0a54e96b..3ef0af9e0 100644 --- a/packages/cli/src/rpc/modules/portal.ts +++ b/packages/cli/src/rpc/modules/portal.ts @@ -1,7 +1,7 @@ import { EntryStatus } from '@chainsafe/discv5' import { ENR } from '@chainsafe/enr' import { BitArray } from '@chainsafe/ssz' -import { bytesToHex, short } from '@ethereumjs/util' +import { hexToBytes, short } from '@ethereumjs/util' import { ContentLookup, ContentMessageType, @@ -17,6 +17,7 @@ import { } from 'portalnetwork' import { INVALID_PARAMS } from '../error-code.js' +import { content_params } from '../schema/index.js' import { isValidId } from '../util.js' import { middleware, validators } from '../validators.js' @@ -84,6 +85,7 @@ const methods = [ 'portal_beaconGetEnr', 'portal_beaconDeleteEnr', 'portal_beaconLookupEnr', + 'portal_beaconOffer', // not included in portal-network-specs 'portal_historyAddEnrs', @@ -186,8 +188,11 @@ export class portal { ]) this.historyOffer = middleware(this.historyOffer.bind(this), 3, [ [validators.enr], - [validators.hex], - [validators.hex], + [content_params.ContentItems], + ]) + this.beaconOffer = middleware(this.beaconOffer.bind(this), 3, [ + [validators.enr], + [content_params.ContentItems], ]) this.historySendOffer = middleware(this.historySendOffer.bind(this), 2, [ [validators.dstId], @@ -195,8 +200,7 @@ export class portal { ]) this.stateOffer = middleware(this.stateOffer.bind(this), 3, [ [validators.enr], - [validators.hex], - [validators.hex], + [content_params.ContentItems], ]) this.stateSendOffer = middleware(this.stateSendOffer.bind(this), 2, [ [validators.dstId], @@ -802,8 +806,10 @@ export class portal { } } } - async historyOffer(params: [string, string, string]) { - const [enrHex, contentKeyHex, contentValueHex] = params + async historyOffer(params: [string, [string, string][]]) { + const [enrHex, contentItems] = params + const contentKeys = contentItems.map((item) => hexToBytes(item[0])) + const contentValues = contentItems.map((item) => hexToBytes(item[1])) const enr = ENR.decodeTxt(enrHex) if (this._history.routingTable.getWithPending(enr.nodeId)?.value === undefined) { const res = await this._history.sendPing(enr) @@ -811,11 +817,21 @@ export class portal { return '0x' } } - const res = await this._history.sendOffer( - enr.nodeId, - [fromHexString(contentKeyHex)], - [fromHexString(contentValueHex)], - ) + const res = await this._history.sendOffer(enr.nodeId, contentKeys, contentValues) + return res + } + async beaconOffer(params: [string, [string, string][]]) { + const [enrHex, contentItems] = params + const contentKeys = contentItems.map((item) => hexToBytes(item[0])) + const contentValues = contentItems.map((item) => hexToBytes(item[1])) + const enr = ENR.decodeTxt(enrHex) + if (this._beacon.routingTable.getWithPending(enr.nodeId)?.value === undefined) { + const res = await this._beacon.sendPing(enr) + if (res === undefined) { + return '0x' + } + } + const res = await this._beacon.sendOffer(enr.nodeId, contentKeys, contentValues) return res } async historySendOffer(params: [string, string[]]) { @@ -825,8 +841,10 @@ export class portal { const enr = this._history.routingTable.getWithPending(dstId)?.value return res && enr && '0x' + enr.seq.toString(16) } - async stateOffer(params: [string, string, string]) { - const [enrHex, contentKeyHex, contentValueHex] = params + async stateOffer(params: [string, [string, string][]]) { + const [enrHex, contentItems] = params + const contentKeys = contentItems.map((item) => fromHexString(item[0])) + const contentValues = contentItems.map((item) => fromHexString(item[1])) const enr = ENR.decodeTxt(enrHex) if (this._state.routingTable.getWithPending(enr.nodeId)?.value === undefined) { const res = await this._state.sendPing(enr) @@ -834,11 +852,7 @@ export class portal { return '0x' } } - const res = await this._state.sendOffer( - enr.nodeId, - [fromHexString(contentKeyHex)], - [fromHexString(contentValueHex)], - ) + const res = await this._state.sendOffer(enr.nodeId, contentKeys, contentValues) return res } async stateSendOffer(params: [string, string[]]) { diff --git a/packages/cli/src/rpc/schema/baseTypes.ts b/packages/cli/src/rpc/schema/baseTypes.ts index 797d390db..424b0878d 100644 --- a/packages/cli/src/rpc/schema/baseTypes.ts +++ b/packages/cli/src/rpc/schema/baseTypes.ts @@ -126,13 +126,13 @@ export const baseTypes = { } }, /** - * bytes2 validator to ensure has `0x` prefix and 66 bytes length + * hex string validator to ensure has `0x` prefix * @param params parameters of method * @param index index of parameter */ get hexString() { return (params: any[], index: number) => { - return validateByteString(params[index], index, 66) + return validateByteString(params[index], index) } }, } diff --git a/packages/cli/src/rpc/schema/content/params.ts b/packages/cli/src/rpc/schema/content/params.ts index 878644b4a..eaf7a6196 100644 --- a/packages/cli/src/rpc/schema/content/params.ts +++ b/packages/cli/src/rpc/schema/content/params.ts @@ -145,4 +145,50 @@ export const content_params = { if (result !== undefined) return result } }, + get ContentItem() { + return (params: any[], index: number) => { + if (!Array.isArray(params[index])) { + return { + code: INVALID_PARAMS, + message: `invalid argument ${index}: argument is not an array`, + } + } + if (params[index].length !== 2) { + return { + code: INVALID_PARAMS, + message: `invalid argument ${index}: array length is not 2`, + } + } + const [key, value] = params[index] + const keyResult = content_params.ContentKey([key], 0) + const valueResult = content_params.ContentValue([value], 1) + if (keyResult !== undefined && valueResult !== undefined) return [keyResult, valueResult] + } + }, + get ContentItems() { + return (params: any[], index: number) => { + if (!Array.isArray(params[index])) { + return { + code: INVALID_PARAMS, + message: `invalid argument ${index}: argument is not an array`, + } + } + if (params[index].length < 1) { + return { + code: INVALID_PARAMS, + message: `invalid argument ${index}: array is empty`, + } + } + if (params[index].length > 64) { + return { + code: INVALID_PARAMS, + message: `invalid argument ${index}: array is too long`, + } + } + for (const value of params[index]) { + const result = content_params.ContentItem([value], 0) + if (result !== undefined) return result + } + } + }, } diff --git a/packages/portalnetwork/src/networks/beacon/beacon.ts b/packages/portalnetwork/src/networks/beacon/beacon.ts index feab28155..8dc78f6a0 100644 --- a/packages/portalnetwork/src/networks/beacon/beacon.ts +++ b/packages/portalnetwork/src/networks/beacon/beacon.ts @@ -768,7 +768,14 @@ export class BeaconLightClientNetwork extends BaseNetwork { * @param dstId node ID of a peer * @param contentKeys content keys being offered as specified by the subnetwork */ - public override sendOffer = async (dstId: string, contentKeys: Uint8Array[]) => { + public override sendOffer = async ( + dstId: string, + contentKeys: Uint8Array[], + contents?: Uint8Array[], + ) => { + if (contents && contents.length !== contentKeys.length) { + throw new Error('Provided Content and content key arrays must be the same length') + } if (contentKeys.length > 0) { this.portal.metrics?.offerMessagesSent.inc() const offerMsg: OfferMessage = { @@ -806,27 +813,35 @@ export class BeaconLightClientNetwork extends BaseNetwork { this.logger.extend(`ACCEPT`)(`ACCEPT message received with uTP id: ${id}`) const requestedData: Uint8Array[] = [] - for await (const key of requestedKeys) { - let value = Uint8Array.from([]) - try { - // We use `findContentLocally` instead of `get` so the content keys for - // optimistic and finality updates are handled correctly - value = (await this.findContentLocally(key)) as Uint8Array - requestedData.push(value) - } catch (err: any) { - this.logger(`Error retrieving content -- ${err.toString()}`) - requestedData.push(value) + if (contents) { + for (const [idx, _] of requestedKeys.entries()) { + if (msg.contentKeys.get(idx) === true) { + requestedData.push(contents[idx]) + } + } + } else { + for await (const key of requestedKeys) { + let value = Uint8Array.from([]) + try { + // We use `findContentLocally` instead of `get` so the content keys for + // optimistic and finality updates are handled correctly + value = (await this.findContentLocally(key)) as Uint8Array + requestedData.push(value) + } catch (err: any) { + this.logger(`Error retrieving content -- ${err.toString()}`) + requestedData.push(value) + } } } - const contents = encodeWithVariantPrefix(requestedData) + const encoded = encodeWithVariantPrefix(requestedData) await this.handleNewRequest({ networkId: this.networkId, contentKeys: requestedKeys, peerId: dstId, connectionId: id, requestCode: RequestCode.OFFER_WRITE, - contents: [contents], + contents: [encoded], }) return msg.contentKeys diff --git a/packages/portalnetwork/src/networks/network.ts b/packages/portalnetwork/src/networks/network.ts index 968a4f918..7d19a5494 100644 --- a/packages/portalnetwork/src/networks/network.ts +++ b/packages/portalnetwork/src/networks/network.ts @@ -414,6 +414,9 @@ export abstract class BaseNetwork extends EventEmitter { * @param networkId network ID of subnetwork being used */ public sendOffer = async (dstId: string, contentKeys: Uint8Array[], content?: Uint8Array[]) => { + if (content && content.length !== contentKeys.length) { + throw new Error('Must provide all content or none') + } if (contentKeys.length > 0) { this.portal.metrics?.offerMessagesSent.inc() const offerMsg: OfferMessage = {