Skip to content

fix(signal): gate the group sender-key advance before the wire - #1027

Merged
jlucaso1 merged 13 commits into
mainfrom
feat/group-sender-key-lease
Jul 15, 2026
Merged

fix(signal): gate the group sender-key advance before the wire#1027
jlucaso1 merged 13 commits into
mainfrom
feat/group-sender-key-lease

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Reframed. The original framing (a perf follow-up to #1026) was wrong: my baseline lacked #1026, so the delta I measured was #1026's DM lease, not this change. Corrected below.

Problem

A warm group send advanced the sender-key chain and stored the record without gating the advance on durability. Sender-key message keys and IVs derive deterministically from the chain iteration, so ciphertext could reach the wire before the advance was persisted; a crash in that window re-derived the same iteration on reload, causing nonce reuse toward every group member.

Instrumentation on the real send path confirmed the gap: 196 of 200 warm group sends took no pre-wire flush. Neither a session reservation nor a sender-key gate covered them.

Root cause

The production path (wacore::send::encrypt_group_message) was a second copy of sender-key encryption. It advanced and stored the chain but did not mark the advance wire-gated. The group gate added in #1026 lived only in the library primitive group_encrypt, which production did not call.

Primary fix

  • De-duplicate the encrypt path. Production delegates to group_encrypt, so chain advance, wire gate, and durability logic have one implementation.
  • Lease sender-key iterations. SenderKeyRecord reserves 64 iterations at a time, mirroring the DM counter lease. Only the send that raises the durable ceiling gates the wire; covered sends use write-behind. Recovery fast-forwards past the ceiling so a possibly spent iteration cannot be re-derived.
  • Keep the cost below the removed duplication. The sender-chain seed is held in a Copy field instead of the generated protobuf allocation, and the thread-local encrypt buffer is retained while only a right-sized ciphertext is returned.

Follow-up: clean reloads must not look like crashes

The P2 review was valid. The three explicit lifecycle signal_cache.clear() calls are conditional, but a successful clean teardown also cleared the cache through teardown_inbound_commits_bounded() in commit_batch. Clean reconnects and clean eviction could therefore burn an unused 64-iteration lease on every reload. After 32 such burns, a peer that missed the intervening messages could see a 2,048-step jump and reject it above MAX_FORWARD_JUMPS (2,000).

The follow-up fixes this for both DM sessions and group sender keys without weakening crash recovery:

  • A live SignalStoreCache owns a random 128-bit store incarnation.
  • Persisted records with a non-zero lease carry that incarnation in local-only protobuf field 101.
  • A reload with the same incarnation is exact: it does not fast-forward.
  • A restart/new cache, lossy clear, missing marker, different marker, malformed marker, or duplicate marker is untrusted and keeps the conservative fast-forward.
  • clear_after_flush() clears only fully settled stores. Any session, sender key, identity, prekey removal, or wire gate created after the preceding flush remains resident until durable; only a real lossy discard rotates reload trust.
  • Direct record deserialization remains recovery-conservative. No durable “clean shutdown” bit is introduced, so there is no window in which later sends could be mistaken for clean state.

This adds no synchronous I/O and no per-message RNG. A marker is generated when the cache is created or when a lossy boundary invalidates trust.

Direct Device stores

The later direct-store P2 was also valid: Device still used generic serialize/deserialize on every database operation, so its sender-key path burned a 64-iteration lease per send. The same defect existed in the direct SessionStore.

Direct stores now use the incarnation-aware encoders with one random 128-bit process incarnation held in OnceLock. Unlike write-back caches, these stores await the backend write before ciphertext can be returned, so a new Device in the same process is a clean reload; a process restart gets a new marker and remains recovery-conservative. This avoids adding a field to the public Device, performs no per-operation RNG or heap allocation, and removes the old SenderKeyRecord::serialize() allocation used only to test whether a loaded record was empty.

Teardown race after a successful flush

A final review found a separate critical interleaving: an outbound operation could install a raised reservation after flush() released a store lock but before teardown called clear_after_flush(). The old dirty branch used discard(), which erased the newly dirty record and its pending gate; needs_pre_wire_flush() could then return false and let ciphertext leave before the new ceiling was durable.

Cleanup now clears a lane only when it is fully settled. Post-flush session, sender-key, identity, and consumed-prekey state remains resident, and reservation/wire gates remain closed until a later successful flush. The incarnation is deliberately not rotated while dirty state is retained: dirty/deleted/checked-out entries cannot be evicted or reloaded, while restart, a new cache, and every actual lossy discard already rotate trust. This avoids turning an unrelated concurrent write into lease burns for clean records.

Safety interleavings

Scenario Guarantee
Crash/restart mid-lease new cache/process incarnation; fast-forward past the durable ceiling
Direct-store reload/new Device in the same process shared process incarnation; resume at the exact next iteration
Clean reconnect or clean eviction matching incarnation; resume at the exact next iteration
Checked-out session skipped by flush newer state and checkout marker remain resident; a restart/new cache still fast-forwards the older backend snapshot
Concurrent dirty sender-key/session after flush record and pending gate remain resident until a later successful flush
Concurrent identity or consumed-prekey write write-back state remains resident and is committed by the next flush
Legacy record no marker; conservative one-time recovery behavior
Malformed or duplicate local marker treated as untrusted, never as proof of a clean reload
Sender-key rotation lease and marker reset; next send reserves against the fresh chain

Performance validation

The original durability work still pays for itself versus main in the 50-member group DHAT run:

Variant allocations/msg vs main
main 213.1
durability gate only 215.0 +1.9
gate + chain-key optimization 206.4 -6.6 (-3.1%)

The clean-reload follow-up was tested A/B in isolated worktrees before the dependency-only rebase: 7489036d (before follow-up) versus 6c557537 (after follow-up), using the same whatsapp-benchs harness and separately built binaries.

Normal group-send: 50 members, 2,000 messages, 200 msg/s, alternating A/B/B/A; averages include two 2,000/2,000 runs per side.

Metric Before After Delta
actual rate 189.23 msg/s 190.56 msg/s +0.70%
client CPU total 0.780 s 0.770 s -1.28%
client peak RSS 21.85 MB 21.80 MB -0.23%

DHAT was rate-limited to 100 msg/s so both instrumented runs completed 2,000/2,000 without mock timeouts:

Metric Before After Delta
total allocated bytes 76,517,247 76,463,313 -53,934 (-0.070%)
allocation blocks 418,102 418,043 -59 (-0.014%)

Reconnect A/B also completed 20/20 clean cycles on both revisions (21 authentications including the initial connection).

The direct-store follow-up was then measured on the rebased commits 18845345 (before) and 1a1a7be9 (after): release builds, in-memory backend, 5,000 direct group_encrypt calls per pass, one warm-up plus four measured runs in alternating A/B/B/A order.

Metric Before After Delta
final emitted iteration 319,936 4,999 exact sequence restored
median time/send 35.45 µs 24.42 µs -31.13%
mean time/send 35.83 µs 24.67 µs -31.15%

The final teardown-race fix was measured on separately built release binaries, 1a1a7be9 versus 72b8e4a0, using the reconnect scenario in A/B/B/A order with 20 reconnects per run. Every run completed 20/20 ready cycles and 21/21 authentications.

Metric Before After Delta
client CPU total/run 0.180 s 0.170 s -5.6%
client peak RSS/run 19.55 MB 19.60 MB +0.05 MB (+0.26%)
benchmark binary 26,492,368 B 26,491,056 B -1,312 B

Measured costs for the cache follow-up were explicit: local persistence adds 19 bytes only to records with an active lease, and its isolated A/B release binary grew by 18,800 bytes (+0.071%). On the final rebased head (72b8e4a0), production binary-size CI passed against main (a5029cec): stripped binary +13.28 KiB (+0.12%), .text +11.19 KiB (+0.12%), allocated sections +13.48 KiB (+0.12%), with dependencies unchanged at 472 crates. The direct-store follow-up adds no heap allocation or Device size, and its hot path is faster as measured above. One 200 msg/s harness run had a known mock-server pong timeout (all 2,000 ACKs arrived); it was excluded and immediately rerun clean.

Regression coverage

  • 33 group sends with a clean flush/reload after every send stay at iterations 0..32; a peer missing the first 32 decrypts the final message. The old behavior emitted 0, 64, ... 2048 and failed.
  • Clean sender-key eviction resumes 0 -> 1 instead of 0 -> 64.
  • DM clean reload is exact, while a new cache still burns the lease.
  • Direct group stores emit 0..32 across per-operation database reloads, a new Device in the same process emits 33, and an untrusted/restart load advances to 64.
  • Direct DM stores reload exactly within the process and retain the same conservative recovery ceiling outside the incarnation.
  • Checked-out DM state and dirty sender-key state after a flush remain resident; a separate cache/restart still treats the older backend snapshot conservatively.
  • A deterministic post-flush teardown window preserves concurrent session, sender-key, identity, and consumed-prekey writes; both outbound durability gates remain closed until the retry flush succeeds.
  • Matching, missing, replaced, malformed, and duplicate incarnation markers are covered for both record types.
  • High-level connection cleanup does not burn a clean group lease.
  • Existing crash/reload, lease amortization, rotation, legacy, corrupt-field, and production-path gate tests remain covered.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --all --tests --quiet
  • cargo test --workspace --exclude e2e-tests --quiet
  • cargo check -p whatsapp-rust --no-default-features
  • WASM release build with no default features and the wasm_js getrandom backend
  • cargo test -p e2e-tests -- --nocapture against the local mock server with CHATSTATE_TTL_SECS=3
  • whatsapp-benchs: normal A/B group-send, DHAT A/B group-send, and reconnect A/B
  • Direct-store release micro A/B: 5,000 group sends × 4 alternating measured runs per revision

All completed successfully; default stress/soak tests remain ignored as designed.

@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a nonce-reuse vulnerability in warm group sends: the production encrypt path (encrypt_group_message) was a separate copy of sender-key encryption that advanced the chain without gating ciphertext on a durability flush, meaning a crash mid-lease could re-derive the same (key, IV) toward every group member. The fix de-duplicates the path by delegating to group_encrypt, adds a 64-iteration lease to SenderKeyRecord mirroring the DM session counter lease, and introduces a 128-bit store incarnation marker to let clean reloads (cache clear after successful flush, same-process Device reload) skip the conservative fast-forward that crash recovery requires.

  • De-duplicate encrypt path: encrypt_group_message now delegates to group_encrypt, so chain advance, wire gate, and lease logic have one implementation and are tested together.
  • Iteration lease + incarnation: SenderKeyRecord gains reserved_iteration (persisted at field 100) and a store_incarnation marker (field 101); reloads with a matching live incarnation are exact, all others fast-forward past the durable ceiling.
  • clear_after_flush(): Replaces the unconditional clear() after a successful flush; preserves incarnation trust when no dirty/pending state exists, rotates the incarnation on any concurrent dirty or wire-gate state, and extends the same treatment to SessionRecord for DM sessions.

Confidence Score: 5/5

Safe to merge. The crypto safety properties are sound, the incarnation mechanism correctly separates clean reloads from crash recovery in all enumerated scenarios, and the lease logic is well-tested at unit, integration, and end-to-end levels.

The encrypt-path de-duplication removes the dual-implementation gap that allowed ungated group sends. The iteration lease, fast-forward ceiling, and incarnation marker all fail closed — malformed fields, duplicate markers, and missing markers each trigger recovery-conservative behavior rather than silently disabling the guard. The new clear_after_flush path is conservative by default (any dirty or wire-gate-pending entry keeps the old incarnation resident), which is the safe direction. Tests cover crash mid-lease, clean reload exact behavior, same-process Device reload, direct-store recovery ceiling, rotation reset, duplicate and malformed incarnation markers, and the forward-jump limit for repeated clean evictions. No gaps found in the safety interleavings described by the PR.

No files require special attention.

Important Files Changed

Filename Overview
wacore/src/send/encrypt.rs Removes the duplicate sender-key encrypt implementation and delegates to group_encrypt; adds .context() to preserve SignalProtocolError as a downcasted source for the SKDM-redistribution retry path.
wacore/libsignal/src/protocol/sender_keys.rs Adds reserved_iteration field and lease logic to SenderKeyRecord; moves sender chain key to a Copy field (eliminating per-clone Bytes allocation); adds incarnation-aware serialize/deserialize_for_store paths and fast_forward_sender_chain for recovery.
wacore/libsignal/src/protocol/local_field.rs New shared decoder/encoder for local-only protobuf fields (reservation + store incarnation); fails closed on malformed or duplicate markers so a corrupt lease cannot re-enable spent counters.
wacore/src/store/signal_cache.rs Adds store incarnation to SessionStoreState and SenderKeyStoreState; introduces clear_after_flush() that preserves incarnation trust on clean eviction/reconnect while rotating it under any dirty or pending-gate state.
wacore/libsignal/src/protocol/state/session.rs Refactors decode_reserved_index into the shared local_field decoder; adds deserialize_for_store / serialize_into_for_store incarnation paths for DM sessions; mirrors sender-key clean-reload trust.
src/store/signal.rs Direct Device store now uses a process-wide OnceLock incarnation for both SessionRecord and SenderKeyRecord serialization, giving same-process Device reloads exact behavior while cross-process restarts remain recovery-conservative.
wacore/libsignal/src/protocol/group_cipher.rs Adds copy_out() to CryptoBuffer (right-sized Box output with capacity retention), generalises group_encrypt to S: SenderKeyStore + ?Sized, and replaces unconditional mark_wire_gated with the iteration lease gate.
src/message/commit_batch.rs Single-line change: replaces clear() with clear_after_flush() after a successful inbound-commit flush so clean reconnects preserve incarnation trust.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant App
    participant encrypt_group_message
    participant group_encrypt
    participant SenderKeyStore
    participant flush_signal_cache
    participant Backend

    App->>encrypt_group_message: plaintext
    encrypt_group_message->>group_encrypt: delegate (single implementation)
    group_encrypt->>SenderKeyStore: load_sender_key
    SenderKeyStore-->>group_encrypt: SenderKeyRecord
    group_encrypt->>group_encrypt: derive message keys from chain
    group_encrypt->>group_encrypt: AES-CBC encrypt → copy_out()

    alt "spent_iteration >= reserved_iteration"
        group_encrypt->>group_encrypt: "reserve_iterations → wire_gated=true"
        group_encrypt->>SenderKeyStore: store_sender_key (wire_gated)
        SenderKeyStore->>flush_signal_cache: pre-wire gate required
        flush_signal_cache->>Backend: persist record with incarnation marker
        Backend-->>flush_signal_cache: ok
        flush_signal_cache-->>App: gate released → ciphertext to wire
    else within lease
        group_encrypt->>SenderKeyStore: store_sender_key (write-behind)
        SenderKeyStore-->>App: ciphertext to wire immediately
    end

    App->>flush_signal_cache: clear_after_flush()
    alt no dirty / no pending gate
        flush_signal_cache->>flush_signal_cache: clear() — preserve incarnation
        Note over flush_signal_cache: next reload is exact, no fast-forward
    else dirty or wire_gate_pending
        flush_signal_cache->>flush_signal_cache: retain state
    end

    Note over Backend: crash/restart: new incarnation → fast-forward past ceiling
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant App
    participant encrypt_group_message
    participant group_encrypt
    participant SenderKeyStore
    participant flush_signal_cache
    participant Backend

    App->>encrypt_group_message: plaintext
    encrypt_group_message->>group_encrypt: delegate (single implementation)
    group_encrypt->>SenderKeyStore: load_sender_key
    SenderKeyStore-->>group_encrypt: SenderKeyRecord
    group_encrypt->>group_encrypt: derive message keys from chain
    group_encrypt->>group_encrypt: AES-CBC encrypt → copy_out()

    alt "spent_iteration >= reserved_iteration"
        group_encrypt->>group_encrypt: "reserve_iterations → wire_gated=true"
        group_encrypt->>SenderKeyStore: store_sender_key (wire_gated)
        SenderKeyStore->>flush_signal_cache: pre-wire gate required
        flush_signal_cache->>Backend: persist record with incarnation marker
        Backend-->>flush_signal_cache: ok
        flush_signal_cache-->>App: gate released → ciphertext to wire
    else within lease
        group_encrypt->>SenderKeyStore: store_sender_key (write-behind)
        SenderKeyStore-->>App: ciphertext to wire immediately
    end

    App->>flush_signal_cache: clear_after_flush()
    alt no dirty / no pending gate
        flush_signal_cache->>flush_signal_cache: clear() — preserve incarnation
        Note over flush_signal_cache: next reload is exact, no fast-forward
    else dirty or wire_gate_pending
        flush_signal_cache->>flush_signal_cache: retain state
    end

    Note over Backend: crash/restart: new incarnation → fast-forward past ceiling
Loading

Reviews (12): Last reviewed commit: "fix(signal): retain post-flush state dur..." | Re-trigger Greptile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6282a17fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/libsignal/src/protocol/group_cipher.rs
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.87 MiB 10.89 MiB +13.28 KiB (+0.12%) 🔺
bin .text 8.87 MiB 8.88 MiB +11.19 KiB (+0.12%) 🔺
bin allocated (text+data+bss) 10.88 MiB 10.89 MiB +13.48 KiB (+0.12%) 🔺
llvm-lines wacore 507,674 508,537 +863 (+0.17%) 🔺
llvm-lines wacore copies 17,378 17,438 +60 (+0.35%) 🔺
llvm-lines whatsapp-rust lib 773,089 774,302 +1,213 (+0.16%) 🔺
llvm-lines whatsapp-rust lib copies 25,116 25,188 +72 (+0.29%) 🔺
deps crates (Cargo.lock) 472 472 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.68 MiB 1.67 MiB -2.62 KiB (-0.15%) 🔽
.text wacore 536.75 KiB 530.46 KiB -6.29 KiB (-1.17%) 🎉
.text wacore_binary 148.45 KiB 148.45 KiB 0
.text wacore_libsignal 183.68 KiB 195.25 KiB +11.57 KiB (+6.30%) ⚠️
.text wacore_appstate 158.25 KiB 158.25 KiB 0
.text wacore_noise 26.03 KiB 26.03 KiB 0
.text waproto 1.60 MiB 1.60 MiB 0
.text whatsapp_rust_sqlite_storage 513.08 KiB 513.08 KiB 0
.text whatsapp_rust_tokio_transport 43.69 KiB 43.69 KiB 0
.text whatsapp_rust_ureq_http_client 10.47 KiB 10.47 KiB 0
.text std 1.01 MiB 1.01 MiB +1.50 KiB (+0.15%) 🔺
.text other deps 2.95 MiB 2.95 MiB +6.81 KiB (+0.23%) 🔺
Top movers (cargo-bloat attribution)
Crate main PR Δ
wacore_libsignal 183.68 KiB 195.25 KiB +11.57 KiB (+6.30%)
wacore 536.75 KiB 530.46 KiB -6.29 KiB (-1.17%)
demo 30.16 KiB 35.58 KiB +5.42 KiB (+17.96%)
whatsapp_rust 1.68 MiB 1.67 MiB -2.62 KiB (-0.15%)
rustix 1.88 KiB 191 B -1.69 KiB (-90.08%)
buffa_descriptor 2.98 KiB 4.67 KiB +1.69 KiB (+56.87%)
std 1.01 MiB 1.01 MiB +1.50 KiB (+0.15%)
anyhow 17.70 KiB 19.09 KiB +1.39 KiB (+7.88%)

Baseline: a5029cec0 (latest main run) · Head: 219223414 · Graphs

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Sender-key records now persist reservation ceilings with store incarnations, restore chains safely after reload, and reset leases on rotation. Group encryption renews leases at spent-iteration boundaries, reuses ciphertext buffers, and delegates through the send path with recovery and compatibility coverage.

Changes

Sender-chain reservation leases

Layer / File(s) Summary
Local metadata contracts
wacore/libsignal/src/protocol/local_field.rs, wacore/libsignal/src/protocol/mod.rs
Shared local-field encoding and decoding carries reservation and store-incarnation metadata with fail-closed validation.
Sender-record lease persistence
wacore/libsignal/src/protocol/sender_keys.rs
Sender-key chains are held in memory, reservation ceilings are serialized and restored, untrusted reloads fast-forward the current chain, and rotation resets leases.
Session and cache recovery
wacore/libsignal/src/protocol/state/session.rs, wacore/src/store/signal_cache.rs, src/store/signal.rs, src/client/tests.rs, src/message/commit_batch.rs
Session and sender-key persistence uses store incarnations, while direct stores and cache teardown preserve or invalidate lease state according to reload conditions.
Lease-driven group encryption
wacore/libsignal/src/protocol/group_cipher.rs
group_encrypt accepts generic stores, returns ciphertext through a reusable buffer, and reserves iterations at lease boundaries with crash-recovery and amortization tests.
Send-path integration
wacore/src/send.rs, wacore/src/send/encrypt.rs, wacore/src/send/tests.rs
The send path delegates encryption to group_encrypt and verifies persisted reservations and concrete missing-key errors.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SendPath
  participant group_encrypt
  participant SenderKeyRecord
  participant SenderKeyStore
  participant SignalCache
  SendPath->>group_encrypt: encrypt group message
  group_encrypt->>SenderKeyRecord: derive keys and evaluate reservation
  group_encrypt->>SenderKeyStore: persist reservation at lease boundary
  SignalCache->>SenderKeyRecord: reload with store incarnation
  SenderKeyRecord-->>SignalCache: fast-forward untrusted sender chain
  group_encrypt-->>SendPath: return encrypted message
Loading

Possibly related PRs

Suggested labels: breaking-change, performance

Suggested reviewers: greptile-apps

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and matches the main change: gating group sender-key advancement before ciphertext is sent.
Description check ✅ Passed The description is clearly about the same sender-key durability and reload changes implemented in the patch.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/group-sender-key-lease

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/libsignal/src/protocol/sender_keys.rs`:
- Around line 548-582: Extract the duplicated tag-scanning logic from
SenderKeyRecord::decode_reserved_iteration and
SessionRecord::decode_reserved_index into a shared internal
decode_local_only_u32_field helper accepting bytes and field_number. Preserve
the existing fail-closed behavior for malformed tags, wrong wire types,
duplicate fields, invalid varints, and values outside u32, while returning 0
when the target field is absent; update both callers to use it.
- Around line 662-667: Update the state replacement logic around
process_sender_key_distribution_message so reserved_iteration is reset only when
the incoming sender-key state has a different chain_id from the current chain.
Preserve the existing reservation when duplicate SKDMs re-add the same chain,
while still resetting it for an actual chain rotation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 54109142-e63f-4d3f-bf49-90d9d132bbcd

📥 Commits

Reviewing files that changed from the base of the PR and between a7dc852 and d6282a1.

📒 Files selected for processing (2)
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/sender_keys.rs

Comment thread wacore/libsignal/src/protocol/sender_keys.rs Outdated
Comment thread wacore/libsignal/src/protocol/sender_keys.rs
@jlucaso1 jlucaso1 changed the title perf(signal): lease sender-key iterations to amortize the per-send group flush fix(signal): gate the group sender-key advance before the wire Jul 14, 2026
@jlucaso1
jlucaso1 force-pushed the feat/group-sender-key-lease branch from d6282a1 to 2d99c4e Compare July 14, 2026 22:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/src/send/encrypt.rs`:
- Around line 29-31: Update the error mapping around
crate::libsignal::protocol::group_encrypt to wrap the original
SignalProtocolError as the anyhow source while adding the “group encrypt failed”
context. Preserve the existing async result flow and ensure callers can still
downcast the underlying protocol error via
downcast_ref::<SignalProtocolError>().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 27f26a5e-e607-4c06-8b8c-eb733a3333a1

📥 Commits

Reviewing files that changed from the base of the PR and between d6282a1 and 2d99c4e.

📒 Files selected for processing (5)
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/src/send.rs
  • wacore/src/send/encrypt.rs
  • wacore/src/send/tests.rs

Comment thread wacore/src/send/encrypt.rs Outdated
@codspeed-hq

codspeed-hq Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by ×2.8

⚡ 1 improved benchmark
✅ 181 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory bench_group_encrypt_message 1,670 B 590 B ×2.8

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing feat/group-sender-key-lease (72b8e4a) with main (a5029ce)

Open in CodSpeed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2d99c4e078

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/send/encrypt.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
wacore/libsignal/src/protocol/sender_keys.rs (2)

674-679: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Only reset the reservation on an actual chain rotation.

We process billions of messages, and performance matters. You're unconditionally resetting reserved_iteration to 0 here. If we process a duplicate SKDM that re-adds the exact same current chain, you're wiping out a live reservation and forcing an entirely unnecessary wire gate.

Update this logic so we only clear the reservation when the incoming sender-key state actually has a different chain_id from our current active chain. We need things to work right and efficiently.

⚡ Proposed fix
-        self.states.push_front(state);
-        // The current chain changed: any reservation belonged to the old current
-        // chain. Reset so the next send re-reserves and gates against the new
-        // one instead of treating stale-covered iterations as durable.
-        self.reserved_iteration = 0;
+        let is_new_current_chain = self.states.front().map_or(true, |s| s.chain_id() != chain_id);
+        self.states.push_front(state);
+        // If the current chain changed, any reservation belonged to the old current
+        // chain. Reset so the next send re-reserves and gates against the new
+        // one instead of treating stale-covered iterations as durable.
+        if is_new_current_chain {
+            self.reserved_iteration = 0;
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/libsignal/src/protocol/sender_keys.rs` around lines 674 - 679, Update
the state replacement logic around self.states.push_front(state) to compare the
incoming state's chain_id with the current active chain's chain_id before
resetting reserved_iteration. Clear the reservation only when the chain IDs
differ; preserve the existing reservation for duplicate SKDMs that re-add the
same current chain.

569-593: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract this duplicated varint-scanning logic to a shared helper.

Look, at Meta we don't just copy-paste critical protocol parsing code. This decode_reserved_iteration logic is structurally identical to the DM side's SessionRecord::decode_reserved_index.

If these two independently drift, we end up with an inconsistent security posture between the DM and group paths. Extract a shared decode_local_only_u32_field(bytes, field_number) helper so a future fix or tightening of the parsing rules automatically protects both paths. We need this codebase to be maintainable and rock-solid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/libsignal/src/protocol/sender_keys.rs` around lines 569 - 593, Extract
the varint-scanning implementation from decode_reserved_iteration into a shared
decode_local_only_u32_field(bytes, field_number) helper, colocated with the
existing SessionRecord::decode_reserved_index logic or another common protocol
utility. Update both decode_reserved_iteration and
SessionRecord::decode_reserved_index to delegate to it while preserving
duplicate-field rejection, Varint wire-type validation, u32 conversion,
unknown-field skipping, and the zero default.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@wacore/libsignal/src/protocol/sender_keys.rs`:
- Around line 674-679: Update the state replacement logic around
self.states.push_front(state) to compare the incoming state's chain_id with the
current active chain's chain_id before resetting reserved_iteration. Clear the
reservation only when the chain IDs differ; preserve the existing reservation
for duplicate SKDMs that re-add the same current chain.
- Around line 569-593: Extract the varint-scanning implementation from
decode_reserved_iteration into a shared decode_local_only_u32_field(bytes,
field_number) helper, colocated with the existing
SessionRecord::decode_reserved_index logic or another common protocol utility.
Update both decode_reserved_iteration and SessionRecord::decode_reserved_index
to delegate to it while preserving duplicate-field rejection, Varint wire-type
validation, u32 conversion, unknown-field skipping, and the zero default.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 56c9c6b6-5c15-4549-bb73-c87054b57380

📥 Commits

Reviewing files that changed from the base of the PR and between 2d99c4e and 5e8e694.

📒 Files selected for processing (1)
  • wacore/libsignal/src/protocol/sender_keys.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e8e69421f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/send/encrypt.rs Outdated
Comment thread wacore/libsignal/src/protocol/sender_keys.rs Outdated
@coderabbitai coderabbitai Bot removed the api-design label Jul 14, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wacore/libsignal/src/protocol/sender_keys.rs (1)

305-310: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Return a Result instead of silently dropping invalid seeds.

Look, we connect billions of people on WhatsApp, and we don't silently swallow crypto key errors. You're doing a silent .ok()? to parse the 32-byte seed here, relying entirely on the caller (deserialize) to have pre-validated it. That's a massive footgun. If another engineer calls this crate-public method and forgets the validation loop upstream, a corrupted 15-byte seed gets treated exactly like a missing seed, silently dropping the sender chain and breaking encryption.

Move fast, but don't break my platform. Let's move the seed validation inside from_protobuf and make it return a Result<Self, SignalProtocolError>. That way the compiler enforces the invariant, and we can remove the redundant eager-validation loops currently sitting in deserialize.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/libsignal/src/protocol/sender_keys.rs` around lines 305 - 310, Update
from_protobuf to return Result<Self, SignalProtocolError> and validate the
sender chain seed there, propagating an explicit error instead of using
try_into().ok()? to silently discard invalid seeds. Adjust callers such as
deserialize to handle the Result, and remove their redundant eager-validation
loops while preserving valid protobuf conversion behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/src/send/encrypt.rs`:
- Around line 23-32: Update the group encryption call in the surrounding
send/encrypt flow to remove the redundant Ok(...?) wrapper and attach the
context message "group encrypt failed" with anyhow::Context. Preserve the
underlying error source so callers can still downcast SignalProtocolError.

---

Outside diff comments:
In `@wacore/libsignal/src/protocol/sender_keys.rs`:
- Around line 305-310: Update from_protobuf to return Result<Self,
SignalProtocolError> and validate the sender chain seed there, propagating an
explicit error instead of using try_into().ok()? to silently discard invalid
seeds. Adjust callers such as deserialize to handle the Result, and remove their
redundant eager-validation loops while preserving valid protobuf conversion
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 199923d9-74ae-4aa2-b78d-88479abfd57e

📥 Commits

Reviewing files that changed from the base of the PR and between 790aac0 and 75203ed.

📒 Files selected for processing (5)
  • wacore/libsignal/src/protocol/local_field.rs
  • wacore/libsignal/src/protocol/mod.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/src/send/encrypt.rs

Comment thread wacore/src/send/encrypt.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
wacore/src/send/encrypt.rs (1)

22-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use .context() to preserve the operational trace.

Look, WhatsApp connects billions of people, and when group encryption fails in production, it's a Sev 1. I need my engineers to have full operational visibility to debug and fix things fast. Wrapping this in Ok(...?) completely swallows the context.

anyhow::Context attaches a contextual message but perfectly preserves the original error source, so your downcast_ref::<SignalProtocolError>() retry logic will still work exactly as intended. We do not compromise on reliability or observability here. Drop the Ok(...?) wrapper, add .context("group encrypt failed"), and let's get this right.

🔧 Proposed refactor
-    Ok(crate::libsignal::protocol::group_encrypt(
+    crate::libsignal::protocol::group_encrypt(
         sender_key_store,
         sender_key_name,
         plaintext,
         csprng,
     )
-    .await?)
+    .await
+    .context("group encrypt failed")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/src/send/encrypt.rs` around lines 22 - 32, Update the group encryption
call in the surrounding send/encrypt flow to remove the redundant Ok wrapper and
attach the operational message “group encrypt failed” with anyhow::Context.
Preserve propagation of the original error so existing SignalProtocolError
downcasting and retry behavior remain intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/libsignal/src/protocol/local_field.rs`:
- Around line 23-43: Update decode_local_only_u32_field to declare the bytes
parameter as mutable and remove the redundant local buf binding; keep the
existing loop and decoding logic using the mutable parameter unchanged.

---

Duplicate comments:
In `@wacore/src/send/encrypt.rs`:
- Around line 22-32: Update the group encryption call in the surrounding
send/encrypt flow to remove the redundant Ok wrapper and attach the operational
message “group encrypt failed” with anyhow::Context. Preserve propagation of the
original error so existing SignalProtocolError downcasting and retry behavior
remain intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eeb7558e-5d48-4926-b0a8-c11215f0f4cb

📥 Commits

Reviewing files that changed from the base of the PR and between 790aac0 and 75203ed.

📒 Files selected for processing (5)
  • wacore/libsignal/src/protocol/local_field.rs
  • wacore/libsignal/src/protocol/mod.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/src/send/encrypt.rs

Comment thread wacore/libsignal/src/protocol/local_field.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wacore/libsignal/src/protocol/group_cipher.rs (1)

499-503: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Provide meaningful expect messages.

Look, I need things to work right, and part of that is knowing why they broke. Why are you putting expect("test") everywhere in these tests? Write proper expect messages so when this fails in CI, our engineers actually know what blew up without digging through the backtrace. Move fast, but leave helpful breadcrumbs for your team.

Please update this and the other instances of expect("test") in this file to something descriptive.

♻️ Proposed refactor for this instance
         let skdm = block_on(create_sender_key_distribution_message(
             &name, &mut bob, &mut rng,
         ))
-        .expect("test");
+        .expect("failed to create distribution message");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wacore/libsignal/src/protocol/group_cipher.rs` around lines 499 - 503,
Replace every expect("test") in the group cipher test code with a descriptive
message that identifies the failed operation and relevant test context,
including the create_sender_key_distribution_message call near skdm. Preserve
the existing behavior and error propagation while ensuring each failure message
helps diagnose the specific operation that failed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/libsignal/src/protocol/group_cipher.rs`:
- Around line 407-408: Replace the temporary zero-filled Vec construction in the
get_buffer() call with a direct resize on the returned buffer, increasing its
length by CryptoBuffer::MAX_RETAINED_CAPACITY * 4 and filling new elements with
zero.

---

Outside diff comments:
In `@wacore/libsignal/src/protocol/group_cipher.rs`:
- Around line 499-503: Replace every expect("test") in the group cipher test
code with a descriptive message that identifies the failed operation and
relevant test context, including the create_sender_key_distribution_message call
near skdm. Preserve the existing behavior and error propagation while ensuring
each failure message helps diagnose the specific operation that failed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 634fbbd0-562c-42b0-85c7-a5252629810b

📥 Commits

Reviewing files that changed from the base of the PR and between ae62355 and 4ab7e5c.

📒 Files selected for processing (2)
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/local_field.rs

Comment thread wacore/libsignal/src/protocol/group_cipher.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9644e24f4b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/send/tests.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c557537b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/libsignal/src/protocol/sender_keys.rs
A warm group send advanced the sender-key chain and stored the record without
gating the advance on durability. Sender-key message keys and IVs derive
deterministically from the chain iteration, so the ciphertext could reach the
wire before the advance was persisted; a crash in that window re-derives the
same iteration on reload, a nonce reuse toward every group member. Confirmed
empirically: 196 of 200 warm group sends took no pre-wire flush (neither a
session reservation nor a sender-key gate).

The production encrypt path (wacore::send::encrypt_group_message) was a second
copy of the sender-key encrypt that never marked the advance wire-gated; the
gate added for groups lived only in the library primitive group_encrypt, which
production does not call. De-duplicate: the production path now delegates to
group_encrypt, so the advance, the wire gate, and the durability logic live in
one place.

Gate cheaply via an iteration lease (mirrors the DM counter lease in
SessionRecord). SenderKeyRecord durably reserves SENDER_CHAIN_RESERVATION_BATCH
iterations at a time; only the send that raises the ceiling flushes
synchronously (1 in 64), the rest ride the coalesced write-behind, and a reload
fast-forwards the current chain past the reserved ceiling so no possibly-spent
iteration is re-derivable. The lease field is local-only (appended after the
generated encoding, field 100), so the vendored proto is untouched and old
readers skip it; the decoder scans it out fail-closed. Reset to 0 on rotation
so the next send re-reserves against the new chain.

Cost: about +2 allocations/msg on the group-send bench (the 1-in-64 flush the
buggy path skipped), versus gating every warm send which would flush on all of
them. Not a throughput change (group send is chain-lock bound).

Tests: crash-mid-lease skips spent iterations and the peer decrypts across the
gap; the gate amortizes to one per batch; rotation resets the lease; legacy
records load byte-identical with reservation 0; a corrupt reservation fails
closed; and the production encrypt_group_message leases the chain (regression
guard for the dead-gate defect). Full workspace suite (excluding e2e) green.
jlucaso1 added 11 commits July 15, 2026 13:30
The sender chain seed is a Bytes in the generated SenderKeyStateStructure, so
every SenderKeyRecord clone and every copy-on-write on an encrypt/decrypt
advance promoted that Bytes to a shared allocation. Mirror the message_keys
trick: keep the current chain key in a Copy field on SenderKeyState, leave the
protobuf field empty in memory, and reassemble it only at as_protobuf.

Measured on the group-send bench (dhat, 50-member group): 214.7 -> 206.4
allocations/msg (-3.8%), more than offsetting the +2/msg the durability gate
adds; net -6.6/msg (-3.1%) vs main. Serialize/deserialize round-trips and the
crash-mid-lease decrypt (which needs the exact reloaded seed) cover the new
representation.
Routing the production group encrypt through the shared primitive brought its
thread-local ENCRYPTION_BUFFER along, whose take_buffer hands the buffer away
and re-allocates a fresh 1 KiB capacity every call. A group skmsg is small, so
that was about +1 KiB of allocated bytes per send (CodSpeed memory:
bench_group_send_10 3.5 -> 4.5 KB, _50 6 -> 7 KB). Copy out a right-sized
ciphertext and keep the buffer for reuse instead: one allocation per send
sized to the message, and the thread-local retains its capacity.

dhat, group-send bench: -1.0 KB/msg (the take_buffer 1 KiB gone), -1 alloc/msg.
…gation

The delegation wrapped the libsignal error with anyhow!("...{e:?}"), which
erases the concrete SignalProtocolError type. The warm-send recovery in
src/send/mod.rs downcasts NoSenderKeyState to clear stale sender-key device
tracking and retry with SKDM redistribution; a string-formatted error made that
downcast miss, so a missing local SenderKeyRecord failed the send instead of
self-healing. Propagate the error unwrapped via ? so the concrete type (and its
downcast) survives.
The DM session record and the group sender-key record both append a u32
reservation as a top-level field the generated protobuf structures skip, then
scan it back out of the raw stream fail-closed. The two scanners were
byte-for-byte twins differing only in the field number and error type, so a
tightened validation rule on one could silently drift from the other, an
inconsistent fail-closed posture for the exact class of bug the reservation
prevents. Extract decode_local_only_u32_field, generic over the error via a
closure so each record keeps its own error type. Also clarify why the
sender-key reservation reset is unconditional (fail-safe, and a no-op on every
production path).
The delegation returned Ok(group_encrypt(...)?), which propagates the error but
carries no hint about which stage failed. Use anyhow's .context("group encrypt
failed") instead: it keeps the concrete SignalProtocolError as the source (so the
warm-send recovery in src/send/mod.rs can still downcast NoSenderKeyState and
retry with SKDM redistribution) while adding a diagnostic layer, and drops the
redundant Ok(...?) wrapper. Add a regression guard asserting the downcast
survives the delegation, which would have caught the earlier anyhow!("{e:?}")
type erasure too.
Take the byte slice by mutable value and advance it directly instead of
copying it into a local cursor binding first. No behavior change.
The reusable ENCRYPTION_BUFFER copies out a right-sized ciphertext and keeps the
buffer, so a small-message workload no longer reallocates 1 KiB per send. But it
also retained whatever capacity a one-off large message grew it to, pinning that
allocation on the thread-local for the rest of the process. Release capacity past
MAX_RETAINED_CAPACITY after copying the result out: small messages (the common
case) still reuse the buffer with no reallocation, while a large message's excess
is dropped instead of retained.
Every expect in the lease tests said "test", so a failure named neither the
operation nor where in the flow it broke. Name the operation instead (whose SKDM,
which iteration, which side of the reload), and identify the failing send by
index in the amortization loop. Also fill the oversized buffer with a direct
resize rather than materializing a throwaway zeroed Vec to copy from.
Per AGENTS.md, comments explain why and stay brief. These recounted the review
thread and the prior implementation instead of the durability invariant each
test protects.
@jlucaso1
jlucaso1 force-pushed the feat/group-sender-key-lease branch from 6c55753 to 1a1a7be Compare July 15, 2026 16:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/src/store/signal_cache.rs`:
- Around line 990-1012: Update clear_after_flush to preserve concurrent dirty
records and pending gates: when sessions or sender_keys are not fully clean,
rotate the recovery incarnation without calling discard, so reservation_pending
and wire_gate_pending remain resident. Continue clearing only fully clean
stores, while retaining the existing recovery-incarnation behavior for dirty
state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2b08daa7-a7b6-4fa6-80e9-56998b799d11

📥 Commits

Reviewing files that changed from the base of the PR and between 7489036 and 1a1a7be.

📒 Files selected for processing (12)
  • src/client/tests.rs
  • src/message/commit_batch.rs
  • src/store/signal.rs
  • wacore/libsignal/src/protocol/group_cipher.rs
  • wacore/libsignal/src/protocol/local_field.rs
  • wacore/libsignal/src/protocol/mod.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/src/send.rs
  • wacore/src/send/encrypt.rs
  • wacore/src/send/tests.rs
  • wacore/src/store/signal_cache.rs

Comment thread wacore/src/store/signal_cache.rs Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant