fix: deterministic contract summaries (BTreeMap/BTreeSet) to stop spurious anti-entropy heals - #416
Merged
Merged
Conversation
…rious anti-entropy heals ## Problem freenet-core byte-compares the room contract's `summarize_state` output to decide peer staleness (`is_stale`). Several `ComposableState::Summary` types used `HashMap`/`HashSet`, which serialize (via ciborium) in a per-process-random order. Two peers holding the IDENTICAL room state therefore produced DIFFERENT summary bytes, so the equal-summary skip never fired and the anti-entropy heartbeat fired spurious full-state heals for every room (~20M `summarize_contract_state` calls seen in production). This also feeds the update-drop divergence in freenet/freenet-core#4857. ## Approach Convert every nondeterministically-serialized Summary collection to a deterministic one (same logical contents, only ordering/serialization changes): - BansV1::Summary HashSet<BanId> -> BTreeSet - MembersV1::Summary HashSet<MemberId> -> BTreeSet - MemberInfoV1::Summary HashMap<MemberId,(u32,Signature)> -> BTreeMap - SecretsSummary version_ids/member_secrets HashSet -> BTreeSet - DirectMessagesSummary message_signatures HashSet<SignatureBytes> -> BTreeSet (SignatureBytes gains Ord/PartialOrd over its raw 64 bytes) No STATE type changes: validate_state still accepts existing stored state byte-for-byte; only summarize_state output ordering changes. All keys were already Ord. Deltas are unchanged (built from canonically-ordered state Vecs; not byte-compared by freenet-core). The macro-generated top-level ChatRoomStateV1Summary is an in-order struct, so fixing the leaves fixes it. ## Testing New common/tests/summary_determinism_test.rs: for each fixed summary type (and the top-level summary), build the same logical summary with elements inserted in two different orders, serialize with ciborium (exactly what summarize_state uses), and assert byte-identity. Tests reference the actual <T as ComposableState>::Summary associated type so they FAIL on a HashSet/ HashMap regression (verified: reverting bans to HashSet fails both the bans and top-level tests). Full river-core / riverctl / river-ui suites stay green. ## Migration (WASM change -> new keys) The change alters both the room-contract and chat-delegate WASM, so both keys change. Registered the current (deputy-generation, live-on-network) hashes as legacy V27 in legacy_delegates.toml and common/legacy_room_contracts.toml, resynced WASMs, and bumped river-core 0.1.14->0.1.15 + riverctl 0.1.76->0.1.77 (the room-contract WASM embeds the crate SVH). check-migration, check-room-contract-migration, and the ui/cli WASM sync check all pass. Adds .claude/rules/contract-summary-determinism.md and references it from AGENTS.md. Refs freenet/freenet-core#4857 [AI-assisted - Claude] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B49wBfvR8EjpfYTw5muNV9
sanity
marked this pull request as ready for review
July 19, 2026 00:50
This was referenced Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
freenet-core byte-compares the room contract's
summarize_stateoutput to decide peer staleness (is_stale). SeveralComposableState::Summarytypes usedHashMap/HashSet, which serialize (via ciborium) in a per-process-random order. Two peers holding the identical room state therefore produced different summary bytes → the equal-summary skip never fires → the anti-entropy heartbeat fires spurious full-state heals for every room (~20Msummarize_contract_statecalls seen in production). This also feeds the update-drop divergence in freenet/freenet-core#4857.Audit — every
ComposableState::Summarytypetype Summaryconfiguration(AuthorizedConfigurationV1)u32bans(BansV1)HashSet<BanId>BTreeSet<BanId>members(MembersV1)HashSet<MemberId>BTreeSet<MemberId>member_info(MemberInfoV1)HashMap<MemberId,(u32,Signature)>BTreeMap<…>secrets(RoomSecretsV1)SecretsSummary { version_ids: HashSet<SecretVersion>, member_secrets: HashSet<(SecretVersion,MemberId)> }BTreeSet× 2recent_messages(MessagesV1)Vec<MessageId>messageskept sorted by(time,id)inapply_deltadirect_messages(DirectMessagesV1)DirectMessagesSummary { message_signatures: HashSet<SignatureBytes>, purge_versions: Vec<(MemberId,u64)> }message_signatures→BTreeSet(purge_versionsalready sorted)upgrade(OptionalUpgradeV1)Option<u8>version(StateVersion)u32ChatRoomStateV1SummarySignatureBytesgainsOrd/PartialOrd(over its raw 64 bytes) so it can live in aBTreeSet. All other keys were alreadyOrd.Deltas: none contain a
HashMap/HashSet; each is aVec/struct built from canonically-ordered stateVecs, and freenet-core does not byte-compare deltas — so noDeltachange was needed.Approach
Convert only the nondeterministic summary collections; no STATE type changes.
validate_statestill accepts existing stored state byte-for-byte — onlysummarize_stateoutput ordering changes. Convergence semantics (member_info_rankselection, equal-version tiebreak, etc.) are preserved.Testing
New
common/tests/summary_determinism_test.rs: for each fixed summary type and the top-levelChatRoomStateV1Summary, build the same logical summary with elements inserted in two different orders, serialize withciborium::ser::into_writer(exactly whatsummarize_stateuses), and assert byte-identity. The tests reference the actual<T as ComposableState>::Summaryassociated type, so they fail on aHashSet/HashMapregression — verified by temporarily revertingBansV1::SummarytoHashSet(thebansandtop_leveltests then fail). Fullriver-core(220+),riverctl(170+), andriver-ui --bins(447) suites stay green;cargo fmtclean.Migration (WASM change → new contract + delegate keys)
The change alters both the room-contract and chat-delegate WASM, so both keys change. Registered the current (deputy-generation, live-on-network) hashes as legacy V27 in
legacy_delegates.tomlandcommon/legacy_room_contracts.toml, resynced WASMs, and bumpedriver-core0.1.14→0.1.15 +riverctl0.1.76→0.1.77 (the room-contract WASM embeds the crate SVH, so the version bump itself changes the bytes — WASMs were resynced after the bump).cargo make check-migration,check-room-contract-migration, the ui/cli WASM sync check, and the no-wasm-bindgen-imports check all pass.2e9a1eda…c53ded28…ee08e689…82da3a0e…Recurrence guard
Adds
.claude/rules/contract-summary-determinism.md(the rule: contractSummary/Deltatypes must useBTreeMap/BTreeSet/sorted-Vec, plus the required determinism test and the WASM-migration coupling) and references it fromAGENTS.md.Refs freenet/freenet-core#4857
[AI-assisted - Claude]