Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ban solvers based on the settlements success rate #3263

Open
wants to merge 29 commits into
base: notify-banned-solvers
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
805946c
DB statistics guard
squadgazzz Jan 31, 2025
7ab2a38
Naming
squadgazzz Jan 31, 2025
cca115a
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Jan 31, 2025
983dc41
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Jan 31, 2025
160e2d9
New ban reason notification
squadgazzz Jan 31, 2025
597d268
min_settlement_success_rate
squadgazzz Jan 31, 2025
7c87087
Separate configs
squadgazzz Jan 31, 2025
df4b77f
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Feb 3, 2025
b2b4c91
Doc
squadgazzz Feb 12, 2025
7ef7db2
Switch to max failure rate
squadgazzz Feb 12, 2025
57cb190
Naming
squadgazzz Feb 12, 2025
1b7829f
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Feb 12, 2025
8af1e4b
Fix after merge
squadgazzz Feb 12, 2025
e7ad14f
Notifications refactoring
squadgazzz Feb 12, 2025
9f4b5ad
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Feb 12, 2025
ee7f492
Naming
squadgazzz Feb 12, 2025
f26d676
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Feb 12, 2025
0d57971
Fix after merge
squadgazzz Feb 12, 2025
839488f
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Feb 14, 2025
c76761a
As str
squadgazzz Feb 17, 2025
1a35e9d
Nit
squadgazzz Feb 17, 2025
173acea
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Feb 17, 2025
204984f
Fix after merge
squadgazzz Feb 17, 2025
e301f64
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Feb 17, 2025
2256282
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Feb 17, 2025
4bc5805
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Feb 18, 2025
2985000
Fix after merge
squadgazzz Feb 18, 2025
4a81454
Copy enum
squadgazzz Feb 18, 2025
6f315bb
Merge branch 'notify-banned-solvers' into db-statistics-based-guard
squadgazzz Feb 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions crates/autopilot/src/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,22 +253,63 @@ pub struct Arguments {

#[derive(Debug, clap::Parser)]
pub struct DbBasedSolverParticipationGuardConfig {
/// Enables or disables the solver participation guard
/// The time-to-live for the solver participation blacklist cache.
#[clap(long, env, default_value = "5m", value_parser = humantime::parse_duration)]
pub solver_blacklist_cache_ttl: Duration,

#[clap(flatten)]
pub non_settling_solvers_finder_config: NonSettlingSolversFinderConfig,

#[clap(flatten)]
pub low_settling_solvers_finder_config: LowSettlingSolversFinderConfig,
}

#[derive(Debug, clap::Parser)]
pub struct NonSettlingSolversFinderConfig {
/// Enables search of non-settling solvers.
#[clap(
id = "db_enabled",
long = "db-based-solver-participation-guard-enabled",
env = "DB_BASED_SOLVER_PARTICIPATION_GUARD_ENABLED",
id = "non_settling_solvers_blacklisting_enabled",
long = "non-settling-solvers-blacklisting-enabled",
env = "NON_SETTLING_SOLVERS_BLACKLISTING_ENABLED",
default_value = "true"
)]
pub enabled: bool,

/// The time-to-live for the solver participation blacklist cache.
#[clap(long, env, default_value = "5m", value_parser = humantime::parse_duration)]
pub solver_blacklist_cache_ttl: Duration,
/// The number of last auctions to check solver participation eligibility.
#[clap(
id = "non_settling_last_auctions_participation_count",
long = "non-settling-last-auctions-participation-count",
env = "NON_SETTLING_LAST_AUCTIONS_PARTICIPATION_COUNT",
default_value = "3"
)]
pub last_auctions_participation_count: u32,
}

#[derive(Debug, clap::Parser)]
pub struct LowSettlingSolversFinderConfig {
/// Enables search of non-settling solvers.
#[clap(
id = "low_settling_solvers_blacklisting_enabled",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is id used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default, clap uses the variable name as an ID. For this particular case, since all the params are flattened, there will be 2 identical IDs for the enabled fields, so clap fails to parse the args without this annotation.

long = "low-settling-solvers-blacklisting-enabled",
env = "LOW_SETTLING_SOLVERS_BLACKLISTING_ENABLED",
default_value = "true"
)]
pub enabled: bool,

/// The number of last auctions to check solver participation eligibility.
#[clap(long, env, default_value = "3")]
pub solver_last_auctions_participation_count: u32,
#[clap(
id = "low_settling_last_auctions_participation_count",
long = "low-settling-last-auctions-participation-count",
env = "LOW_SETTLING_LAST_AUCTIONS_PARTICIPATION_COUNT",
default_value = "100"
)]
pub last_auctions_participation_count: u32,

/// A max failure rate for a solver to remain eligible for
/// participation in the competition. Otherwise, the solver will be
/// banned.
#[clap(long, env, default_value = "0.9")]
pub solver_max_settlement_failure_rate: f64,
}

