Skip to content

Commit

Permalink
feat: add reputation tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Vid201 committed Jan 30, 2023
1 parent 539c459 commit 46733fa
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 54 deletions.
90 changes: 87 additions & 3 deletions src/types/reputation.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
use educe::Educe;
use ethers::types::{Address, U256};
use ethers::{
abi::AbiEncode,
types::{Address, U256},
};
use jsonrpsee::types::ErrorObject;
use serde::{Deserialize, Serialize};
use serde_json::json;

pub const MIN_INCLUSION_RATE_DENOMINATOR: u64 = 10;
pub const THROTTLING_SLACK: u64 = 10;
pub const BAN_SLACK: u64 = 50;
const ENTITY_BANNED_ERROR_CODE: i32 = -32504;
const STAKE_TOO_LOW_ERROR_CODE: i32 = -32505;

pub type ReputationError = ErrorObject<'static>;

#[derive(Clone, Copy, Educe, PartialEq, Eq, Serialize, Deserialize)]
#[educe(Debug)]
Expand All @@ -10,7 +23,7 @@ pub enum ReputationStatus {
BANNED,
}

#[derive(Clone, Copy, Educe, Serialize, Deserialize)]
#[derive(Clone, Copy, Educe, Eq, PartialEq, Serialize, Deserialize)]
#[educe(Debug)]
pub struct ReputationEntry {
pub address: Address,
Expand All @@ -19,10 +32,81 @@ pub struct ReputationEntry {
pub status: ReputationStatus,
}

#[derive(Clone, Copy, Educe, Serialize, Deserialize)]
#[derive(Clone, Copy, Educe, Eq, PartialEq, Serialize, Deserialize)]
#[educe(Debug)]
pub struct StakeInfo {
pub address: Address,
pub stake: U256,
pub unstake_delay: U256, // seconds
}

pub enum BadReputationError {
EntityBanned {
address: Address,
title: String,
},
StakeTooLow {
address: Address,
title: String,
stake: U256,
min_stake: U256,
min_unstake_delay: U256,
},
UnstakeDelayTooLow {
address: Address,
title: String,
unstake_delay: U256,
min_stake: U256,
min_unstake_delay: U256,
},
}

impl From<BadReputationError> for ReputationError {
fn from(error: BadReputationError) -> Self {
match error {
BadReputationError::EntityBanned { address, title } => ReputationError::owned(
ENTITY_BANNED_ERROR_CODE,
format!("{title} with address {} is banned", address),
Some(json!({
title: address.to_string(),
})),
),
BadReputationError::StakeTooLow {
address,
title,
stake,
min_stake,
min_unstake_delay,
} => ReputationError::owned(
STAKE_TOO_LOW_ERROR_CODE,
format!(
"{title} with address {} stake {} is lower than {}",
address, stake, min_stake
),
Some(json!({
title: address.to_string(),
"minimumStake": AbiEncode::encode_hex(min_stake),
"minimumUnstakeDelay": AbiEncode::encode_hex(min_unstake_delay),
})),
),
BadReputationError::UnstakeDelayTooLow {
address,
title,
unstake_delay,
min_stake,
min_unstake_delay,
} => ReputationError::owned(
STAKE_TOO_LOW_ERROR_CODE,
format!(
"{title} with address {} unstake delay {} is lower than {}",
address, unstake_delay, min_unstake_delay
),
Some(json!({
title: address.to_string(),
"minimumStake": AbiEncode::encode_hex(min_stake),
"minimumUnstakeDelay": AbiEncode::encode_hex(min_unstake_delay),
})),
),
}
}
}
183 changes: 143 additions & 40 deletions src/uopool/memory_reputation.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
use async_trait::async_trait;
use educe::Educe;
use ethers::{
abi::AbiEncode,
types::{Address, U256},
};
use ethers::types::{Address, U256};
use parking_lot::RwLock;
use serde_json::json;
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};

use crate::types::reputation::{ReputationEntry, ReputationStatus, StakeInfo};
use crate::types::reputation::{
BadReputationError, ReputationEntry, ReputationError, ReputationStatus, StakeInfo,
};

