Files
lighthouse/beacon_node/lighthouse_network/tests/common.rs
Akihito Nakano 1324d3d3c4 Delayed RPC Send Using Tokens (#5923)
closes https://github.com/sigp/lighthouse/issues/5785


  The diagram below shows the differences in how the receiver (responder) behaves before and after this PR. The following sentences will detail the changes.

```mermaid
flowchart TD

subgraph "*** After ***"
Start2([START]) --> AA[Receive request]
AA --> COND1{Is there already an active request <br> with the same protocol?}
COND1 --> |Yes| CC[Send error response]
CC --> End2([END])
%% COND1 --> |No| COND2{Request is too large?}
%% COND2 --> |Yes| CC
COND1 --> |No| DD[Process request]
DD --> EE{Rate limit reached?}
EE --> |Yes| FF[Wait until tokens are regenerated]
FF --> EE
EE --> |No| GG[Send response]
GG --> End2
end

subgraph "*** Before ***"
Start([START]) --> A[Receive request]
A --> B{Rate limit reached <br> or <br> request is too large?}
B -->|Yes| C[Send error response]
C --> End([END])
B -->|No| E[Process request]
E --> F[Send response]
F --> End
end
```

### `Is there already an active request with the same protocol?`

This check is not performed in `Before`. This is taken from the PR in the consensus-spec, which proposes updates regarding rate limiting and response timeout.
https://github.com/ethereum/consensus-specs/pull/3767/files
> The requester MUST NOT make more than two concurrent requests with the same ID.

The PR mentions the requester side. In this PR, I introduced the `ActiveRequestsLimiter` for the `responder` side to restrict more than two requests from running simultaneously on the same protocol per peer. If the limiter disallows a request, the responder sends a rate-limited error and penalizes the requester.



### `Rate limit reached?` and `Wait until tokens are regenerated`

UPDATE: I moved the limiter logic to the behaviour side. https://github.com/sigp/lighthouse/pull/5923#issuecomment-2379535927

~~The rate limiter is shared between the behaviour and the handler.  (`Arc<Mutex<RateLimiter>>>`) The handler checks the rate limit and queues the response if the limit is reached. The behaviour handles pruning.~~

~~I considered not sharing the rate limiter between the behaviour and the handler, and performing all of these either within the behaviour or handler. However, I decided against this for the following reasons:~~

- ~~Regarding performing everything within the behaviour: The behaviour is unable to recognize the response protocol when `RPC::send_response()` is called, especially when the response is `RPCCodedResponse::Error`. Therefore, the behaviour can't rate limit responses based on the response protocol.~~
- ~~Regarding performing everything within the handler: When multiple connections are established with a peer, there could be multiple handlers interacting with that peer. Thus, we cannot enforce rate limiting per peer solely within the handler. (Any ideas? 🤔 )~~
2025-04-24 03:46:16 +00:00

278 lines
8.7 KiB
Rust

#![cfg(test)]
use lighthouse_network::service::Network as LibP2PService;
use lighthouse_network::Enr;
use lighthouse_network::EnrExt;
use lighthouse_network::Multiaddr;
use lighthouse_network::{NetworkConfig, NetworkEvent};
use std::sync::Arc;
use std::sync::Weak;
use tokio::runtime::Runtime;
use tracing::{debug, error, info_span, Instrument};
use tracing_subscriber::EnvFilter;
use types::{
ChainSpec, EnrForkId, Epoch, EthSpec, FixedBytesExtended, ForkContext, ForkName, Hash256,
MinimalEthSpec, Slot,
};
type E = MinimalEthSpec;
use lighthouse_network::rpc::config::InboundRateLimiterConfig;
use tempfile::Builder as TempBuilder;
/// Returns a dummy fork context
pub fn fork_context(fork_name: ForkName) -> ForkContext {
let mut chain_spec = E::default_spec();
let altair_fork_epoch = Epoch::new(1);
let bellatrix_fork_epoch = Epoch::new(2);
let capella_fork_epoch = Epoch::new(3);
let deneb_fork_epoch = Epoch::new(4);
let electra_fork_epoch = Epoch::new(5);
let fulu_fork_epoch = Epoch::new(6);
chain_spec.altair_fork_epoch = Some(altair_fork_epoch);
chain_spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch);
chain_spec.capella_fork_epoch = Some(capella_fork_epoch);
chain_spec.deneb_fork_epoch = Some(deneb_fork_epoch);
chain_spec.electra_fork_epoch = Some(electra_fork_epoch);
chain_spec.fulu_fork_epoch = Some(fulu_fork_epoch);
let current_slot = match fork_name {
ForkName::Base => Slot::new(0),
ForkName::Altair => altair_fork_epoch.start_slot(E::slots_per_epoch()),
ForkName::Bellatrix => bellatrix_fork_epoch.start_slot(E::slots_per_epoch()),
ForkName::Capella => capella_fork_epoch.start_slot(E::slots_per_epoch()),
ForkName::Deneb => deneb_fork_epoch.start_slot(E::slots_per_epoch()),
ForkName::Electra => electra_fork_epoch.start_slot(E::slots_per_epoch()),
ForkName::Fulu => fulu_fork_epoch.start_slot(E::slots_per_epoch()),
};
ForkContext::new::<E>(current_slot, Hash256::zero(), &chain_spec)
}
pub struct Libp2pInstance(
LibP2PService<E>,
#[allow(dead_code)]
// This field is managed for lifetime purposes may not be used directly, hence the `#[allow(dead_code)]` attribute.
async_channel::Sender<()>,
);
impl std::ops::Deref for Libp2pInstance {
type Target = LibP2PService<E>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for Libp2pInstance {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[allow(unused)]
pub fn build_tracing_subscriber(level: &str, enabled: bool) {
if enabled {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_new(level).unwrap())
.try_init()
.unwrap();
}
}
pub fn build_config(
mut boot_nodes: Vec<Enr>,
disable_peer_scoring: bool,
inbound_rate_limiter: Option<InboundRateLimiterConfig>,
) -> Arc<NetworkConfig> {
let mut config = NetworkConfig::default();
// Find unused ports by using the 0 port.
let port = 0;
let random_path: u16 = rand::random();
let path = TempBuilder::new()
.prefix(&format!("libp2p_test_{}", random_path))
.tempdir()
.unwrap();
config.set_ipv4_listening_address(std::net::Ipv4Addr::UNSPECIFIED, port, port, port);
config.enr_address = (Some(std::net::Ipv4Addr::LOCALHOST), None);
config.boot_nodes_enr.append(&mut boot_nodes);
config.network_dir = path.into_path();
config.disable_peer_scoring = disable_peer_scoring;
config.inbound_rate_limiter_config = inbound_rate_limiter;
Arc::new(config)
}
pub async fn build_libp2p_instance(
rt: Weak<Runtime>,
boot_nodes: Vec<Enr>,
fork_name: ForkName,
chain_spec: Arc<ChainSpec>,
service_name: String,
disable_peer_scoring: bool,
inbound_rate_limiter: Option<InboundRateLimiterConfig>,
) -> Libp2pInstance {
let config = build_config(boot_nodes, disable_peer_scoring, inbound_rate_limiter);
// launch libp2p service
let (signal, exit) = async_channel::bounded(1);
let (shutdown_tx, _) = futures::channel::mpsc::channel(1);
let executor = task_executor::TaskExecutor::new(rt, exit, shutdown_tx, service_name);
let libp2p_context = lighthouse_network::Context {
config,
enr_fork_id: EnrForkId::default(),
fork_context: Arc::new(fork_context(fork_name)),
chain_spec,
libp2p_registry: None,
};
Libp2pInstance(
LibP2PService::new(executor, libp2p_context)
.await
.expect("should build libp2p instance")
.0,
signal,
)
}
#[allow(dead_code)]
pub fn get_enr(node: &LibP2PService<E>) -> Enr {
node.local_enr()
}
// Protocol for the node pair connection.
pub enum Protocol {
Tcp,
Quic,
}
// Constructs a pair of nodes with separate loggers. The sender dials the receiver.
// This returns a (sender, receiver) pair.
#[allow(dead_code)]
pub async fn build_node_pair(
rt: Weak<Runtime>,
fork_name: ForkName,
spec: Arc<ChainSpec>,
protocol: Protocol,
disable_peer_scoring: bool,
inbound_rate_limiter: Option<InboundRateLimiterConfig>,
) -> (Libp2pInstance, Libp2pInstance) {
let mut sender = build_libp2p_instance(
rt.clone(),
vec![],
fork_name,
spec.clone(),
"sender".to_string(),
disable_peer_scoring,
inbound_rate_limiter.clone(),
)
.await;
let mut receiver = build_libp2p_instance(
rt,
vec![],
fork_name,
spec.clone(),
"receiver".to_string(),
disable_peer_scoring,
inbound_rate_limiter,
)
.await;
// let the two nodes set up listeners
let sender_fut = async {
loop {
if let NetworkEvent::NewListenAddr(addr) = sender.next_event().await {
// Only end once we've listened on the protocol we care about
match protocol {
Protocol::Tcp => {
if addr.iter().any(|multiaddr_proto| {
matches!(multiaddr_proto, libp2p::multiaddr::Protocol::Tcp(_))
}) {
return addr;
}
}
Protocol::Quic => {
if addr.iter().any(|multiaddr_proto| {
matches!(multiaddr_proto, libp2p::multiaddr::Protocol::QuicV1)
}) {
return addr;
}
}
}
}
}
}
.instrument(info_span!("Sender", who = "sender"));
let receiver_fut = async {
loop {
if let NetworkEvent::NewListenAddr(addr) = receiver.next_event().await {
match protocol {
Protocol::Tcp => {
if addr.iter().any(|multiaddr_proto| {
matches!(multiaddr_proto, libp2p::multiaddr::Protocol::Tcp(_))
}) {
return addr;
}
}
Protocol::Quic => {
if addr.iter().any(|multiaddr_proto| {
matches!(multiaddr_proto, libp2p::multiaddr::Protocol::QuicV1)
}) {
return addr;
}
}
}
}
}
}
.instrument(info_span!("Receiver", who = "receiver"));
let joined = futures::future::join(sender_fut, receiver_fut);
let receiver_multiaddr = joined.await.1;
match sender.testing_dial(receiver_multiaddr.clone()) {
Ok(()) => {
debug!(address = ?receiver_multiaddr, "Sender dialed receiver")
}
Err(_) => error!("Dialing failed"),
};
(sender, receiver)
}
// Returns `n` peers in a linear topology
#[allow(dead_code)]
pub async fn build_linear(
rt: Weak<Runtime>,
n: usize,
fork_name: ForkName,
spec: Arc<ChainSpec>,
) -> Vec<Libp2pInstance> {
let mut nodes = Vec::with_capacity(n);
for _ in 0..n {
nodes.push(
build_libp2p_instance(
rt.clone(),
vec![],
fork_name,
spec.clone(),
"linear".to_string(),
false,
None,
)
.await,
);
}
let multiaddrs: Vec<Multiaddr> = nodes
.iter()
.map(|x| get_enr(x).multiaddr()[1].clone())
.collect();
for i in 0..n - 1 {
match nodes[i].testing_dial(multiaddrs[i + 1].clone()) {
Ok(()) => debug!("Connected"),
Err(_) => error!("Failed to connect"),
};
}
nodes
}