impl std::fmt::Display for Arguments {
Expand Down
210 changes: 146 additions & 64 deletions crates/autopilot/src/domain/competition/participation_guard/db.rs
Original file line number Diff line number Diff line change
@@ -1,44 +1,52 @@
use {
crate::{
arguments::{
DbBasedSolverParticipationGuardConfig,
LowSettlingSolversFinderConfig,
NonSettlingSolversFinderConfig,
},
domain::{eth, Metrics},
infra,
infra::{self, solvers::dto},
},
ethrpc::block_stream::CurrentBlockWatcher,
model::time::now_in_epoch_seconds,
std::{
collections::HashMap,
collections::{HashMap, HashSet},
sync::Arc,
time::{Duration, Instant},
},
tokio::join,
};

/// Checks the DB by searching for solvers that won N last consecutive auctions
/// but never settled any of them.
/// and either never settled any of them or their settlement success rate is
/// lower than `min_settlement_success_rate`.
#[derive(Clone)]
pub(super) struct Validator(Arc<Inner>);
pub(super) struct SolverValidator(Arc<Inner>);

struct Inner {
persistence: infra::Persistence,
banned_solvers: dashmap::DashMap<eth::Address, Instant>,
ttl: Duration,
last_auctions_count: u32,
non_settling_config: NonSettlingSolversFinderConfig,
low_settling_config: LowSettlingSolversFinderConfig,
drivers_by_address: HashMap<eth::Address, Arc<infra::Driver>>,
}

impl Validator {
impl SolverValidator {
pub fn new(
persistence: infra::Persistence,
current_block: CurrentBlockWatcher,
competition_updates_receiver: tokio::sync::mpsc::UnboundedReceiver<()>,
ttl: Duration,
last_auctions_count: u32,
db_based_validator_config: DbBasedSolverParticipationGuardConfig,
drivers_by_address: HashMap<eth::Address, Arc<infra::Driver>>,
) -> Self {
let self_ = Self(Arc::new(Inner {
persistence,
banned_solvers: Default::default(),
ttl,
last_auctions_count,
ttl: db_based_validator_config.solver_blacklist_cache_ttl,
non_settling_config: db_based_validator_config.non_settling_solvers_finder_config,
low_settling_config: db_based_validator_config.low_settling_solvers_finder_config,
drivers_by_address,
}));

Expand All @@ -59,66 +67,140 @@ impl Validator {
tokio::spawn(async move {
while competition_updates_receiver.recv().await.is_some() {
let current_block = current_block.borrow().number;
match self_
.0
.persistence
.find_non_settling_solvers(self_.0.last_auctions_count, current_block)
.await
{
Ok(non_settling_solvers) => {
let non_settling_drivers = non_settling_solvers
.into_iter()
.filter_map(|solver| {
self_.0.drivers_by_address.get(&solver).map(|driver| {
Metrics::get()
.non_settling_solver
.with_label_values(&[&driver.name]);

driver.clone()
})
})
.collect::<Vec<_>>();

let non_settling_solver_names = non_settling_drivers
.iter()
.map(|driver| driver.name.clone())
.collect::<Vec<_>>();

tracing::debug!(solvers = ?non_settling_solver_names, "found non-settling solvers");

let non_settling_drivers = non_settling_drivers
.into_iter()
// Check if solver accepted this feature. This should be removed once a CIP is
// approved.
.filter(|driver| driver.accepts_unsettled_blocking)
.collect::<Vec<_>>();

let now = Instant::now();
let banned_until_timestamp =
u64::from(now_in_epoch_seconds()) + self_.0.ttl.as_secs();
infra::notify_non_settling_solvers(
&non_settling_drivers,
banned_until_timestamp,
);

for driver in non_settling_drivers {
self_
.0
.banned_solvers
.insert(driver.submission_address, now);
}
}
Err(err) => {
tracing::warn!(?err, "error while searching for non-settling solvers")
}
}

let (non_settling_solvers, mut low_settling_solvers) = join!(
self_.find_non_settling_solvers(current_block),
self_.find_low_settling_solvers(current_block)
);
// Non-settling issue has a higher priority, remove duplicates from low-settling
// solvers.
low_settling_solvers.retain(|solver| !non_settling_solvers.contains(solver));

let found_at = Instant::now();
let banned_until_timestamp =
u64::from(now_in_epoch_seconds()) + self_.0.ttl.as_secs();

self_.post_process(
&non_settling_solvers,
&dto::notify::BanReason::UnsettledConsecutiveAuctions,
found_at,
banned_until_timestamp,
);
self_.post_process(
&low_settling_solvers,
&dto::notify::BanReason::HighSettleFailureRate,
found_at,
banned_until_timestamp,
);
}
});
}

async fn find_non_settling_solvers(&self, current_block: u64) -> HashSet<eth::Address> {
if !self.0.non_settling_config.enabled {
return Default::default();
}

match self
.0
.persistence
.find_non_settling_solvers(
self.0.non_settling_config.last_auctions_participation_count,
current_block,
)
.await
{
Ok(solvers) => solvers.into_iter().collect(),
Err(err) => {
tracing::warn!(?err, "error while searching for non-settling solvers");
Default::default()
}
}
}

async fn find_low_settling_solvers(&self, current_block: u64) -> HashSet<eth::Address> {
if !self.0.low_settling_config.enabled {
return Default::default();
}

match self
.0
.persistence
.find_low_settling_solvers(
self.0.low_settling_config.last_auctions_participation_count,
current_block,
self.0
.low_settling_config
.solver_max_settlement_failure_rate,
)
.await
{
Ok(solvers) => solvers.into_iter().collect(),
Err(err) => {
tracing::warn!(?err, "error while searching for low-settling solvers");
Default::default()
}
}
}

/// Updates the cache and notifies the solvers.
fn post_process(
&self,
solvers: &HashSet<eth::Address>,
ban_reason: &dto::notify::BanReason,
found_at: Instant,
banned_until_timestamp: u64,
) {
if solvers.is_empty() {
return;
}

let drivers = solvers
.iter()
.filter_map(|solver| self.0.drivers_by_address.get(solver).cloned())
.collect::<Vec<_>>();

let log_message = match ban_reason {
dto::notify::BanReason::UnsettledConsecutiveAuctions => "found non-settling solvers",
dto::notify::BanReason::HighSettleFailureRate => {
"found high-failure-settlement solvers"
}
};
let solver_names = drivers
.iter()
.map(|driver| driver.name.clone())
.collect::<Vec<_>>();
tracing::debug!(solvers = ?solver_names, log_message);

let reason = match ban_reason {
dto::notify::BanReason::UnsettledConsecutiveAuctions => "non_settling",
dto::notify::BanReason::HighSettleFailureRate => "high_settle_failure_rate",
};

for solver in solver_names {
Metrics::get()
.banned_solver
.with_label_values(&[&solver, reason]);
}

let banned_drivers = drivers
.into_iter()
// Notify and block only solvers that accept unsettled blocking feature. This should be removed once a CIP is approved.
.filter(|driver| driver.accepts_unsettled_blocking)
.collect::<Vec<_>>();

infra::notify_banned_solvers(&banned_drivers, ban_reason, banned_until_timestamp);

for driver in banned_drivers {
self.0
.banned_solvers
.insert(driver.submission_address, found_at);
}
}
}

#[async_trait::async_trait]
impl super::Validator for Validator {
impl super::SolverValidator for SolverValidator {
async fn is_allowed(&self, solver: &eth::Address) -> anyhow::Result<bool> {
if let Some(entry) = self.0.banned_solvers.get(solver) {
if Instant::now().duration_since(*entry.value()) < self.0.ttl {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub struct SolverParticipationGuard(Arc<Inner>);

struct Inner {
/// Stores the validators in order they will be called.
validators: Vec<Box<dyn Validator + Send + Sync>>,
validators: Vec<Box<dyn SolverValidator + Send + Sync>>,
}

impl SolverParticipationGuard {
Expand All @@ -24,20 +24,17 @@ impl SolverParticipationGuard {
db_based_validator_config: DbBasedSolverParticipationGuardConfig,
drivers_by_address: HashMap<eth::Address, Arc<infra::Driver>>,
) -> Self {
let mut validators: Vec<Box<dyn Validator + Send + Sync>> = Vec::new();
let mut validators: Vec<Box<dyn SolverValidator + Send + Sync>> = Vec::new();

if db_based_validator_config.enabled {
let current_block = eth.current_block().clone();
let database_solver_participation_validator = db::Validator::new(
persistence,
current_block,
competition_updates_receiver,
db_based_validator_config.solver_blacklist_cache_ttl,
db_based_validator_config.solver_last_auctions_participation_count,
drivers_by_address,
);
validators.push(Box::new(database_solver_participation_validator));
}
let current_block = eth.current_block().clone();
let database_solver_participation_validator = db::SolverValidator::new(
persistence,
current_block,
competition_updates_receiver,
db_based_validator_config,
drivers_by_address,
);
validators.push(Box::new(database_solver_participation_validator));

let onchain_solver_participation_validator = onchain::Validator { eth };
validators.push(Box::new(onchain_solver_participation_validator));
Expand All @@ -62,6 +59,6 @@ impl SolverParticipationGuard {
}

#[async_trait::async_trait]
trait Validator: Send + Sync {
trait SolverValidator: Send + Sync {
async fn is_allowed(&self, solver: &eth::Address) -> anyhow::Result<bool>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub(super) struct Validator {
}

#[async_trait::async_trait]
impl super::Validator for Validator {
impl super::SolverValidator for Validator {
async fn is_allowed(&self, solver: &eth::Address) -> anyhow::Result<bool> {
Ok(self
.eth
Expand Down
Loading
Loading