Skip to content

Commit

Permalink
fix: allow custom services to depend on each other (#2588)
Browse files Browse the repository at this point in the history
Derive the type of the components arg from a union of the internal
components plus the service map type.
  • Loading branch information
achingbrain authored Jun 12, 2024
1 parent 863b3de commit 0447913
Show file tree
Hide file tree
Showing 11 changed files with 54 additions and 60 deletions.
24 changes: 12 additions & 12 deletions packages/interface/src/connection-gater/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface ConnectionGater {
*
* Return true to prevent dialing the passed peer.
*/
denyDialPeer?(peerId: PeerId): Promise<boolean>
denyDialPeer?(peerId: PeerId): Promise<boolean> | boolean

/**
* denyDialMultiaddr tests whether we're permitted to dial the specified
Expand All @@ -23,7 +23,7 @@ export interface ConnectionGater {
*
* Return true to prevent dialing the passed peer on the passed multiaddr.
*/
denyDialMultiaddr?(multiaddr: Multiaddr): Promise<boolean>
denyDialMultiaddr?(multiaddr: Multiaddr): Promise<boolean> | boolean

/**
* denyInboundConnection tests whether an incipient inbound connection is allowed.
Expand All @@ -33,7 +33,7 @@ export interface ConnectionGater {
*
* Return true to deny the incoming passed connection.
*/
denyInboundConnection?(maConn: MultiaddrConnection): Promise<boolean>
denyInboundConnection?(maConn: MultiaddrConnection): Promise<boolean> | boolean

/**
* denyOutboundConnection tests whether an incipient outbound connection is allowed.
Expand All @@ -43,7 +43,7 @@ export interface ConnectionGater {
*
* Return true to deny the incoming passed connection.
*/
denyOutboundConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean>
denyOutboundConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean> | boolean

/**
* denyInboundEncryptedConnection tests whether a given connection, now encrypted,
Expand All @@ -55,7 +55,7 @@ export interface ConnectionGater {
*
* Return true to deny the passed secured connection.
*/
denyInboundEncryptedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean>
denyInboundEncryptedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean> | boolean

/**
* denyOutboundEncryptedConnection tests whether a given connection, now encrypted,
Expand All @@ -67,7 +67,7 @@ export interface ConnectionGater {
*
* Return true to deny the passed secured connection.
*/
denyOutboundEncryptedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean>
denyOutboundEncryptedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean> | boolean

/**
* denyInboundUpgradedConnection tests whether a fully capable connection is allowed.
Expand All @@ -77,7 +77,7 @@ export interface ConnectionGater {
*
* Return true to deny the passed upgraded connection.
*/
denyInboundUpgradedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean>
denyInboundUpgradedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean> | boolean

/**
* denyOutboundUpgradedConnection tests whether a fully capable connection is allowed.
Expand All @@ -87,15 +87,15 @@ export interface ConnectionGater {
*
* Return true to deny the passed upgraded connection.
*/
denyOutboundUpgradedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean>
denyOutboundUpgradedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise<boolean> | boolean

/**
* denyInboundRelayReservation tests whether a remote peer is allowed make a
* relay reservation on this node.
*
* Return true to deny the relay reservation.
*/
denyInboundRelayReservation?(source: PeerId): Promise<boolean>
denyInboundRelayReservation?(source: PeerId): Promise<boolean> | boolean

/**
* denyOutboundRelayedConnection tests whether a remote peer is allowed to open a relayed
Expand All @@ -106,7 +106,7 @@ export interface ConnectionGater {
*
* Return true to deny the relayed connection.
*/
denyOutboundRelayedConnection?(source: PeerId, destination: PeerId): Promise<boolean>
denyOutboundRelayedConnection?(source: PeerId, destination: PeerId): Promise<boolean> | boolean

/**
* denyInboundRelayedConnection tests whether a remote peer is allowed to open a relayed
Expand All @@ -117,12 +117,12 @@ export interface ConnectionGater {
*
* Return true to deny the relayed connection.
*/
denyInboundRelayedConnection?(relay: PeerId, remotePeer: PeerId): Promise<boolean>
denyInboundRelayedConnection?(relay: PeerId, remotePeer: PeerId): Promise<boolean> | boolean

/**
* Used by the address book to filter passed addresses.
*
* Return true to allow storing the passed multiaddr for the passed peer.
*/
filterMultiaddrForPeer?(peer: PeerId, multiaddr: Multiaddr): Promise<boolean>
filterMultiaddrForPeer?(peer: PeerId, multiaddr: Multiaddr): Promise<boolean> | boolean
}
9 changes: 0 additions & 9 deletions packages/interface/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,15 +684,6 @@ export interface LoggerOptions {
log: Logger
}

/**
* Returns a new type with all fields marked optional.
*
* Borrowed from the tsdef module.
*/
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends Array<infer I> ? Array<RecursivePartial<I>> : T[P] extends (...args: any[]) => any ? T[P] : RecursivePartial<T[P]>
}

