Skip to main content

SigDigest

Struct SigDigest 

Source
pub struct SigDigest(pub [u8; 16]);
Expand description

16-byte BLAKE3 digest of a signature: the equal-version tiebreak discriminator in [member_info_rank], and the value MemberInfoV1’s summary carries per member.

WHY A DIGEST AND NOT THE SIGNATURE (freenet/river#571, landed as PR #572; every “#571” elsewhere in this file is that ISSUE, not the PR): the summary carried the raw ed25519 Signature per member — ~124 of ~134 CBOR bytes per entry, about 92% of it. That summary is re-sent on every state change to every interested peer, and interest_sync_summaries was measured as the largest single consumer of outbound bytes on the Freenet network (49.8%). The signature is never verified here — it is only ever compared for equality and ordering — so a digest serves the identical purpose. Measured through the real summarize() on 470 entries with realistic MemberIds: 134.08 → 28.01 CBOR bytes per entry, a 4.8x reduction. A 64-bit digest would be exactly 8 bytes per entry cheaper; the next paragraph is why those 8 bytes are bought deliberately. Both figures are measured, not derived, by member_info_summary_stays_small_per_entry, which rebuilds the old shape from the same records.

WHY 124 AND NOT 66, since 66 is the number the issue and the first draft of this change both used: ed25519::Signature’s Serialize calls serialize_tuple(64), which ciborium encodes as a CBOR ARRAY of 64 integers, and a uniformly random byte costs 2 bytes there whenever it is >= 24. 66 is the CBOR BYTE STRING encoding — what River’s own crate::room_state::direct_messages::SignatureBytes newtype produces via serialize_bytes, and what the deferred DirectMessagesSummary follow-up will actually be saving. The member_info summary never used that type. Two different encodings of the same 64 bytes; do not reason about both with one number.

The 29.1 KB mean interest_sync_summaries message that motivated #571 is a FLEET-WIDE mean across all rooms, so it is not this room’s own summary size and the two figures must not be multiplied together: at the Official room’s ~470 records the member_info term ALONE measures ~63 KB, well above the fleet mean, because that mean also averages in many far smaller rooms. No per-room summary measurement is on record, so no claim is made about what any single room’s total summary weighed before this change. (The issue’s own arithmetic did not close for the same reason this doc’s did not: it used 66 rather than ~124 for the signature.)

WHY 128 BITS AND NOT 64: a collision here is not cosmetic, and it is not self-correcting. Two same-version records whose discriminators tie are INDISTINGUISHABLE to anti-entropy — summarize advertises an identical (version, digest) on both peers, delta filters on strict > so neither peer ever offers its record to the other, apply_delta replaces only on strict > so each keeps whichever arrived first, and full-state merge does not rescue it either (freenet-scaffold implements merge as summarize → delta → apply_delta). The two halves of the network then disagree permanently and SILENTLY on that member’s deputies, i.e. on who may ban whom (#411 round 4 B is the bug that added this discriminator in the first place). A member SELF-SIGNS their own record and has unlimited grinding entropy for it — preferred_nickname is free-form and deputies entries are never validated for membership — so the attacker controls BOTH sides of the comparison: at 64 bits that is a ~2^32 birthday search, which is hours on commodity hardware. 128 bits puts it at ~2^64.

This mirrors crate::room_state::direct_messages::PurgeToken, which derives a 16-byte BLAKE3 value from a signature for the same reason under a strictly WEAKER threat model (there the attacker cannot influence the other side of the comparison, and it still chose 128 bits). The two are deliberately NOT factored into a shared helper: each is an independent wire-format commitment — PurgeToken’s bytes live in stored state, these live in the summary — and they must stay free to evolve separately.

WHY BLAKE3 AND NOT freenet_scaffold::util::fast_hash: fast_hash is a base-31 polynomial, fine for the accidental collisions MessageId and BanId care about but trivially collidable by construction, which would price the attack above at roughly nothing regardless of its width.

WIRE FORMAT, load-bearing in two ways. freenet-core byte-compares summarize_state output for staleness, so this must be a fixed function of the signature bytes; and the digest orders the records, so every peer must derive the same bytes and compare them the same way. Both are pinned by sig_digest_golden_vector:

  • the digest is the FIRST 16 bytes of blake3(signature.to_bytes()), kept in their natural order — there is no integer conversion, hence no endianness decision to get wrong (the 64-bit form needed from_le_bytes for this);
  • ordering is plain lexicographic over those bytes (the derived Ord);
  • it serializes as a CBOR byte string (17 bytes), via the hand-written Serialize below rather than the derive, which would emit a 16-element CBOR array — ~32 bytes for random digest content, since each byte >= 24 costs two — and undo most of the saving.

None of the three may change without re-keying the contract. See .claude/rules/contract-summary-determinism.md and freenet/freenet-core#4857.

Tuple Fields§

§0: [u8; 16]

Trait Implementations§

Source§

impl Clone for SigDigest

Source§

fn clone(&self) -> SigDigest

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for SigDigest

Source§

impl Debug for SigDigest

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for SigDigest

Source§

fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error>

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Eq for SigDigest

Source§

impl Hash for SigDigest

Source§

fn hash<__H: Hasher>(&self, state: &mut __H)

Feeds this value into the given Hasher. Read more
1.3.0 · Source§

fn hash_slice<H>(data: &[Self], state: &mut H)
where H: Hasher, Self: Sized,

Feeds a slice of this type into the given Hasher. Read more
Source§

impl Ord for SigDigest

Source§

fn cmp(&self, other: &SigDigest) -> Ordering

This method returns an Ordering between self and other. Read more
1.21.0 (const: unstable) · Source§

fn max(self, other: Self) -> Self
where Self: Sized,

Compares and returns the maximum of two values. Read more
1.21.0 (const: unstable) · Source§

fn min(self, other: Self) -> Self
where Self: Sized,

Compares and returns the minimum of two values. Read more
1.50.0 (const: unstable) · Source§

fn clamp(self, min: Self, max: Self) -> Self
where Self: Sized,

Restrict a value to a certain interval. Read more
Source§

fn clamp_to<R>(self, range: R) -> Self
where Self: Sized, R: ClampBounds<Self>,

🔬This is a nightly-only experimental API. (clamp_to)
Restrict a value to a certain range. Read more
Source§

impl PartialEq for SigDigest

Source§

fn eq(&self, other: &SigDigest) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl PartialOrd for SigDigest

Source§

fn partial_cmp(&self, other: &SigDigest) -> Option<Ordering>

This method returns an ordering between self and other values if one exists. Read more
1.0.0 (const: unstable) · Source§

fn lt(&self, other: &Rhs) -> bool

Tests less than (for self and other) and is used by the < operator. Read more
1.0.0 (const: unstable) · Source§

fn le(&self, other: &Rhs) -> bool

Tests less than or equal to (for self and other) and is used by the <= operator. Read more
1.0.0 (const: unstable) · Source§

fn gt(&self, other: &Rhs) -> bool

Tests greater than (for self and other) and is used by the > operator. Read more
1.0.0 (const: unstable) · Source§

fn ge(&self, other: &Rhs) -> bool

Tests greater than or equal to (for self and other) and is used by the >= operator. Read more
Source§

impl Serialize for SigDigest

Source§

fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for SigDigest

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.