Skip to content

feat(core): expose signal record components and dirty events - #1062

Merged
jlucaso1 merged 10 commits into
mainfrom
agent/signal-record-components-and-dirty-events
Jul 21, 2026
Merged

feat(core): expose signal record components and dirty events#1062
jlucaso1 merged 10 commits into
mainfrom
agent/signal-record-components-and-dirty-events

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add owned, validated projections for pairwise-session and sender-key records without exposing codec-generated structures
  • import and export components through the canonical protocol state, including compact seed derivation and canonical public-key encoding
  • enforce sender/receiver chain roles at handoff: sender chains are structurally complete, component-provided receiver chains cannot carry private material, and persisted receiver-only private fields are safely discarded
  • preserve counter safety across handoff by advancing reserved sender ranges, retiring a leased chain before fresh-state archival, and dropping only sender chains that are too stale to advance within the bounded-work limit
  • validate that a present sender chain is operationally usable instead of treating mere protobuf presence as sufficient
  • bound retained sender-key history across component and binary record loading
  • redact cryptographic material from component debug output and cover projected fields with round-trip validation
  • surface valid dirty-state markers as typed events while retaining existing cleanup and resynchronization behavior
  • retain backend error chains when adding signal-cache flush context

Why

Record interchange previously required callers to depend on generated structures or reproduce protocol-specific normalization and key derivation. The projections keep those details inside the Signal implementation, validate fixed-width material at the boundary, and make generated schema changes fail compilation at explicit mappings instead of silently dropping state.

Sender and receiver chains share one persisted protobuf shape but have different semantic roles. A present sender chain requires its ratchet public/private keys and complete chain key. Receiver chains never own the remote private key: strict component construction rejects any such field, while persisted-record projection tolerates and discards it in line with the canonical reader. No private receiver material is copied, validated as a key, or exposed.

Reservation metadata is local to a live record. Export consumes the record and advances its current sender range to the exclusive ceiling. Current and archived states follow the same bounded policy used during promotion: chains that can be advanced are burned, while chains too stale to advance safely are removed without blocking export of the otherwise valid record. Fresh-state promotion retires the outgoing chain to its prior ceiling before resetting the record-level lease, so the fresh chain starts at zero without leaving a reusable archived range.

Sender-key mutation already retains a fixed number of recent states. Component import and binary loading now preserve that invariant as well, preventing obsolete state from making retained memory, lookup, and serialization work grow without bound.

Dirty markers already drive internal cleanup and selected resynchronization paths. A typed event lets observers refresh domain-specific derived state without parsing raw stanzas or changing built-in behavior.

API and compatibility notes

  • The component types and record conversion methods are additive.
  • Raw and canonically serialized public keys are accepted; exported public keys use canonical serialization.
  • Session message-key seeds are accepted as a compact import form and expanded through the existing derivation; exports contain derived material.
  • A present sender component must contain a valid public key, a 32-byte private key, and a complete 32-byte chain key with its index.
  • Receiver component imports require private material to be absent. Persisted receiver private fields of any length are accepted and projected as None, matching the canonical reader's role-based behavior.
  • has_usable_sender_chain returns Ok(false) for absence, Ok(true) for a complete chain, and a typed error for a malformed present chain.
  • Archived pairwise sessions and sender-key states remain bounded by their established record limits.
  • DirtyState and EventKind::DirtyState are additive non-exhaustive event API additions.

Validation

  • cargo fmt --all -- --check
  • cargo test -p wacore-libsignal
  • cargo test --workspace --exclude e2e-tests
  • cargo clippy --all --tests -- -D warnings
  • focused regression coverage for role-aware component conversion, canonical session round-trips, malformed sender chains, stale current and archived handoff, fresh-promotion lease retirement, and cache-flush error causes
  • the complete integration and performance matrix runs in this PR's Actions

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds validated Signal session and sender-key component conversions, Curve25519 key-length constants, typed dirty-state events from IB handling, preserved cache-flush error chains, and bounded record handoff behavior.

Changes

Signal record component conversions

Layer / File(s) Summary
Component contracts and validation
wacore/libsignal/src/core/curve.rs, wacore/libsignal/src/protocol/..., clippy.toml
Component projections, key-length constants, validation helpers, redacted debugging, protocol exports, and lint guidance are updated.
Session and sender-key codec conversions
wacore/libsignal/src/protocol/record_components.rs
Session and sender-key components convert to and from protobuf records with key-size validation, normalization, required-field checks, and codec tests.
Record import, export, and reservation handoff
wacore/libsignal/src/protocol/state/session.rs, wacore/libsignal/src/protocol/sender_keys.rs
Records gain component conversion APIs, bounded history, reservation fast-forwarding, metadata reset, ownership optimizations, and handoff validation.

Dirty-state events

