Update for new KarmaTiers smart contract (#23)

* Update for new KarmaTiers smart contract
* Add Anvil unit test for KarmaTiers SC
* Add unit test for KarmaSC
* Add rln sc unit test
This commit is contained in:
Sydhds
2025-07-31 11:40:50 +02:00
committed by GitHub
parent b802b80664
commit 400d0155a7
21 changed files with 983 additions and 634 deletions

60
Cargo.lock generated
View File

@@ -62,6 +62,7 @@ dependencies = [
"alloy-eips",
"alloy-genesis",
"alloy-network",
"alloy-node-bindings",
"alloy-provider",
"alloy-pubsub",
"alloy-rpc-client",
@@ -244,6 +245,19 @@ dependencies = [
"serde",
]
[[package]]
name = "alloy-hardforks"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819a3620fe125e0fff365363315ee5e24c23169173b19747dfd6deba33db8990"
dependencies = [
"alloy-chains",
"alloy-eip2124",
"alloy-primitives",
"auto_impl",
"dyn-clone",
]
[[package]]
name = "alloy-json-abi"
version = "1.2.0"
@@ -309,6 +323,27 @@ dependencies = [
"serde",
]
[[package]]
name = "alloy-node-bindings"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f70cdf0ba711fb6f1a427fd026e149bd6d64c9da4c020c1c96c69245c4721ac1"
dependencies = [
"alloy-genesis",
"alloy-hardforks",
"alloy-network",
"alloy-primitives",
"alloy-signer",
"alloy-signer-local",
"k256",
"rand 0.8.5",
"serde_json",
"tempfile",
"thiserror 2.0.12",
"tracing",
"url",
]
[[package]]
name = "alloy-primitives"
version = "1.2.0"
@@ -349,9 +384,11 @@ dependencies = [
"alloy-json-rpc",
"alloy-network",
"alloy-network-primitives",
"alloy-node-bindings",
"alloy-primitives",
"alloy-pubsub",
"alloy-rpc-client",
"alloy-rpc-types-anvil",
"alloy-rpc-types-eth",
"alloy-signer",
"alloy-sol-types",
@@ -453,6 +490,19 @@ name = "alloy-rpc-types"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c000cab4ec26a4b3e29d144e999e1c539c2fa0abed871bf90311eb3466187ca8"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-anvil",
"alloy-rpc-types-eth",
"alloy-serde",
"serde",
]
[[package]]
name = "alloy-rpc-types-anvil"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ad84b1927e3c5985afca4a3c3a3a5bdce433de30cf8b2f7e52b642758d235d9"
dependencies = [
"alloy-primitives",
"alloy-rpc-types-eth",
@@ -1967,6 +2017,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ecdsa"
version = "0.16.9"
@@ -4840,8 +4896,10 @@ version = "0.1.0"
dependencies = [
"alloy",
"async-trait",
"derive_more",
"claims",
"log",
"thiserror 2.0.12",
"tokio",
"url",
]

View File

@@ -14,9 +14,10 @@ ark-serialize = "0.5"
ark-groth16 = "0.5"
ark-ff = "0.5"
url = { version = "2.5.4", features = ["serde"] }
alloy = { version = "1.0", features = ["getrandom", "sol-types", "contract", "provider-ws"] }
alloy = { version = "1.0", features = ["getrandom", "sol-types", "contract", "provider-ws", "provider-anvil-node"] }
async-trait = "0.1"
derive_more = "2.0.1"
thiserror = "2.0"
# dev
criterion = { version = "0.6", features = ["async_tokio"] }

View File

@@ -14,7 +14,7 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing = "0.1.41"
alloy.workspace = true
thiserror = "2.0"
thiserror.workspace = true
futures = "0.3"
ark-bn254.workspace = true
ark-serialize.workspace = true

View File

@@ -1,5 +1,5 @@
use criterion::{BenchmarkId, Throughput};
use criterion::Criterion;
use criterion::{BenchmarkId, Throughput};
use criterion::{criterion_group, criterion_main};
// std
@@ -175,19 +175,23 @@ fn proof_generation_bench(c: &mut Criterion) {
let proof_count = 5;
group.throughput(Throughput::Elements(proof_count as u64));
group.bench_with_input(BenchmarkId::new("proof generation", proof_count), &proof_count, |b, &_s| {
b.to_async(&rt).iter(|| {
async {
let mut set = JoinSet::new();
set.spawn(proof_collector(port, proof_count));
set.spawn(proof_sender(port, addresses.clone(), proof_count).map(|_r| vec![]));
// Wait for proof_sender + proof_collector to complete
let res = set.join_all().await;
// Check we receive enough proof
assert_eq!(res[1].len(), proof_count);
}
});
});
group.bench_with_input(
BenchmarkId::new("proof generation", proof_count),
&proof_count,
|b, &_s| {
b.to_async(&rt).iter(|| {
async {
let mut set = JoinSet::new();
set.spawn(proof_collector(port, proof_count));
set.spawn(proof_sender(port, addresses.clone(), proof_count).map(|_r| vec![]));
// Wait for proof_sender + proof_collector to complete
let res = set.join_all().await;
// Check we receive enough proof
assert_eq!(res[1].len(), proof_count);
}
});
},
);
group.finish();
}

View File

@@ -1,6 +1,7 @@
use alloy::transports::{RpcError, TransportErrorKind};
use ark_serialize::SerializationError;
use rln::error::ProofError;
use smart_contract::GetScTiersError;
// internal
use crate::epoch_service::WaitUntilError;
use crate::user_db_error::{RegisterError, UserMerkleTreeIndexError};
@@ -20,7 +21,7 @@ pub enum AppError {
#[error(transparent)]
RegistryError(#[from] HandleTransferError),
#[error(transparent)]
ContractError(#[from] alloy::contract::Error),
TierLimitsError(#[from] GetScTiersError),
}
#[derive(thiserror::Error, Debug)]

View File

@@ -188,7 +188,7 @@ where
let id_co =
U256::from_le_slice(BigUint::from(id_commitment).to_bytes_le().as_slice());
if let Err(e) = self.karma_rln_sc.register(id_co).await {
if let Err(e) = self.karma_rln_sc.register_user(&user, id_co).await {
// Fail to register user on smart contract
// Remove the user in internal Db
if !self.user_db.remove_user(&user, false) {

View File

@@ -47,7 +47,7 @@ use crate::user_db_error::RegisterError;
use crate::user_db_service::UserDbService;
use crate::user_db_types::RateLimit;
use rln_proof::RlnIdentifier;
use smart_contract::KarmaTiersSC::KarmaTiersSCInstance;
use smart_contract::KarmaTiers::KarmaTiersInstance;
use smart_contract::TIER_LIMITS;
const RLN_IDENTIFIER_NAME: &[u8] = b"test-rln-identifier";
@@ -63,9 +63,9 @@ pub async fn run_prover(
let epoch_service = EpochService::try_from((Duration::from_secs(60 * 2), GENESIS))
.expect("Failed to create epoch service");
let mut tier_limits = if app_args.ws_rpc_url.is_some() {
let tier_limits = if app_args.ws_rpc_url.is_some() {
TierLimits::from(
KarmaTiersSCInstance::get_tiers(
KarmaTiersInstance::get_tiers(
app_args.ws_rpc_url.clone().unwrap(),
app_args.tsc_address.unwrap(),
)
@@ -78,7 +78,6 @@ pub async fn run_prover(
tl
};
tier_limits.filter_inactive();
tier_limits.validate()?;
// User db service

View File

@@ -8,8 +8,10 @@ use metrics::{counter, histogram};
use parking_lot::RwLock;
use rln::hashers::hash_to_field;
use rln::protocol::serialize_proof_values;
use tracing::{Instrument, // debug,
debug_span, info
use tracing::{
Instrument, // debug,
debug_span,
info,
};
// internal
use crate::epoch_service::{Epoch, EpochSlice};
@@ -84,7 +86,6 @@ impl ProofService {
// see https://ryhl.io/blog/async-what-is-blocking/
rayon::spawn(move || {
let proof_generation_start = std::time::Instant::now();
let message_id = {
@@ -110,7 +111,8 @@ impl ProofService {
};
let epoch = hash_to_field(epoch_bytes.as_slice());
let merkle_proof = match user_db.get_merkle_proof(&proof_generation_data.tx_sender) {
let merkle_proof = match user_db.get_merkle_proof(&proof_generation_data.tx_sender)
{
Ok(merkle_proof) => merkle_proof,
Err(e) => {
let _ = send.send(Err(ProofGenerationError::MerkleProofError(e)));
@@ -138,28 +140,25 @@ impl ProofService {
// Serialize proof
let mut output_buffer = Cursor::new(Vec::with_capacity(PROOF_SIZE));
if let Err(e) = proof
.serialize_compressed(&mut output_buffer)
{
if let Err(e) = proof.serialize_compressed(&mut output_buffer) {
let _ = send.send(Err(ProofGenerationError::Serialization(e)));
return;
}
if let Err(e) = output_buffer
.write_all(&serialize_proof_values(&proof_values)) {
if let Err(e) = output_buffer.write_all(&serialize_proof_values(&proof_values)) {
let _ = send.send(Err(ProofGenerationError::SerializationWrite(e)));
return;
}
histogram!(PROOF_SERVICE_GEN_PROOF_TIME.name, "prover" => "proof service")
.record(proof_generation_start.elapsed().as_secs_f64());
.record(proof_generation_start.elapsed().as_secs_f64());
// println!("[proof service {counter_id}] proof generation time: {:?} secs", proof_generation_start.elapsed().as_secs_f64());
let labels = [("prover", format!("proof service id: {counter_id}"))];
counter!(PROOF_SERVICE_PROOF_COMPUTED.name, &labels).increment(1);
// Send the result back to Tokio.
let _ = send.send(
Ok::<Vec<u8>, ProofGenerationError>(output_buffer.into_inner())
);
let _ = send.send(Ok::<Vec<u8>, ProofGenerationError>(
output_buffer.into_inner(),
));
});
// Wait for the rayon task.
@@ -205,7 +204,7 @@ mod tests {
use claims::assert_matches;
use futures::TryFutureExt;
use tokio::sync::broadcast;
use tracing::{info, debug};
use tracing::{debug, info};
// third-party: zerokit
use rln::{
circuit::{Curve, zkey_from_folder},

View File

@@ -1,10 +1,10 @@
use std::collections::{BTreeMap, HashSet};
use std::collections::HashSet;
use std::ops::ControlFlow;
// third-party
use alloy::primitives::U256;
use derive_more::{Deref, DerefMut, From, Into};
// internal
use smart_contract::{Tier, TierIndex};
use smart_contract::Tier;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, From, Into)]
pub struct TierLimit(u32);
@@ -19,11 +19,11 @@ impl From<&str> for TierName {
}
#[derive(Debug, Clone, Default, From, Into, Deref, DerefMut, PartialEq)]
pub struct TierLimits(BTreeMap<TierIndex, Tier>);
pub struct TierLimits(Vec<Tier>);
impl<const N: usize> From<[(TierIndex, Tier); N]> for TierLimits {
fn from(value: [(TierIndex, Tier); N]) -> Self {
Self(BTreeMap::from(value))
impl<const N: usize> From<[Tier; N]> for TierLimits {
fn from(value: [Tier; N]) -> Self {
Self(Vec::from(value))
}
}
@@ -34,17 +34,10 @@ pub enum TierMatch {
/// Karma is above the highest tier
AboveHighest,
/// Karma is in the range of a defined tier.
Matched(TierIndex, Tier),
Matched(Tier),
}
impl TierLimits {
/// Filter inactive Tier (rejected by function validate)
pub(crate) fn filter_inactive(&mut self) -> Self {
let map = std::mem::take(&mut self.0);
let map_filtered = map.into_iter().filter(|(_k, v)| v.active).collect();
Self(map_filtered)
}
/// Validate tier limits (unique names, increasing min & max karma ...)
pub(crate) fn validate(&self) -> Result<(), ValidateTierLimitsError> {
#[derive(Debug, Default)]
@@ -53,49 +46,38 @@ impl TierLimits {
prev_min: Option<&'a U256>,
prev_max: Option<&'a U256>,
prev_tx_per_epoch: Option<&'a u32>,
prev_index: Option<&'a TierIndex>,
}
let _context =
self.0
.iter()
.try_fold(Context::default(), |mut state, (tier_index, tier)| {
if !tier.active {
return Err(ValidateTierLimitsError::InactiveTier);
}
if *tier_index != (*state.prev_index.unwrap_or(&TierIndex::default()) + 1) {
// Tier index must be increasing and consecutive
// cf function `_validateNoOverlap` in KarmaTiers.sol
return Err(ValidateTierLimitsError::InvalidTierIndex);
}
let _context = self
.0
.iter()
.try_fold(Context::default(), |mut state, tier| {
if tier.min_karma <= *state.prev_min.unwrap_or(&U256::ZERO) {
return Err(ValidateTierLimitsError::InvalidMinKarmaAmount);
}
if tier.min_karma <= *state.prev_min.unwrap_or(&U256::ZERO) {
return Err(ValidateTierLimitsError::InvalidMinKarmaAmount);
}
if tier.min_karma <= *state.prev_max.unwrap_or(&U256::ZERO) {
return Err(ValidateTierLimitsError::InvalidMinKarmaAmount);
}
if tier.min_karma <= *state.prev_max.unwrap_or(&U256::ZERO) {
return Err(ValidateTierLimitsError::InvalidMinKarmaAmount);
}
if tier.min_karma >= tier.max_karma {
return Err(ValidateTierLimitsError::InvalidMaxKarmaAmount);
}
if tier.min_karma >= tier.max_karma {
return Err(ValidateTierLimitsError::InvalidMaxKarmaAmount);
}
if tier.tx_per_epoch <= *state.prev_tx_per_epoch.unwrap_or(&0) {
return Err(ValidateTierLimitsError::InvalidTierLimit);
}
if tier.tx_per_epoch <= *state.prev_tx_per_epoch.unwrap_or(&0) {
return Err(ValidateTierLimitsError::InvalidTierLimit);
}
if state.tier_names.contains(&tier.name) {
return Err(ValidateTierLimitsError::NonUniqueTierName);
}
if state.tier_names.contains(&tier.name) {
return Err(ValidateTierLimitsError::NonUniqueTierName);
}
state.prev_min = Some(&tier.min_karma);
state.prev_max = Some(&tier.max_karma);
state.prev_tx_per_epoch = Some(&tier.tx_per_epoch);
state.tier_names.insert(tier.name.clone());
state.prev_index = Some(tier_index);
Ok(state)
})?;
state.prev_min = Some(&tier.min_karma);
state.prev_max = Some(&tier.max_karma);
state.prev_tx_per_epoch = Some(&tier.tx_per_epoch);
state.tier_names.insert(tier.name.clone());
Ok(state)
})?;
Ok(())
}
@@ -103,33 +85,27 @@ impl TierLimits {
/// Given some karma amount, find the matching Tier. Assume all tiers are active.
pub(crate) fn get_tier_by_karma(&self, karma_amount: &U256) -> TierMatch {
struct Context<'a> {
current: Option<(&'a TierIndex, &'a Tier)>,
current: Option<&'a Tier>,
}
let ctx_initial = Context { current: None };
let ctx = self
.0
.iter()
.try_fold(ctx_initial, |mut state, (tier_index, tier)| {
// Assume all the tier are active but checks it at dev time
debug_assert!(tier.active, "Find a non active tier");
if karma_amount < &tier.min_karma {
// Early break - above lowest tier (< lowest_tier.min_karma)
ControlFlow::Break(state)
} else if karma_amount >= &tier.min_karma && karma_amount <= &tier.max_karma {
// Found a match - update ctx and break
state.current = Some((tier_index, tier));
ControlFlow::Break(state)
} else {
ControlFlow::Continue(state)
}
});
let ctx = self.0.iter().try_fold(ctx_initial, |mut state, tier| {
if karma_amount < &tier.min_karma {
// Early break - above lowest tier (< lowest_tier.min_karma)
ControlFlow::Break(state)
} else if karma_amount >= &tier.min_karma && karma_amount <= &tier.max_karma {
// Found a match - update ctx and break
state.current = Some(tier);
ControlFlow::Break(state)
} else {
ControlFlow::Continue(state)
}
});
if let Some(ctx) = ctx.break_value() {
// ControlFlow::Break
if let Some((tier_index, tier)) = ctx.current {
TierMatch::Matched(*tier_index, tier.clone())
if let Some(tier) = ctx.current {
TierMatch::Matched(tier.clone())
} else {
TierMatch::UnderLowest
}
@@ -148,12 +124,8 @@ pub enum ValidateTierLimitsError {
InvalidMaxKarmaAmount,
#[error("Invalid Tier limit (must be increasing)")]
InvalidTierLimit,
#[error("Invalid Tier index (must be increasing & consecutive)")]
InvalidTierIndex,
#[error("Non unique Tier name")]
NonUniqueTierName,
#[error("Non active Tier")]
InactiveTier,
}
#[cfg(test)]
@@ -162,98 +134,21 @@ mod tests {
use alloy::primitives::U256;
use claims::assert_matches;
#[test]
fn test_filter_inactive() {
let mut tier_limits = TierLimits::from([
(
TierIndex::from(0),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 6,
active: true,
},
),
(
TierIndex::from(1),
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 120,
active: true,
},
),
(
TierIndex::from(2),
Tier {
name: "Power User".to_string(),
min_karma: U256::from(500),
max_karma: U256::from(999),
tx_per_epoch: 86400,
active: false,
},
),
]);
let filtered = tier_limits.filter_inactive();
assert_eq!(filtered.len(), 2);
}
#[test]
fn test_validate_fails_with_inactive_tier() {
let tier_limits = TierLimits::from([
(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 6,
active: true,
},
),
(
TierIndex::from(2),
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 120,
active: false,
},
),
]);
assert_matches!(
tier_limits.validate(),
Err(ValidateTierLimitsError::InactiveTier)
);
}
#[test]
fn test_validate_failed_when_karma_overlapping_between_tier() {
let tier_limits = TierLimits::from([
(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(100),
tx_per_epoch: 6,
active: true,
},
),
(
TierIndex::from(2),
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(150),
tx_per_epoch: 120,
active: true,
},
),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(100),
tx_per_epoch: 6,
},
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(150),
tx_per_epoch: 120,
},
]);
assert_matches!(
@@ -264,32 +159,24 @@ mod tests {
#[test]
fn test_validate_fails_when_min_karma_equal_or_greater_max_karma() {
let tier_limits = TierLimits::from([(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(100),
max_karma: U256::from(100),
tx_per_epoch: 6,
active: true,
},
)]);
let tier_limits = TierLimits::from([Tier {
name: "Basic".to_string(),
min_karma: U256::from(100),
max_karma: U256::from(100),
tx_per_epoch: 6,
}]);
assert_matches!(
tier_limits.validate(),
Err(ValidateTierLimitsError::InvalidMaxKarmaAmount)
);
let tier_limits = TierLimits::from([(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(500),
max_karma: U256::from(100),
tx_per_epoch: 6,
active: true,
},
)]);
let tier_limits = TierLimits::from([Tier {
name: "Basic".to_string(),
min_karma: U256::from(500),
max_karma: U256::from(100),
tx_per_epoch: 6,
}]);
assert_matches!(
tier_limits.validate(),
@@ -302,26 +189,18 @@ mod tests {
// Case 1: Duplicate min_karma values
{
let tier_limits = TierLimits::from([
(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 6,
active: true,
},
),
(
TierIndex::from(2),
Tier {
name: "Active".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(99),
tx_per_epoch: 120,
active: true,
},
),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 6,
},
Tier {
name: "Active".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(99),
tx_per_epoch: 120,
},
]);
assert_matches!(
@@ -333,26 +212,18 @@ mod tests {
// Case 2: Decreasing min_karma values
{
let tier_limits = TierLimits::from([
(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 6,
active: true,
},
),
(
TierIndex::from(2),
Tier {
name: "Active".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 120,
active: true,
},
),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 6,
},
Tier {
name: "Active".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 120,
},
]);
assert_matches!(
@@ -367,26 +238,18 @@ mod tests {
// Case 1: Duplicate tx_per_epoch values
{
let tier_limits = TierLimits::from([
(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 120,
active: true,
},
),
(
TierIndex::from(2),
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 120,
active: true,
},
),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 120,
},
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 120,
},
]);
assert_matches!(
@@ -398,26 +261,18 @@ mod tests {
// Case 2: Decreasing tx_per_epoch values
{
let tier_limits = TierLimits::from([
(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 120,
active: true,
},
),
(
TierIndex::from(2),
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 6,
active: true,
},
),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 120,
},
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 6,
},
]);
assert_matches!(
@@ -430,26 +285,18 @@ mod tests {
#[test]
fn test_validate_fails_with_duplicate_tier_names() {
let tier_limits = TierLimits::from([
(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 6,
active: true,
},
),
(
TierIndex::from(2),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 120,
active: true,
},
),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 6,
},
Tier {
name: "Basic".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 120,
},
]);
assert_matches!(
@@ -458,40 +305,6 @@ mod tests {
);
}
#[test]
fn test_validate_fails_tier_index() {
// Non-consecutive tier index
{
let tier_limits = TierLimits::from([
(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 6,
active: true,
},
),
(
TierIndex::from(3),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 120,
active: true,
},
),
]);
assert_matches!(
tier_limits.validate(),
Err(ValidateTierLimitsError::InvalidTierIndex)
);
}
}
#[test]
fn test_validate_and_get_tier_by_karma_with_empty_tier_limits() {
let tier_limits = TierLimits::default();
@@ -505,36 +318,24 @@ mod tests {
#[test]
fn test_get_tier_by_karma_bounds_and_ranges() {
let tier_limits = TierLimits::from([
(
TierIndex::from(1),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 6,
active: true,
},
),
(
TierIndex::from(2),
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 120,
active: true,
},
),
(
TierIndex::from(3),
Tier {
name: "Regular".to_string(),
min_karma: U256::from(100),
max_karma: U256::from(499),
tx_per_epoch: 720,
active: true,
},
),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 6,
},
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 120,
},
Tier {
name: "Regular".to_string(),
min_karma: U256::from(100),
max_karma: U256::from(499),
tx_per_epoch: 720,
},
]);
// Case 1: Zero karma
@@ -547,8 +348,7 @@ mod tests {
// Case 3: Exact match on min_karma (start of first tier)
let result = tier_limits.get_tier_by_karma(&U256::from(10));
if let TierMatch::Matched(index, tier) = result {
assert_eq!(index, TierIndex::from(1));
if let TierMatch::Matched(tier) = result {
assert_eq!(tier.name, "Basic");
} else {
panic!("Expected TierMatch::Matched, got {:?}", result);
@@ -556,8 +356,7 @@ mod tests {
// Case 4: Exact match on a tier boundary (start of second tier)
let result = tier_limits.get_tier_by_karma(&U256::from(50));
if let TierMatch::Matched(index, tier) = result {
assert_eq!(index, TierIndex::from(2));
if let TierMatch::Matched(tier) = result {
assert_eq!(tier.name, "Active");
} else {
panic!("Expected TierMatch::Matched, got {:?}", result);
@@ -565,8 +364,7 @@ mod tests {
// Case 5: Karma within a tier range (between third tier)
let result = tier_limits.get_tier_by_karma(&U256::from(250));
if let TierMatch::Matched(index, tier) = result {
assert_eq!(index, TierIndex::from(3));
if let TierMatch::Matched(tier) = result {
assert_eq!(tier.name, "Regular");
} else {
panic!("Expected TierMatch, got {:?}", result);
@@ -574,8 +372,7 @@ mod tests {
// Case 6: Exact match on max_karma (end of the third tier)
let result = tier_limits.get_tier_by_karma(&U256::from(499));
if let TierMatch::Matched(index, tier) = result {
assert_eq!(index, TierIndex::from(3));
if let TierMatch::Matched(tier) = result {
assert_eq!(tier.name, "Regular");
} else {
panic!("Expected TierMatch, got {:?}", result);
@@ -585,33 +382,4 @@ mod tests {
let result = tier_limits.get_tier_by_karma(&U256::from(1000));
assert_eq!(result, TierMatch::AboveHighest);
}
#[test]
#[should_panic(expected = "Find a non active tier")]
fn test_get_tier_by_karma_ignores_inactive_tiers() {
let tier_limits = TierLimits::from([
(
TierIndex::from(0),
Tier {
name: "Basic".to_string(),
min_karma: U256::from(10),
max_karma: U256::from(49),
tx_per_epoch: 6,
active: false,
},
),
(
TierIndex::from(1),
Tier {
name: "Active".to_string(),
min_karma: U256::from(50),
max_karma: U256::from(99),
tx_per_epoch: 120,
active: true,
},
),
]);
let _result = tier_limits.get_tier_by_karma(&U256::from(25));
}
}

View File

@@ -9,8 +9,10 @@ use futures::StreamExt;
use tracing::error;
// internal
use crate::error::AppError;
use crate::tier::TierLimits;
use crate::user_db::UserDb;
use smart_contract::{AlloyWsProvider, KarmaTiersSC, Tier, TierIndex};
use smart_contract::KarmaTiers::KarmaTiersInstance;
use smart_contract::{AlloyWsProvider, KarmaTiers};
pub(crate) struct TiersListener {
rpc_url: String,
@@ -40,42 +42,43 @@ impl TiersListener {
let filter = alloy::rpc::types::Filter::new()
.address(self.sc_address)
.event(KarmaTiersSC::TierAdded::SIGNATURE)
.event(KarmaTiersSC::TierUpdated::SIGNATURE);
.event(KarmaTiers::TiersUpdated::SIGNATURE);
// Subscribe to logs matching the filter.
let subscription = provider.subscribe_logs(&filter).await?;
let subscription = provider.clone().subscribe_logs(&filter).await?;
let mut stream = subscription.into_stream();
// Loop through the incoming event logs
while let Some(log) = stream.next().await {
if let Ok(tier_added) = KarmaTiersSC::TierAdded::decode_log_data(log.data()) {
let tier_id: TierIndex = tier_added.tierId.into();
if let Err(e) = self.user_db.on_new_tier(tier_id, Tier::from(tier_added)) {
if let Ok(_tu) = KarmaTiers::TiersUpdated::decode_log_data(log.data()) {
let tier_limits =
match KarmaTiersInstance::get_tiers_from_provider(&provider, &self.sc_address)
.await
{
Ok(tier_limits) => tier_limits,
Err(e) => {
error!(
"Error while getting tiers limits from smart contract: {}",
e
);
return Err(AppError::TierLimitsError(e));
}
};
if let Err(e) = self
.user_db
.on_tier_limits_updated(TierLimits::from(tier_limits))
{
// If there is an error here, we assume this is an error by the user
// updating the Tier limits (and thus we don't want to shut down the prover)
error!("Error while adding tier (index: {:?}): {}", tier_id, e);
error!("Error while updating tier limits: {}", e);
}
} else {
match KarmaTiersSC::TierUpdated::decode_log_data(log.data()) {
Ok(tier_updated) => {
let tier_id: TierIndex = tier_updated.tierId.into();
if let Err(e) = self
.user_db
.on_tier_updated(tier_updated.tierId.into(), Tier::from(tier_updated))
{
// If there is an error here, we assume this is an error by the user
// updating the Tier limits (and thus we don't want to shut down the prover)
error!("Error while updating tier (index: {:?}): {}", tier_id, e);
};
}
Err(e) => {
eprintln!("Error decoding log data: {e:?}");
// It's also useful to print the raw log data for debugging
eprintln!("Raw log topics: {:?}", log.topics());
eprintln!("Raw log data: {:?}", log.data());
}
}
// Should never happen as TiersUpdated is empty
eprintln!("Error decoding log data");
// It's also useful to print the raw log data for debugging
eprintln!("Raw log topics: {:?}", log.topics());
eprintln!("Raw log data: {:?}", log.data());
}
}

View File

@@ -35,7 +35,7 @@ use crate::user_db_serialization::{
};
use crate::user_db_types::{EpochCounter, EpochSliceCounter, MerkleTreeIndex, RateLimit};
use rln_proof::{RlnUserIdentity, ZerokitMerkleTree};
use smart_contract::{KarmaAmountExt, Tier, TierIndex};
use smart_contract::KarmaAmountExt;
const MERKLE_TREE_HEIGHT: usize = 20;
pub const USER_CF: &str = "user";
@@ -534,41 +534,10 @@ impl UserDb {
Ok(tier_limits)
}
pub(crate) fn on_new_tier(
pub(crate) fn on_tier_limits_updated(
&self,
tier_index: TierIndex,
tier: Tier,
tier_limits: TierLimits,
) -> Result<(), SetTierLimitsError> {
let mut tier_limits = self.get_tier_limits()?;
tier_limits.insert(tier_index, tier);
tier_limits.validate()?;
// Serialize
let tier_limits_serializer = TierLimitsSerializer::default();
let mut buffer = Vec::with_capacity(tier_limits_serializer.size_hint(tier_limits.len()));
// Unwrap safe - already validated - should always serialize
tier_limits_serializer
.serialize(&tier_limits, &mut buffer)
.unwrap();
// Write
let cf = self.get_tier_limits_cf();
self.db
.put_cf(cf, TIER_LIMITS_NEXT_KEY.as_slice(), buffer)
.map_err(SetTierLimitsError::Db)
}
pub(crate) fn on_tier_updated(
&self,
tier_index: TierIndex,
tier: Tier,
) -> Result<(), SetTierLimitsError> {
let mut tier_limits = self.get_tier_limits()?;
if !tier_limits.contains_key(&tier_index) {
return Err(SetTierLimitsError::InvalidUpdateTierIndex);
}
tier_limits.entry(tier_index).and_modify(|e| *e = tier);
tier_limits.validate()?;
// Serialize
@@ -622,7 +591,7 @@ impl UserDb {
tier_limit: None,
};
if let TierMatch::Matched(_tier_index, tier) = tier_match {
if let TierMatch::Matched(tier) = tier_match {
t.tier_name = Some(tier.name.into());
t.tier_limit = Some(TierLimit::from(tier.tx_per_epoch));
}
@@ -661,25 +630,6 @@ mod tests {
const ADDR_1: Address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045");
const ADDR_2: Address = address!("0xb20a608c624Ca5003905aA834De7156C68b2E1d0");
/*
struct MockKarmaSc2 {}
#[async_trait]
impl KarmaAmountExt for MockKarmaSc2 {
type Error = DummyError;
async fn karma_amount(&self, address: &Address) -> Result<U256, Self::Error> {
if address == &ADDR_1 {
Ok(U256::from(10))
} else if address == &ADDR_2 {
Ok(U256::from(2000))
} else {
Ok(U256::ZERO)
}
}
}
*/
#[test]
fn test_user_register() {
let temp_folder = tempfile::tempdir().unwrap();

View File

@@ -59,8 +59,6 @@ pub enum UserMerkleTreeIndexError {
pub enum SetTierLimitsError {
#[error(transparent)]
Validate(#[from] ValidateTierLimitsError),
#[error("Updating an invalid tier index")]
InvalidUpdateTierIndex,
#[error(transparent)]
Db(#[from] rocksdb::Error),
}

View File

@@ -1,4 +1,3 @@
use std::collections::BTreeMap;
use std::num::TryFromIntError;
use std::string::FromUtf8Error;
// third-party
@@ -17,7 +16,7 @@ use rln_proof::RlnUserIdentity;
// internal
use crate::tier::TierLimits;
use crate::user_db_types::MerkleTreeIndex;
use smart_contract::{Tier, TierIndex};
use smart_contract::Tier;
pub(crate) struct RlnUserIdentitySerializer {}
@@ -106,7 +105,6 @@ impl TierSerializer {
buffer.extend(name_len.to_le_bytes());
buffer.extend(value.name.as_bytes());
buffer.extend(value.tx_per_epoch.to_le_bytes().as_slice());
buffer.push(u8::from(value.active));
Ok(())
}
@@ -155,8 +153,6 @@ impl TierDeserializer {
let name = String::from_utf8(name.to_vec())
.map_err(|e| nom::Err::Error(TierDeserializeError::Utf8Error(e)))?;
let (input, tx_per_epoch) = le_u32(input)?;
let (input, active) = take(1usize)(input)?;
let active = active[0] != 0;
Ok((
input,
@@ -165,7 +161,6 @@ impl TierDeserializer {
max_karma,
name,
tx_per_epoch,
active,
},
))
}
@@ -185,9 +180,8 @@ impl TierLimitsSerializer {
let len = value.len() as u32;
buffer.extend(len.to_le_bytes());
let mut tier_buffer = Vec::with_capacity(self.tier_serializer.size_hint());
value.iter().try_for_each(|(k, v)| {
buffer.push(k.into());
self.tier_serializer.serialize(v, &mut tier_buffer)?;
value.iter().try_for_each(|t| {
self.tier_serializer.serialize(t, &mut tier_buffer)?;
buffer.extend_from_slice(&tier_buffer);
tier_buffer.clear();
Ok(())
@@ -209,16 +203,14 @@ impl TierLimitsDeserializer {
&self,
buffer: &'a [u8],
) -> IResult<&'a [u8], TierLimits, TierDeserializeError<&'a [u8]>> {
let (input, tiers): (&[u8], BTreeMap<TierIndex, Tier>) = length_count(
let (input, tiers): (&[u8], Vec<Tier>) = length_count(
le_u32,
context("Tier index & Tier deser", |input: &'a [u8]| {
let (input, tier_index) = take(1usize)(input)?;
let tier_index = TierIndex::from(tier_index[0]);
let (input, tier) = self.tier_deserializer.deserialize(input)?;
Ok((input, (tier_index, tier)))
Ok((input, tier))
}),
)
.map(BTreeMap::from_iter)
.map(Vec::from_iter)
.parse(buffer)?;
Ok((input, TierLimits::from(tiers)))
@@ -270,7 +262,6 @@ mod tests {
max_karma: U256::from(u64::MAX),
name: "All".to_string(),
tx_per_epoch: 10_000_000,
active: false,
};
let serializer = TierSerializer {};
@@ -290,20 +281,15 @@ mod tests {
max_karma: U256::from(4),
name: "Basic".to_string(),
tx_per_epoch: 10_000,
active: false,
};
let tier_2 = Tier {
min_karma: U256::from(10),
max_karma: U256::from(u64::MAX),
name: "Premium".to_string(),
tx_per_epoch: 1_000_000_000,
active: true,
};
let tier_limits = TierLimits::from(BTreeMap::from([
(TierIndex::from(1), tier_1),
(TierIndex::from(2), tier_2),
]));
let tier_limits = TierLimits::from([tier_1, tier_2]);
let serializer = TierLimitsSerializer::default();
let mut buffer = Vec::with_capacity(serializer.size_hint(tier_limits.len()));

View File

@@ -4,10 +4,9 @@ mod user_db_tests {
use std::path::PathBuf;
use std::sync::Arc;
// third-party
use alloy::primitives::{Address, address};
use claims::assert_matches;
use parking_lot::RwLock;
use crate::epoch_service::{Epoch, EpochSlice};
use alloy::primitives::{Address, address};
use parking_lot::RwLock;
// internal
use crate::user_db::UserDb;
use crate::user_db_types::{EpochCounter, EpochSliceCounter, MerkleTreeIndex};
@@ -17,7 +16,6 @@ mod user_db_tests {
#[tokio::test]
async fn test_incr_tx_counter_2() {
// Same as test_incr_tx_counter but multi users AND multi incr
let temp_folder = tempfile::tempdir().unwrap();
@@ -35,7 +33,7 @@ mod user_db_tests {
Default::default(),
Default::default(),
)
.unwrap();
.unwrap();
// Register users
user_db.register(ADDR_1).unwrap();
@@ -83,7 +81,6 @@ mod user_db_tests {
user_db.get_tx_counter(&ADDR_2),
Ok((EpochCounter::from(2), EpochSliceCounter::from(2)))
);
}
#[tokio::test]

View File

@@ -111,7 +111,6 @@ async fn test_grpc_register_users() {
}
async fn proof_sender(port: u16, addresses: Vec<Address>, proof_count: usize) {
let start = std::time::Instant::now();
let chain_id = GrpcU256 {
@@ -148,7 +147,11 @@ async fn proof_sender(port: u16, addresses: Vec<Address>, proof_count: usize) {
count += 1;
}
println!("[proof_sender] sent {} tx - elapsed: {} secs", count, start.elapsed().as_secs_f64());
println!(
"[proof_sender] sent {} tx - elapsed: {} secs",
count,
start.elapsed().as_secs_f64()
);
/*
let tx_hash = U256::from(42).to_le_bytes::<32>().to_vec();
@@ -166,7 +169,6 @@ async fn proof_sender(port: u16, addresses: Vec<Address>, proof_count: usize) {
}
async fn proof_collector(port: u16, proof_count: usize) -> Vec<RlnProofReply> {
let start = std::time::Instant::now();
let result = Arc::new(RwLock::new(vec![]));
@@ -190,7 +192,10 @@ async fn proof_collector(port: u16, proof_count: usize) -> Vec<RlnProofReply> {
if count >= proof_count {
break;
}
println!("count {count} - elapsed: {} secs", start_per_message.elapsed().as_secs_f64());
println!(
"count {count} - elapsed: {} secs",
start_per_message.elapsed().as_secs_f64()
);
start_per_message = std::time::Instant::now();
}
};
@@ -198,7 +203,10 @@ async fn proof_collector(port: u16, proof_count: usize) -> Vec<RlnProofReply> {
let _res = tokio::time::timeout(Duration::from_secs(500), receiver).await;
println!("_res: {:?}", _res);
let res = std::mem::take(&mut *result.write());
println!("[proof_collector] elapsed: {} secs", start.elapsed().as_secs_f64());
println!(
"[proof_collector] elapsed: {} secs",
start.elapsed().as_secs_f64()
);
res
}

View File

@@ -7,5 +7,10 @@ edition = "2024"
alloy.workspace = true
url.workspace = true
async-trait.workspace = true
derive_more.workspace = true
log = "0.4.27"
thiserror.workspace = true
log = "0.4.27"
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
claims = "0.8"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@ mod rln_sc;
pub use common::AlloyWsProvider;
pub use karma_sc::{KarmaAmountExt, KarmaSC};
pub use karma_tiers::{KarmaTiersSC, Tier, TierIndex};
pub use karma_tiers::{GetScTiersError, KarmaTiers, Tier};
pub use rln_sc::{KarmaRLNSC, RLNRegister};
pub use mock::{MockKarmaRLNSc, MockKarmaSc, TIER_LIMITS};

View File

@@ -1,9 +1,8 @@
use crate::karma_tiers::{Tier, TierIndex};
use crate::karma_tiers::Tier;
use crate::{KarmaAmountExt, RLNRegister};
use alloy::primitives::{Address, U256};
use async_trait::async_trait;
use log::debug;
use std::collections::BTreeMap;
use std::sync::LazyLock;
pub struct MockKarmaSc {}
@@ -23,66 +22,50 @@ pub struct MockKarmaRLNSc {}
impl RLNRegister for MockKarmaRLNSc {
type Error = alloy::contract::Error;
async fn register(&self, identity_commitment: U256) -> Result<(), Self::Error> {
async fn register_user(
&self,
address: &Address,
identity_commitment: U256,
) -> Result<(), Self::Error> {
debug!(
"Register user with identity_commitment: {:?}",
identity_commitment
"Register user ({}) with identity_commitment: {:?}",
address, identity_commitment
);
Ok(())
}
}
pub static TIER_LIMITS: LazyLock<BTreeMap<TierIndex, Tier>> = LazyLock::new(|| {
BTreeMap::from([
(
TierIndex::from(0),
Tier {
min_karma: U256::from(10),
max_karma: U256::from(49),
name: "Basic".to_string(),
tx_per_epoch: 6,
active: true,
},
),
(
TierIndex::from(1),
Tier {
min_karma: U256::from(50),
max_karma: U256::from(99),
name: "Active".to_string(),
tx_per_epoch: 120,
active: true,
},
),
(
TierIndex::from(2),
Tier {
min_karma: U256::from(100),
max_karma: U256::from(499),
name: "Regular".to_string(),
tx_per_epoch: 720,
active: true,
},
),
(
TierIndex::from(3),
Tier {
min_karma: U256::from(500),
max_karma: U256::from(999),
name: "Power User".to_string(),
tx_per_epoch: 86400,
active: true,
},
),
(
TierIndex::from(4),
Tier {
min_karma: U256::from(1000),
max_karma: U256::from(4999),
name: "S-Tier".to_string(),
tx_per_epoch: 432000,
active: true,
},
),
])
pub static TIER_LIMITS: LazyLock<Vec<Tier>> = LazyLock::new(|| {
vec![
Tier {
min_karma: U256::from(10),
max_karma: U256::from(49),
name: "Basic".to_string(),
tx_per_epoch: 6,
},
Tier {
min_karma: U256::from(50),
max_karma: U256::from(99),
name: "Active".to_string(),
tx_per_epoch: 120,
},
Tier {
min_karma: U256::from(100),
max_karma: U256::from(499),
name: "Regular".to_string(),
tx_per_epoch: 720,
},
Tier {
min_karma: U256::from(500),
max_karma: U256::from(999),
name: "Power User".to_string(),
tx_per_epoch: 86400,
},
Tier {
min_karma: U256::from(1000),
max_karma: U256::from(4999),
name: "S-Tier".to_string(),
tx_per_epoch: 432000,
},
]
});

File diff suppressed because one or more lines are too long