Skip to content

Seamless Integration of Mix Protocol with Existing libp2p Protocols #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

AkshayaMani
Copy link
Collaborator

Summary

This pull request introduces seamless integration of existing libp2p protocols with the Mix Protocol. By abstracting the entry and exit connections to the Mix protocol, this implementation allows existing protocols to anonymize messages without requiring any modifications to their code.

Key Abstractions

To achieve this functionality, we introduce two abstractions:

  1. MixEntryConnection:

    • Acts as an interface between the end protocol and the mix protocol at the sender side.
    • Intercepts outgoing messages from the end protocol and invokes a callback (MixDialer) to route them through the mix network.
    • Example: When NoRespPing.noRespPing is called, MixEntryConnection ensures that the message is anonymized and routed through the mix network.
  2. MixExitConnection:

    • Handles incoming messages at the exit node.
    • Unwraps sphinx packets, extracts protocol identifiers, and buffers payloads.
    • Invokes the appropriate end protocol handler (e.g., NoRespPing) to process these messages seamlessly.

Implementation Details

  1. MixDialer Callback:

    • At the entry point, a MixDialer callback is defined to route outgoing messages through anonymizeLocalProtocolSend in the MixProtocol.
    • This callback is passed to MixEntryConnection, which invokes it whenever an outgoing message is written.
  2. Protocol Handler Callback:

    • At the exit point, a callback is invoked when a message reaches its destination.
    • The callback buffers the unwrapped message and invokes the appropriate protocol handler on the same switch.
  3. Simulation:

    • A custom NoRespPing protocol is used to test end-to-end functionality.
    • The simulation demonstrates how a ping message is sent through the mix network using MixEntryConnection at the sender side and processed by MixExitConnection at the receiver side.

Run PoC

nimble -l c -r ./src/poc_noresp_ping.nim

Benefits

  1. No modifications required to existing libp2p protocol implementations.
  2. Protocol-agnostic
  3. Preserves core benefits of having mix as a protocol.
  4. Seamless integration with existing libp2p's architecture.

- Added NoRespPing codec
- Added protocol handler callback
- Tests end-to-end functionality of mix protocol using custom protocol NoRespPing
- Added callbacks for entry and exit mix interface
@chaitanyaprem
Copy link

Nice, but it looks like there is a dependency on other protocols
As per here, looks like mix layer should be aware of protocols to invoke the handler.

mix/src/protocol.nim

Lines 8 to 13 in 1ed184b

type ProtocolType* = enum
Ping = PingCodec
GossipSub12 = GossipSubCodec_12
GossipSub11 = GossipSubCodec_11
GossipSub10 = GossipSubCodec_10
NoRespPing = NoRespPingCodec

Or is this code supposed to be part of user of mix e.g waku?

@@ -9,6 +10,7 @@ type ProtocolType* = enum
GossipSub12 = GossipSubCodec_12
GossipSub11 = GossipSubCodec_11
GossipSub10 = GossipSubCodec_10
NoRespPing = NoRespPingCodec
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ProtocolType still needed, or would it be possible to just pass the codec string in callHandler without having to do a conversion to this type? The advantage of it would be that it would not be necessary to modify this enum when we want to add an additional protocol.

@@ -9,6 +10,7 @@ type ProtocolType* = enum
GossipSub12 = GossipSubCodec_12
GossipSub11 = GossipSubCodec_11
GossipSub10 = GossipSubCodec_10
NoRespPing = NoRespPingCodec
OtherProtocol = "other" # Placeholder for other protocols

type ProtocolHandler* =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not introduced in this PR but it would be ideal if the {.async.} tag had a raises to indicate which exceptions can be thrown


method callHandler*(
switch: Switch, conn: Connection, proto: ProtocolType
): Future[void] {.base, async.} =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here:

Suggested change
): Future[void] {.base, async.} =
): Future[void] {.base, async: (raises[]).} =

It's likely this would throw an LPError or CancelledError, but not sure

