From 6bde634be209788ef5283330cace612e275005ea Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 3 Nov 2022 12:46:42 -0300 Subject: [PATCH] Documentation of p2p layer in Tendermint v0.34 (#9348) * spec: overview of p2p in v0.34 (#9120) * Iniital comments on v0.34 p2p * Added conf, updated text * Moved everything to spec * Update README.md * spec: overview of the p2p implementation in v0.34 (#9126) * Spec: p2p v0.34 doc, switch initial documentation * Spec: p2p v0.34 doc, list of source files * Spec: p2p v0.34 doc, transport documentation * Spec: p2p v0.34 doc, transport error handling * Spec: p2p v0.34 doc, PEX initial documentation * PEX protocol documentation is a separated file * PEX reactor documentation with a general documentation, including the address book and its role as (outbound) peer manager. * Spec: p2p v0.34 doc, PEX protocol documentation * Spec: p2p v0.34 doc, PEX protocol on seed nodes * Spec: p2p v0.34 doc, address book * Spec: p2p v0.34 doc, address book, more details * Spec: p2p v0.34 doc, address book persistence * Spec: p2p v0.34 doc, address book random samples * Spec: p2p v0.34 doc, status of this documentation * Spec: p2p v0.34 doc, pex reactor documentation * Spec: p2p v0.34 doc, addressing PR #9126 comments Co-authored-by: Jasmina Malicevic * Spec: p2p v0.34 doc, peer manager, outbound peers Co-authored-by: Daniel Cason Co-authored-by: Jasmina Malicevic * spec:p2p v0.34 introduction (#9319) * restructure README.md initial * Fix typos * Reorganization * spec: overview of p2p in v0.34 (#9120) * Iniital comments on v0.34 p2p * Added conf, updated text * Moved everything to spec * Update README.md * spec: overview of the p2p implementation in v0.34 (#9126) * Spec: p2p v0.34 doc, switch initial documentation * Spec: p2p v0.34 doc, list of source files * Spec: p2p v0.34 doc, transport documentation * Spec: p2p v0.34 doc, transport error handling * Spec: p2p v0.34 doc, PEX initial documentation * PEX protocol documentation is a separated file * PEX reactor documentation with a general documentation, including the address book and its role as (outbound) peer manager. * Spec: p2p v0.34 doc, PEX protocol documentation * Spec: p2p v0.34 doc, PEX protocol on seed nodes * Spec: p2p v0.34 doc, address book * Spec: p2p v0.34 doc, address book, more details * Spec: p2p v0.34 doc, address book persistence * Spec: p2p v0.34 doc, address book random samples * Spec: p2p v0.34 doc, status of this documentation * Spec: p2p v0.34 doc, pex reactor documentation * Spec: p2p v0.34 doc, addressing PR #9126 comments Co-authored-by: Jasmina Malicevic * Spec: p2p v0.34 doc, peer manager, outbound peers Co-authored-by: Daniel Cason Co-authored-by: Jasmina Malicevic * spec:p2p v0.34 introduction (#9319) * restructure README.md initial * Fix typos * Reorganization * spec: p2p v0.34, addressbook review * spec: p2p v0.34, peer manager review * spec: p2p v0.34, peer manager review * spec: p2p v0.34, peer manager review * spec: p2p v0.34, peer manager review * spec: p2p v0.34, peer manager review * spec: p2p v0.34, peer manager review * spec: p2p v0.34, peer manager review * Filled config description * spec: p2p v0.34, transport review * spec: p2p v0.34, switch review * spec: p2p v0.34, overview, first version * spec: p2p v0.34, peer manager review * spec: p2p v0.34, shorter readme * Configuration update * Configuration update * Shortened README * spec: p2p v0.34, readme intro * spec: p2p v0.34, readme contents * spec: p2p v0.34, readme references * spec: p2p readme points to v0.34 * spec: p2p, v0.34, fixing brokend markdown links * Makrdown fix * Apply suggestions from code review Co-authored-by: Adi Seredinschi Co-authored-by: Zarko Milosevic * spec: p2p v0.34, address book new intro * spec: p2p v0.34, address book buckets summary * spec: p2p v0.34, peer manager, issue link * spec: p2p v0.34, fixing links * spec: p2p v0.34, addressing comments from reviews * spec: p2p v0.34, addressing comments from reviews * Apply suggestions from Jasmina's code review Co-authored-by: Jasmina Malicevic * spec: p2p v0.34, addressing comments from reviews * Apply suggestions from code review Co-authored-by: Sergio Mena * Apply suggestions from code review Co-authored-by: Jasmina Malicevic Co-authored-by: Sergio Mena * spec: p2p, v0.34, address book section reorganized * spec: p2p, v0.34, addressing review comments * Typos * Typo * spec: p2p, v0.34, address book markbad Co-authored-by: Jasmina Malicevic Co-authored-by: Daniel Cason Co-authored-by: Adi Seredinschi Co-authored-by: Zarko Milosevic Co-authored-by: Sergio Mena --- spec/p2p/readme.md | 7 + spec/p2p/v0.34/README.md | 70 ++++++ spec/p2p/v0.34/addressbook.md | 367 +++++++++++++++++++++++++++++++ spec/p2p/v0.34/configuration.md | 51 +++++ spec/p2p/v0.34/img/p2p_state.png | Bin 0 -> 132059 bytes spec/p2p/v0.34/peer_manager.md | 147 +++++++++++++ spec/p2p/v0.34/pex-protocol.md | 240 ++++++++++++++++++++ spec/p2p/v0.34/pex.md | 111 ++++++++++ spec/p2p/v0.34/switch.md | 237 ++++++++++++++++++++ spec/p2p/v0.34/transport.md | 222 +++++++++++++++++++ spec/p2p/v0.34/types.md | 239 ++++++++++++++++++++ 11 files changed, 1691 insertions(+) create mode 100644 spec/p2p/v0.34/README.md create mode 100644 spec/p2p/v0.34/addressbook.md create mode 100644 spec/p2p/v0.34/configuration.md create mode 100644 spec/p2p/v0.34/img/p2p_state.png create mode 100644 spec/p2p/v0.34/peer_manager.md create mode 100644 spec/p2p/v0.34/pex-protocol.md create mode 100644 spec/p2p/v0.34/pex.md create mode 100644 spec/p2p/v0.34/switch.md create mode 100644 spec/p2p/v0.34/transport.md create mode 100644 spec/p2p/v0.34/types.md diff --git a/spec/p2p/readme.md b/spec/p2p/readme.md index 96867aad0..0cb6a0cf9 100644 --- a/spec/p2p/readme.md +++ b/spec/p2p/readme.md @@ -4,3 +4,10 @@ parent: title: P2P order: 6 --- + +# Peer-to-Peer Communication + +The operation of the p2p adopted in production Tendermint networks is [HERE](./v0.34/). + +> This is part of an ongoing [effort](https://github.com/tendermint/tendermint/issues/9089) +> to produce a high-level specification of the operation of the p2p layer. diff --git a/spec/p2p/v0.34/README.md b/spec/p2p/v0.34/README.md new file mode 100644 index 000000000..cfb132ce4 --- /dev/null +++ b/spec/p2p/v0.34/README.md @@ -0,0 +1,70 @@ +# Peer-to-Peer Communication + +This document describes the implementation of the peer-to-peer (p2p) +communication layer in Tendermint. + +It is part of an [effort](https://github.com/tendermint/tendermint/issues/9089) +to produce a high-level specification of the operation of the p2p layer adopted +in production Tendermint networks. + +This documentation, therefore, considers the releases `0.34.*` of Tendermint, more +specifically, the branch [`v0.34.x`](https://github.com/tendermint/tendermint/tree/v0.34.x) +of this repository. + +## Overview + +A Tendermint network is composed of multiple Tendermint instances, hereafter +called **nodes**, that interact by exchanging messages. + +Tendermint assumes a partially-connected network model. +This means that a node is not assumed to be directly connected to every other +node in the network. +Instead, each node is directly connected to a subset of other nodes in the +network, hereafter called its **peers**. + +The peer-to-peer (p2p) communication layer is responsible for establishing +connections between nodes in a Tendermint network, +for managing the communication between a node and its peers, +and for intermediating the exchange of messages between peers in Tendermint protocols. + +## Contents + +The documentation follows the organization of the `p2p` package of Tendermint, +which implements the following abstractions: + +- [Transport](./transport.md): establishes secure and authenticated + connections with peers; +- [Switch](./switch.md): responsible for dialing peers and accepting + connections from peers, for managing established connections, and for + routing messages between the reactors and peers, + that is, between local and remote instances of the Tendermint protocols; +- [PEX Reactor](./pex.md): a reactor is the implementation of a protocol which + exchanges messages through the p2p layer. The PEX reactor manages the [Address Book](./addressbook.md) and implements both the [PEX protocol](./pex-protocol.md) and the [Peer Manager](./peer_manager.md) role. + - [Peer Exchange protocol](./pex-protocol.md): enables nodes to exchange peer addresses, thus implementing a peer discovery service; + - [Address Book](./addressbook.md): stores discovered peer addresses and + quality metrics associated to peers with which the node has interacted; + - [Peer Manager](./peer_manager.md): defines when and to which peers a node + should dial, in order to establish outbound connections; +- Finally, [Types](./types.md) and [Configuration](./configuration.md) provide + a list of existing types and configuration parameters used by the p2p layer implementation. + +## Further References + +Existing documentation referring to the p2p layer: + +- https://github.com/tendermint/tendermint/tree/main/spec/p2p: p2p-related + configuration flags; overview of connections, peer instances, and reactors; + overview of peer discovery and node types; peer identity, secure connections + and peer authentication handshake. +- https://github.com/tendermint/tendermint/tree/main/spec/p2p/messages: message + types and channel IDs of Block Sync, Mempool, Evidence, State Sync, PEX, and + Consensus reactors. +- https://docs.tendermint.com/v0.34/tendermint-core: the p2p layer + configuration and operation is documented in several pages. + This content is not necessarily up-to-date, some settings and concepts may + refer to the release `v0.35`, that was [discontinued][v35postmorten]. +- https://github.com/tendermint/tendermint/tree/master/docs/tendermint-core/pex: + peer types, peer discovery, peer management overview, address book and peer + ranking. This documentation refers to the release `v0.35`, that was [discontinued][v35postmorten]. + +[v35postmorten]: https://interchain-io.medium.com/discontinuing-tendermint-v0-35-a-postmortem-on-the-new-networking-layer-3696c811dabc diff --git a/spec/p2p/v0.34/addressbook.md b/spec/p2p/v0.34/addressbook.md new file mode 100644 index 000000000..b51338121 --- /dev/null +++ b/spec/p2p/v0.34/addressbook.md @@ -0,0 +1,367 @@ +# Address Book + +The address book tracks information about peers, i.e., about other nodes in the network. + +The primary information stored in the address book are peer addresses. +A peer address is composed by a node ID and a network address; a network +address is composed by an IP address or a DNS name plus a port number. +The same node ID can be associated to multiple network addresses. + +There are two sources for the addresses stored in the address book. +The [Peer Exchange protocol](./pex-protocol.md) stores in the address book +the peer addresses it discovers, i.e., it learns from connected peers. +And the [Switch](./switch.md) registers the addresses of peers with which it +has interacted: to which it has dialed or from which it has accepted a +connection. + +The address book also records additional information about peers with which the +node has interacted, from which is possible to rank peers. +The Switch reports [connection attempts](#dial-attempts) to a peer address; too +much failed attempts indicate that a peer address is invalid. +Reactors, in they turn, report a peer as [good](#good-peers) when it behaves as +expected, or as a [bad peer](#bad-peers), when it misbehaves. + +There are two entities that retrieve peer addresses from the address book. +The [Peer Manager](./peer_manager.md) retrieves peer addresses to dial, so to +establish outbound connections. +This selection is random, but has a configurable bias towards peers that have +been marked as good peers. +The [Peer Exchange protocol](./pex-protocol.md) retrieves random samples of +addresses to offer (send) to peers. +This selection is also random but it includes, in particular for nodes that +operate in seed mode, some bias toward peers marked as good ones. + +## Buckets + +Peer addresses are stored in buckets. +There are buckets for new addresses and buckets for old addresses. +The buckets for new addresses store addresses of peers about which the node +does not have much information; the first address registered for a peer ID is +always stored in a bucket for new addresses. +The buckets for old addresses store addresses of peers with which the node has +interacted and that were reported as [good peers](#good-peers) by a reactor. +An old address therefore can be seen as an alias for a good address. + +> Note that new addresses does not mean bad addresses. +> The addresses of peers marked as [bad peers](#bad-peers) are removed from the +> buckets where they are stored, and temporarily kept in a table of banned peers. + +The number of buckets is fixed and there are more buckets for new addresses +(`256`) than buckets for old addresses (`64`), a ratio of 4:1. +Each bucket can store up to `64` addresses. +When a bucket becomes full, the peer address with the lowest ranking is removed +from the bucket. +The first choice is to remove bad addresses, with multiple failed attempts +associated. +In the absence of those, the *oldest* address in the bucket is removed, i.e., +the address with the oldest last attempt to dial. + +When a bucket for old addresses becomes full, the lowest-ranked peer address in +the bucket is moved to a bucket of new addresses. +When a bucket for new addresses becomes full, the lowest-ranked peer address in +the bucket is removed from the address book. +In other words, exceeding old or good addresses are downgraded to new +addresses, while exceeding new addresses are dropped. + +The bucket that stores an `address` is defined by the following two methods, +for new and old addresses: + +- `calcNewBucket(address, source) = hash(key + groupKey(source) + hash(key + groupKey(address) + groupKey(source)) % newBucketsPerGroup) % newBucketCount` +- `calcOldBucket(address) = hash(key + groupKey(address) + hash(key + address) % oldBucketsPerGroup) % oldBucketCount` + +The `key` is a fixed random 96-bit (8-byte) string. +The `groupKey` for an address is a string representing its network group. +The `source` of an address is the address of the peer from which we learn the +address.. +The first (internal) hash is reduced to an integer up to `newBucketsPerGroup = +32`, for new addresses, and `oldBucketsPerGroup = 4`, for old addresses. +The second (external) hash is reduced to bucket indexes, in the interval from 0 +to the number of new (`newBucketCount = 256`) or old (`oldBucketCount = 64`) buckets. + +Notice that new addresses with sources from the same network group are more +likely to end up in the same bucket, therefore to competing for it. +For old address, instead, two addresses are more likely to end up in the same +bucket when they belong to the same network group. + +## Adding addresses + +The `AddAddress` method adds the address of a peer to the address book. + +The added address is associated to a *source* address, which identifies the +node from which the peer address was learned. + +Addresses are added to the address book in the following situations: + +1. When a peer address is learned via PEX protocol, having the sender + of the PEX message as its source +2. When an inbound peer is added, in this case the peer itself is set as the + source of its own address +3. When the switch is instructed to dial addresses via the `DialPeersAsync` + method, in this case the node itself is set as the source + +If the added address contains a node ID that is not registered in the address +book, the address is added to a [bucket](#buckets) of new addresses. +Otherwise, the additional address for an existing node ID is **not added** to +the address book when: + +- The last address added with the same node ID is stored in an old bucket, so + it is considered a "good" address +- There are addresses associated to the same node ID stored in + `maxNewBucketsPerAddress = 4` distinct buckets +- Randomly, with a probability that increases exponentially with the number of + buckets in which there is an address with the same node ID. + So, a new address for a node ID which is already present in one bucket is + added with 50% of probability; if the node ID is present in two buckets, the + probability decreases to 25%; and if it is present in three buckets, the + probability is 12.5%. + +The new address is also added to the `addrLookup` table, which stores +`knownAddress` entries indexed by their node IDs. +If the new address is from an unknown peer, a new entry is added to the +`addrLookup` table; otherwise, the existing entry is updated with the new +address. +Entries of this table contain, among other fields, the list of buckets where +addresses of a peer are stored. +The `addrLookup` table is used by most of the address book methods (e.g., +`HasAddress`, `IsGood`, `MarkGood`, `MarkAttempt`), as it provides fast access +to addresses. + +### Errors + +- if the added address or the associated source address are nil +- if the added address is invalid +- if the added address is the local node's address +- if the added address ID is of a [banned](#bad-peers) peer +- if either the added address or the associated source address IDs are configured as private IDs +- if `routabilityStrict` is set and the address is not routable +- in case of failures computing the bucket for the new address (`calcNewBucket` method) +- if the added address instance, which is a new address, is configured as an + old address (sanity check of `addToNewBucket` method) + +## Need for Addresses + +The `NeedMoreAddrs` method verifies whether the address book needs more addresses. + +It is invoked by the PEX reactor to define whether to request peer addresses +to a new outbound peer or to a randomly selected connected peer. + +The address book needs more addresses when it has less than `1000` addresses +registered, counting all buckets for new and old addresses. + +## Pick address + +The `PickAddress` method returns an address stored in the address book, chosen +at random with a configurable bias towards new addresses. + +It is invoked by the Peer Manager to obtain a peer address to dial, as part of +its `ensurePeers` routine. +The bias starts from 10%, when the peer has no outbound peers, increasing by +10% for each outbound peer the node has, up to 90%, when the node has at least +8 outbound peers. + +The configured bias is a parameter that influences the probability of choosing +an address from a bucket of new addresses or from a bucket of old addresses. +A second parameter influencing this choice is the number of new and old +addresses stored in the address book. +In the absence of bias (i.e., if the configured bias is 50%), the probability +of picking a new address is given by the square root of the number of new +addresses divided by the sum of the square roots of the numbers of new and old +addresses. +By adding a bias toward new addresses (i.e., configured bias larger than 50%), +the portion on the sample occupied by the square root of the number of new +addresses increases, while the corresponding portion for old addresses decreases. +As a result, it becomes more likely to pick a new address at random from this sample. + +> The use of the square roots softens the impact of disproportional numbers of +> new and old addresses in the address book. This is actually the expected +> scenario, as there are 4 times more buckets for new addresses than buckets +> for old addresses. + +Once the type of address, new or old, is defined, a non-empty bucket of this +type is selected at random. +From the selected bucket, an address is chosen at random and returned. +If all buckets of the selected type are empty, no address is returned. + +## Random selection + +The `GetSelection` method returns a selection of addresses stored in the +address book, with no bias toward new or old addresses. + +It is invoked by the PEX protocol to obtain a list of peer addresses with two +purposes: + +- To send to a peer in a PEX response, in the case of outbound peers or of + nodes not operating in seed mode +- To crawl, in the case of nodes operating in seed mode, as part of every + interaction of the `crawlPeersRoutine` + +The selection is a random subset of the peer addresses stored in the +`addrLookup` table, which stores the last address added for each peer ID. +The target size of the selection is `23%` (`getSelectionPercent`) of the +number of addresses stored in the address book, but it should not be lower than +`32` (`minGetSelection`) --- if it is, all addresses in the book are returned +--- nor greater than `250` (`maxGetSelection`). + +> The random selection is produced by: +> - Retrieving all entries of the `addrLookup` map, which by definition are +> returned in random order. +> - Randomly shuffling the retrieved list, using the Fisher-Yates algorithm + +## Random selection with bias + +The `GetSelectionWithBias` method returns a selection of addresses stored in +the address book, with bias toward new addresses. + +It is invoked by the PEX protocol to obtain a list of peer addresses to be sent +to a peer in a PEX response. +This method is only invoked by seed nodes, when replying to a PEX request +received from an inbound peer (i.e., a peer that dialed the seed node). +The bias used in this scenario is hard-coded to 30%, meaning that 70% of +the returned addresses are expected to be old addresses. + +The number of addresses that compose the selection is computed in the same way +as for the non-biased random selection. +The bias toward new addresses is implemented by requiring that the configured +bias, interpreted as a percentage, of the select addresses come from buckets of +new addresses, while the remaining come from buckets of old addresses. +Since the number of old addresses is typically lower than the number of new +addresses, it is possible that the address book does not have enough old +addresses to include in the selection. +In this case, additional new addresses are included in the selection. +Thus, the configured bias, in practice, is towards old addresses, not towards +new addresses. + +To randomly select addresses of a type, the address book considers all +addresses present in every bucket of that type. +This list of all addresses of a type is randomly shuffled, and the requested +number of addresses are retrieved from the tail of this list. +The returned selection contains, at its beginning, a random selection of new +addresses in random order, followed by a random selection of old addresses, in +random order. + +## Dial Attempts + +The `MarkAttempt` method records a failed attempt to connect to an address. + +It is invoked by the Peer Manager when it fails dialing a peer, but the failure +is not in the authentication step (`ErrSwitchAuthenticationFailure` error). +In case of authentication errors, the peer is instead marked as a [bad peer](#bad-peers). + +The failed connection attempt is recorded in the address registered for the +peer's ID in the `addrLookup` table, which is the last address added with that ID. +The known address' counter of failed `Attempts` is increased and the failure +time is registered in `LastAttempt`. + +The possible effect of recording multiple failed connect attempts to a peer is +to turn its address into a *bad* address (do not confuse with banned addresses). +A known address becomes bad if it is stored in buckets of new addresses, and +when connection attempts: + +- Have not been made over a week, i.e., `LastAttempt` is older than a week +- Have failed 3 times and never succeeded, i.e., `LastSucess` field is unset +- Have failed 10 times in the last week, i.e., `LastSucess` is older than a week + +Addresses marked as *bad* are the first candidates to be removed from a bucket of +new addresses when the bucket becomes full. + +> Note that failed connection attempts are reported for a peer address, but in +> fact the address book records them for a peer. +> +> More precisely, failed connection attempts are recorded in the entry of the +> `addrLookup` table with reported peer ID, which contains the last address +> added for that node ID, which is not necessarily the reported peer address. + +## Good peers + +The `MarkGood` method marks a peer ID as good. + +It is invoked by the consensus reactor, via switch, when the number of useful +messages received from a peer is a multiple of `10000`. +Vote and block part messages are considered for this number, they must be valid +and not be duplicated messages to be considered useful. + +> The `SwitchReporter` type of `behaviour` package also invokes the `MarkGood` +> method when a "reason" associated with consensus votes and block parts is +> reported. +> No reactor, however, currently provides these "reasons" to the `SwitchReporter`. + +The effect of this action is that the address registered for the peer's ID in the +`addrLookup` table, which is the last address added with that ID, is marked as +good and moved to a bucket of old addresses. +An address marked as good has its failed to connect counter and timestamp reset. +If the destination bucket of old addresses is full, the oldest address in the +bucket is moved (downgraded) to a bucket of new addresses. + +Moving the peer address to a bucket of old addresses has the effect of +upgrading, or increasing the ranking of a peer in the address book. + +## Bad peers + +The `MarkBad` method marks a peer as bad and bans it for a period of time. + +This method is only invoked within the PEX reactor, with a banning time of 24 +hours, for the following reasons: + +- A peer misbehaves in the [PEX protocol](pex-protocol.md#misbehavior) +- When the `maxAttemptsToDial` limit (`16`) is reached for a peer +- If an `ErrSwitchAuthenticationFailure` error is returned when dialing a peer + +The effect of this action is that the address registered for the peer's ID in the +`addrLookup` table, which is the last address added with that ID, is banned for +a period of time. +The banned peer is removed from the `addrLookup` table and from all buckets +where its addresses are stored. + +The information about banned peers, however, is not discarded. +It is maintained in the `badPeers` map, indexed by peer ID. +This allows, in particular, addresses of banned peers to be +[reinstated](#reinstating-addresses), i.e., to be added +back to the address book, when their ban period expires. + +## Reinstating addresses + +The `ReinstateBadPeers` method attempts to re-add banned addresses to the address book. + +It is invoked by the PEX reactor when dialing new peers. +This action is taken before requesting additional addresses to peers, +in the case that the node needs more peer addresses. + +The set of banned peer addresses is retrieved from the `badPeers` map. +Addresses that are not any longer banned, i.e., whose banned period has expired, +are added back to the address book as new addresses, while the corresponding +node IDs are removed from the `badPeers` map. + +## Removing addresses + +The `RemoveAddress` method removes an address from the address book. + +It is invoked by the switch when it dials a peer or accepts a connection from a +peer that ends up being the node itself (`IsSelf` error). +In both cases, the address dialed or accepted is also added to the address book +as a local address, via the `AddOurAddress` method. + +The same logic is also internally used by the address book for removing +addresses of a peer that is [marked as a bad peer](#bad-peers). + +The entry registered with the peer ID of the address in the `addrLookup` table, +which is the last address added with that ID, is removed from all buckets where +it is stored and from the `addrLookup` table. + +> FIXME: is it possible that addresses with the same ID as the removed address, +> but with distinct network addresses, are kept in buckets of the address book? +> While they will not be accessible anymore, as there is no reference to them +> in the `addrLookup`, they will still be there. + +## Persistence + +The `loadFromFile` method, called when the address book is started, reads +address book entries from a file, passed to the address book constructor. +The file, at this point, does not need to exist. + +The `saveRoutine` is started when the address book is started. +It saves the address book to the configured file every `dumpAddressInterval`, +hard-coded to 2 minutes. +It is also possible to save the content of the address book using the `Save` +method. +Saving the address book content to a file acquires the address book lock, also +employed by all other public methods. diff --git a/spec/p2p/v0.34/configuration.md b/spec/p2p/v0.34/configuration.md new file mode 100644 index 000000000..1962f6ebc --- /dev/null +++ b/spec/p2p/v0.34/configuration.md @@ -0,0 +1,51 @@ +# Tendermint p2p configuration + +This document contains configurable parameters a node operator can use to tune the p2p behaviour. + +| Parameter| Default| Description | +| --- | --- | ---| +| ListenAddress | "tcp://0.0.0.0:26656" | Address to listen for incoming connections (0.0.0.0:0 means any interface, any port) | +| ExternalAddress | "" | Address to advertise to peers for them to dial | +| [Seeds](pex-protocol.md#seed-nodes) | empty | Comma separated list of seed nodes to connect to (ID@host:port )| +| [Persistent peers](peer_manager.md#persistent-peers) | empty | Comma separated list of nodes to keep persistent connections to (ID@host:port ) | +| UPNP | false | UPNP port forwarding enabled | +| [AddrBook](addressbook.md) | defaultAddrBookPath | Path do address book | +| AddrBookStrict | true | Set true for strict address routability rules and false for private or local networks | +| [MaxNumInboundPeers](switch.md#accepting-peers) | 40 | Maximum number of inbound peers | +| [MaxNumOutboundPeers](peer_manager.md#ensure-peers) | 10 | Maximum number of outbound peers to connect to, excluding persistent peers | +| [UnconditionalPeers](switch.md#accepting-peers) | empty | These are IDs of the peers which are allowed to be (re)connected as both inbound or outbound regardless of whether the node reached `max_num_inbound_peers` or `max_num_outbound_peers` or not. | +| PersistentPeersMaxDialPeriod| 0 * time.Second | Maximum pause when redialing a persistent peer (if zero, exponential backoff is used) | +| FlushThrottleTimeout |100 * time.Millisecond| Time to wait before flushing messages out on the connection | +| MaxPacketMsgPayloadSize | 1024 | Maximum size of a message packet payload, in bytes | +| SendRate | 5120000 (5 mB/s) | Rate at which packets can be sent, in bytes/second | +| RecvRate | 5120000 (5 mB/s) | Rate at which packets can be received, in bytes/second| +| [PexReactor](pex.md) | true | Set true to enable the peer-exchange reactor | +| SeedMode | false | Seed mode, in which node constantly crawls the network and looks for. Does not work if the peer-exchange reactor is disabled. | +| PrivatePeerIDs | empty | Comma separated list of peer IDsthat we do not add to the address book or gossip to other peers. They stay private to us. | +| AllowDuplicateIP | false | Toggle to disable guard against peers connecting from the same ip.| +| [HandshakeTimeout](transport.md#connection-upgrade) | 20 * time.Second | Timeout for handshake completion between peers | +| [DialTimeout](switch.md#dialing-peers) | 3 * time.Second | Timeout for dialing a peer | + + +These parameters can be set using the `$TMHOME/config/config.toml` file. A subset of them can also be changed via command line using the following command line flags: + +| Parameter | Flag| Example| +| --- | --- | ---| +| Listen address| `p2p.laddr` | "tcp://0.0.0.0:26656" | +| Seed nodes | `p2p.seeds` | `--p2p.seeds “id100000000000000000000000000000000@1.2.3.4:26656,id200000000000000000000000000000000@2.3.4.5:4444”` | +| Persistent peers | `p2p.persistent_peers` | `--p2p.persistent_peers “id100000000000000000000000000000000@1.2.3.4:26656,id200000000000000000000000000000000@2.3.4.5:26656”` | +| Unconditional peers | `p2p.unconditional_peer_ids` | `--p2p.unconditional_peer_ids “id100000000000000000000000000000000,id200000000000000000000000000000000”` | + | UPNP | `p2p.upnp` | `--p2p.upnp` | + | PexReactor | `p2p.pex` | `--p2p.pex` | + | Seed mode | `p2p.seed_mode` | `--p2p.seed_mode` | + | Private peer ids | `p2p.private_peer_ids` | `--p2p.private_peer_ids “id100000000000000000000000000000000,id200000000000000000000000000000000”` | + + **Note on persistent peers** + + If `persistent_peers_max_dial_period` is set greater than zero, the +pause between each dial to each persistent peer will not exceed `persistent_peers_max_dial_period` +during exponential backoff and we keep trying again without giving up. + +If `seeds` and `persistent_peers` intersect, +the user will be warned that seeds may auto-close connections +and that the node may not be able to keep the connection persistent. \ No newline at end of file diff --git a/spec/p2p/v0.34/img/p2p_state.png b/spec/p2p/v0.34/img/p2p_state.png new file mode 100644 index 0000000000000000000000000000000000000000..c86d01682624fd92ead0739519d906bf65a82ec3 GIT binary patch literal 132059 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 FIXME: The current logic was introduced in [#3762](https://github.com/tendermint/tendermint/pull/3762). +> Although it fix the issue, the delay between receiving an address and dialing +> the peer, it does not impose and limit on how many addresses are dialed in this +> scenario. +> So, all addresses received from a seed node are dialed, regardless of the +> current number of outbound peers, the number of dialing routines, or the +> `MaxNumOutboundPeers` parameter. +> +> Issue [#9548](https://github.com/tendermint/tendermint/issues/9548) was +> created to handle this situation. + +### First round + +When the PEX reactor is started, the `ensurePeersRoutine` is created and it +runs thorough the operation of a node, periodically invoking the `ensurePeers` +method. +However, if when the persistent routine is started the node already has some +peers, either inbound or outbound peers, or is dialing some addresses, the +first invocation of `ensurePeers` is delayed by a random amount of time from 0 +to `ensurePeersPeriod`. + +### Persistent peers + +The node configuration can contain a list of *persistent peers*. +Those peers have preferential treatment compared to regular peers and the node +is always trying to connect to them. +Moreover, these peers are not removed from the address book in the case of +multiple failed dial attempts. + +On startup, the node immediately tries to dial the configured persistent peers +by calling the switch's [`DialPeersAsync`](./switch.md#manual-operation) method. +This is not done in the p2p package, but it is part of the procedure to set up a node. + +> TODO: the handling of persistent peers should be described in more detail. + +### Life cycle + +The picture below is a first attempt of illustrating the life cycle of an outbound peer: + + + +A peer can be in the following states: + +- Candidate peers: peer addresses stored in the address boook, that can be + retrieved via the [`PickAddress`](./addressbook.md#pick-address) method +- [Dialing](switch.md#dialing-peers): peer addresses that are currently being + dialed. This state exists to ensure that a single dialing routine exist per peer. +- [Reconnecting](switch.md#reconnect-to-peer): persistent peers to which a node + is currently reconnecting, as a previous connection attempt has failed. +- Connected peers: peers that a node has successfully dialed, added as outbound peers. +- [Bad peers](addressbook.md#bad-peers): peers marked as bad in the address + book due to exhibited [misbehavior](pex-protocol.md#misbehavior). + Peers can be reinstated after being marked as bad. + +## Pending of documentation + +The `dialSeeds` method of the PEX reactor. + +The `dialPeer` method of the PEX reactor. +This includes `dialAttemptsInfo`, `maxBackoffDurationForPeer` methods. diff --git a/spec/p2p/v0.34/pex-protocol.md b/spec/p2p/v0.34/pex-protocol.md new file mode 100644 index 000000000..6497e1385 --- /dev/null +++ b/spec/p2p/v0.34/pex-protocol.md @@ -0,0 +1,240 @@ +# Peer Exchange Protocol + +The Peer Exchange (PEX) protocol enables nodes to exchange peer addresses, thus +implementing a peer discovery mechanism. + +The PEX protocol uses two messages: + +- `PexRequest`: sent by a node to [request](#requesting-addresses) peer + addresses to a peer +- `PexAddrs`: a list of peer addresses [provided](#providing-addresses) to a + peer as response to a `PexRequest` message + +While all nodes, with few exceptions, participate on the PEX protocol, +a subset of nodes, configured as [seed nodes](#seed-nodes) have a particular +role in the protocol. +They crawl the network, connecting to random peers, in order to learn as many +peer addresses as possible to provide to other nodes. + +## Requesting Addresses + +A node requests peer addresses by sending a `PexRequest` message to a peer. + +For regular nodes, not operating in seed mode, a PEX request is sent when +the node *needs* peers addresses, a condition checked: + +1. When an *outbound* peer is added, causing the node to request addresses from + the new peer +2. Periodically, by the `ensurePeersRoutine`, causing the node to request peer + addresses to a randomly selected peer + +A node needs more peer addresses when its addresses book has +[less than 1000 records](./addressbook.md#need-for-addresses). +It is thus reasonable to assume that the common case is that a peer needs more +peer addresses, so that PEX requests are sent whenever the above two situations happen. + +A PEX request is sent when a new *outbound* peer is added. +The same does not happen with new inbound peers because the implementation +considers outbound peers, that the node has chosen for dialing, more +trustworthy than inbound peers, that the node has accepted. +Moreover, when a node is short of peer addresses, it dials the configured seed nodes; +since they are added as outbound peers, the node can immediately request peer addresses. + +The `ensurePeersRoutine` periodically checks, by default every 30 seconds (`ensurePeersPeriod`), +whether the node has enough outbound peers. +If it does not have, the node tries dialing some peer addresses stored in the address book. +As part of this procedure, the node selects a peer at random, +from the set of connected peers retrieved from the switch, +and sends a PEX request to the selected peer. + +Sending a PEX request to a peer is implemented by the `RequestAddrs` method of +the PEX reactor. + +### Responses + +After a PEX request is sent to a peer, the node expects to receive, +as a response, a `PexAddrs` message from the peer. +This message encodes a list of peer addresses that are +[added to address book](./addressbook.md#adding-addresses), +having the peer from which the PEX response was received as their source. + +Received PEX responses are handled by the `ReceiveAddrs` method of the PEX reactor. +In the case of a PEX response received from a peer which is configured as +a seed node, the PEX reactor attempts immediately to dial the provided peer +addresses, as detailed [here](./peer_manager.md#fast-dialing). + +### Misbehavior + +Sending multiple PEX requests to a peer, before receiving a reply from it, +is considered a misbehavior. +To prevent it, the node maintains a `requestsSent` set of outstanding +requests, indexed by destination peers. +While a peer ID is present in the `requestsSent` set, the node does not send +further PEX requests to that peer. +A peer ID is removed from the `requestsSent` set when a PEX response is +received from it. + +Sending a PEX response to a peer that has not requested peer addresses +is also considered a misbehavior. +So, if a PEX response is received from a peer that is not registered in +the `requestsSent` set, a `ErrUnsolicitedList` error is produced. +This leads the peer to be disconnected and [marked as a bad peer](addressbook.md#bad-peers). + +## Providing Addresses + +When a node receives a `PexRequest` message from a peer, +it replies with a `PexAddrs` message. + +This message encodes a [random selection of peer addresses](./addressbook.md#random-selection) +retrieved from the address book. + +Sending a PEX response to a peer is implemented by the `SendAddrs` method of +the PEX reactor. + +### Misbehavior + +Requesting peer addresses too often is considered a misbehavior. +Since node are expected to send PEX requests every `ensurePeersPeriod`, +the minimum accepted interval between requests from the same peer is set +to `ensurePeersPeriod / 3`, 10 seconds by default. + +The `receiveRequest` method is responsible for verifying this condition. +The node keeps a `lastReceivedRequests` map with the time of the last PEX +request received from every peer. +If the interval between successive requests is less than the minimum accepted +one, the peer is disconnected and [marked as a bad peer](addressbook.md#bad-peers). +An exception is made for the first two PEX requests received from a peer. + +> The probably reason is that, when a new peer is added, the two conditions for +> a node to request peer addresses can be triggered with an interval lower than +> the minimum accepted interval. +> Since this is a legit behavior, it should not be punished. + +## Seed nodes + +A seed node is a node configured to operate in `SeedMode`. + +### Crawling peers + +Seed nodes crawl the network, connecting to random peers and sending PEX +requests to them, in order to learn as many peer addresses as possible. +More specifically, a node operating in seed mode sends PEX requests in two cases: + +1. When an outbound peer is added, and the seed node needs more peer addresses, + it requests peer addresses to the new peer +2. Periodically, the `crawlPeersRoutine` sends PEX requests to a random set of + peers, whose addresses are registered in the Address Book + +The first case also applies for nodes not operating in seed mode. +The second case replaces the second for regular nodes, as seed nodes do not +run the `ensurePeersRoutine`, as regular nodes, +but run the `crawlPeersRoutine`, which is not run by regular nodes. + +The `crawlPeersRoutine` periodically, every 30 seconds (`crawlPeerPeriod`), +starts a new peer discovery round. +First, the seed node retrieves a random selection of peer addresses from its +Address Book. +This selection is produced in the same way as in the random selection of peer +addresses that are [provided](#providing-addresses) to a requesting peer. +Peers that the seed node has crawled recently, +less than 2 minutes ago (`minTimeBetweenCrawls`), are removed from this selection. +The remaining peer addresses are registered in the `crawlPeerInfos` table. + +The seed node is not necessarily connected to the peer whose address is +selected for each round of crawling. +So, the seed node dials the selected peer addresses. +This is performed in foreground, one peer at a time. +As a result, a round of crawling can take a substantial amount of time. +For each selected peer it succeeds dialing to, this include already connected +peers, the seed node sends a PEX request. + +Dialing a selected peer address can fail for multiple reasons. +The seed node might have attempted to dial the peer too many times. +In this case, the peer address is marked as [bad in the address book](addressbook.md#bad-peers). +The seed node might have attempted to dial the peer recently, without success, +and the exponential `backoffDuration` has not yet passed. +Or the current connection attempt might fail, which is registered in the address book. + +Failures to dial to a peer address produce an information that is important for +a seed node. +They indicate that a peer is unreachable, or is not operating correctly, and +therefore its address should not be provided to other nodes. +This occurs when, due to multiple failed connection attempts or authentication +failures, the peer address ends up being removed from the address book. +As a result, the periodically crawling of selected peers not only enables the +discovery of new peers, but also allows the seed node to stop providing +addresses of bad peers. + +### Offering addresses + +Nodes operating in seed mode handle PEX requests differently than regular +nodes, whose operation is described [here](#providing-addresses). + +This distinction exists because nodes dial a seed node with the main, if not +exclusive goal of retrieving peer addresses. +In other words, nodes do not dial a seed node because they intend to have it as +a peer in the multiple Tendermint protocols, but because they believe that a +seed node is a good source of addresses of nodes to which they can establish +connections and interact in the multiple Tendermint protocols. + +So, when a seed node receives a `PexRequest` message from an inbound peer, +it sends a `PexAddrs` message, containing a selection of peer +addresses, back to the peer and *disconnects* from it. +Seed nodes therefore treat inbound connections from peers as a short-term +connections, exclusively intended to retrieve peer addresses. +Once the requested peer addresses are sent, the connection with the peer is closed. + +Moreover, the selection of peer addresses provided to inbound peers by a seed +node, although still essentially random, has a [bias toward old +addresses](./addressbook.md#random-selection-with-bias). +The selection bias is defined by `biasToSelectNewPeers`, hard-coded to `30%`, +meaning that `70%` of the peer addresses provided by a seed node are expected +to be old addresses. +Although this nomenclature is not clear, *old* addresses are the addresses that +survived the most in the address book, that is, are addresses that the seed +node believes being from *good* peers (more details [here](./addressbook.md#good-peers)). + +Another distinction is on the handling of potential [misbehavior](#misbehavior-1) +of peers requesting addresses. +A seed node does not enforce, a priori, a minimal interval between PEX requests +from inbound peers. +Instead, it does not reply to more than one PEX request per peer inbound +connection, and, as above mentioned, it disconnects from incoming peers after +responding to them. +If the same peer dials again to the seed node and requests peer addresses, the +seed node will reply to this peer like it was the first time it has requested +peer addresses. + +> This is more an implementation restriction than a desired behavior. +> The `lastReceivedRequests` map stores the last time a PEX request was +> received from a peer, and the entry relative to a peer is removed from this +> map when the peer is disconnected. +> +> It is debatable whether this approach indeed prevents abuse against seed nodes. + +### Disconnecting from peers + +Seed nodes treat connections with peers as short-term connections, which are +mainly, if not exclusively, intended to exchange peer addresses. + +In the case of inbound peers, that have dialed the seed node, the intent of the +connection is achieved once a PEX response is sent to the peer. +The seed node thus disconnects from an inbound peer after sending a `PexAddrs` +message to it. + +In the case of outbound peers, which the seed node has dialed for crawling peer +addresses, the intent of the connection is essentially achieved when a PEX +response is received from the peer. +The seed node, however, does not disconnect from a peer after receiving a +selection of peer addresses from it. +As a result, after some rounds of crawling, a seed node will have established +connections to a substantial amount of peers. + +To couple with the existence of multiple connections with peers that have no +longer purpose for the seed node, the `crawlPeersRoutine` also invokes, after +each round of crawling, the `attemptDisconnects` method. +This method retrieves the list of connected peers from the switch, and +disconnects from peers that are not persistent peers, and with which a +connection is established for more than `SeedDisconnectWaitPeriod`. +This period is a configuration parameter, set to 28 hours when the PEX reactor +is created by the default node constructor. diff --git a/spec/p2p/v0.34/pex.md b/spec/p2p/v0.34/pex.md new file mode 100644 index 000000000..f976cc83b --- /dev/null +++ b/spec/p2p/v0.34/pex.md @@ -0,0 +1,111 @@ +# PEX Reactor + +The PEX reactor is one of the reactors running in a Tendermint node. + +Its implementation is located in the `p2p/pex` package, and it is considered +part of the implementation of the p2p layer. + +This document overviews the implementation of the PEX reactor, describing how +the methods from the `Reactor` interface are implemented. + +The actual operation of the PEX reactor is presented in documents describing +the roles played by the PEX reactor in the p2p layer: + +- [Address Book](./addressbook.md): stores known peer addresses and information + about peers to which the node is connected or has attempted to connect +- [Peer Manager](./peer_manager.md): manages connections established with peers, + defining when a node should dial peers and which peers it should dial +- [Peer Exchange protocol](./pex-protocol.md): enables nodes to exchange peer + addresses, thus implementing a peer discovery service + +## OnStart + +The `OnStart` method implements `BaseService` and starts the PEX reactor. + +The [address book](./addressbook.md), which is a `Service` is started. +This loads the address book content from disk, +and starts a routine that periodically persists the address book content to disk. + +The PEX reactor is configured with the addresses of a number of seed nodes, +the `Seeds` parameter of the `ReactorConfig`. +The addresses of seed nodes are parsed into `NetAddress` instances and resolved +into IP addresses, which is implemented by the `checkSeeds` method. +Valid seed node addresses are stored in the `seedAddrs` field, +and are used by the `dialSeeds` method to contact the configured seed nodes. + +The last action is to start one of the following persistent routines, based on +the `SeedMode` configuration parameter: + +- Regular nodes run the `ensurePeersRoutine` to check whether the node has + enough outbound peers, dialing peers when necessary +- Seed nodes run the `crawlPeersRoutine` to periodically start a new round + of [crawling](./pex-protocol.md#Crawling-peers) to discover as many peer + addresses as possible + +### Errors + +Errors encountered when loading the address book from disk are returned, +and prevent the reactor from being started. +An exception is made for the `service.ErrAlreadyStarted` error, which is ignored. + +Errors encountered when parsing the configured addresses of seed nodes +are returned and cause the reactor startup to fail. +An exception is made for DNS resolution `ErrNetAddressLookup` errors, +which are not deemed fatal and are only logged as invalid addresses. + +If none of the configured seed node addresses is valid, and the loaded address +book is empty, the reactor is not started and an error is returned. + +## OnStop + +The `OnStop` method implements `BaseService` and stops the PEX reactor. + +The address book routine that periodically saves its content to disk is stopped. + +## GetChannels + +The `GetChannels` method, from the `Reactor` interface, returns the descriptor +of the channel used by the PEX protocol. + +The channel ID is `PexChannel` (0), with priority `1`, send queue capacity of +`10`, and maximum message size of `64000` bytes. + +## AddPeer + +The `AddPeer` method, from the `Reactor` interface, +adds a new peer to the PEX protocol. + +If the new peer is an **inbound peer**, i.e., if the peer has dialed the node, +the peer's address is [added to the address book](./addressbook.md#adding-addresses). +Since the peer was authenticated when establishing a secret connection with it, +the source of the peer address is trusted, and its source is set by the peer itself. +In the case of an outbound peer, the node should already have its address in +the address book, as the switch has dialed the peer. + +If the peer is an **outbound peer**, i.e., if the node has dialed the peer, +and the PEX protocol needs more addresses, +the node [sends a PEX request](./pex-protocol.md#Requesting-Addresses) to the peer. +The same is not done when inbound peers are added because they are deemed least +trustworthy than outbound peers. + +## RemovePeer + +The `RemovePeer` method, from the `Reactor` interface, +removes a peer from the PEX protocol. + +The peer's ID is removed from the tables tracking PEX requests +[sent](./pex-protocol.md#misbehavior) but not yet replied +and PEX requests [received](./pex-protocol.md#misbehavior-1). + +## Receive + +The `Receive` method, from the `Reactor` interface, +handles a message received by the PEX protocol. + +A node receives two type of messages as part of the PEX protocol: + +- `PexRequest`: a request for addresses received from a peer, handled as + described [here](./pex-protocol.md#providing-addresses) +- `PexAddrs`: a list of addresses received from a peer, as a reponse to a PEX + request sent by the node, as described [here](./pex-protocol.md#responses) + diff --git a/spec/p2p/v0.34/switch.md b/spec/p2p/v0.34/switch.md new file mode 100644 index 000000000..54a0f63c2 --- /dev/null +++ b/spec/p2p/v0.34/switch.md @@ -0,0 +1,237 @@ +# Switch + +The switch is a core component of the p2p layer. +It manages the procedures for [dialing peers](#dialing-peers) and +[accepting](#accepting-peers) connections from peers, which are actually +implemented by the [transport](./transport.md). +It also manages the reactors, i.e., protocols implemented by the node that +interact with its peers. +Once a connection with a peer is established, the peer is [added](#add-peer) to +the switch and all registered reactors. +Reactors may also instruct the switch to [stop a peer](#stop-peer), namely +disconnect from it. +The switch, in this case, makes sure that the peer is removed from all +registered reactors. + +## Dialing peers + +Dialing a peer is implemented by the `DialPeerWithAddress` method. + +This method is invoked by the [peer manager](./peer_manager.md#ensure-peers) +to dial a peer address and establish a connection with an outbound peer. + +The switch keeps a single dialing routine per peer ID. +This is ensured by keeping a synchronized map `dialing` with the IDs of peers +to which the peer is dialing. +A peer ID is added to `dialing` when the `DialPeerWithAddress` method is called +for that peer, and it is removed when the method returns for whatever reason. +The method returns immediately when invoked for a peer which ID is already in +the `dialing` structure. + +The actual dialing is implemented by the [`Dial`](./transport.md#dial) method +of the transport configured for the switch, in the `addOutboundPeerWithConfig` +method. +If the transport succeeds establishing a connection, the returned `Peer` is +added to the switch using the [`addPeer`](#add-peer) method. +This operation can fail, returning an error. In this case, the switch invokes +the transport's [`Cleanup`](./transport.md#cleanup) method to clean any resources +associated with the peer. + +If the transport fails to establish a connection with the peer that is configured +as a persistent peer, the switch spawns a routine to [reconnect to the peer](#reconnect-to-peer). +If the peer is already in the `reconnecting` state, the spawned routine has no +effect and returns immediately. +This is in fact a likely scenario, as the `reconnectToPeer` routine relies on +this same `DialPeerWithAddress` method for dialing peers. + +### Manual operation + +The `DialPeersAsync` method receives a list of peer addresses (strings) +and dials all of them in parallel. +It is invoked in two situations: + +- In the [setup](https://github.com/tendermint/tendermint/blob/29c5a062d23aaef653f11195db55c45cd9e02715/node/node.go#L985) of a node, to establish connections with every configured + persistent peer +- In the RPC package, to implement two unsafe RPC commands, not used in production: + [`DialSeeds`](https://github.com/tendermint/tendermint/blob/29c5a062d23aaef653f11195db55c45cd9e02715/rpc/core/net.go#L47) and + [`DialPeers`](https://github.com/tendermint/tendermint/blob/29c5a062d23aaef653f11195db55c45cd9e02715/rpc/core/net.go#L87) + +The received list of peer addresses to dial is parsed into `NetAddress` instances. +In case of parsing errors, the method returns. An exception is made for +DNS resolution `ErrNetAddressLookup` errors, which do not interrupt the procedure. + +As the peer addresses provided to this method are typically not known by the node, +contrarily to the addressed dialed using the `DialPeerWithAddress` method, +they are added to the node's address book, which is persisted to disk. + +The switch dials the provided peers in parallel. +The list of peer addresses is randomly shuffled, and for each peer a routine is +spawned. +Each routine sleeps for a random interval, up to 3 seconds, then invokes the +`DialPeerWithAddress` method that actually dials the peer. + +### Reconnect to peer + +The `reconnectToPeer` method is invoked when a connection attempt to a peer fails, +and the peer is configured as a persistent peer. + +The `reconnecting` synchronized map keeps the peer's in this state, identified +by their IDs (string). +This should ensure that a single instance of this method is running at any time. +The peer is kept in this map while this method is running for it: it is set on +the beginning, and removed when the method returns for whatever reason. +If the peer is already in the `reconnecting` state, nothing is done. + +The remaining of the method performs multiple connection attempts to the peer, +via `DialPeerWithAddress` method. +If a connection attempt succeeds, the methods returns and the routine finishes. +The same applies when an `ErrCurrentlyDialingOrExistingAddress` error is +returned by the dialing method, as it indicates that peer is already connected +or that another routine is attempting to (re)connect to it. + +A first set of connection attempts is done at (about) regular intervals. +More precisely, between two attempts, the switch waits for a interval of +`reconnectInterval`, hard-coded to 5 seconds, plus a random jitter up to +`dialRandomizerIntervalMilliseconds`, hard-coded to 3 seconds. +At most `reconnectAttempts`, hard-coded to 20, are made using this +regular-interval approach. + +A second set of connection attempts is done with exponentially increasing +intervals. +The base interval `reconnectBackOffBaseSeconds` is hard-coded to 3 seconds, +which is also the increasing factor. +The exponentially increasing dialing interval is adjusted as well by a random +jitter up to `dialRandomizerIntervalMilliseconds`. +At most `reconnectBackOffAttempts`, hard-coded to 10, are made using this approach. + +> Note: the first sleep interval, to which a random jitter is applied, is 1, +> not `reconnectBackOffBaseSeconds`, as the first exponent is `0`... + +## Accepting peers + +The `acceptRoutine` method is a persistent routine that handles connections +accepted by the transport configured for the switch. + +The [`Accept`](./transport.md#accept) method of the configured transport +returns a `Peer` with which an inbound connection was established. +The switch accepts a new peer if the maximum number of inbound peers was not +reached, or if the peer was configured as an _unconditional peer_. +The maximum number of inbound peers is determined by the `MaxNumInboundPeers` +configuration parameter, whose default value is `40`. + +If accepted, the peer is added to the switch using the [`addPeer`](#add-peer) method. +If the switch does not accept the established incoming connection, or if the +`addPeer` method returns an error, the switch invokes the transport's +[`Cleanup`](./transport.md#cleanup) method to clean any resources associated +with the peer. + +The transport's `Accept` method can also return a number of errors. +Errors of `ErrRejected` or `ErrFilterTimeout` types are ignored, +an `ErrTransportClosed` causes the accepted routine to be interrupted, +while other errors cause the routine to panic. + +> TODO: which errors can cause the routine to panic? + +## Add peer + +The `addPeer` method adds a peer to the switch, +either after dialing (by `addOutboundPeerWithConfig`, called by `DialPeerWithAddress`) +a peer and establishing an outbound connection, +or after accepting (`acceptRoutine`) a peer and establishing an inbound connection. + +The first step is to invoke the `filterPeer` method. +It checks whether the peer is already in the set of connected peers, +and whether any of the configured `peerFilter` methods reject the peer. +If the peer is already present or it is rejected by any filter, the `addPeer` +method fails and returns an error. + +Then, the new peer is started, added to the set of connected peers, and added +to all reactors. +More precisely, first the new peer's information is first provided to every +reactor (`InitPeer` method). +Next, the peer's sending and receiving routines are started, and the peer is +added to set of connected peers. +These two operations can fail, causing `addPeer` to return an error. +Then, in the absence of previous errors, the peer is added to every reactor (`AddPeer` method). + +> Adding the peer to the peer set returns a `ErrSwitchDuplicatePeerID` error +> when a peer with the same ID is already presented. +> +> TODO: Starting a peer could be reduced as starting the MConn with that peer? + +## Stop peer + +There are two methods for stopping a peer, namely disconnecting from it, and +removing it from the table of connected peers. + +The `StopPeerForError` method is invoked to stop a peer due to an external +error, which is provided to method as a generic "reason". + +The `StopPeerGracefully` method stops a peer in the absence of errors or, more +precisely, not providing to the switch any "reason" for that. + +In both cases the `Peer` instance is stopped, the peer is removed from all +registered reactors, and finally from the list of connected peers. + +> Issue https://github.com/tendermint/tendermint/issues/3338 is mentioned in +> the internal `stopAndRemovePeer` method explaining why removing the peer from +> the list of connected peers is the last action taken. + +When there is a "reason" for stopping the peer (`StopPeerForError` method) +and the peer is a persistent peer, the method creates a routine to attempt +reconnecting to the peer address, using the `reconnectToPeer` method. +If the peer is an outbound peer, the peer's address is know, since the switch +has dialed the peer. +Otherwise, the peer address is retrieved from the `NodeInfo` instance from the +connection handshake. + +## Add reactor + +The `AddReactor` method registers a `Reactor` to the switch. + +The reactor is associated to the set of channel ids it employs. +Two reactors (in the same node) cannot share the same channel id. + +There is a call back to the reactor, in which the switch passes itself to the +reactor. + +## Remove reactor + +The `RemoveReactor` method unregisters a `Reactor` from the switch. + +The reactor is disassociated from the set of channel ids it employs. + +There is a call back to the reactor, in which the switch passes `nil` to the +reactor. + +## OnStart + +This is a `BaseService` method. + +All registered reactors are started. + +The switch's `acceptRoutine` is started. + +## OnStop + +This is a `BaseService` method. + +All (connected) peers are stopped and removed from the peer's list using the +`stopAndRemovePeer` method. + +All registered reactors are stopped. + +## Broadcast + +This method broadcasts a message on a channel, by sending the message in +parallel to all connected peers. + +The method spawns a thread for each connected peer, invoking the `Send` method +provided by each `Peer` instance with the provided message and channel ID. +The return value (a boolean) of these calls are redirected to a channel that is +returned by the method. + +> TODO: detail where this method is invoked: +> - By the consensus protocol, in `broadcastNewRoundStepMessage`, +> `broadcastNewValidBlockMessage`, and `broadcastHasVoteMessage` +> - By the state sync protocol diff --git a/spec/p2p/v0.34/transport.md b/spec/p2p/v0.34/transport.md new file mode 100644 index 000000000..9bb486a3d --- /dev/null +++ b/spec/p2p/v0.34/transport.md @@ -0,0 +1,222 @@ +# Transport + +The transport establishes secure and authenticated connections with peers. + +The transport [`Dial`](#dial)s peer addresses to establish outbound connections, +and [`Listen`](#listen)s in a configured network address +to [`Accept`](#accept) inbound connections from peers. + +The transport establishes raw TCP connections with peers +and [upgrade](#connection-upgrade) them into authenticated secret connections. +The established secret connection is then wrapped into `Peer` instance, which +is returned to the caller, typically the [switch](./switch.md). + +## Dial + +The `Dial` method is used by the switch to establish an outbound connection with a peer. +It is a synchronous method, which blocks until a connection is established or an error occurs. +The method returns an outbound `Peer` instance wrapping the established connection. + +The transport first dials the provided peer's address to establish a raw TCP connection. +The dialing maximum duration is determined by `dialTimeout`, hard-coded to 1 second. +The established raw connection is then submitted to a set of [filters](#connection-filtering), +which can reject it. +If the connection is not rejected, it is recorded in the table of established connections. + +The established raw TCP connection is then [upgraded](#connection-upgrade) into +an authenticated secret connection. +This procedure should ensure, in particular, that the public key of the remote peer +matches the ID of the dialed peer, which is part of peer address provided to this method. +In the absence of errors, +the established secret connection (`conn.SecretConnection` type) +and the information about the peer (`NodeInfo` record) retrieved and verified +during the version handshake, +are wrapped into an outbound `Peer` instance and returned to the switch. + +## Listen + +The `Listen` method produces a TCP listener instance for the provided network +address, and spawns an `acceptPeers` routine to handle the raw connections +accepted by the listener. +The `NetAddress` method exports the listen address configured for the transport. + +The maximum number of simultaneous incoming connections accepted by the listener +is bound to `MaxNumInboundPeer` plus the configured number of unconditional peers, +using the `MultiplexTransportMaxIncomingConnections` option, +in the node [initialization](https://github.com/tendermint/tendermint/blob/29c5a062d23aaef653f11195db55c45cd9e02715/node/node.go#L563). + +This method is called when a node is [started](https://github.com/tendermint/tendermint/blob/29c5a062d23aaef653f11195db55c45cd9e02715/node/node.go#L972). +In case of errors, the `acceptPeers` routine is not started and the error is returned. + +## Accept + +The `Accept` method returns to the switch inbound connections established with a peer. +It is a synchronous method, which blocks until a connection is accepted or an error occurs. +The method returns an inbound `Peer` instance wrapping the established connection. + +The transport handles incoming connections in the `acceptPeers` persistent routine. +This routine is started by the [`Listen`](#listen) method +and accepts raw connections from a TCP listener. +A new routine is spawned for each accepted connection. +The raw connection is submitted to a set of [filters](#connection-filtering), +which can reject it. +If the connection is not rejected, it is recorded in the table of established connections. + +The established raw TCP connection is then [upgraded](#connection-upgrade) into +an authenticated secret connection. +The established secret connection (`conn.SecretConnection` type), +the information about the peer (`NodeInfo` record) retrieved and verified +during the version handshake, +as well any error returned in this process are added to a queue of accepted connections. +This queue is consumed by the `Accept` method. + +> Handling accepted connection asynchronously was introduced due to this issue: +> https://github.com/tendermint/tendermint/issues/2047 + +## Connection Filtering + +The `filterConn` method is invoked for every new raw connection established by the transport. +Its main goal is avoid the transport to maintain duplicated connections with the same peer. +It also runs a set of configured connection filters. + +The transports keeps a table `conns` of established connections. +The table maps the remote address returned by a generic connection to a list of +IP addresses, to which the connection remote address is resolved. +If the remote address of the new connection is already present in the table, +the connection is rejected. +Otherwise, the connection's remote address is resolved into a list of IPs, +which are recorded in the established connections table. + +The connection and the resolved IPs are then passed through a set of connection filters, +configured via the `MultiplexTransportConnFilters` transport option. +The maximum duration for the filters execution, which is performed in parallel, +is determined by `filterTimeout`. +Its default value is 5 seconds, +which can be changed using the `MultiplexTransportFilterTimeout` transport option. + +If the connection and the resolved remote addresses are not filtered out, +the transport registers them into the `conns` table and returns. + +In case of errors, the connection is removed from the table of established +connections and closed. + +### Errors + +If the address of the new connection is already present in the `conns` table, +an `ErrRejected` error with the `isDuplicate` reason is returned. + +If the IP resolution of the connection's remote address fails, +an `AddrError` or `DNSError` error is returned. + +If any of the filters reject the connection, +an `ErrRejected` error with the `isRejected` reason is returned. + +If the filters execution times out, +an `ErrFilterTimeout` error is returned. + +## Connection Upgrade + +The `upgrade` method is invoked for every new raw connection established by the +transport that was not [filtered out](#connection-filtering). +It upgrades an established raw TCP connection into a secret authenticated +connection, and validates the information provided by the peer. + +This is a complex procedure, that can be summarized by the following three +message exchanges between the node and the new peer: + +1. Encryption: the nodes produce ephemeral key pairs and exchange ephemeral + public keys, from which are derived: (i) a pair of secret keys used to + encrypt the data exchanged between the nodes, and (ii) a challenge message. +1. Authentication: the nodes exchange their persistent public keys and a + signature of the challenge message produced with the their persistent + private keys. This allows validating the peer's persistent public key, + which plays the role of node ID. +1. Version handshake: nodes exchange and validate each other `NodeInfo` records. + This records contain, among other fields, their node IDs, the network/chain + ID they are part of, and the list of supported channel IDs. + +Steps (1) and (2) are implemented in the `conn` package. +In case of success, they produce the secret connection that is actually used by +the node to communicate with the peer. +An overview of this procedure, which implements the station-to-station (STS) +[protocol][sts-paper] ([PDF][sts-paper-pdf]), can be found [here][peer-sts]. +The maximum duration for establishing a secret connection with the peer is +defined by `handshakeTimeout`, hard-coded to 3 seconds. + +The established secret connection stores the persistent public key of the peer, +which has been validated via the challenge authentication of step (2). +If the connection being upgraded is an outbound connection, i.e., if the node has +dialed the peer, the dialed peer's ID is compared to the peer's persistent public key: +if they do not match, the connection is rejected. +This verification is not performed in the case of inbound (accepted) connections, +as the node does not know a priori the remote node's ID. + +Step (3), the version handshake, is performed by the transport. +Its maximum duration is also defined by `handshakeTimeout`, hard-coded to 3 seconds. +The version handshake retrieves the `NodeInfo` record of the new peer, +which can be rejected for multiple reasons, listed [here][peer-handshake]. + +If the connection upgrade succeeds, the method returns the established secret +connection, an instance of `conn.SecretConnection` type, +and the `NodeInfo` record of the peer. + +In case of errors, the connection is removed from the table of established +connections and closed. + +### Errors + +The timeouts for steps (1) and (2), and for step (3), are configured as the +deadline for operations on the TCP connection that is being upgraded. +If this deadline it is reached, the connection produces an +`os.ErrDeadlineExceeded` error, returned by the corresponding step. + +Any error produced when establishing a secret connection with the peer (steps 1 and 2) or +during the version handshake (step 3), including timeouts, +is encapsulated into an `ErrRejected` error with reason `isAuthFailure` and returned. + +If the upgraded connection is an outbound connection, and the peer ID learned in step (2) +does not match the dialed peer's ID, +an `ErrRejected` error with reason `isAuthFailure` is returned. + +If the peer's `NodeInfo` record, retrieved in step (3), is invalid, +or if reports a node ID that does not match peer ID learned in step (2), +an `ErrRejected` error with reason `isAuthFailure` is returned. +If it reports a node ID equals to the local node ID, +an `ErrRejected` error with reason `isSelf` is returned. +If it is not compatible with the local `NodeInfo`, +an `ErrRejected` error with reason `isIncompatible` is returned. + +## Close + +The `Close` method closes the TCP listener created by the `Listen` method, +and sends a signal for interrupting the `acceptPeers` routine. + +This method is called when a node is [stopped](https://github.com/tendermint/tendermint/blob/46badfabd9d5491c78283a0ecdeb695e21785508/node/node.go#L1019). + +## Cleanup + +The `Cleanup` method receives a `Peer` instance, +and removes the connection established with a peer from the table of established connections. +It also invokes the `Peer` interface method to close the connection associated with a peer. + +It is invoked when the connection with a peer is closed. + +## Supported channels + +The `AddChannel` method registers a channel in the transport. + +The channel ID is added to the list of supported channel IDs, +stored in the local `NodeInfo` record. + +The `NodeInfo` record is exchanged with peers in the version handshake. +For this reason, this method is not invoked with a started transport. + +> The only call to this method is performed in the `CustomReactors` constructor +> option of a node, i.e., before the node is started. +> Note that the default list of supported channel IDs, including the default reactors, +> is provided to the transport as its original `NodeInfo` record. + +[peer-sts]: https://github.com/tendermint/tendermint/blob/main/spec/p2p/peer.md#authenticated-encryption-handshake +[peer-handshake]:https://github.com/tendermint/tendermint/blob/main/spec/p2p/peer.md#tendermint-version-handshake +[sts-paper]: https://link.springer.com/article/10.1007/BF00124891 +[sts-paper-pdf]: https://github.com/tendermint/tendermint/blob/0.1/docs/sts-final.pdf diff --git a/spec/p2p/v0.34/types.md b/spec/p2p/v0.34/types.md new file mode 100644 index 000000000..ba43980ee --- /dev/null +++ b/spec/p2p/v0.34/types.md @@ -0,0 +1,239 @@ +# Types adopted in the p2p implementation + +This document lists the packages and source files, excluding test units, that +implement the p2p layer, and summarizes the main types they implement. +Types play the role of classes in Go. + +The reference version for this documentation is the branch +[`v0.34.x`](https://github.com/tendermint/tendermint/tree/v0.34.x/p2p). + +State of August 2022. + +## Package `p2p` + +Implementation of the p2p layer of Tendermint. + +### `base_reactor.go` + +`Reactor` interface. + +`BaseReactor` implements `Reactor`. + +**Not documented yet**. + +### `conn_set.go` + +`ConnSet` interface, a "lookup table for connections and their ips". + +Internal type `connSet` implements the `ConnSet` interface. + +Used by the [transport](#transportgo) to store connected peers. + +### `errors.go` + +Defines several error types. + +`ErrRejected` enumerates a number of reason for which a peer was rejected. +Mainly produced by the [transport](#transportgo), +but also by the [switch](#switchgo). + +`ErrSwitchDuplicatePeerID` is produced by the `PeerSet` used by the [switch](#switchgo). + +`ErrSwitchConnectToSelf` is handled by the [switch](#switchgo), +but currently is not produced outside tests. + +`ErrSwitchAuthenticationFailure` is handled by the [PEX reactor](#pex_reactorgo), +but currently is not produced outside tests. + +`ErrTransportClosed` is produced by the [transport](#transportgo) +and handled by the [switch](#switchgo). + +`ErrNetAddressNoID`, `ErrNetAddressInvalid`, and `ErrNetAddressLookup` +are parsing a string to create an instance of `NetAddress`. +It can be returned in the setup of the [switch](#switchgo) +and of the [PEX reactor](#pex_reactorgo), +as well when the [transport](#transportgo) validates a `NodeInfo`, as part of +the connection handshake. + +`ErrCurrentlyDialingOrExistingAddress` is produced by the [switch](#switchgo), +and handled by the switch and the [PEX reactor](#pex_reactorgo). + +### `fuzz.go` + +For testing purposes. + +`FuzzedConnection` wraps a `net.Conn` and injects random delays. + +### `key.go` + +`NodeKey` is the persistent key of a node, namely its private key. + +The `ID` of a node is a string representing the node's public key. + +### `metrics.go` + +Prometheus `Metrics` exposed by the p2p layer. + +### `netaddress.go` + +Type `NetAddress` contains the `ID` and the network address (IP and port) of a node. + +The API of the [address book](#addrbookgo) receives and returns `NetAddress` instances. + +This source file was adapted from [`btcd`](https://github.com/btcsuite/btcd), +a Go implementation of Bitcoin. + +### `node_info.go` + +Interface `NodeInfo` stores the basic information about a node exchanged with a +peer during the handshake. + +It is implemented by `DefaultNodeInfo` type. + +The [switch](#switchgo) stores the local `NodeInfo`. + +The `NodeInfo` of connected peers is produced by the +[transport](#transportgo) during the handshake, and stored in [`Peer`](#peergo) instances. + +### `peer.go` + +Interface `Peer` represents a connected peer. + +It is implemented by the internal `peer` type. + +The [transport](#transportgo) API methods return `Peer` instances, +wrapping established secure connection with peers. + +The [switch](#switchgo) API methods receive `Peer` instances. +The switch stores connected peers in a `PeerSet`. + +The [`Reactor`](#base_reactorgo) methods, invoked by the switch, receive `Peer` instances. + +### `peer_set.go` + +Interface `IPeerSet` offers methods to access a table of [`Peer`](#peergo) instances. + +Type `PeerSet` implements a thread-safe table of [`Peer`](#peergo) instances, +used by the [switch](#switchgo). + +The switch provides limited access to this table by returing a `IPeerSet` +instance, used by the [PEX reactor](#pex_reactorgo). + +### `switch.go` + +Documented in [switch](./switch.md). + +The `Switch` implements the [peer manager](./peer_manager.md) role for inbound peers. + +[`Reactor`](#base_reactorgo)s have access to the `Switch` and may invoke its methods. +This includes the [PEX reactor](#pex_reactorgo). + +### `transport.go` + +Documented in [transport](./transport.md). + +The `Transport` interface is implemented by `MultiplexTransport`. + +The [switch](#switchgo) contains a `Transport` and uses it to establish +connections with peers. + +### `types.go` + +Aliases for p2p's `conn` package types. + +## Package `p2p.conn` + +Implements the connection between Tendermint nodes, +which is encrypted, authenticated, and multiplexed. + +### `connection.go` + +Implements the `MConnection` type and the `Channel` abstraction. + +A `MConnection` multiplexes a generic network connection (`net.Conn`) into +multiple independent `Channel`s, used by different [`Reactor`](#base_reactorgo)s. + +A [`Peer`](#peergo) stores the `MConnection` instance used to interact with a +peer, which multiplex a [`SecretConnection`](#secret_connectiongo). + +### `conn_go110.go` + +Support for go 1.10. + +### `secret_connection.go` + +Implements the `SecretConnection` type, which is an encrypted authenticated +connection built atop a raw network (TCP) connection. + +A [`Peer`](#peergo) stores the `SecretConnection` established by the transport, +which is the underlying connection multiplexed by [`MConnection`](#connectiongo). + +As briefly documented in the [transport](./transport.md#Connection-Upgrade), +a `SecretConnection` implements the Station-To-Station (STS) protocol. + +The `SecretConnection` type implements the `net.Conn` interface, +which is a generic network connection. + +## Package `p2p.mock` + +Mock implementations of [`Peer`](#peergo) and [`Reactor`](#base_reactorgo) interfaces. + +## Package `p2p.mocks` + +Code generated by `mockery`. + +## Package `p2p.pex` + +Implementation of the [PEX reactor](./pex.md). + +### `addrbook.go` + +Documented in [address book](./addressbook.md). + +This source file was adapted from [`btcd`](https://github.com/btcsuite/btcd), +a Go implementation of Bitcoin. + +### `errors.go` + +A number of errors produced and handled by the [address book](#addrbookgo). + +`ErrAddrBookNilAddr` is produced by the address book, but handled (logged) by +the [PEX reactor](#pex_reactorgo). + +`ErrUnsolicitedList` is produced and handled by the [PEX protocol](#pex_reactorgo). + +### `file.go` + +Implements the [address book](#addrbookgo) persistence. + +### `known_address.go` + +Type `knownAddress` represents an address stored in the [address book](#addrbookgo). + +### `params.go` + +Constants used by the [address book](#addrbookgo). + +### `pex_reactor.go` + +Implementation of the [PEX reactor](./pex.md), which is a [`Reactor`](#base_reactorgo). + +This includes the implementation of the [PEX protocol](./pex-protocol.md) +and of the [peer manager](./peer_manager.md) role for outbound peers. + +The PEX reactor also manages an [address book](#addrbookgo) instance. + +## Package `p2p.trust` + +Go documentation of `Metric` type: + +> // Metric - keeps track of peer reliability +> // See tendermint/docs/architecture/adr-006-trust-metric.md for details + +Not imported by any other Tendermint source file. + +## Package `p2p.upnp` + +This package implementation was taken from "taipei-torrent". + +It is used by the `probe-upnp` command of the Tendermint binary.