We instantiate a p2p network and call start(). This will begin
running a single p2p network until stop() is called.
There are 3 session types:
InboundSession, concerned with incoming connectionsOutboundSession, concerned with outgoing connectionsSeedSession is a special session type which connects to seed nodes
to populate the hosts pool, then finishes once synced.Connections are made by either Acceptor or Connector for incoming
or outgoing respectively. They have multiple transport types; see
src/net/transport/ for the full list.
Connections are then wrapped in a Channel abstraction which allows
protocols to be attached. See src/net/protocol/ and run fd protocol
for custom application specific network protocols. Also see the follow
tutorial:
The outbound session is responsible to ensure the hosts pool is populated, either through currently connected nodes or using the seed session. It performs this algorithm:
Then each slot performs this algorithm:
Node maintain a hostlist consisting of three parts, a whitelist, a
greylist and an anchorlist. Each hostlist entry is a tuple of two parts,
a URL address and a last_seen data field, which is a timestamp of the
last time the peer was interacted with.
The lists are ordered chronologically according to last_seen, with the
most recently seen peers at the top of the list. The whitelist max size
is 1000. The greylist max size is 5000. If the number of peers in these
lists reach this maximum, then the peers with the oldest last_seen
fields are removed from the list.
Each time a node receives info about a set of peers, the info is
inserted into its greylist. To discover peers, nodes broadcast GetAddr
messages. Upon receiving a GetAddr message, peers reply with an Addr
message containing their whitelist. The requester inserts the received
peer data into its greylist.
Nodes update their hostlists through a mechanism called
"greylist housekeeping", which periodically pings randomly selected peers
from its greylist. If a peer is responsive, then it is promoted to the
whitelist with an updated last_seen field, otherwise it is removed
from the greylist.
On shutdown, whitelist entries are downgraded to greylist. This forces all whitelisted entries through the greylist refinery each time a node is started, further ensuring that whitelisted entries are active.
If a connection is established to a host, that host is promoted to anchorlist. If anchorlist or whitelist nodes disconnect or cannot be connected to, those hosts are downgraded to greylist.
Nodes can configure how many anchorlist connections or what percentage
of whitelist connections they would like to make, and this configuration
influences the connection behavior in OutboundSession. If there's
not enough anchorlist entries, the connection loop will select from
the whitelist. If there's not enough whitelist entries in the hostlist,
it will select from the greylist.
This design has been largely informed by the Monero p2p algo
The main attacks are:
From libp2p2 DoS mitigation: "An attack is considered viable if it takes fewer resources to execute than the damage it does. In other words, if the payoff is higher than the investment it is a viable attack and should be mitigated."
channel.ban() which immediately disconnects and
blacklists the address.Core protocols should be modeled and analyzed with DoS protections added. Below are suggestions to start the investigation.
channel.ban().Apps should be able to configure:
blacklist which allows
> us to reject hosts by addr.TODO: research how this is handled on bittorrent. How do we lookup nodes in the swarm? Does the network maintain routing tables? Is this done through a DHT like Kademlia?
Swarming means more efficient downloading of data specific to a certain subset. A new p2p instance is spawned with a clean hosts table. This subnetwork is self contained.
An application is for example DarkIRC where everyday a new event graph is spawned. With swarming, you would connect to nodes maintaining this particular day's event graph.
The feature allows overlaying multiple different features in a single network such as tau, darkirc and so on. New networks require nodes to bootstrap, but with swarming, we reduce all these networks to a single bootstrap. The overlay network maintaining the routing tables is a kind of decentralized lilith which keeps track of all the swarms.
Possibly a post-mainnet feature depending on the scale of architectural changes or new code required in the net submodule.
To faciliate this future upgrade, we have made the peer discovery
process a generic trait called PeerDiscoveryBase. Currently there is
only one imeplementation, PeerDiscovery, which implements the peer
discovery process in outbound sesssion. In the future
PeerDiscoveryBase can be implemented to make new forms of peer
discovery (i.e. subnets vs overlay peer discovery processes).
Connections should maintain a scoring system. Protocols can increment the score.
The score backs off exponentially. If the watermark is crossed then the connection is dropped.
In libp2p, resource usage is constrained by a Resource Manager that
defines resource usage limits. The Resource Manager checks whether
a given request is within a limit and returns an error if it exceeds
a limit.
Resources are deliminated by Resource Management Scopes. Each scope has a corresponding limit that resources cannot exceed.
Limits are calculated by measuring the following resources:
Memory
File descriptors
Connections (Inbound connections have stricter limits than outbound connections)
Streams: an object of interaction between nodes (~analogous to
Channel). Streams are not metered directly- rather they are
constrained within the protocol and service scope (defined below).
Inbound streams are more tightly controlled than outbound streams.
Resource Management Scopes are hierarchial and downstream resource usage is aggregated at higher levels.
System
+------------> Transient.............+................+
| . .
+------------> Service------------- . ----------+ .
| . | .
+-------------> Protocol----------- . ----------+ .
| . | .
+-------------->* Peer \/ | .
+------------> Connection | .
| \/ \/
+---------------------------> Stream
Session) scopes. Logical groupings of streams
that implement protocol flow and may additionally consume resources
such memory.Channel) scopes. Begins when a stream is
created and ends when the stream is closed.There is also:
These are System and Transcient scopes for the allowlist, which is a
list of honest peer anagolous to our goldlist. Allowlist scopes can
continue to use (and meter) resources while the System scope has
already reached its limit (to protect against ellipse attack).
Limits have a default setting that can be configured. It's also possible to scale limits with a particular config that allows for scaling to different machines.
The goal is to make something simple that we can extend later if
necessary given how it behaves in the wild. We have simplified the
libp2p Resource management scopes into a straightforward hierarchy:
node
+
channel
+
+------|------+
+ +
message protocol
Resource usage is calculated from Message and Protocol and stored
in Channel. We can sum the total resources by adding the total amount
of resources used by each channel using the p2p method channels(),
(this is easy since Channel has access to p2p via a weak ptr).
Resources are arranged in a struct called AbstractComputer which
contains resource usage indicators such as: CPU, Memory, hard disk,
bandwidth, etc.
The ScoringSubsystem monitors scoring actions such as send_message
or recv_message (and other actions that make use of resources defined
by the AbstractComputer) and increments the resource usage.
There is also a Controller that defines limits and decides on what
action to take when a given limit has been breached (such as
channel.ban(), channel.throttle() (TODO), or choke(), snub()
etc (also TODO). It is important that the limits set by the
Controller are configurable and can be injected in at runtime since
Message and Protocol are dynamic, user-defined types.
TODO:
ScoringSubsystem, AbstractComputer, and Controller.We start with a score that keeps track of the resources used. Concretely the resources are: CPU, bandwidth, memory, disk space - any computing source. But to keep things simple, lets condense these measures into just two: work (representing CPU) and bytes (bandwidth/memory). These are primarily the main cause of resource starvation anyway (disk space is cheap).
struct ScoreTable {
/// CPU
work: u32,
/// Memory and bandwidth
bytes: u32
}
There is an existing trait Message. We add a new trait method that
takes a message and returns its score:
impl Message for FooMessage {
fn score(&self) -> ScoreTable {
// calculate the score of this message depending on its data
}
// ...
}
We store the scores like this:
struct ScoringMetric {
scores: VecDeque<(Timestamp, ScoreTable)>,
}
impl ScoringMetric {
fn add(&mut self, score: ScoreTable) {
self.scores.push((Timestamp::now(), score));
}
}
The channels then store a scores: ScoringMetric. When it receives a
message then:
scores.update() which does:
scores.pop_front() for any scores who are below the
threshold (they have expired).This logic is implemented in a ScoringMetric class.
Now summing the vec will give you the cumulative score. Depending on how high this score is, we will:
channel.ban().The same mechanism can also be used by protocols, however mostly we can probably just use the channel scoring for most usecases as a good enough approximation. However there might be tight edge situations like the current dag sync which require more fine grained control.
In which case the protocol will instantiate its own ScoringMetric and
keep track itself including calling ban on the channel.
The remote will ban us if we cross the limit. So our channel must also
have a scores_remote which keeps track of our score (how the remote
sees us). If we start getting too high then ease up on the messages to
avoid getting blacklisted channel.ban() called on us.