These are historical integration brainstorming notes. For the implemented baseline, see Mix Transport Logos Storage Integration Example; for the current sequence of work after PR 1526, see Mix Transport Logos Storage Integration Plan. Provisional statements below are not the current implementation contract.
- Discovery: this gives us traditional
peerId.- Normally, as part of the discovery process,
connecton the switch is called, which populates thepeersstores in both the network (NetworkPeer) and the engine (PeerCtx) in response to thePeerEvent.Joined:- we cannot do that here, because we have to use Mix to communicate with this peer
- yet, we still populate the above mentioned
storeswith the discoveredpeerId - so far the behavior is identical except for calling
connect
- Normally, as part of the discovery process,
- Some time later, we will be trying to download some content.
- We will be selecting the candidate peers from the engine
peersstore.- peerId is added to the swarm
self.network.request.sendWantList- we descend to the network and will be asking for the ranges.- this will reach
BlockExcNetwork.send- it gets the
NetworkPeercorresponding topeerIdfrom itspeersstore - normally, via
NetworkPeer.connectifself.sendConnis not active, it will callSwitch.dial(we should change this misleading naming to match the Switch operation)- we cannot do that - we are going via MIX
- instead we have to use custom
self.sendConn = await self.getConn(), which will get us a MIX connection from the Mix transport - we start reading loop on that transport MIX connection (as before)
- we call
await conn.write(buf)- effectively sending ourWantListto the remote peer over MIX
- it gets the
- this will reach
- on the provider side, our MIX Transport will get
WantList(and maybe already some SURBs)- obviously, there is no
peerIdat this end - instead we have some
sessionId. MIX Transport assigns thissessionIdto the underlyingConnection.peerId - the provider gets the incoming connection form the MixTransport
- normally it would already have the connection
peerIdadded to itspeersstores via “joined” Switch event (engine’s peer context) andgetOrCreatePeer(peerId)(NetworkPeer).- we will be probably creating a
NetworkPeerbut I am not yet sure if we will be adding it to the mentionedpeersstores or to some other store. - read loop is started for the incoming stream
- after sending what was to be sent, MIX Transport my decide to close that stream
- but the corresponding
NetworkPeerobject stays.
- we process the
WantHavemessage.BlockExcEngine.wantListHandlerneeds thepeerIdto be present in itspeersstore.- if the provider has anything to offer, it will
sendPresencevia network (it will need thepeerIdto be in network’speersstore). - this
peerIdwill be MIX transportsessionId, which should let the MIX transport associate find the corresponding connection with the SURBs and if necessary request additional SURBs from the requester- this is a bit different than in the original where the outgoing messages will use a separate stream (with its corresponding
readLoopif there is anything to read) - in the original protocol we basically do not know on which of the two stream the control messages will come - it is a bit messy…
- this is a bit different than in the original where the outgoing messages will use a separate stream (with its corresponding
- in the end, the presence list should land on the requester’s MIX transport side.
- if the provider has anything to offer, it will
- we will be probably creating a
- obviously, there is no
- back on the requester with the presence list
- back in the incoming stream’s
readLoop- this is not stream really any more, it is the Mix Transport calling protocol’s
handlewith its own connection object from while thereadLoopwill read as from a regular stream - the requester knows both Mix session id and the original provider’s
peerId.
- this is not stream really any more, it is the Mix Transport calling protocol’s
- as we know that some of the swarm peers have the data we want, we start requesting actual blocks
self.sendWantBlocksRequestreachesNetworkPeer.sendWantBlocksRequestNetworkPeer.sendConnis (re)used - again it is a Mix Transport connection
- back in the incoming stream’s
- again at the provider via incoming Mix Transport connection
- this time
readLoophandlesmtWantBlocksRequestand writes the answer directly to itsconn(Mix Transport)- SURBs has to be requested as needed
- this time
- requester receives the block,
- etc…
Initializing Mix Protocol
StorageServer.start already provides conditional Mix Protocol initialization for the purpose of DHT queries over Mix. More specifically, config.mixEnabled, config.mixPoolJson, and config.mixPool can be reused also for blocks exchange protocol.
At the end of the if s.config.mixEnabled: code path, mixProto holds fully initialized instance of the MixProtocol. This is the right moment create an instance of the MixTransport, passing mixProto as a constructor argument. We set the instance of the MixTransport as optional on the BlockExcNetwork.mixTransport. If it is set, it means that Mix is enabled and should be used to carry the block exchange and manifest protocols.
Initially I thought that MixTransport will not be another libp2p protocol on its own, but because of how current Mix implementation works this may be the easiest if not the only option. Mix implementation always needs to send the data to a concrete protocol: if one is not mounted on the Switch, we will get an error:
libp2p_mix/exit_layer.nim:
var hasHandler: bool = false
for index, handler in enumerate(self.switch.ms.handlers):
if codec in handler.protos:
try:
hasHandler = true
await handler.protocol.handler(exitConn, codec)
except CatchableError as e:
error "Error during execution of MixProtocol handler: ", err = e.msg
if not hasHandler:
error "Handler doesn't exist", codec = codec
returnThus, MixTransport will be having its own protocol - MixTransportProtocol.
We add the MixTransport initialization code to storage/storage.nim, in StorageServer.start under the s.config.mixEnabled code path where we initialize Mix Transport passing the already initialized instance of MixProtocol to it:
proc startMixTransport*(s: StorageServer, mixProto: MixProtocol) {.async: (raises: [CancelledError, StorageError]).} =
if not s.config.mixEnabled or mixProto.isNil:
return
let switch = s.storageNode.switch
let mixTransport = newMixTransport(switch, mixProto)
(await mixTransport.start()).isOkOr:
raise newException(StorageError, "Failed to start Mix transport: " & error.msg)
s.storageNode.engine.network.mixTransport = some(mixTransport)As we see, it also sets the mixTransport optional on the instance of BlockExcNetwork, which is defined as:
mixTransport*: Option[MixTransport] = none(MixTransport)With this, the BlockExcNetwork can easily detect if Mix is enabled.
Initiating outgoing connections
The party that wants to connect to other peers anonymously, will naturally avoid using the libp2p-level Switch.connect or Switch.dial operation: these are irrelevant in the Mix environment as connecting to a remote peer using a direct libp2p connection would de-anonymize the original sender. Yet, before adding the discovered provider to list of peer, it might be desired to established an anonymous connection with the discovered provider in order to confirm that this peer is reachable before attempting regular block exchange traffic with that peer.
To facilitate this, MixTransport provides its own connect operation. connect will attempt an anonymous round-trip exchange with the remote peer, sending it a number of SURBs that fit within a single Sphynx block. The remote provider will use some of these SURBs to acknowledge its reachability.
To remote provider will use a number of SURBs to establish some redundancy in order to increase the chance that at least one packet arrives at the original sender.
This initial exchange will be of a simple request-response type, using the default Mix protocol implementation.
After successful exchange, the originating peerId can be added to the list of network peers in the same way as it is done for the regular block exchange protocol.
Recall there are two local stores keeping track of the peerId: one on the network level and one on the engine level. Both are normally populated in response to the PeerEventKind.Joined event handlers. Correspondingly, a peer is normally removed from the stores in response to the PeerEventKind.Left event. Explicit removal via BlockExcNetwork.dropPeer delegates to the switch, which in turn which cause the PeerEventKind.Left event to be triggered.
We mention it here as it will be important to properly manege the peerId lifecycle also in the Mix Transport as the original PeerEventKind.Joined and PeerEventKind.Left will not be triggered in this case (see also excludedPeers: HashSet[PeerId] property on BlockExcNetwork).
For the peer initiating the connection (the original sender in the Mix terms), the very first connection to the remote provider will be caused by the discoveryTaskLoop, where for each discovered peerId that advertises the given CID
proc discoveryTaskLoop(b: DiscoveryEngine) {.async: (raises: []).} =
## Run discovery tasks
## Peer availability is tracked per-download in DownloadContext.swarm.
## This loop just runs discovery for CIDs that are queued.
try:
while b.discEngineRunning:
let cid = await b.discoveryQueue.get()
if cid in b.inFlightDiscReqs:
trace "Discovery request already in progress", cid
continue
trace "Running discovery task for cid", cid
let request = b.discovery.find(cid)
b.inFlightDiscReqs[cid] = request
storage_inflight_discovery.set(b.inFlightDiscReqs.len.int64)
defer:
b.inFlightDiscReqs.del(cid)
storage_inflight_discovery.set(b.inFlightDiscReqs.len.int64)
if (await request.withTimeout(DefaultDiscoveryTimeout)) and
peers =? (await request).catch:
let dialed = await allFinished(peers.mapIt(b.network.dialPeer(it.data)))
for i, f in dialed:
if f.failed:
await b.discovery.removeProvider(peers[i].data.peerId)
except CancelledError:
trace "Discovery task cancelled"
return
info "Exiting discovery task runner"
Processing Incoming Frames
The Mix Transport protocol helper will use the provided libp2p Connection object to read the incoming frames, process them (asynchronously) write back to it using the sessionId that we will use as the Mix Transport equivelnt of the regular connection’s peerId.