/**
* When a routing operation involves reading values, these options allow
* controlling where the values are read from. By default libp2p will check
Expand Down
10 changes: 5 additions & 5 deletions packages/libp2p/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
import mergeOptions from 'merge-options'
import { codes, messages } from './errors.js'
import type { Libp2pInit } from './index.js'
import type { ServiceMap, RecursivePartial } from '@libp2p/interface'
import type { ServiceMap } from '@libp2p/interface'
import type { Multiaddr } from '@multiformats/multiaddr'

const DefaultConfig: Partial<Libp2pInit> = {
const DefaultConfig: Libp2pInit = {
addresses: {
listen: [],
announce: [],
Expand All @@ -26,14 +26,14 @@ const DefaultConfig: Partial<Libp2pInit> = {
}
}

export async function validateConfig <T extends ServiceMap = Record<string, unknown>> (opts: RecursivePartial<Libp2pInit<T>>): Promise<Libp2pInit<T>> {
const resultingOptions: Libp2pInit<T> = mergeOptions(DefaultConfig, opts)
export async function validateConfig <T extends ServiceMap = Record<string, unknown>> (opts: Libp2pInit<T>): Promise<Libp2pInit<T> & Required<Pick<Libp2pInit<T>, 'peerId'>>> {
const resultingOptions: Libp2pInit<T> & Required<Pick<Libp2pInit<T>, 'peerId'>> = mergeOptions(DefaultConfig, opts)

if (resultingOptions.connectionProtector === null && globalThis.process?.env?.LIBP2P_FORCE_PNET != null) { // eslint-disable-line no-undef
throw new CodeError(messages.ERR_PROTECTOR_REQUIRED, codes.ERR_PROTECTOR_REQUIRED)
}

if (!(await peerIdFromKeys(resultingOptions.privateKey.public.bytes, resultingOptions.privateKey.bytes)).equals(resultingOptions.peerId)) {
if (resultingOptions.privateKey != null && !(await peerIdFromKeys(resultingOptions.privateKey.public.bytes, resultingOptions.privateKey.bytes)).equals(resultingOptions.peerId)) {
throw new CodeError('Private key doesn\'t match peer id', codes.ERR_INVALID_KEY)
}

Expand Down
26 changes: 13 additions & 13 deletions packages/libp2p/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ import type { AddressManagerInit } from './address-manager/index.js'
import type { Components } from './components.js'
import type { ConnectionManagerInit } from './connection-manager/index.js'
import type { TransportManagerInit } from './transport-manager.js'
import type { Libp2p, ServiceMap, RecursivePartial, ComponentLogger, NodeInfo, ConnectionProtector, ConnectionEncrypter, ConnectionGater, ContentRouting, Metrics, PeerDiscovery, PeerId, PeerRouting, StreamMuxerFactory, Transport, PrivateKey } from '@libp2p/interface'
import type { Libp2p, ServiceMap, ComponentLogger, NodeInfo, ConnectionProtector, ConnectionEncrypter, ConnectionGater, ContentRouting, Metrics, PeerDiscovery, PeerId, PeerRouting, StreamMuxerFactory, Transport, PrivateKey } from '@libp2p/interface'
import type { PersistentPeerStoreInit } from '@libp2p/peer-store'
import type { DNS } from '@multiformats/dns'
import type { Datastore } from 'interface-datastore'

export type ServiceFactoryMap<T extends Record<string, unknown> = Record<string, unknown>> = {
[Property in keyof T]: (components: Components) => T[Property]
[Property in keyof T]: (components: Components & T) => T[Property]
}

/**
Expand All @@ -35,49 +35,49 @@ export interface Libp2pInit<T extends ServiceMap = { x: Record<string, unknown>
/**
* peerId instance (it will be created if not provided)
*/
peerId: PeerId
peerId?: PeerId

/**
* Private key associated with the peerId
*/
privateKey: PrivateKey
privateKey?: PrivateKey

/**
* Metadata about the node - implementation name, version number, etc
*/
nodeInfo: NodeInfo
nodeInfo?: NodeInfo

/**
* Addresses for transport listening and to advertise to the network
*/
addresses: AddressManagerInit
addresses?: AddressManagerInit

/**
* libp2p Connection Manager configuration
*/
connectionManager: ConnectionManagerInit
connectionManager?: ConnectionManagerInit

/**
* A connection gater can deny new connections based on user criteria
*/
connectionGater: ConnectionGater
connectionGater?: ConnectionGater

/**
* libp2p transport manager configuration
*/
transportManager: TransportManagerInit
transportManager?: TransportManagerInit