use super::{Reputation, ReputationError, ENTITY_BANNED_ERROR_CODE, STAKE_TOO_LOW_ERROR_CODE};
use super::Reputation;

#[derive(Default, Educe)]
#[educe(Debug)]
Expand Down Expand Up @@ -169,44 +167,33 @@ impl Reputation for MemoryReputation {

if let Some(entity) = entities.get(&stake_info.address) {
let error = if entity.status == ReputationStatus::BANNED {
ReputationError::owned(
ENTITY_BANNED_ERROR_CODE,
format!("{title} with address {} is banned", stake_info.address),
Some(json!({
title: stake_info.address.to_string(),
})),
)
BadReputationError::EntityBanned {
address: stake_info.address,
title: title.to_string(),
}
} else if stake_info.stake < self.min_stake {
ReputationError::owned(
STAKE_TOO_LOW_ERROR_CODE,
format!(
"{title} with address {} stake {} is lower than {}",
stake_info.address, stake_info.stake, self.min_stake
),
Some(json!({
title: stake_info.address.to_string(),
"minimumStake": AbiEncode::encode_hex(self.min_stake),
"minimumUnstakeDelay": AbiEncode::encode_hex(self.min_unstake_delay),
})),
)
BadReputationError::StakeTooLow {
address: stake_info.address,
title: title.to_string(),
stake: stake_info.stake,
min_stake: self.min_stake,
min_unstake_delay: self.min_unstake_delay,
}
} else if stake_info.unstake_delay < self.min_unstake_delay {
ReputationError::owned(
STAKE_TOO_LOW_ERROR_CODE,
format!(
"{title} with address {} unstake delay {} is lower than {}",
stake_info.address, stake_info.unstake_delay, self.min_unstake_delay
),
Some(json!({
title: stake_info.address.to_string(),
"minimumStake": AbiEncode::encode_hex(self.min_stake),
"minimumUnstakeDelay": AbiEncode::encode_hex(self.min_unstake_delay),
})),
)
BadReputationError::UnstakeDelayTooLow {
address: stake_info.address,
title: title.to_string(),
unstake_delay: stake_info.unstake_delay,
min_stake: self.min_stake,
min_unstake_delay: self.min_unstake_delay,
}
} else {
return Ok(());
};

return Err(anyhow::anyhow!(serde_json::to_string(&error)?));
return Err(anyhow::anyhow!(serde_json::to_string(
&ReputationError::from(error)
)?));
}
}

Expand All @@ -233,4 +220,120 @@ impl Reputation for MemoryReputation {
}
}

