Address Manager

Before diving into the details of P2P networking, let's understand the crucial address manager module.

In the last section, we saw that the create_connection method uses the self.address_man.get_address_to_connect method to obtain a suitable address for connection. This method belongs to the AddressMan struct (a field from NodeCommon), which is responsible for maintaining a record of known peer addresses and providing them to the node when needed.

Filename: p2p_wire/address_man.rs

// Path: floresta-wire/src/p2p_wire/address_man.rs

pub struct AddressMan {
    addresses: HashMap<usize, LocalAddress>,
    good_addresses: Vec<usize>,
    good_peers_by_service: HashMap<ServiceFlags, Vec<usize>>,
    peers_by_service: HashMap<ServiceFlags, Vec<usize>>,
}

This struct is straightforward: it keeps known peer addresses in a HashMap (as LocalAddress), the indexes of good peers in the map, and associates peers with the services they support.

Local Address

We have also encountered the LocalAddress type a few times, which is implemented in this module. This type encapsulates all the information our node knows about each peer, effectively serving as our local representation of the peer's details.

// Path: floresta-wire/src/p2p_wire/address_man.rs

pub struct LocalAddress {
    /// An actual address
    address: AddrV2,
    /// Last time we successfully connected to this peer, in secs, only relevant if state == State::Tried
    last_connected: u64,
    /// Our local state for this peer, as defined in AddressState
    state: AddressState,
    /// Network services announced by this peer
    services: ServiceFlags,
    /// Network port this peers listens to
    port: u16,
    /// Random id for this peer
    pub id: usize,
}

The actual address is stored in the form of an AddrV2 enum, which is implemented by the bitcoin crate. AddrV2 represents various network address types supported in Bitcoin's P2P protocol, as defined in BIP155.

Concretely, AddrV2 includes variants for IPv4, IPv6, Tor (v2 and v3), I2P, Cjdns addresses, and an Unknown variant for unrecognized address types. This design allows the protocol to handle a diverse set of network addresses.

The LocalAddress also stores the last connection date or time, measured as seconds since the UNIX_EPOCH, an AddressState struct, the network services announced by the peer, the port that the peer listens to, and its identifier.

Below is the definition of AddressState, which tracks the current status and history of our interactions with this peer:

// Path: floresta-wire/src/p2p_wire/address_man.rs

pub enum AddressState {
    /// We never tried this peer before, so we don't know what to expect. This variant
    /// also applies to peers that we tried to connect, but failed, or we didn't connect
    /// to for a long time.
    NeverTried,
    /// We tried this peer before, and had success at least once, so we know what to expect
    Tried(u64),
    /// This peer misbehaved and we banned them
    Banned(u64),
    /// We are connected to this peer right now
    Connected,
    /// We tried connecting, but failed
    Failed(u64),
}

Get Address to Connect

Let's finally inspect the get_address_to_connect method on the AddressMan, which we use to create connections.

This method selects a peer address for a new connection based on required services and whether the connection is a feeler. First of all, we will return None if the address manager doesn't have any peers. Otherwise:

  • For feeler connections, it randomly picks an address, or returns None if the peer is Banned.
  • For regular connections, it prioritizes peers supporting the required services or falls back to a random address. Peers that are Connected are excluded. Banned and Failed ones are only accepted if enough time has passed. And those in the NeverTried and Tried states are considered valid. If no suitable address is found, it returns None.
// Path: floresta-wire/src/p2p_wire/address_man.rs

/// Returns a new random address to open a new connection, we try to get addresses with
/// a set of features supported for our peers
pub fn get_address_to_connect(
    &mut self,
    required_service: ServiceFlags,
    feeler: bool,
) -> Option<(usize, LocalAddress)> {
    if self.addresses.is_empty() {
        return None;
    }

    // Feeler connection are used to test if a peer is still alive, we don't care about
    // the features it supports or even if it's a valid peer. The only thing we care about
    // is that we haven't banned it.
    if feeler {
        let idx = rand::random::<usize>() % self.addresses.len();
        let peer = self.addresses.keys().nth(idx)?;
        let address = self.addresses.get(peer)?.to_owned();
        if let AddressState::Banned(_) = address.state {
            return None;
        }
        return Some((*peer, address));
    };

    for _ in 0..10 {
        let (id, peer) = self
            .get_address_by_service(required_service)
            .or_else(|| self.get_random_address(required_service))?;

        match peer.state {
            AddressState::NeverTried | AddressState::Tried(_) => {
                return Some((id, peer));
            }

            AddressState::Banned(when) | AddressState::Failed(when) => {
                let now = SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .unwrap()
                    .as_secs();

                if when + RETRY_TIME < now {
                    return Some((id, peer));
                }

                if let Some(peers) = self.good_peers_by_service.get_mut(&required_service) {
                    peers.retain(|&x| x != id)
                }

                self.good_addresses.retain(|&x| x != id);
            }

            AddressState::Connected => {}
        }
    }

    None
}

Dump Peers

Another key functionality implemented in this module is the ability to write the current peer data to a peers.json file, enabling the node to resume peer connections after a restart without repeating the initial peer discovery process.

To save each LocalAddress we use a slightly modified type called DiskLocalAddress, similar to how we used the DiskBlockHeader type to persist BlockHeaders.

// Path: floresta-wire/src/p2p_wire/address_man.rs

pub fn dump_peers(&self, datadir: &str) -> std::io::Result<()> {
    let peers: Vec<_> = self
        .addresses
        .values()
        .cloned()
        .map(Into::<DiskLocalAddress>::into)
        .collect::<Vec<_>>();
    let peers = serde_json::to_string(&peers);
    if let Ok(peers) = peers {
        std::fs::write(datadir.to_owned() + "/peers.json", peers)?;
    }
    Ok(())
}

Similarly, there's a dump_utreexo_peers method for persisting the utreexo peers into an anchors.json file. Peers that support utreexo are very valuable for our node; we need their utreexo proofs for validating blocks, and they are rare in the network.

// Path: floresta-wire/src/p2p_wire/address_man.rs

/// Dumps the connected utreexo peers to a file on dir `datadir/anchors.json` in json format `
/// inputs are the directory to save the file and the list of ids of the connected utreexo peers
pub fn dump_utreexo_peers(&self, datadir: &str, peers_id: &[usize]) -> std::io::Result<()> {
    // ...
    let addresses: Vec<DiskLocalAddress> = peers_id
        .iter()
        .filter_map(|id| Some(self.addresses.get(id)?.to_owned().into()))
        .collect();
    let addresses: Result<String, serde_json::Error> = serde_json::to_string(&addresses);
    if let Ok(addresses) = addresses {
        std::fs::write(datadir.to_owned() + "/anchors.json", addresses)?;
    }
    Ok(())
}

Great! This concludes the chapter. In the next chapter, we will dive into P2P communication and networking, focusing on the Peer type.