/**
* An optional datastore to persist peer information, DHT records, etc.
*
* An in-memory datastore will be used if one is not provided.
*/
datastore: Datastore
datastore?: Datastore

/**
* libp2p PeerStore configuration
*/
peerStore: PersistentPeerStoreInit
peerStore?: PersistentPeerStoreInit

/**
* An array that must include at least 1 compliant transport
Expand All @@ -102,7 +102,7 @@ export interface Libp2pInit<T extends ServiceMap = { x: Record<string, unknown>
/**
* Arbitrary libp2p modules
*/
services: ServiceFactoryMap<T>
services?: ServiceFactoryMap<T>

/**
* An optional logging implementation that can be used to write runtime logs.
Expand Down Expand Up @@ -135,7 +135,7 @@ export interface Libp2pInit<T extends ServiceMap = { x: Record<string, unknown>

export type { Libp2p }

export type Libp2pOptions<T extends ServiceMap = Record<string, unknown>> = RecursivePartial<Libp2pInit<T>> & { start?: boolean }
export type Libp2pOptions<T extends ServiceMap = Record<string, unknown>> = Libp2pInit<T> & { start?: boolean }

/**
* Returns a new instance of the Libp2p interface, generating a new PeerId
Expand Down
11 changes: 7 additions & 4 deletions packages/libp2p/src/libp2p.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ export class Libp2pNode<T extends ServiceMap = Record<string, unknown>> extends
public logger: ComponentLogger
public status: Libp2pStatus

public components: Components
public components: Components & T[keyof T]
private readonly log: Logger

constructor (init: Libp2pInit<T>) {
constructor (init: Libp2pInit<T> & Required<Pick<Libp2pInit<T>, 'peerId'>>) {
super()

this.status = 'stopped'
Expand All @@ -66,6 +66,7 @@ export class Libp2pNode<T extends ServiceMap = Record<string, unknown>> extends
this.log = this.logger.forComponent('libp2p')
// @ts-expect-error {} may not be of type T
this.services = {}
// @ts-expect-error defaultComponents is missing component types added later
const components = this.components = defaultComponents({
peerId: init.peerId,
privateKey: init.privateKey,
Expand Down Expand Up @@ -111,7 +112,7 @@ export class Libp2pNode<T extends ServiceMap = Record<string, unknown>> extends
this.components.upgrader = new DefaultUpgrader(this.components, {
connectionEncryption: (init.connectionEncryption ?? []).map((fn, index) => this.configureComponent(`connection-encryption-${index}`, fn(this.components))),
muxers: (init.streamMuxers ?? []).map((fn, index) => this.configureComponent(`stream-muxers-${index}`, fn(this.components))),
inboundUpgradeTimeout: init.connectionManager.inboundUpgradeTimeout
inboundUpgradeTimeout: init.connectionManager?.inboundUpgradeTimeout
})

// Setup the transport manager
Expand Down Expand Up @@ -159,6 +160,7 @@ export class Libp2pNode<T extends ServiceMap = Record<string, unknown>> extends
if (init.services != null) {
for (const name of Object.keys(init.services)) {
const createService = init.services[name]
// @ts-expect-error components type is not fully formed yet
const service: any = createService(this.components)

if (service == null) {
Expand Down Expand Up @@ -194,6 +196,7 @@ export class Libp2pNode<T extends ServiceMap = Record<string, unknown>> extends
this.log.error('component %s was null or undefined', name)
}

// @ts-expect-error cannot assign props
this.components[name] = component

return component
Expand Down Expand Up @@ -413,7 +416,7 @@ export async function createLibp2pNode <T extends ServiceMap = Record<string, un
throw new CodeError('peer id was missing private key', 'ERR_MISSING_PRIVATE_KEY')
}

options.privateKey ??= await unmarshalPrivateKey(peerId.privateKey as Uint8Array)
options.privateKey ??= await unmarshalPrivateKey(peerId.privateKey)

return new Libp2pNode(await validateConfig(options))
}
12 changes: 7 additions & 5 deletions packages/libp2p/test/connection-manager/direct.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { yamux } from '@chainsafe/libp2p-yamux'
import { type Connection, type ConnectionProtector, isConnection, type PeerId, type Stream } from '@libp2p/interface'
import { type Connection, type ConnectionProtector, isConnection, type PeerId, type Stream, type Libp2p } from '@libp2p/interface'
import { AbortError, ERR_TIMEOUT, TypedEventEmitter, start, stop } from '@libp2p/interface'
import { mockConnection, mockConnectionGater, mockDuplex, mockMultiaddrConnection, mockUpgrader } from '@libp2p/interface-compliance-tests/mocks'
import { defaultLogger } from '@libp2p/logger'
Expand All @@ -30,7 +30,8 @@ import { defaultComponents, type Components } from '../../src/components.js'
import { DialQueue } from '../../src/connection-manager/dial-queue.js'
import { DefaultConnectionManager } from '../../src/connection-manager/index.js'
import { codes as ErrorCodes } from '../../src/errors.js'
import { createLibp2pNode, type Libp2pNode } from '../../src/libp2p.js'
import { createLibp2p } from '../../src/index.js'
import { createLibp2pNode } from '../../src/libp2p.js'
import { DefaultPeerRouting } from '../../src/peer-routing.js'
import { DefaultTransportManager } from '../../src/transport-manager.js'
import { ECHO_PROTOCOL, echo } from '../fixtures/echo-service.js'
Expand Down Expand Up @@ -281,8 +282,8 @@ describe('dialing (direct, TCP)', () => {
describe('libp2p.dialer (direct, TCP)', () => {
let peerId: PeerId
let remotePeerId: PeerId
let libp2p: Libp2pNode
let remoteLibp2p: Libp2pNode
let libp2p: Libp2p
let remoteLibp2p: Libp2p
let remoteAddr: Multiaddr

beforeEach(async () => {
Expand All @@ -291,7 +292,7 @@ describe('libp2p.dialer (direct, TCP)', () => {
createEd25519PeerId()
])

remoteLibp2p = await createLibp2pNode({
remoteLibp2p = await createLibp2p({
peerId: remotePeerId,
addresses: {
listen: [listenAddr.toString()]
Expand Down Expand Up @@ -565,6 +566,7 @@ describe('libp2p.dialer (direct, TCP)', () => {

const dials = 10
const error = new Error('Boom')
// @ts-expect-error private field access
Sinon.stub(libp2p.components.transportManager, 'dial').callsFake(async () => Promise.reject(error))

await libp2p.peerStore.patch(remotePeerId, {
Expand Down
6 changes: 3 additions & 3 deletions packages/libp2p/test/connection-manager/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,8 @@ describe('libp2p.connections', () => {
})

describe('connection gater', () => {
let libp2p: Libp2pNode
let remoteLibp2p: Libp2pNode
let libp2p: Libp2p
let remoteLibp2p: Libp2p

beforeEach(async () => {
remoteLibp2p = await createNode({
Expand Down Expand Up @@ -485,7 +485,7 @@ describe('libp2p.connections', () => {
await libp2p.peerStore.patch(remoteLibp2p.peerId, {
multiaddrs: remoteLibp2p.getMultiaddrs()
})
await libp2p.components.connectionManager.openConnection(remoteLibp2p.peerId)
await libp2p.dial(remoteLibp2p.peerId)

for (const multiaddr of remoteLibp2p.getMultiaddrs()) {
expect(denyDialMultiaddr.calledWith(multiaddr)).to.be.true()
Expand Down
4 changes: 2 additions & 2 deletions packages/libp2p/test/registrar/protocols.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { expect } from 'aegir/chai'
import pDefer from 'p-defer'
import { createLibp2pNode } from '../../src/libp2p.js'
import type { Components } from '../../src/components.js'
import type { Libp2pNode } from '../../src/libp2p.js'
import type { Libp2p } from '@libp2p/interface'

describe('registrar protocols', () => {
let libp2p: Libp2pNode
let libp2p: Libp2p

it('should be able to register and unregister a handler', async () => {
const deferred = pDefer<Components>()
Expand Down
4 changes: 2 additions & 2 deletions packages/metrics-devtools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { start, stop } from '@libp2p/interface'
import { enable, disable } from '@libp2p/logger'
import { simpleMetrics } from '@libp2p/simple-metrics'
import { base64 } from 'multiformats/bases/base64'
import type { ComponentLogger, Connection, Libp2pEvents, Logger, Metrics, MultiaddrConnection, PeerId, Peer as PeerStorePeer, PeerStore, PeerUpdate, Stream, TypedEventEmitter } from '@libp2p/interface'
import type { ComponentLogger, Connection, Libp2pEvents, Logger, Metrics, MultiaddrConnection, PeerId, Peer as PeerStorePeer, PeerStore, PeerUpdate, Stream, TypedEventTarget } from '@libp2p/interface'
import type { TransportManager, Registrar, ConnectionManager } from '@libp2p/interface-internal'

export const SOURCE_DEVTOOLS = '@libp2p/devtools-metrics:devtools'
Expand Down Expand Up @@ -214,7 +214,7 @@ export interface DevToolsMetricsInit {

export interface DevToolsMetricsComponents {
logger: ComponentLogger
events: TypedEventEmitter<Libp2pEvents>
events: TypedEventTarget<Libp2pEvents>
peerId: PeerId
transportManager: TransportManager
registrar: Registrar
Expand Down
Loading

0 comments on commit 0447913

Please sign in to comment.