Skip to content
marie-fourier edited this page Jan 6, 2025 · 1 revision

Node

This package is responsible for p2p interface of Skandha.

Structure

P2P layer consists of 3 domains:

  • discovery domain for finding peers
  • req/resp domain that is used during initial handshake process where bundlers exchange metadata like what kind of mempools they support, also if they find some mempool that they support, during this handshake process, req/resp domain is used for exchanging userops that are already in the mempool of the bundlers, so that the bundler that just connected to shared mempool could catch up with everyone
  • gossip domain - is used only to publish and to listen for newly submitted userops. Each mempool has a separate libp2p topic in gossip domain.

Discovery

Libp2p discovery is used to find peers. When a node starts, it connects to a bootstrap node and starts to discover other peers

Most of the discovery logic is implemented by libp2p itself. We just need to provide a list of bootstrap nodes and libp2p will take care of the rest.

We only need to run a worker script that will connect to the bootstrap nodes and start discovering other peers.

The worker script is located in node/src/network/discv5

Req/Resp

There are 3 types of messages that are exchanged between peers during the initial handshake process.

  • Metadata
  • PooledUserOpHashes
  • PooledUserOpsByHash

Metadata is exchanged first. It contains information about the mempool that the bundler supports. If the bundler finds some mempool that it supports, it will request PooledUserOpHashes from the other bundler. The other bundler will respond with PooledUserOpHashes. After that, the bundler will request PooledUserOpsByHash from the other bundler. The other bundler will respond with UserOperation[].

Syncing

The initial handshake process is called syncing. The logic for syncing is implemented in node/src/sync::SyncService

The addPeer and addPeerMetadata stores peers and their metadata info on local storage.

private addPeer = (peerId: PeerId, status: ts.Status): void => {
  /// ...
}

private addPeerMetadata = (peerId: PeerId, metadata: ts.Metadata): void => {
  /// ...
}

The startSyncing runs requestBatches function that requests PooledUserOpHashes from the list of known peers and then PooledUserOpsByHash for each hash from the list of known peers.

private startSyncing(): void {
  logger.debug(`Sync service: attempt syncing, status = ${this.state}`);
  if (this.state === SyncState.Syncing) {
    return; // Skip, already started
  }

  this.state = SyncState.Syncing;

  void this.requestBatches();
}

private async requestBatches(): Promise<void> {
  logger.debug("Sync service: requested batches");
  /// ...
}

Handling Req/Resp messages

Received req/resp message triggers ReqResp::on[MessageType] methods.

/// ...
private async *onStatus(
  req: ts.Status,
  peerId: PeerId
): AsyncIterable<EncodedPayload<ts.Status>> {
  this.onIncomingRequestBody(
    { method: ReqRespMethod.Status, body: req },
    peerId
  );

  yield* this.reqRespHandlers.onStatus();
}

private async *onGoodbye(
  req: ts.Goodbye,
  peerId: PeerId
): AsyncIterable<EncodedPayload<ts.Goodbye>> {
  this.onIncomingRequestBody(
    { method: ReqRespMethod.Goodbye, body: req },
    peerId
  );

  yield { type: EncodedPayloadType.ssz, data: BigInt(0) };
}

private async *onPing(
  req: ts.Ping,
  peerId: PeerId
): AsyncIterable<EncodedPayload<ts.Ping>> {
  this.onIncomingRequestBody(
    { method: ReqRespMethod.Ping, body: req },
    peerId
  );
  yield {
    type: EncodedPayloadType.ssz,
    data: this.metadataController.seq_number,
  };
}
/// ...

Handlers for a PooledUserOpsByHash or PooledUserOpHashes messages are located in node/src/network/reqresp/handlers

There are other message types that are described in p2p specs:

  • Status
  • Goodbye
  • Ping

Handlers for them are also located in node/src/network/reqresp/ReqRespNode.ts

Gossip

Clone this wiki locally