Comment on lines +23 to +25
type
NoRespPingHandler* {.public.} =
proc(peer: PeerId): Future[void] {.gcsafe, raises: [].}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe let's try this?

Suggested change
type
NoRespPingHandler* {.public.} =
proc(peer: PeerId): Future[void] {.gcsafe, raises: [].}
type
NoRespPingHandler* {.public.} =
proc(peer: PeerId): Future[void] {.async:(raises: [CancelledError]).}

else:
self.message = @[]

# ToDo: Check readLine, readVarint, readLp implementations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These implementations seem very similar to those in libp2p LpStream (libp2p/stream/connection.nim), Do you think we could inherit from those instead of duplicating its implementation?

let paddedMsg = padMessage(mixMsg, peerID)
let paddedMsg = padMessage(serialized, peerID)

info "# Sent: ", sender = multiAddr, message = msg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably be trace or debug

Comment on lines +243 to +254
let parts = multiAddrs[0].split("/p2p/")
if parts.len != 2:
error "Invalid multiaddress format", parts = parts
return

let firstMixAddr = MultiAddress.init(parts[0]).valueOr:
error "Failed to initialize MultiAddress", err = error
return

let firstMixPeerId = PeerId.init(parts[1]).valueOr:
error "Failed to initialize PeerId", err = error
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we try to split multiaddresses by "p2p" in diff areas of the code. Maybe we can use some utils functions to extract the peerId and the address from a multiaddress:

func stripPeerId(multiaddr: MultiAddress): MultiAddress =
  if not multiaddr.contains(multiCodec("p2p")).get():
    return multiaddr

  var cleanAddr = MultiAddress.init()
  for item in multiaddr.items:
    if item.value.protoName().get() != "p2p":
      # Add all parts except p2p peerId
      discard cleanAddr.append(item.value)

  return cleanAddr

proc peerID*(ma: MultiAddress): Result[PeerId, string] =
  let p2pPart = ?ma[^1]
  if ?p2pPart.protoCode != multiCodec("p2p"):
    return err("Missing p2p part from multiaddress!")
  ok(?PeerId.init(?p2pPart.protoArgument()).orErr("invalid peerid"))

Then we can use them like this

  let ma = MultiAddress.init(parts[0]).valueOr:
    error "Failed to initialize MultiAddress", err = error
    return
    
   let firstMixPeerId = ma.peerID().valueOr:
    error "Failed to initialize PeerId", err = error
    return
    
   let firstMixAddr = ma.stripPeerId()

Comment on lines +61 to +63
let multiAddr = MultiAddress.init(multiAddrStr.split("/p2p/")[0]).valueOr:
error "Failed to initialize MultiAddress", err = error
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the function from prev comment, it's likely we can do some quick changes to avoid having to initialize a multiaddress here:

  1. MixNodeInfo multiAddr field should be a Multiaddress instead of a string. The multiaddress initialization can happen when you are loading the node info from a file.
  2. getMixNodeInfo would then return a Multiaddress instead of a string.

That would allow you to do something like:

let multiAddr = mixNodeMultiAddr.stripPeerId()

Comment on lines +55 to +62
proc noRespPing*(p: NoRespPing, conn: Connection): Future[seq[byte]] {.async, public.} =
trace "initiating ping"
var randomBuf: array[NoRespPingSize, byte]
hmacDrbgGenerate(p.rng[], randomBuf)
trace "sending ping"
await conn.write(@randomBuf)
trace "sent ping: ", ping = @randomBuf
return @randomBuf
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that protocols that want to use mix would need to implement a function that would allow them to use a provided conn instead of use one obtained from the switch?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you foresee any issues with this approach? I don't believe there are any. The mix and the top-level protocol are part of the same switch, so ideally, they can communicate directly.

We’re establishing a virtual connection here to allow us to intercept messages from the top-level protocol easily. As a result, creating an MixEntryConnection is necessary for this to work as intended.

- Simplify with valueOr

Co-authored-by: richΛrd <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants