P2P Networks

Rakis relies on redundant peer-to-peer (P2P) networks to ensure reliable and decentralized message passing between nodes. This section covers the P2P network implementation in Rakis, including private key cryptography, chain identity management, and the supported P2P networks.

Private Key Cryptography

In Rakis, each node has a unique synthientId and synthientPrivKey generated using the ed25519 elliptic curve cryptography library. The synthientId is the public key, and the synthientPrivKey is the corresponding private key. These keys are used to sign and verify the authenticity of messages exchanged between nodes.

Here's an example of how the keys are generated:

export function createNewEmptyIdentity(): ClientInfo {
  const privKey = ed.utils.randomPrivateKey();
  const pubKey = ed.getPublicKey(privKey);
 
  const newIdentity: ClientInfo = {
    synthientId: ed.etc.bytesToHex(pubKey),
    synthientPrivKey: ed.etc.bytesToHex(privKey),
    chainIds: [],
    deviceInfo: getDeviceInfo(),
  };
 
  return newIdentity;
}

The signJSONObject and verifySignatureOnJSONObject functions are used to sign and verify the authenticity of messages, respectively:

export function signJSONObject(privKey: string, object: any): string {
  const messageBytes = ed.etc.encodeUTF8(JSON.stringify(object));
  return ed.sign(messageBytes, hexToBytes(privKey));
}
 
export function verifySignatureOnJSONObject(
  pubKey: string,
  signature: string,
  object: any
): boolean {
  const messageBytes = ed.etc.encodeUTF8(JSON.stringify(object));
  return ed.verify(messageBytes, hexToBytes(signature), hexToBytes(pubKey));
}

Chain Identities

In addition to the synthientId, nodes in Rakis can connect their identities to blockchain addresses. This is accomplished by signing a message with the node's synthientPrivKey and verifying the signature against the blockchain address.

Here's an example of how a new chain identity is added:

async addChainIdentity(
  signature: `0x${string}`,
  chain: string,
  signedWithWallet: string
) {
  const address = await recoverEthChainAddressFromSignature(
    this.synthientId,
    signature
  );
 
  if (this.chainIdentities.find((identity) => identity.address === address)) {
    return true;
  }
 
  if (!address) {
    return false;
  }
 
  try {
    this.chainIdentities.push({
      address,
      chain,
      signedWithWallet,
      synthientIdSignature: signature,
    });
 
    await saveIdentity(this.clientInfo, this.identityPassword);
 
    await this.packetDB.transmitPacket({
      type: "peerConnectedChain",
      createdAt: stringifyDateWithOffset(new Date()),
      identities: this.chainIdentities,
    });
  } catch (err) {
    return false;
  }
 
  return true;
}

The chainIdentities array is part of the ClientInfo object, which represents the persistent identity information for a node.

Supported P2P Networks

Rakis supports multiple P2P networks for message passing, providing redundancy and ensuring reliable communication. The supported networks are:

  • GunDB (PewPewDB)
  • NKN
  • Trystero (Nostr and Torrent)
💡

The specific P2P networks used can be configured in the settings. By default, Rakis uses GunDB, NKN, and Trystero (both Nostr and Torrent).

These networks are implemented as separate classes extending the P2PNetworkInstance abstract class, which defines the common interface for broadcasting and listening to packets.

Here's an example of how a P2P network instance is created:

export class P2PNetworkFactory {
  static createP2PNetworkInstance(
    network: SupportedP2PDeliveryNetwork,
    synthientId: string
  ): P2PNetworkInstance<any, any> {
    switch (network) {
      case "gun":
        return new GunP2PNetworkInstance(synthientId, {
          gunPeers: p2pConfig.PEWPEW.bootstrapPeers,
          gunTopic: p2pConfig.PEWPEW.topic,
          startupDelayMs: p2pConfig.PEWPEW.bootFixedDelayMs,
        });
      case "nkn":
        return new NknP2PNetworkInstance(
          synthientId,
          {
            nknTopic: p2pConfig.NKN.topic,
            nknWalletPassword: "password",
          },
          p2pConfig.NKN
        );
      case "nostr":
        return new TrysteroP2PNetworkInstance(
          synthientId,
          {
            relayRedundancy: p2pConfig.TRYSTERO.relayRedundancy,
            rtcConfig: p2pConfig.TRYSTERO.rtcConfig,
            trysteroTopic: p2pConfig.TRYSTERO.topic,
            trysteroAppId: p2pConfig.TRYSTERO.appId,
            trysteroType: "nostr",
          },
          p2pConfig.TRYSTERO
        );
      case "torrent":
        return new TrysteroP2PNetworkInstance(
          synthientId,
          {
            relayRedundancy: p2pConfig.TRYSTERO.relayRedundancy,
            rtcConfig: p2pConfig.TRYSTERO.rtcConfig,
            trysteroTopic: p2pConfig.TRYSTERO.topic,
            trysteroAppId: p2pConfig.TRYSTERO.appId,
            trysteroType: "torrent",
          },
          p2pConfig.TRYSTERO
        );
      default:
        throw new Error(`Unsupported P2P network: ${network}`);
    }
  }
}

Each P2P network instance implements the broadcastPacket and listenForPacket methods, which are used to send and receive messages, respectively.

Step 1

The P2P networks are initialized during the Rakis Domain startup process:

const p2pNetworkInstances: P2PNetworkInstance<any, any>[] =
  settings.theDomainSettings.enabledP2PNetworks.map((network) =>
    P2PNetworkFactory.createP2PNetworkInstance(network, clientInfo.synthientId)
  );
 
const workingP2PNetworkInstances =
  await P2PNetworkFactory.initializeP2PNetworks(
    p2pNetworkInstances,
    settings.theDomainSettings.waitForP2PBootupMs
  );

Step 2

Once the P2P networks are initialized, they are used to broadcast and receive messages:

// Broadcast a packet over all P2P networks
const broadcastPacket = async (packet: TransmittedPeerPacket) => {
  await Promise.all(
    this.p2pNetworkInstances.map((p) => p.broadcastPacket(packet))
  );
};
 
// Listen for packets on all P2P networks
for (const p2pNetwork of this.p2pNetworkInstances) {
  const listener = p2pNetwork.listenForPacket(async (packet) => {
    this.packetDB.receivePacket(packet);
  });
 
  // Add listener to shutdown listeners
  this.shutdownListeners.push(() => listener());
}

The P2P networks are responsible for reliable message passing between nodes, ensuring that messages are propagated throughout the network. This redundancy helps maintain the decentralized nature of Rakis and ensures that the network remains operational even if one or more P2P networks experience issues.