// tests
#[cfg(test)]
mod tests {
use crate::uopool::{BAN_SLACK, MIN_INCLUSION_RATE_DENOMINATOR, THROTTLING_SLACK};

use super::*;

#[tokio::test]
async fn memory_reputation() {
let mut reputation = MemoryReputation::default();
reputation.init(
MIN_INCLUSION_RATE_DENOMINATOR,
THROTTLING_SLACK,
BAN_SLACK,
U256::from(1),
U256::from(0),
);

let mut addresses: Vec<Address> = vec![];

for _ in 0..5 {
let address = Address::random();
assert_eq!(
reputation.get(&address).await.unwrap(),
ReputationEntry {
address,
uo_seen: 0,
uo_included: 0,
status: ReputationStatus::OK,
}
);
addresses.push(address);
}

assert_eq!(reputation.add_whitelist(&addresses[2]).await.unwrap(), ());
assert_eq!(reputation.add_blacklist(&addresses[1]).await.unwrap(), ());

assert_eq!(reputation.is_whitelist(&addresses[2]).await.unwrap(), true);
assert_eq!(reputation.is_whitelist(&addresses[1]).await.unwrap(), false);
assert_eq!(reputation.is_blacklist(&addresses[1]).await.unwrap(), true);
assert_eq!(reputation.is_blacklist(&addresses[2]).await.unwrap(), false);

assert_eq!(
reputation.remove_whitelist(&addresses[2]).await.unwrap(),
true
);
assert_eq!(
reputation.remove_whitelist(&addresses[1]).await.unwrap(),
false
);
assert_eq!(
reputation.remove_blacklist(&addresses[1]).await.unwrap(),
true
);
assert_eq!(
reputation.remove_blacklist(&addresses[2]).await.unwrap(),
false
);

assert_eq!(reputation.add_whitelist(&addresses[2]).await.unwrap(), ());
assert_eq!(reputation.add_blacklist(&addresses[1]).await.unwrap(), ());

assert_eq!(
reputation.get_status(&addresses[2]).await.unwrap(),
ReputationStatus::OK
);
assert_eq!(
reputation.get_status(&addresses[1]).await.unwrap(),
ReputationStatus::BANNED
);
assert_eq!(
reputation.get_status(&addresses[3]).await.unwrap(),
ReputationStatus::OK
);

assert_eq!(reputation.increment_seen(&addresses[2]).await.unwrap(), ());
assert_eq!(reputation.increment_seen(&addresses[2]).await.unwrap(), ());
assert_eq!(reputation.increment_seen(&addresses[3]).await.unwrap(), ());
assert_eq!(reputation.increment_seen(&addresses[3]).await.unwrap(), ());

assert_eq!(
reputation.increment_included(&addresses[2]).await.unwrap(),
()
);
assert_eq!(
reputation.increment_included(&addresses[2]).await.unwrap(),
()
);
assert_eq!(
reputation.increment_included(&addresses[3]).await.unwrap(),
()
);

assert_eq!(
reputation
.update_handle_ops_reverted(&addresses[3])
.await
.unwrap(),
()
);

for _ in 0..250 {
assert_eq!(reputation.increment_seen(&addresses[3]).await.unwrap(), ());
}
assert_eq!(
reputation.get_status(&addresses[3]).await.unwrap(),
ReputationStatus::THROTTLED
);

for _ in 0..500 {
assert_eq!(reputation.increment_seen(&addresses[3]).await.unwrap(), ());
}
assert_eq!(
reputation.get_status(&addresses[3]).await.unwrap(),
ReputationStatus::BANNED
);
}
}
18 changes: 7 additions & 11 deletions src/uopool/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::{
types::{
reputation::{ReputationEntry, ReputationStatus, StakeInfo},
reputation::{
ReputationEntry, ReputationStatus, StakeInfo, BAN_SLACK,
MIN_INCLUSION_RATE_DENOMINATOR, THROTTLING_SLACK,
},
user_operation::{UserOperation, UserOperationHash},
},
uopool::{
Expand All @@ -18,7 +21,7 @@ use ethers::{
types::{Address, H256, U256},
utils::keccak256,
};
use jsonrpsee::{tracing::info, types::ErrorObject};
use jsonrpsee::tracing::info;
use parking_lot::RwLock;
use std::{collections::HashMap, fmt::Debug, net::SocketAddr, sync::Arc, time::Duration};

Expand All @@ -28,21 +31,14 @@ pub mod server;
pub mod services;

pub type MempoolId = H256;
pub type ReputationError = ErrorObject<'static>;

const MIN_INCLUSION_RATE_DENOMINATOR: u64 = 10;
const THROTTLING_SLACK: u64 = 10;
const BAN_SLACK: u64 = 50;
const ENTITY_BANNED_ERROR_CODE: i32 = -32504;
const STAKE_TOO_LOW_ERROR_CODE: i32 = -32505;
pub type MempoolBox<T> = Box<dyn Mempool<UserOperations = T>>;
pub type ReputationBox<T> = Box<dyn Reputation<ReputationEntries = T>>;

pub fn mempool_id(entry_point: Address, chain_id: U256) -> MempoolId {
H256::from_slice(keccak256([entry_point.encode(), chain_id.encode()].concat()).as_slice())
}

pub type MempoolBox<T> = Box<dyn Mempool<UserOperations = T>>;
pub type ReputationBox<T> = Box<dyn Reputation<ReputationEntries = T>>;

#[async_trait]
pub trait Mempool: Debug + Send + Sync + 'static {
type UserOperations: IntoIterator<Item = UserOperation>;
Expand Down

0 comments on commit 46733fa

Please sign in to comment.