Layer / File(s) Summary
Dirty-state event contract
wacore/src/types/events.rs
DirtyState is added with a dirty type, optional timestamp, kind mapping, serialization behavior, and tests.
IB dirty marker dispatch
src/handlers/ib.rs
The IB handler dispatches typed dirty-state events containing parsed markers and timestamps.

Supporting error handling

Layer / File(s) Summary
Preserved cache-flush errors
src/client/adapters.rs
Cache-flush failures retain their original backend error while adding context.

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

Sequence Diagram(s)

sequenceDiagram
  participant SessionRecord
  participant SenderKeyRecord
  participant RecordComponents
  participant SignalProtobuf
  SessionRecord->>RecordComponents: export validated session components
  SenderKeyRecord->>RecordComponents: export validated sender-key components
  RecordComponents->>SignalProtobuf: convert components to protocol structures
Loading

Possibly related PRs

Suggested labels: api-design

Suggested reviewers: greptile-apps

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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 clearly captures the main change: exposing signal record components and adding dirty events.
Description check ✅ Passed The description is directly aligned with the code changes and explains the new components, event, and supporting behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/signal-record-components-and-dirty-events

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.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR exposes validated Signal record components and typed dirty-state events. The main changes are:

  • Adds owned projections for session and sender-key records.
  • Validates key material and chain roles during record handoff.
  • Preserves reserved counters and bounds retained sender-key history.
  • Dispatches typed events for valid dirty-state markers.
  • Preserves backend error chains when signal-cache flushing fails.

Confidence Score: 5/5

This looks safe to merge.

  • Sender-key component import and binary loading now enforce the established history limit.
  • Truncation preserves the current and newest states because record order is newest first.
  • No blocking issue remains in the updated history-loading paths.

Important Files Changed

Filename Overview
wacore/libsignal/src/protocol/sender_keys.rs Bounds sender-key history during component import and binary loading while preserving newest-first record order.
wacore/libsignal/src/protocol/record_components.rs Adds validated component projections and canonical conversion helpers for session and sender-key records.

Reviews (6): Last reviewed commit: "fix(signal): preserve records on stale h..." | Re-trigger Greptile

Comment thread wacore/libsignal/src/protocol/sender_keys.rs

@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 `@src/handlers/ib.rs`:
- Around line 71-77: Ensure the DirtyState dispatch in the IB watch event
handler uses the non-blocking concurrent event delivery path, so downstream
handlers cannot stall the IQ processing loop; preserve dispatching this event
before sending the clean IQ.
🪄 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: eaf72c97-ff1e-4d3b-91c7-4a1844c9833f

📥 Commits

Reviewing files that changed from the base of the PR and between cd29cef and de502c2.

📒 Files selected for processing (9)
  • clippy.toml
  • src/handlers/ib.rs
  • wacore/libsignal/src/core/curve.rs
  • wacore/libsignal/src/protocol/mod.rs
  • wacore/libsignal/src/protocol/protocol.rs
  • wacore/libsignal/src/protocol/record_components.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/src/types/events.rs

Comment thread src/handlers/ib.rs
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 9.62 MiB 9.62 MiB +3.84 KiB (+0.04%) 🔺
bin .text 7.69 MiB 7.69 MiB +2.94 KiB (+0.04%) 🔺
bin allocated (text+data+bss) 9.61 MiB 9.62 MiB +4.62 KiB (+0.05%) 🔺
llvm-lines wacore 478,624 478,645 +21 (+0.00%) 🔺
llvm-lines wacore copies 15,824 15,826 +2 (+0.01%) 🔺
llvm-lines whatsapp-rust lib 674,459 674,878 +419 (+0.06%) 🔺
llvm-lines whatsapp-rust lib copies 21,677 21,719 +42 (+0.19%) 🔺
deps crates (Cargo.lock) 470 470 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.69 MiB 1.70 MiB +5.84 KiB (+0.34%) 🔺
.text wacore 596.35 KiB 596.04 KiB -312 B (-0.05%) 🔽
.text wacore_binary 87.04 KiB 87.04 KiB 0
.text wacore_libsignal 160.82 KiB 160.96 KiB +146 B (+0.09%) 🔺
.text wacore_appstate 22.36 KiB 22.36 KiB 0
.text wacore_noise 22.54 KiB 22.98 KiB +444 B (+1.92%) ⚠️
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 510.12 KiB 510.12 KiB 0
.text whatsapp_rust_tokio_transport 39.84 KiB 39.84 KiB 0
.text whatsapp_rust_ureq_http_client 10.28 KiB 10.28 KiB 0
.text std 949.54 KiB 949.82 KiB +288 B (+0.03%) 🔺
.text other deps 1.88 MiB 1.88 MiB -3.60 KiB (-0.19%) 🔽
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust 1.69 MiB 1.70 MiB +5.84 KiB (+0.34%)
metrics_exporter_prometheus 3.96 KiB (removed) -3.96 KiB (-100.00%)

