pub struct PixFillConfig {
pub enabled: bool,
pub heartbeat: Duration,
pub finish_fraction: f64,
pub loss_margin: f64,
pub max_rate: u32,
pub min_surb_reserve: u64,
pub drain_after_close: bool,
}Expand description
How the Exit fills the return direction of a funded PIX cycle when the application will not.
§Why the Exit has to send anything at all
A PIX Exit is paid per return packet. Each Exit → Entry packet spends one SURB carrying one
share, and a funded cycle only recovers — only unlocks the deposit it has already been paid — once
polys × (threshold + surplus) of them have gone out. That figure is 327 680 at the shipped
dimensions. Nothing in the protocol makes the application send them: an idle VPN client, a
tunnel between keystrokes, or a client that simply reads more than it writes all leave the Exit
holding a funded cycle that will never complete. max_recovery_time then closes it, and because
the deposit address derives from client and server commitments together, the money is stranded
rather than refunded.
So the Exit sends its own keep-alives — “fill” — at whatever rate the remaining emission needs to clear the deadline, minus whatever the application is already providing. The mechanism is deliberately the existing SURB-level keep-alive stream rather than a new message: every shipped Entry already parses it, already counts it as a consumed SURB (so its balancer refills), and already credits it as service against the successor gate.
§What it costs the client
One cycle’s quota per deadline while idle, and nothing at all while the application is busier than
the required rate — heartbeat is what fill drops to when organic egress
already covers the need. The client accepts this as the price of a Session that completes its
cycles; it funds successors exactly as it would have.
§What bounds it
Four separate things, because a rule that sends is the one rule in this module that could be turned against the node running it:
max_ratecaps the instantaneous rate.min_surb_reservestops fill from spending the SURBs the Session needs for its own replies, as a ceiling on a reserve derived per Session from the Entry’s announced buffer target.- The stall rule holds fill at the heartbeat once the target cycle stops making progress, so a doomed cycle or a
share-less SURB supply cannot burn
max_rate × max_recovery_timeSURBs. - The existing deadlines and the service gate are unchanged, and remain the backstop for a client that stops supplying SURBs at all.
Fields§
§enabled: boolWhether the Exit originates fill keep-alives at all.
Disabling it restores the behaviour of an Exit that only ever answers: funded cycles then
complete if and only if the application returns enough traffic, and the ones that do not are
closed by max_recovery_time with their deposits stranded. That is a defensible choice only
for a deployment whose clients are known to saturate their Sessions.
Default: true.
heartbeat: DurationThe floor fill rate: one packet per this period, sent even when nothing needs sending.
Fill never drops to silence while a funded cycle is in flight, for two reasons that have nothing to do with the deadline. The Entry’s own idle eviction is refreshed by this traffic, and a Session whose Exit has gone quiet is one whose SURB balancer has no level report to act on. It is also what makes the ramp observable: a rate that is only ever the computed one cannot be told apart from a planner that has stopped planning.
Expressed as a period rather than a rate because it is far below one packet per second and
FillRate carries its own unit, so 1 / 60 s is exact rather than a rounded fraction.
Default: 60 s.
finish_fraction: f64Fraction of SupervisorConfig::max_recovery_time by which a cycle should be finished.
The planner aims at hard_deadline − (1 − finish_fraction) × max_recovery_time rather than at
the deadline itself, so the last quarter of the budget is margin: for the loss the
loss_margin does not cover, for a mixnet delay spike, and for the
round trip the successor’s commitment and deposit still need after this cycle recovers. Aiming
at the deadline exactly would make every cycle a photo finish, and a cycle that misses is
worth nothing at all rather than nearly everything.
Default: 0.75.
loss_margin: f64Fraction by which the remaining emission is inflated to cover return-path loss.
Fill counts packets it sends; the cycle advances on shares that arrive. The difference is return-path loss, which the Exit cannot observe directly — it sees the reconstructor’s progress and nothing about the packets that produced none. Rather than infer a loss rate from a signal that also moves for a dozen other reasons, the planner simply sends this much extra and lets the ordinary re-planning each second absorb whatever the real figure turns out to be.
Default: 0.05.
max_rate: u32Ceiling on the fill rate, in packets per second.
This is the value that decides how much bandwidth an idle Session may cost, and it has to
clear the floor a cycle actually needs: the largest quota this node accepts, spread over
finish_fraction × max_recovery_time. validate_incoming_session_pix_config enforces exactly
that, so a cap set below it is refused at load rather than discovered one stranded deposit at a
time. It counts the cap only up to
MAX_ASSUMED_SESSION_PACKET_RATE, since fill faster
than a Session is assumed to carry finishes nothing sooner.
Default: 250 packets/s, which is ~2.9 Mbps of Session data at full SESSION_MTU segments and
about four times the 64 packets/s the shipped defaults require.
min_surb_reserve: u64Ceiling on the SURBs that must remain estimated-available before a fill packet is emitted.
Fill is the one egress on the Exit that has no application waiting on it, so it is the one
that must yield. Spending the last SURBs on fill is not merely wasteful: a return packet with
no SURB to ride holds every packet this node originates for the resolver’s
surb_resolution_wait, so an over-eager filler stalls the Session it is trying to save. The
SURB-level notification is exempt — it is the signal that asks for more SURBs — but the
heartbeat is not, being fill at its floor rather than a different message.
A ceiling rather than the figure used, because the two sides of the comparison are chosen by
different nodes: the reserve is measured against the Exit’s estimate of its own SURB buffer for
the Session, and how deep that buffer is is the Entry’s decision, announced with its
initiation. The effective reserve is therefore never more than a quarter of the Entry’s
announced buffer target, floored at one. Without that, an operator sizing this against a
production buffer would also be deciding — unintentionally — that every Session whose Entry
asks for a shallower one can never be filled at all: the estimate never reaches the reserve,
every fill packet is withheld, and the funded cycle strands on
max_recovery_time.
Default: 500, matching SurbStoreConfig::distress_threshold. Against the 7 000-SURB balancer
target of the worked profile a quarter is 1 750, so the configured value is what binds there
and the derivation only engages on a Session whose Entry asked for less than four times it.
drain_after_close: boolWhether a Session closed at this Exit keeps draining its buffered SURBs into the funded cycle.
A Session can end at the Exit long before its funded cycle does: the session server closes it,
the operator calls SessionManager::close_session, or the peer reports a SessionError. The
deposit that cycle was paid is only released once its whole emission has ridden back to the
Entry, and the address it sits at derives from both nodes’ commitments — so a cycle abandoned
midway strands money both sides have already parted with rather than refunding it. With this
on, the Exit answers such a close by keeping the keep-alive stream running until the cycle
recovers, spending SURBs it is already holding on shares it has already been paid for.
Deliberately narrow, and inert unless enabled is set — a drain is fill,
planned by the same rate law and carried by the same stream, so a disabled planner emits no
rate to drain at. Three further conditions bound what it can cost:
- it is entered only when the Exit’s estimated SURB buffer, net of
min_surb_reserve, covers the cycle’s whole remaining emission — a drain that runs out partway spends the reserve and recovers nothing, which is worse than not starting; max_rateand that same reserve bound it exactly as they bound ordinary fill, and the SURB-level notification — fill’s one reserve-exempt packet — is switched off for the duration, since it asks for more SURBs on behalf of a Session that is gone: the Entry itself may well still be there, but nothing is left to act on the request, so no refill answers it;max_recovery_idleandmax_recovery_timeend it whether or not it succeeds.
Setting it to false restores the immediate teardown on every close path, byte for byte.
Default: true.
Trait Implementations§
Source§impl Clone for PixFillConfig
impl Clone for PixFillConfig
Source§fn clone(&self) -> PixFillConfig
fn clone(&self) -> PixFillConfig
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for PixFillConfig
impl Debug for PixFillConfig
Source§impl Default for PixFillConfig
impl Default for PixFillConfig
Source§impl<'de> Deserialize<'de> for PixFillConfigwhere
PixFillConfig: Default,
impl<'de> Deserialize<'de> for PixFillConfigwhere
PixFillConfig: Default,
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Source§impl PartialEq for PixFillConfig
impl PartialEq for PixFillConfig
Source§impl Serialize for PixFillConfig
impl Serialize for PixFillConfig
impl StructuralPartialEq for PixFillConfig
Auto Trait Implementations§
impl Freeze for PixFillConfig
impl RefUnwindSafe for PixFillConfig
impl Send for PixFillConfig
impl Sync for PixFillConfig
impl Unpin for PixFillConfig
impl UnsafeUnpin for PixFillConfig
impl UnwindSafe for PixFillConfig
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<'de, T> BorrowedRpcObject<'de> for Twhere
T: RpcBorrow<'de> + RpcSend,
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<T> Conv for T
impl<T> Conv for T
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
§impl<T> FmtForward for T
impl<T> FmtForward for T
§fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
fn fmt_binary(self) -> FmtBinary<Self>where
Self: Binary,
self to use its Binary implementation when Debug-formatted.§fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
fn fmt_display(self) -> FmtDisplay<Self>where
Self: Display,
self to use its Display implementation when
Debug-formatted.§fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
fn fmt_lower_exp(self) -> FmtLowerExp<Self>where
Self: LowerExp,
self to use its LowerExp implementation when
Debug-formatted.§fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
fn fmt_lower_hex(self) -> FmtLowerHex<Self>where
Self: LowerHex,
self to use its LowerHex implementation when
Debug-formatted.§fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
fn fmt_octal(self) -> FmtOctal<Self>where
Self: Octal,
self to use its Octal implementation when Debug-formatted.§fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
fn fmt_pointer(self) -> FmtPointer<Self>where
Self: Pointer,
self to use its Pointer implementation when
Debug-formatted.§fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
fn fmt_upper_exp(self) -> FmtUpperExp<Self>where
Self: UpperExp,
self to use its UpperExp implementation when
Debug-formatted.§fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
fn fmt_upper_hex(self) -> FmtUpperHex<Self>where
Self: UpperHex,
self to use its UpperHex implementation when
Debug-formatted.§fn fmt_list(self) -> FmtList<Self>where
&'a Self: for<'a> IntoIterator,
fn fmt_list(self) -> FmtList<Self>where
&'a Self: for<'a> IntoIterator,
§impl<T> FutureExt for T
impl<T> FutureExt for T
§fn with_context(self, otel_cx: Context) -> WithContext<Self>
fn with_context(self, otel_cx: Context) -> WithContext<Self>
§fn with_current_context(self) -> WithContext<Self>
fn with_current_context(self) -> WithContext<Self>
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more§impl<T> Pipe for Twhere
T: ?Sized,
impl<T> Pipe for Twhere
T: ?Sized,
§fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
fn pipe<R>(self, func: impl FnOnce(Self) -> R) -> Rwhere
Self: Sized,
§fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref<'a, R>(&'a self, func: impl FnOnce(&'a Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read more§fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
fn pipe_ref_mut<'a, R>(&'a mut self, func: impl FnOnce(&'a mut Self) -> R) -> Rwhere
R: 'a,
self and passes that borrow into the pipe function. Read more§fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
fn pipe_borrow<'a, B, R>(&'a self, func: impl FnOnce(&'a B) -> R) -> R
§fn pipe_borrow_mut<'a, B, R>(
&'a mut self,
func: impl FnOnce(&'a mut B) -> R,
) -> R
fn pipe_borrow_mut<'a, B, R>( &'a mut self, func: impl FnOnce(&'a mut B) -> R, ) -> R
§fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
fn pipe_as_ref<'a, U, R>(&'a self, func: impl FnOnce(&'a U) -> R) -> R
self, then passes self.as_ref() into the pipe function.§fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
fn pipe_as_mut<'a, U, R>(&'a mut self, func: impl FnOnce(&'a mut U) -> R) -> R
self, then passes self.as_mut() into the pipe
function.§fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
fn pipe_deref<'a, T, R>(&'a self, func: impl FnOnce(&'a T) -> R) -> R
self, then passes self.deref() into the pipe function.§impl<T> Pointable for T
impl<T> Pointable for T
§impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
impl<'de, T> RpcBorrow<'de> for T
impl<T> RpcObject for Twhere
T: RpcSend + RpcRecv,
impl<T> RpcRecv for T
impl<T> RpcSend for T
§impl<T> Tap for T
impl<T> Tap for T
§fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow<B>(self, func: impl FnOnce(&B)) -> Self
Borrow<B> of a value. Read more§fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut<B>(self, func: impl FnOnce(&mut B)) -> Self
BorrowMut<B> of a value. Read more§fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref<R>(self, func: impl FnOnce(&R)) -> Self
AsRef<R> view of a value. Read more§fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut<R>(self, func: impl FnOnce(&mut R)) -> Self
AsMut<R> view of a value. Read more§fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref<T>(self, func: impl FnOnce(&T)) -> Self
Deref::Target of a value. Read more§fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
fn tap_deref_mut<T>(self, func: impl FnOnce(&mut T)) -> Self
Deref::Target of a value. Read more§fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
fn tap_dbg(self, func: impl FnOnce(&Self)) -> Self
.tap() only in debug builds, and is erased in release builds.§fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
fn tap_mut_dbg(self, func: impl FnOnce(&mut Self)) -> Self
.tap_mut() only in debug builds, and is erased in release
builds.§fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
fn tap_borrow_dbg<B>(self, func: impl FnOnce(&B)) -> Self
.tap_borrow() only in debug builds, and is erased in release
builds.§fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
fn tap_borrow_mut_dbg<B>(self, func: impl FnOnce(&mut B)) -> Self
.tap_borrow_mut() only in debug builds, and is erased in release
builds.§fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
fn tap_ref_dbg<R>(self, func: impl FnOnce(&R)) -> Self
.tap_ref() only in debug builds, and is erased in release
builds.§fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
fn tap_ref_mut_dbg<R>(self, func: impl FnOnce(&mut R)) -> Self
.tap_ref_mut() only in debug builds, and is erased in release
builds.§fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
fn tap_deref_dbg<T>(self, func: impl FnOnce(&T)) -> Self
.tap_deref() only in debug builds, and is erased in release
builds.