rotating bloomfilters

This commit is contained in:
Jędrzej Stuczyński
2025-05-09 17:13:37 +01:00
parent 90d6dddd32
commit 5ba44b5bdf
6 changed files with 220 additions and 12 deletions
+2 -1
View File
@@ -21,7 +21,7 @@ use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams};
use nym_mixnet_contract_common::rewarding::RewardEstimate;
use nym_mixnet_contract_common::{
EpochId, GatewayBond, IdentityKey, Interval, KeyRotationState, MixNode, NodeId, Percent,
EpochId, GatewayBond, IdentityKey, Interval, MixNode, NodeId, Percent,
};
use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
use nym_node_requests::api::v1::authenticator::models::Authenticator;
@@ -41,6 +41,7 @@ use thiserror::Error;
use time::{Date, OffsetDateTime};
use utoipa::{IntoParams, ToResponse, ToSchema};
pub use nym_mixnet_contract_common::KeyRotationState;
pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+67 -5
View File
@@ -1,24 +1,46 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::key_rotation::manager::SphinxKeyManager;
use crate::node::nym_apis_client::NymApisClient;
use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilters;
use crate::node::replay_protection::manager::ReplayProtectionBloomfiltersManager;
use futures::pin_mut;
use nym_task::ShutdownToken;
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::models::KeyRotationInfoResponse;
use nym_validator_client::models::{KeyRotationInfoResponse, KeyRotationState};
use std::time::Duration;
use time::OffsetDateTime;
use tokio::time::{interval, sleep, Instant};
use tracing::{error, info, trace, warn};
struct RotationConfig {
epoch_duration: Duration,
rotation_state: KeyRotationState,
}
impl RotationConfig {
fn rotation_lifetime(&self) -> Duration {
(self.rotation_state.validity_epochs + 1) * self.epoch_duration
}
}
impl From<KeyRotationInfoResponse> for RotationConfig {
fn from(value: KeyRotationInfoResponse) -> Self {
RotationConfig {
epoch_duration: value.epoch_duration,
rotation_state: value.key_rotation_state,
}
}
}
pub(crate) struct KeyRotationController {
// regular polling rate to catch any changes in the system config. they shouldn't happen too often
// so the requests can be sent quite infrequently
regular_polling_interval: Duration,
replay_protection_bloomfilters: ReplayProtectionBloomfilters,
rotation_config: RotationConfig,
replay_protection_manager: ReplayProtectionBloomfiltersManager,
client: NymApisClient,
managed_keys: SphinxKeyManager,
shutdown_token: ShutdownToken,
@@ -54,8 +76,21 @@ enum KeyRotationActionState {
}
impl KeyRotationController {
pub(crate) fn new(client: NymApisClient, shutdown_token: ShutdownToken) -> Self {
pub(crate) async fn new(
regular_polling_interval: Duration,
client: NymApisClient,
replay_protection_manager: ReplayProtectionBloomfiltersManager,
managed_keys: SphinxKeyManager,
shutdown_token: ShutdownToken,
) -> Self {
todo!()
// KeyRotationController {
// regular_polling_interval,
// replay_protection_manager,
// client,
// managed_keys,
// shutdown_token,
// }
}
async fn determine_next_action(&self) -> NextAction {
@@ -151,14 +186,41 @@ impl KeyRotationController {
Ok(key) => key,
};
if self
.replay_protection_manager
.allocate_pre_announced(rotation_id, self.rotation_config.rotation_lifetime())
.is_err()
{
// mutex poisoning - we have to exit
self.shutdown_token.cancel();
return;
}
self.client.broadcast_pre_announced_key(public_key).await;
}
KeyRotationActionState::SwapDefault => {
if let Err(err) = self.managed_keys.rotate_keys() {
error!("failed to perform sphinx key swap: {err}")
};
if self
.replay_protection_manager
.promote_pre_announced()
.is_err()
{
// mutex poisoning - we have to exit
self.shutdown_token.cancel();
return;
}
}
KeyRotationActionState::PurgeOld => {
if let Err(err) = self.managed_keys.remove_overlap_key() {
error!("failed to remove old sphinx key: {err}");
};
if self.replay_protection_manager.purge_secondary().is_err() {
// mutex poisoning - we have to exit
self.shutdown_token.cancel();
return;
}
}
KeyRotationActionState::PurgeOld => {}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::NymNodeError;
use crate::node::key_rotation::key::SphinxPublicKey;
@@ -6,25 +6,27 @@ use bloomfilter::Bloom;
use human_repr::HumanDuration;
use nym_sphinx_types::REPLAY_TAG_SIZE;
use std::collections::HashMap;
use std::mem;
use std::path::Path;
use std::sync::{Arc, PoisonError, TryLockError};
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::Instant;
use tracing::{debug, info};
use tracing::{debug, error, info};
// auxiliary data associated with the bloomfilter to get some statistics from the time of its creation
// this is needed in order to more accurately resize it upon reset
struct ReplayProtectionBloomfilterMetadata {
#[derive(Copy, Clone)]
pub(crate) struct ReplayProtectionBloomfilterMetadata {
// used in the unlikely case of epoch durations being changed. it doesn't really cost us anything
// to include it, so might as well
creation_time: Instant,
pub(crate) creation_time: Instant,
/// Number of packets that this node has received since startup, as recorded when this bloomfilter was created.
/// Used for determining the approximate packet rate and thus number of entries in the bloomfilter
packets_received_at_creation: usize,
pub(crate) packets_received_at_creation: usize,
rotation_id: u32,
pub(crate) rotation_id: u32,
}
// it appears that now std Mutex is faster (or comparable) to parking_lot
@@ -73,6 +75,83 @@ impl ReplayProtectionBloomfilters {
self.disabled
}
pub(crate) fn allocate_pre_announced(
&self,
items_count: usize,
fp_p: f64,
packets_received_at_creation: usize,
rotation_id: u32,
) -> Result<(), NymNodeError> {
// build the new filter
let filter =
Bloom::new_for_fp_rate(items_count, fp_p).map_err(NymNodeError::bloomfilter_failure)?;
let mut guard = self
.inner
.lock()
.map_err(|_| NymNodeError::BloomfilterFailure {
message: "mutex got poisoned",
})?;
guard.pre_announced = Some(RotationFilter {
metadata: ReplayProtectionBloomfilterMetadata {
creation_time: Instant::now(),
packets_received_at_creation,
rotation_id,
},
data: filter,
});
Ok(())
}
pub(crate) fn promote_pre_announced(&self) -> Result<(), NymNodeError> {
let mut guard = self
.inner
.lock()
.map_err(|_| NymNodeError::BloomfilterFailure {
message: "mutex got poisoned",
})?;
let Some(mut pre_announced) = guard.pre_announced.take() else {
error!("there was no pre-announced bloomfilter to promote");
return Ok(());
};
// pre_announced -> primary
// primary -> temp (pre_announced)
mem::swap(&mut guard.primary, &mut pre_announced);
// temp (pre_announced) -> secondary
guard.secondary = Some(pre_announced);
Ok(())
}
pub(crate) fn purge_secondary(&self) -> Result<(), NymNodeError> {
let mut guard = self
.inner
.lock()
.map_err(|_| NymNodeError::BloomfilterFailure {
message: "mutex got poisoned",
})?;
guard.secondary = None;
Ok(())
}
pub(crate) fn primary_metadata(
&self,
) -> Result<ReplayProtectionBloomfilterMetadata, NymNodeError> {
let metadata = self
.inner
.lock()
.map_err(|_| NymNodeError::BloomfilterFailure {
message: "mutex got poisoned",
})?
.primary
.metadata;
Ok(metadata)
}
pub(crate) fn reset(&self, items_count: usize, fp_p: f64) -> Result<(), NymNodeError> {
// 1. build the new filter
todo!()
@@ -0,0 +1,65 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::NymNodeError;
use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilters;
use crate::node::replay_protection::items_in_bloomfilter;
use human_repr::HumanCount;
use nym_node_metrics::NymNodeMetrics;
use std::cmp::max;
use std::time::Duration;
use tracing::info;
pub(crate) struct ReplayProtectionBloomfiltersManager {
target_fp_p: f64,
minimum_bloomfilter_packets_per_second: usize,
bloomfilter_size_multiplier: f64,
metrics: NymNodeMetrics,
filters: ReplayProtectionBloomfilters,
}
impl ReplayProtectionBloomfiltersManager {
pub(crate) fn purge_secondary(&self) -> Result<(), NymNodeError> {
self.filters.purge_secondary()
}
pub(crate) fn promote_pre_announced(&self) -> Result<(), NymNodeError> {
self.filters.promote_pre_announced()
}
// TODO: actually do add some metrics
pub(crate) fn allocate_pre_announced(
&self,
rotation_id: u32,
rotation_lifetime: Duration,
) -> Result<(), NymNodeError> {
// 1. estimated the number of items in the filter based on the extrapolated items received
// by the primary filter
let received = self.metrics.mixnet.ingress.forward_hop_packets_received()
+ self.metrics.mixnet.ingress.final_hop_packets_received();
let primary = self.filters.primary_metadata()?;
let time_delta = primary.creation_time.elapsed();
let received_since_creation = received - primary.packets_received_at_creation;
let received_per_second =
(received_since_creation as f64 / time_delta.as_secs_f64()).round() as usize;
let bf_received = max(
received_per_second,
self.minimum_bloomfilter_packets_per_second,
);
let items_in_new_filter = items_in_bloomfilter(rotation_lifetime, bf_received);
let adjusted =
(items_in_new_filter as f64 * self.bloomfilter_size_multiplier).round() as usize;
info!(
"allocating new bloom filter. new expected number of packets: {} that preserve fp rate of {}",
adjusted.human_count_bare(),
self.target_fp_p
);
self.filters
.allocate_pre_announced(adjusted, self.target_fp_p, received, rotation_id)
}
}
@@ -6,6 +6,7 @@ use std::time::Duration;
pub(crate) mod background_task;
pub(crate) mod bloomfilter;
pub(crate) mod manager;
pub fn bitmap_size(false_positive_rate: f64, items_in_filter: usize) -> usize {
/// Equivalent to ln(1 / 2^ln(2)) = ln^2(2)