Baseline: cd29cef43 (latest main run) · Head: 8bffb1342 · Graphs

@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: de502c25e7

ℹ️ 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/record_components.rs Outdated
Comment thread wacore/libsignal/src/protocol/state/session.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: 115c8f2102

ℹ️ 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/state/session.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: c213e3f782

ℹ️ 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/state/session.rs

@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 (2)
src/client/adapters.rs (1)

107-111: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Chain .context() directly on the Result.

Look, we need to execute at a high level, and that means writing lean, idiomatic Rust. Wrapping .context() inside a .map_err() closure is completely unnecessary boilerplate here. The anyhow crate was specifically designed so you can chain .context() directly onto the Result. Let's drop the closure, keep the codebase clean, and move fast. (Just make sure anyhow::Context is in scope at the top of the file). Ship it.

🚀 Proposed refactor
-        self.signal_cache
-            .flush(&*backend)
-            .await
-            .map_err(|error| error.context("Failed to flush signal cache"))
+        self.signal_cache
+            .flush(&*backend)
+            .await
+            .context("Failed to flush signal cache")
🤖 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 `@src/client/adapters.rs` around lines 107 - 111, In the signal-cache flush
flow, replace the unnecessary map_err closure after
signal_cache.flush(&*backend).await with direct anyhow context chaining,
ensuring anyhow::Context is imported and the existing error message and Result
behavior remain unchanged.
wacore/libsignal/src/protocol/state/session.rs (1)

727-787: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

into_components can lose the whole record on one corrupted-current-chain edge case — let's not let a rare bug eat the archived states too.

Archived sessions get the forgiving treatment (fast_forward_sender_chain_or_drop — log and clear), but the current session's fast-forward at Line 763 propagates the error straight out via ?. Since into_components(mut self) consumes the record by value, a hit on that error path (implausible reservation distance on the current chain) destroys self — including all the perfectly fine archived sessions — with nothing recoverable for the caller. Given the whole point of this handoff path is safe export/migration, an export that can atomically vaporize valid data on a corrupted edge case is worth tightening up, even if reserve_sender_chain_counters's batching makes this unreachable in the happy path today.

🔧 Suggested fix: make the current-session path symmetric with the archived-session path
-        if reserved_sender_chain_index > 0
-            && let Some(state) = self.current_session.as_mut()
-        {
-            state.fast_forward_sender_chain(reserved_sender_chain_index)?;
-        }
+        if reserved_sender_chain_index > 0
+            && let Some(state) = self.current_session.as_mut()
+        {
+            state.fast_forward_sender_chain_or_drop(reserved_sender_chain_index);
+        }
🤖 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/state/session.rs` around lines 727 - 787,
Update SessionRecord::into_components so current_session fast-forward uses the
same forgiving fast_forward_sender_chain_or_drop behavior as archived sessions
instead of propagating an error with ?. Preserve export of the current session
and archived sessions, clearing/logging the current chain when fast-forwarding
fails rather than returning early and consuming the record unrecoverably.
🤖 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 `@src/client/adapters.rs`:
- Line 226: Update the test JID construction in the `peer` initialization to use
the reserved fictitious phone number `15550101111` instead of `15550001111`,
while preserving the existing `Server::Pn` and device configuration.

---

Outside diff comments:
In `@src/client/adapters.rs`:
- Around line 107-111: In the signal-cache flush flow, replace the unnecessary
map_err closure after signal_cache.flush(&*backend).await with direct anyhow
context chaining, ensuring anyhow::Context is imported and the existing error
message and Result behavior remain unchanged.

In `@wacore/libsignal/src/protocol/state/session.rs`:
- Around line 727-787: Update SessionRecord::into_components so current_session
fast-forward uses the same forgiving fast_forward_sender_chain_or_drop behavior
as archived sessions instead of propagating an error with ?. Preserve export of
the current session and archived sessions, clearing/logging the current chain
when fast-forwarding fails rather than returning early and consuming the record
unrecoverably.
🪄 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: 83bc6f74-a0d9-401a-a37e-2af549454ea0

📥 Commits

Reviewing files that changed from the base of the PR and between 115c8f2 and c213e3f.

📒 Files selected for processing (3)
  • src/client/adapters.rs
  • wacore/libsignal/src/protocol/record_components.rs
  • wacore/libsignal/src/protocol/state/session.rs

Comment thread src/client/adapters.rs Outdated
@jlucaso1
jlucaso1 merged commit 87fbcb5 into main Jul 21, 2026
21 checks passed
@jlucaso1
jlucaso1 deleted the agent/signal-record-components-and-dirty-events branch July 21, 2026 14:10
jlucaso1 added a commit that referenced this pull request Jul 21, 2026
Integrate Signal record components and DirtyState from #1062. Retain the new event test subscription and gate DirtyState materialization through aggregate event interest.
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