Skip to content

feat: lid pn mapping and edge route support - #181

Merged
jlucaso1 merged 8 commits into
mainfrom
feat-lid-pn-mapping
Dec 13, 2025
Merged

feat: lid pn mapping and edge route support#181
jlucaso1 merged 8 commits into
mainfrom
feat-lid-pn-mapping

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Dec 12, 2025

Copy link
Copy Markdown
Collaborator

Carefully has some breaking changes

Summary by CodeRabbit

  • New Features

    • LID↔phone bidirectional cache with async lookups and LID-aware session reuse for encryption.
    • Per-chat mailbox for ordered message processing.
    • Edge-routing pre-intro on connect and optional TLS configuration for transports.
    • Background sender ensuring ordered frame sends.
  • Bug Fixes

    • Improved offline-sync completion signaling, reduced races, and more robust decryption/identity retry handling.
    • Backpressure-aware enqueueing and safer ACK/receipt flows.
  • Chores

    • Database migration to persist LID↔phone mappings and expanded test coverage.

✏️ Tip: You can customize this high-level summary in your review settings.

@jlucaso1
jlucaso1 requested a review from Copilot December 12, 2025 03:26
@coderabbitai

coderabbitai Bot commented Dec 12, 2025

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a new LID↔phone-number in-memory cache with persistence bindings and DB migrations, migrates stanza handling from borrowed NodeRef to Arc with per-chat enqueue workers, introduces edge-routing pre-intro in handshake and a task-based Noise send pipeline, and wires LID-aware session resolution across send/receive flows.

Changes

Cohort / File(s) Summary
LID-PN cache & public API
src/lid_pn_cache.rs, src/lib.rs, wacore/src/store/traits.rs
New LidPnCache, LidPnEntry, LearningSource, SharedLidPnCache; adds LidPnMappingStore trait and LidPnMappingEntry; crate root exposes lid_pn_cache.
Client integration & SendContext
src/client.rs, src/client/context_impl.rs, wacore/src/client/context.rs, wacore/src/send.rs, wacore/src/types/jid.rs
Client gains lid_pn_cache and related caches/locks; adds async get_lid_for_phone to SendContextResolver; send/prepare flows resolve PN↔LID sessions and adjust encryption_jid/addressing semantics.
Handler API migration (Node ownership)
src/handlers/traits.rs, src/handlers/router.rs, src/handlers/*.rs
(src/handlers/basic.rs, src/handlers/ib.rs, src/handlers/iq.rs, src/handlers/message.rs, src/handlers/notification.rs, src/handlers/receipt.rs, src/handlers/unimplemented.rs)
StanzaHandler::handle signatures changed from &NodeRef<'_> to Arc<Node>; router and all handlers updated to accept/forward Arc<Node> and use the new Node API.
Per-chat mailbox & message handling
src/handlers/message.rs, src/client.rs (related fields)
Per-chat mpsc queues/workers added; messages enqueued as Arc<Node> and processed sequentially; enqueue-lock cache added to preserve arrival order and provide backpressure.
Storage: schema, store, migrations
storages/sqlite-storage/migrations/*, storages/sqlite-storage/src/schema.rs, storages/sqlite-storage/src/sqlite_store.rs, storages/sqlite-storage/Cargo.toml
Adds lid_pn_mapping table and edge_routing_info column; Diesel schema updated; SqliteStore extended with LidPnMapping CRUD (per-device helpers + trait impl), edge_routing_info in DeviceRow, pool/pragmas adjusted, and migrations added.
Test scaffolding: MockBackend impl
src/appstate_sync.rs
Adds test-only impl wacore::store::traits::LidPnMappingStore for MockBackend returning benign defaults to satisfy tests.
Handshake, noise socket & transport TLS
src/handshake.rs, src/socket/noise_socket.rs, transports/tokio-transport/src/lib.rs, transports/tokio-transport/Cargo.toml, Cargo.toml
Edge-routing pre-intro builder added and optionally prepended to ClientHello; NoiseSocket send path rewritten to task-based SendJob queue (ordered sender task + oneshot); TLS connector factory added and wired, plus danger-skip-tls-verify feature.
Message/session handling & retries
src/message.rs, src/retry.rs, src/send.rs, src/receipt.rs, src/pair.rs, src/pdo.rs, src/usync.rs
LID-PN mapping extraction/population in usync/pair/message flows; per-sender/session locking added; message ownership changed from Arc<wa::Message> to &wa::Message across send/retry paths; persistence calls for LID mappings added.
Signal/libsignal adjustments
wacore/libsignal/src/protocol/session_cipher.rs, wacore/libsignal/src/protocol/state/session.rs
Introduces DecryptionResult (plaintext + used_previous_session) and refactors receiver-chain helpers to index-based in-place mutations to reduce cloning.
AppState & pairing tweaks
wacore/appstate/src/processor.rs, wacore/src/pair.rs
validate_patch_macs gains had_no_prior_state param and skips MAC checks when no prior state; pairing HMAC mismatch check commented out (no error on mismatch).
Owned↔borrowed node helpers & JID changes
wacore/binary/src/node.rs, wacore/binary/src/jid.rs, wacore/tests/jid_test.rs
Node::as_node_ref and NodeContent::as_content_ref helpers added; HOSTED_LID_SERVER and JidExt::is_hosted added; JID display/formatting adjusted and tests updated.
Signal adapter & session cache cleanup
src/store/signal_adapter.rs, storages/sqlite-storage/src/sqlite_store.rs
Removed in-memory session_cache from SignalProtocolStoreAdapter; SqliteStore adds batch address-with-sessions helper and propagates edge_routing_info in device rows.
Misc build/dep updates
wacore/binary/Cargo.toml, transports/tokio-transport/Cargo.toml, storages/sqlite-storage/Cargo.toml
Makes serde a required build-dep in wacore/binary; adds rustls/webpki deps and features for tokio transport; enables 32-column-tables diesel feature and adds log dep for sqlite storage.

Sequence Diagram(s)

mermaid
sequenceDiagram
autonumber
participant Client
participant LidPnCache
participant SqliteStore
participant DB
Note over Client,LidPnCache: Add/persist new LID↔PN mapping
Client->>LidPnCache: add(entry) (async)
LidPnCache->>LidPnCache: RwLock write, update lid->entry and pn->entry (timestamp logic)
LidPnCache-->>Client: Ok
Client->>SqliteStore: put_lid_pn_mapping(entry) (async)
SqliteStore->>DB: INSERT/UPSERT lid_pn_mapping row
DB-->>SqliteStore: OK
SqliteStore-->>Client: Ok

mermaid
sequenceDiagram
autonumber
participant Transport
participant Router
participant MessageHandler
participant PerChatWorker
Note over Transport,Router: Incoming frame decrypted -> Arc
Transport->>Router: dispatch(Arc)
Router->>MessageHandler: handle(Arc)
MessageHandler->>PerChatWorker: enqueue(Arc) (per-chat queue via mpsc)
PerChatWorker->>MessageHandler: handle_encrypted_message(Arc) (sequential processing)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus areas:
    • Consistency and correctness of NodeRef → Arc migration across router, handlers, spawned tasks, and tests (Arc::try_unwrap vs clone semantics).
    • LidPnCache concurrency and timestamp conflict resolution; correctness of SqliteStore LidPnMapping queries, indexes, and migrations.
    • LID↔PN session resolution in encrypt/decrypt paths, group addressing conversion, and hosted-device filtering logic.
    • Handshake edge-routing pre-intro integration and NoiseSocket sender-task correctness (ordering, error handling, Drop behavior).
    • Security-sensitive change in pairing where HMAC mismatch check was commented out (wacore/src/pair.rs).

Possibly related PRs

Poem

🐰 I hopped through nodes and cached each link,

LIDs and phones in tidy bins I sync,
Queues kept order, handshakes learned to pre-intro,
I threaded mappings where the sessions go,
A tiny rabbit, storing every blink.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: introducing LID-to-phone-number mapping support and edge routing information handling across the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-lid-pn-mapping

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 and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR implements LID-to-Phone Number mapping and edge routing support to improve WhatsApp protocol compatibility and session management. The changes address critical issues with Signal protocol session handling when users have both phone number and LID identities.

Key Changes:

  • Fixed Signal protocol address format to match WhatsApp Web's behavior (device encoded in name, device_id always 0)
  • Implemented LID-PN bidirectional mapping cache with persistent storage
  • Added edge routing support for optimized server reconnection
  • Refactored message processing to use Arc-wrapped nodes instead of cloning
  • Improved connection lifecycle management with generation tracking

Reviewed changes

Copilot reviewed 46 out of 47 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
wacore/src/types/jid.rs Updated protocol address generation to match WhatsApp Web's format with device in name
wacore/src/usync.rs Added LID mapping extraction from usync responses
wacore/src/send.rs Implemented LID session lookup to reuse existing sessions instead of creating duplicates
src/lid_pn_cache.rs New bidirectional cache for LID-PN mappings with timestamp-based conflict resolution
src/client.rs Refactored node processing to use Arc, added LID cache warmup and connection generation tracking
storages/sqlite-storage/src/sqlite_store.rs Added LID-PN mapping table, session/identity caching, and foreign key constraints
wacore/libsignal/src/protocol/state/session.rs Optimized receiver chain lookups to avoid unnecessary cloning
wacore/libsignal/src/protocol/session_cipher.rs Fixed identity trust handling for previous sessions to prevent UntrustedIdentity errors
src/message.rs Updated message decryption to use LID addresses for session lookup when mapping is known
src/handlers/message.rs Refactored to use per-chat worker queues with strict ordering guarantees
wacore/src/pair.rs Disabled HMAC verification temporarily (marked with comment indicating incomplete implementation)
wacore/src/store/device.rs Added edge_routing_info field for optimized reconnection
transports/tokio-transport/src/lib.rs Added TLS certificate verification skip feature and custom connector support
Comments suppressed due to low confidence (4)

wacore/src/pair.rs:1

  • HMAC verification is disabled in pairing flow. This is a critical security issue - the commented code shows HMAC validation should verify the device identity signature. The variable is renamed to _hmac_bytes (unused) and the verification is commented out, which could allow forged pairing requests. This should be re-enabled or documented with a clear explanation of why it's safe to skip.
    wacore/src/pair.rs:1
  • HMAC verification is disabled in pairing flow. This is a critical security issue - the commented code shows HMAC validation should verify the device identity signature. The variable is renamed to _hmac_bytes (unused) and the verification is commented out, which could allow forged pairing requests. This should be re-enabled or documented with a clear explanation of why it's safe to skip.
    storages/sqlite-storage/src/sqlite_store.rs:1
  • The comment says 'Limit concurrent connections' but the pool_size was increased from 4 to 32. The comment is misleading - it should mention that the pool was increased to support higher concurrency, not reduced to limit it.
    wacore/binary/Cargo.toml:1
  • The optional = true flag was removed from the build-dependencies serde entry. This means serde is now always included in build dependencies even when not needed. The removal should be intentional - if the serde feature is always required for build scripts, this is correct, but it should be verified.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/socket/noise_socket.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: 10

Caution

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

⚠️ Outside diff range comments (10)
wacore/libsignal/src/protocol/state/session.rs (2)

425-442: Avoid panics: replace expect(...) with an error return.
expect("called set_message_keys for a non-existent chain") can crash the process if state is corrupted or the API is misused. Since this already returns Result<_, InvalidSessionError>, prefer a typed error.

-        let chain_idx = self
-            .get_receiver_chain_index(sender)?
-            .expect("called set_message_keys for a non-existent chain");
+        let chain_idx = self
+            .get_receiver_chain_index(sender)?
+            .ok_or(InvalidSessionError("called set_message_keys for a non-existent chain"))?;

444-460: Same here: avoid panics in set_receiver_chain_key.
This should fail gracefully (and consistently with other session-structure validation) rather than aborting.

-        let chain_idx = self
-            .get_receiver_chain_index(sender)?
-            .expect("called set_receiver_chain_key for a non-existent chain");
+        let chain_idx = self
+            .get_receiver_chain_index(sender)?
+            .ok_or(InvalidSessionError("called set_receiver_chain_key for a non-existent chain"))?;
wacore/src/pair.rs (2)

125-154: Reinstate HMAC verification (pairing auth is currently bypassed).
The mac.verify_slice(...) check is commented out, so a malicious device_identity_bytes can pass without the outer HMAC integrity check.

Suggested fix (also avoids the “unused” issue by using hmac_bytes again):

-        let _hmac_bytes = hmac_container
+        let hmac_bytes = hmac_container
             .hmac
             .as_deref()
             .ok_or_else(|| PairCryptoError {
                 code: 500,
                 text: "internal-error",
                 source: anyhow::anyhow!("HMAC container missing hmac"),
             })?;
@@
         if is_hosted_account {
             mac.update(ADV_HOSTED_PREFIX_ACCOUNT_SIGNATURE);
         }
         mac.update(details_bytes);
-        // if mac.verify_slice(hmac_bytes).is_err() {
-        //     return Err(PairCryptoError {
-        //         code: 401,
-        //         text: "hmac-mismatch",
-        //         source: anyhow::anyhow!("HMAC mismatch"),
-        //     });
-        // }
+        if mac.verify_slice(hmac_bytes).is_err() {
+            return Err(PairCryptoError {
+                code: 401,
+                text: "hmac-mismatch",
+                source: anyhow::anyhow!("HMAC mismatch"),
+            });
+        }

201-218: Fix RNG construction in calculate_signature call — current code is syntactically invalid.

Line 212: &mut rand::rngs::OsRng::unwrap_err(rand_core::OsRng) will not compile. The unwrap_err() method is for Result types, but OsRng is not a Result.

Correct approach for rand/rand_core 0.9:

+        let mut rng = rand_core::OsRng;
         let device_signature = device_state
             .identity_key
             .private_key
             .calculate_signature(
                 &msg_to_sign,
-                &mut rand::rngs::OsRng::unwrap_err(rand_core::OsRng),
+                &mut rng,
             )
wacore/binary/src/jid.rs (1)

224-259: Bug risk: hosted.lid isn’t included in agent-suppression (and likely should be in is_ad()).

If agent > 0 ever appears on @hosted.lid, Display currently prints user.<agent>:<device>@hosted.lid, which seems contrary to the intent of the parity fixes for @hosted / @lid.

Proposed patch:

 pub trait JidExt {
@@
     fn is_ad(&self) -> bool {
         self.device() > 0
             && (self.server() == DEFAULT_USER_SERVER
                 || self.server() == HIDDEN_USER_SERVER
-                || self.server() == HOSTED_SERVER)
+                || self.server() == HOSTED_SERVER
+                || self.server() == HOSTED_LID_SERVER)
     }
@@
 impl fmt::Display for Jid {
@@
                 let server_str = self.server(); // Use trait method
                 if server_str != DEFAULT_USER_SERVER
                     && server_str != HIDDEN_USER_SERVER
                     && server_str != HOSTED_SERVER
+                    && server_str != HOSTED_LID_SERVER
                 {
                     write!(f, ".{}", self.agent)?;
                 }
@@
 impl<'a> fmt::Display for JidRef<'a> {
@@
                 let server_str = self.server(); // Use trait method
                 if server_str != DEFAULT_USER_SERVER
                     && server_str != HIDDEN_USER_SERVER
                     && server_str != HOSTED_SERVER
+                    && server_str != HOSTED_LID_SERVER
                 {
                     write!(f, ".{}", self.agent)?;
                 }

Also applies to: 488-519, 522-554

src/receipt.rs (1)

10-66: Good Arc<Node> migration for spawn safety; consider downgrading receipt metadata logs to debug to avoid leaking phone numbers.

If logs are enabled in user environments, from commonly embeds phone numbers. Suggest:

-use log::info;
+use log::{debug, info};
@@
-        info!("Received receipt type '{receipt_type:?}' for message {id} from {from}");
+        debug!("Received receipt type '{receipt_type:?}' for message {id} from {from}");
src/handlers/notification.rs (1)

36-86: Potential perf + behavior issues: deep Node clones for events, and "server_sync scheduling" not implemented.

  • Event::Notification requires Event to be Clone (used by BotEventHandler in bot.rs), which means node.clone() performs a deep copy at dispatch time. The codebase has SharedData<T> wrapper for exactly this pattern; consider using SharedData<Node> in Event::Notification, or changing to Arc<Node> in the Event enum definition.
  • In server_sync (lines 50–62), the log says "scheduling app state sync(s)" but only iterates and logs collection metadata. The actual sync should enqueue MajorSyncTask::AppStateSync for each collection (similar to how process_app_state_sync_task is invoked from bot.rs), or rename the log to "discovered" to avoid misleading operators.
src/retry.rs (1)

170-179: Likely broken RNG usage (rand::rngs::OsRng.unwrap_err()), please fix to a real CSPRNG instance.

This looks like it won’t compile / isn’t the intended rand API. Suggest:

-        let new_prekey_keypair = KeyPair::generate(&mut rand::rngs::OsRng.unwrap_err());
+        let mut rng = rand::rngs::OsRng;
+        let new_prekey_keypair = KeyPair::generate(&mut rng);

(Adjust if KeyPair::generate expects a different trait bound for your wacore::libsignal version.)

src/message.rs (2)

393-393: Critical bug: unwrap_err() on RNG will panic on success.

OsRng.unwrap_err() calls Result::unwrap_err() which panics when the Result is Ok. Since OsRng typically succeeds, this will panic at runtime. The TryRngCore trait's try_fill_bytes returns Result<(), TryError>, and the pattern used elsewhere suggests this should be OsRng directly or use the appropriate RNG interface.

Looking at line 350, rand::rngs::OsRng is created without unwrap_err(), which is correct. The unwrap_err() calls on lines 393 and 469 appear to be copy-paste errors.

-            &mut rng.unwrap_err(),
+            &mut rand::rngs::OsRng,

469-469: Same critical bug: unwrap_err() will panic.

This is the same issue as line 393 - calling unwrap_err() on OsRng will panic when the RNG succeeds.

-                            &mut rng.unwrap_err(),
+                            &mut rand::rngs::OsRng,
🧹 Nitpick comments (15)
src/socket/noise_socket.rs (2)

12-12: Validate the throughput/latency tradeoff: mutex is held across encryption + transport.send().await.
Holding send_mutex across spawn_blocking completion and the awaited network send guarantees strict on-the-wire ordering, but it also forces a single-flight send pipeline (potential backpressure if the transport is slow). That may be exactly what you want, but it’s worth verifying this doesn’t regress high-throughput scenarios. Also, increasing INLINE_ENCRYPT_THRESHOLD to 128 KiB increases crypto work on the async executor; please validate/benchmark that this doesn’t starve other tasks (per guideline: heavy CPU → spawn_blocking).

If you observe regressions, consider a dedicated ordered send queue (single sender task) so callers can enqueue frames quickly while preserving order (instead of holding a mutex across network I/O).

Also applies to: 57-60, 96-108, 132-134


5-5: The send_mutex correctly serializes sends, but encrypt_into is a public footgun.

send_mutex properly ensures all frames sent via encrypt_and_send use write_counter in order. However, encrypt_into (line 41) is a public method that increments write_counter without the lock—meaning if external code calls it, encrypts data, then sends those frames separately (or concurrently with encrypt_and_send), the counters will be out of order.

Currently, encrypt_into is unused in the codebase, so the actual sending remains correctly serialized. But since it's part of the public API, consider either:

  • Making it private if not intended for external use, or
  • Documenting that it must not be used for frames intended to be sent on this socket, only for data encryption where counter ordering doesn't matter.
wacore/libsignal/src/protocol/session_cipher.rs (1)

683-686: Debug logging includes sensitive key material.

The debug logs output cryptographic key material (mac_key, root_key, ephemeral keys) in hexadecimal format. While this is valuable for debugging ratchet issues, it poses a risk if debug logging is accidentally enabled in production environments.

Ensure that:

  • Debug logging is strictly disabled in production builds
  • If debug logs are captured, they are stored securely and access is restricted

This is generally acceptable practice for cryptographic debugging, but worth noting for security awareness.

Also applies to: 755-768

transports/tokio-transport/src/lib.rs (1)

188-222: Revisit channel capacity bump to 10,000 (memory/backpressure).
If the consumer stalls, this can accumulate a lot of buffered TransportEvent::DataReceived(Bytes) in memory. Consider making it configurable, or documenting why 10,000 is required vs applying backpressure sooner.

src/store/signal_adapter.rs (1)

56-92: Avoid holding tokio::RwLock guards across backend .await (contention risk).
Both load_session and store_session keep the device.read().await guard while awaiting backend I/O. If possible, clone/extract the backend handle while under the lock, drop the guard, then await the backend call. This reduces lock coupling and improves concurrency.

wacore/src/client/context.rs (2)

7-19: Avoid hard-coded JID servers; use constants/constructors to future-proof.
"lid" / "s.whatsapp.net" are duplicated string literals; prefer using wacore_binary::jid constants (e.g., default/hidden server constants) and/or a Jid::new(...)-style constructor so future Jid field additions don’t silently get dropped in manual struct literals.

Also applies to: 86-101


120-129: Minor: clean up unused-param handling in default get_lid_for_phone.
Prefer naming the arg _phone_user: &str and removing let _ = phone_user;.

wacore/src/store/device.rs (1)

154-172: Init to None is fine; consider enforcing a max size at ingestion time (untrusted server data).

wacore/binary/src/node.rs (2)

25-36: Borrowing conversion is correct; clarify allocation behavior in docs if perf-sensitive.

NodeContentRef::Nodes(Box<NodeVec>) requires collecting into a new Vec, so this is not allocation-free. Consider documenting that this is a “borrowed view” but still allocates containers.


62-75: Node::as_node_ref() looks correct; same note about container allocations.

wacore/binary/src/jid.rs (1)

747-916: Tests are solid; add one parity test for “hosted.lid decoded with agent > 0”.

You already cover parity for s.whatsapp.net, lid, and hosted; adding one explicit “agent should be omitted” case for hosted.lid would close the gap if that decode path ever produces an agent.

src/request.rs (1)

165-179: Potential expensive deep-clone on IQ hot path when Arc::try_unwrap fails.

If any other task keeps an Arc<Node> alive, (*arc).clone() may copy the full stanza tree. Consider switching the waiter channel to carry Arc<Node> (cheap Arc::clone) unless you have a strong reason to require ownership of Node.

src/send.rs (1)

121-126: Don’t silently swallow SKDM-recipient persistence errors.

get_skdm_recipients(...).await.unwrap_or_default() hides DB failures; at least log a warning on error so repeated full SKDM distribution isn’t “mysterious”.

src/handlers/message.rs (1)

89-91: Consider adding context to the warning log.

When message enqueueing fails, including the chat ID would help with debugging.

         if let Err(e) = tx.send(node).await {
-            warn!("Failed to enqueue message for processing: {e}");
+            warn!("Failed to enqueue message for chat {}: {e}", chat_id);
         }
src/lid_pn_cache.rs (1)

211-224: Consider batch insertion for warm_up to reduce lock contention.

Calling self.add(entry).await in a loop acquires and releases both RwLocks for each entry. For large datasets, this could be slow at startup. A batch approach that acquires each lock once would be more efficient.

Example optimization (optional):

     pub async fn warm_up(&self, entries: Vec<LidPnEntry>) {
         let count = entries.len();
         let start = std::time::Instant::now();
 
-        for entry in entries {
-            self.add(entry).await;
+        // Batch insert to minimize lock acquisitions
+        {
+            let mut lid_map = self.lid_to_entry.write().await;
+            let mut pn_map = self.pn_to_entry.write().await;
+            
+            for entry in entries {
+                lid_map.insert(entry.lid.clone(), entry.clone());
+                
+                let should_update = match pn_map.get(&entry.phone_number) {
+                    Some(existing) => existing.created_at <= entry.created_at,
+                    None => true,
+                };
+                
+                if should_update {
+                    pn_map.insert(entry.phone_number.clone(), entry);
+                }
+            }
         }
 
         log::info!(
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31f7b94 and 132a591.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (46)
  • src/appstate_sync.rs (1 hunks)
  • src/client.rs (33 hunks)
  • src/client/context_impl.rs (1 hunks)
  • src/handlers/basic.rs (5 hunks)
  • src/handlers/ib.rs (3 hunks)
  • src/handlers/iq.rs (2 hunks)
  • src/handlers/message.rs (3 hunks)
  • src/handlers/notification.rs (3 hunks)
  • src/handlers/receipt.rs (2 hunks)
  • src/handlers/router.rs (8 hunks)
  • src/handlers/traits.rs (2 hunks)
  • src/handlers/unimplemented.rs (2 hunks)
  • src/handshake.rs (2 hunks)
  • src/lib.rs (1 hunks)
  • src/lid_pn_cache.rs (1 hunks)
  • src/message.rs (8 hunks)
  • src/pair.rs (6 hunks)
  • src/pdo.rs (2 hunks)
  • src/receipt.rs (2 hunks)
  • src/request.rs (2 hunks)
  • src/retry.rs (4 hunks)
  • src/send.rs (7 hunks)
  • src/socket/noise_socket.rs (3 hunks)
  • src/store/signal_adapter.rs (2 hunks)
  • src/usync.rs (1 hunks)
  • storages/sqlite-storage/Cargo.toml (1 hunks)
  • storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/down.sql (1 hunks)
  • storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/up.sql (1 hunks)
  • storages/sqlite-storage/src/schema.rs (3 hunks)
  • storages/sqlite-storage/src/sqlite_store.rs (23 hunks)
  • transports/tokio-transport/Cargo.toml (2 hunks)
  • transports/tokio-transport/src/lib.rs (2 hunks)
  • wacore/appstate/src/processor.rs (3 hunks)
  • wacore/binary/Cargo.toml (1 hunks)
  • wacore/binary/src/jid.rs (6 hunks)
  • wacore/binary/src/node.rs (2 hunks)
  • wacore/libsignal/src/protocol/session_cipher.rs (10 hunks)
  • wacore/libsignal/src/protocol/state/session.rs (5 hunks)
  • wacore/src/client/context.rs (6 hunks)
  • wacore/src/pair.rs (2 hunks)
  • wacore/src/send.rs (11 hunks)
  • wacore/src/store/device.rs (2 hunks)
  • wacore/src/store/traits.rs (3 hunks)
  • wacore/src/types/jid.rs (1 hunks)
  • wacore/src/usync.rs (2 hunks)
  • wacore/tests/jid_test.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
wacore/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases

Files:

  • wacore/src/usync.rs
  • wacore/src/store/device.rs
  • wacore/binary/src/node.rs
  • wacore/src/pair.rs
  • wacore/src/types/jid.rs
  • wacore/tests/jid_test.rs
  • wacore/appstate/src/processor.rs
  • wacore/src/send.rs
  • wacore/src/store/traits.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • wacore/binary/src/jid.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • wacore/src/client/context.rs
{src,wacore}/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

{src,wacore}/**/*.rs: Use thiserror for custom error types (e.g., SocketError)
Use anyhow::Error for functions with multiple failure modes

Files:

  • wacore/src/usync.rs
  • wacore/src/store/device.rs
  • src/usync.rs
  • wacore/binary/src/node.rs
  • src/handlers/iq.rs
  • wacore/src/pair.rs
  • wacore/src/types/jid.rs
  • src/request.rs
  • src/socket/noise_socket.rs
  • wacore/tests/jid_test.rs
  • src/handlers/unimplemented.rs
  • wacore/appstate/src/processor.rs
  • src/handlers/traits.rs
  • src/handshake.rs
  • src/appstate_sync.rs
  • wacore/src/send.rs
  • src/lib.rs
  • src/pdo.rs
  • wacore/src/store/traits.rs
  • src/message.rs
  • src/retry.rs
  • src/receipt.rs
  • src/client/context_impl.rs
  • src/handlers/ib.rs
  • src/lid_pn_cache.rs
  • src/handlers/message.rs
  • src/store/signal_adapter.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • wacore/binary/src/jid.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • src/handlers/router.rs
  • src/handlers/receipt.rs
  • src/send.rs
  • src/handlers/basic.rs
  • src/handlers/notification.rs
  • wacore/src/client/context.rs
  • src/pair.rs
  • src/client.rs
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: All I/O in the main crate must use Tokio; be mindful of race conditions
Wrap blocking I/O (e.g., ureq) and heavy CPU-bound tasks (e.g., media encryption) in tokio::task::spawn_blocking
Avoid .unwrap() and .expect() outside of tests and truly unrecoverable paths

Files:

  • src/usync.rs
  • src/handlers/iq.rs
  • src/request.rs
  • src/socket/noise_socket.rs
  • src/handlers/unimplemented.rs
  • src/handlers/traits.rs
  • src/handshake.rs
  • src/appstate_sync.rs
  • src/lib.rs
  • src/pdo.rs
  • src/message.rs
  • src/retry.rs
  • src/receipt.rs
  • src/client/context_impl.rs
  • src/handlers/ib.rs
  • src/lid_pn_cache.rs
  • src/handlers/message.rs
  • src/store/signal_adapter.rs
  • src/handlers/router.rs
  • src/handlers/receipt.rs
  • src/send.rs
  • src/handlers/basic.rs
  • src/handlers/notification.rs
  • src/pair.rs
  • src/client.rs
src/{client,send,message}.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use Client::chat_locks to serialize per-chat operations

Files:

  • src/message.rs
  • src/send.rs
  • src/client.rs
🧠 Learnings (6)
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to wacore/**/*.rs : wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases

Applied to files:

  • src/request.rs
  • storages/sqlite-storage/Cargo.toml
  • src/appstate_sync.rs
  • transports/tokio-transport/Cargo.toml
  • wacore/binary/Cargo.toml
  • storages/sqlite-storage/src/sqlite_store.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations

Applied to files:

  • src/socket/noise_socket.rs
  • src/message.rs
  • src/retry.rs
  • src/handlers/message.rs
  • src/send.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/**/*.rs : All I/O in the main crate must use Tokio; be mindful of race conditions

Applied to files:

  • src/socket/noise_socket.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: The whatsapp-rust (main) crate integrates wacore with Tokio for async and Diesel for SQLite persistence

Applied to files:

  • storages/sqlite-storage/Cargo.toml
  • src/handlers/traits.rs
  • transports/tokio-transport/Cargo.toml
  • storages/sqlite-storage/src/sqlite_store.rs
  • src/pair.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to waproto/build.rs : In waproto, use prost in build.rs to compile Protocol Buffers into Rust structs

Applied to files:

  • wacore/binary/Cargo.toml
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to {src,wacore}/**/*.rs : Use anyhow::Error for functions with multiple failure modes

Applied to files:

  • src/send.rs
🧬 Code graph analysis (25)
wacore/src/usync.rs (1)
wacore/binary/src/jid.rs (2)
  • new (305-311)
  • new (362-370)
wacore/src/store/device.rs (1)
storages/sqlite-storage/src/sqlite_store.rs (2)
  • None (586-586)
  • None (592-592)
src/usync.rs (1)
wacore/src/usync.rs (1)
  • parse_lid_mappings_from_response (71-109)
src/handlers/iq.rs (2)
wacore/src/xml.rs (1)
  • DisplayableNode (48-52)
src/handlers/traits.rs (1)
  • handle (26-26)
src/socket/noise_socket.rs (1)
wacore/src/handshake/utils.rs (1)
  • generate_iv (37-41)
wacore/tests/jid_test.rs (2)
src/jid_utils.rs (1)
  • server_jid (6-14)
wacore/libsignal/src/core/address.rs (1)
  • name (296-298)
src/handlers/unimplemented.rs (8)
src/handlers/basic.rs (4)
  • handle (25-28)
  • handle (49-52)
  • handle (73-76)
  • handle (97-105)
src/handlers/ib.rs (1)
  • handle (37-40)
src/handlers/iq.rs (1)
  • handle (31-36)
src/handlers/message.rs (1)
  • handle (35-96)
src/handlers/notification.rs (1)
  • handle (30-33)
src/handlers/receipt.rs (1)
  • handle (28-31)
src/handlers/router.rs (1)
  • handle (106-115)
src/handlers/traits.rs (1)
  • handle (26-26)
storages/sqlite-storage/src/schema.rs (1)
src/store/persistence_manager.rs (1)
  • device_id (103-105)
src/appstate_sync.rs (2)
storages/sqlite-storage/src/sqlite_store.rs (5)
  • get_lid_pn_mapping_by_lid (2244-2249)
  • get_lid_pn_mapping_by_phone (2251-2256)
  • put_lid_pn_mapping (2258-2260)
  • get_all_lid_pn_mappings (2262-2264)
  • delete_lid_pn_mapping (2266-2268)
wacore/src/store/traits.rs (5)
  • get_lid_pn_mapping_by_lid (103-103)
  • get_lid_pn_mapping_by_phone (106-106)
  • put_lid_pn_mapping (109-109)
  • get_all_lid_pn_mappings (112-112)
  • delete_lid_pn_mapping (115-115)
wacore/src/send.rs (2)
wacore/binary/src/jid.rs (10)
  • is_hosted (257-259)
  • user (219-219)
  • user (290-292)
  • user (347-349)
  • server (220-220)
  • server (293-295)
  • server (350-352)
  • device (221-221)
  • device (296-298)
  • device (353-355)
src/client/context_impl.rs (1)
  • get_lid_for_phone (32-34)
wacore/src/store/traits.rs (1)
storages/sqlite-storage/src/sqlite_store.rs (5)
  • get_lid_pn_mapping_by_lid (2244-2249)
  • get_lid_pn_mapping_by_phone (2251-2256)
  • put_lid_pn_mapping (2258-2260)
  • get_all_lid_pn_mappings (2262-2264)
  • delete_lid_pn_mapping (2266-2268)
src/message.rs (5)
wacore/binary/src/attrs.rs (2)
  • jid (75-78)
  • jid (201-204)
src/client.rs (1)
  • new (169-268)
src/store/persistence_manager.rs (3)
  • new (20-62)
  • device_id (103-105)
  • backend (120-122)
wacore/binary/src/node.rs (2)
  • attrs (83-85)
  • attrs (181-185)
src/lid_pn_cache.rs (1)
  • get_current_lid (156-159)
src/receipt.rs (1)
wacore/binary/src/node.rs (2)
  • attrs (83-85)
  • attrs (181-185)
src/client/context_impl.rs (2)
wacore/src/client/context.rs (1)
  • get_lid_for_phone (125-129)
wacore/src/send.rs (1)
  • get_lid_for_phone (917-919)
src/handlers/ib.rs (1)
wacore/binary/src/node.rs (2)
  • attrs (83-85)
  • attrs (181-185)
src/lid_pn_cache.rs (1)
src/client.rs (1)
  • new (169-268)
src/store/signal_adapter.rs (1)
wacore/libsignal/src/protocol/state/session.rs (1)
  • deserialize (575-594)
wacore/libsignal/src/protocol/state/session.rs (3)
wacore/libsignal/src/protocol/ratchet/keys.rs (4)
  • index (131-133)
  • new (121-123)
  • new (160-162)
  • counter (106-108)
wacore/libsignal/src/protocol/sender_keys.rs (3)
  • new (36-47)
  • new (86-91)
  • new (138-159)
wacore/libsignal/src/protocol/state/signed_prekey.rs (1)
  • new (66-81)
src/handlers/router.rs (1)
wacore/binary/src/node.rs (2)
  • attrs (83-85)
  • attrs (181-185)
src/handlers/receipt.rs (5)
src/handlers/ib.rs (1)
  • handle (37-40)
src/handlers/iq.rs (1)
  • handle (31-36)
src/handlers/notification.rs (1)
  • handle (30-33)
src/handlers/traits.rs (1)
  • handle (26-26)
src/handlers/unimplemented.rs (1)
  • handle (49-52)
src/send.rs (2)
src/client.rs (1)
  • new (169-268)
src/store/persistence_manager.rs (1)
  • new (20-62)
storages/sqlite-storage/src/sqlite_store.rs (1)
wacore/src/store/traits.rs (5)
  • get_lid_pn_mapping_by_lid (103-103)
  • get_lid_pn_mapping_by_phone (106-106)
  • put_lid_pn_mapping (109-109)
  • get_all_lid_pn_mappings (112-112)
  • delete_lid_pn_mapping (115-115)
src/handlers/basic.rs (6)
src/handlers/ib.rs (1)
  • handle (37-40)
src/handlers/iq.rs (1)
  • handle (31-36)
src/handlers/notification.rs (1)
  • handle (30-33)
src/handlers/receipt.rs (1)
  • handle (28-31)
src/handlers/traits.rs (1)
  • handle (26-26)
src/handlers/unimplemented.rs (1)
  • handle (49-52)
wacore/src/client/context.rs (2)
wacore/binary/src/jid.rs (2)
  • new (305-311)
  • new (362-370)
src/client/context_impl.rs (1)
  • get_lid_for_phone (32-34)
src/pair.rs (5)
wacore/binary/src/attrs.rs (8)
  • jid (75-78)
  • jid (201-204)
  • s (86-86)
  • s (107-107)
  • s (138-138)
  • s (213-213)
  • s (235-235)
  • s (266-266)
src/bot.rs (1)
  • client (88-90)
wacore/binary/src/node.rs (2)
  • children (76-81)
  • children (134-139)
wacore/src/pair.rs (1)
  • build_ack_node (68-83)
wacore/binary/src/jid.rs (3)
  • user (219-219)
  • user (290-292)
  • user (347-349)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: Build & Test
🔇 Additional comments (73)
wacore/libsignal/src/protocol/state/session.rs (4)

227-247: Good helper extraction; consider documenting the “fail closed” behavior on corrupted chains.
get_receiver_chain_index now errors out if any receiver chain is missing/invalid rather than skipping it. That’s likely correct for detecting corrupted session state, but it’s a behavior worth being explicit about since callers may interpret “not found” vs “invalid state” differently.


249-258: LGTM: get_receiver_chain now returns (Chain, idx) cleanly.
Using get_receiver_chain_index keeps the search logic centralized and avoids repeated clone/search patterns elsewhere.


260-283: Nice: avoids cloning the full chain when only deriving ChainKey.
This refactor keeps validation (missing key/index, invalid key length) local and avoids allocating a cloned Chain just to read chain_key.


391-423: Correct in-place removal pattern; borrow discipline looks sound.
Finding the position first and only then mutating the underlying Vec avoids cloning and keeps the mutation localized to the “found” path.

storages/sqlite-storage/Cargo.toml (3)

4-4: Verify Rust edition setting.

The edition is set to "2024", which is uncommon. While Rust 2024 edition may be stable by now, most projects still use 2021. Please confirm this is intentional and not a typo.


13-17: Diesel feature addition aligns with LID/PN mapping changes.

The new "32-column-tables" feature is appropriately scoped to support the additional tables (LidPnMapping, etc.) introduced by the broader LID/PN mapping infrastructure.

Please verify that diesel 2.2.12 supports the "32-column-tables" feature and that it is correctly documented in the diesel changelog or release notes.


24-25: Dependencies verified as secure.

Both log = "0.4.29" and moka = { version = "0.12", features = ["future"] } are legitimate, available versions with no known security advisories as of December 2025. The moka "future" feature is appropriate for async Rust applications.

wacore/appstate/src/processor.rs (2)

154-161: Good: had_no_prior_state is captured before mutating state

Capturing state.version and the “zero hash” condition before updating state avoids TOCTOU issues during later MAC decisions.


185-188: Public signature change: ensure all callers were updated

validate_patch_macs(..., had_no_prior_state) is pub, making this a breaking API change for any external consumers of the wacore crate. Within the repository, the single call site at line 187 has been correctly updated with the new had_no_prior_state parameter, but external callers cannot be verified from within this codebase.

wacore/libsignal/src/protocol/session_cipher.rs (5)

453-460: Well-designed result type.

The DecryptionResult struct is clean and well-documented. The explicit flag for tracking session origin makes the control flow clear and enables different identity trust handling paths.


263-263: LGTM: Clean integration of DecryptionResult.

The function correctly adapts to the new return type, extracting the plaintext for the final return while maintaining all existing identity and pre-key handling logic.

Also applies to: 286-286


519-521: Correct: Preserve DuplicatedMessage error semantics.

The explicit propagation of DuplicatedMessage errors ensures that callers can distinguish duplicate messages (which carry chain and counter information) from generic decryption failures. This preserves important error semantics that would otherwise be masked.

Also applies to: 584-586


468-468: LGTM: Clear session origin tracking.

The function correctly returns DecryptionResult with the appropriate used_previous_session flag. The flag is set accurately: false when the current session succeeds, and true after promoting a previous session.

Also applies to: 514-517, 596-599


317-353: The security concerns raised are not substantiated by the implementation.

The code's logic for skipping the identity trust check when a previous session is used is correct and secure. Here's why:

  1. Identity is cryptographically bound to session at creation: Each SessionState stores the remote identity key at session establishment time (line 74 in state/session.rs). Once created, a session's identity cannot change. When an old session is promoted to current, its original identity moves with it.

  2. Duplicate message protection prevents replays: The code immediately returns DuplicatedMessage error (lines 519-520, 584-585) if any message attempts to decrypt with a counter that has already been used in any session. This is checked before attempting promotion, preventing attackers from replaying old ciphertexts to repeatedly promote old sessions.

  3. Session promotion is intentional design for out-of-order delivery: When a message decrypts successfully with a previous (archived) session, promoting that session to current is the correct behavior for handling out-of-order message arrival after protocol renegotiation. The identity from that session—which was trusted when the session was originally established—is therefore safe to save without re-checking trust.

  4. The trusting assumption is valid: The comment's rationale is accurate: "When we successfully decrypt with a previous (archived) session, we already had a valid session with that identity—it was trusted when the session was established."

No action is required. The implementation properly handles the out-of-order message delivery scenario and maintains security invariants through session-identity binding and duplicate detection.

wacore/binary/Cargo.toml (1)

23-27: Confirm serde must be a non-optional build-dependency.
Making build-time serde unconditional (Line 25) means it’s pulled in even when the crate’s runtime serde feature is off. If build.rs only needs serde for specific feature paths, consider gating the build-dep behind a feature to avoid extra compile/dependency surface.

transports/tokio-transport/Cargo.toml (1)

10-14: Add guardrails so danger-skip-tls-verify can’t be enabled accidentally.
The naming/comment helps, but please double-check workspace feature unification (e.g., top-level crate features) so this never becomes enabled in production builds unintentionally.

Also applies to: 34-42

src/lib.rs (1)

36-38: Public module export looks fine.

wacore/src/usync.rs (2)

6-13: UsyncLidMapping shape looks right for persistence.


69-109: Confirm MSRV supports let-chains in this crate (if let Ok(...) = ... && ...).

If MSRV is lower than let-chains support, rewrite to nested if let / match. Otherwise, parsing/filtering behavior looks good (safe fallbacks via continue).

wacore/tests/jid_test.rs (2)

31-37: Updated server-only JID formatting expectation is consistent and improves coverage.


126-145: Good coverage for WhatsApp-Web-like ProtocolAddress formatting on LID JIDs.

wacore/src/store/device.rs (1)

120-124: #[serde(default)] on edge_routing_info is the right compatibility move.

src/client/context_impl.rs (1)

31-35: Nice, minimal SendContextResolver integration (delegates to cache cleanly).

src/handlers/receipt.rs (2)

5-6: Import update is consistent with the Arc handler migration.


28-31: Receipt handler now matches the new StanzaHandler interface; flow is straightforward.

storages/sqlite-storage/src/schema.rs (1)

29-49: Schema additions look consistent (table + allowlist + nullable column).

Please double-check the corresponding migration and Diesel model structs reflect:

  • device.edge_routing_info as Option<Vec<u8>>
  • lid_pn_mapping primary key (lid, device_id)

Also applies to: 59-68, 112-124

src/handlers/iq.rs (1)

6-8: Arc-based handler update is consistent with the new StanzaHandler API.

Also applies to: 31-36

src/pdo.rs (1)

144-146: The &wa::Message borrow is safe—no latent lifetime issue exists here.

add_recent_message serializes the message synchronously via encode_to_vec() and stores only the encoded bytes, never the reference. The wacore prepare_* functions also take borrowed references and complete synchronously without spawning tasks or enqueuing with the message. No background task retains the borrow past the await boundary.

src/handlers/traits.rs (1)

4-4: LGTM! Sound API change for async handler pattern.

Switching from &NodeRef<'_> to Arc<Node> eliminates lifetime constraints at async boundaries, enabling handlers to store or forward nodes without cloning. The updated doc comment accurately reflects the new ownership semantics.

Also applies to: 20-20, 26-26

src/handlers/router.rs (2)

49-60: LGTM! Router dispatch correctly updated for Arc.

The dispatch method properly accepts Arc<Node> and passes it through to handlers. The tag lookup via node.tag.as_str() is correct for the HashMap<&'static str, _> key type.


163-170: LGTM! Test scaffolding correctly migrated to Arc.

Tests properly construct owned Node instances wrapped in Arc, matching the updated dispatch signature. Both handler-found and handler-not-found paths are covered.

Also applies to: 194-201

src/handlers/unimplemented.rs (1)

49-52: LGTM! Consistent migration to Arc.

The handler correctly accepts Arc<Node> and accesses the tag via &node.tag. The control flow remains unchanged (always returns true after handling).

src/appstate_sync.rs (1)

520-552: LGTM! Test mock correctly implements new LidPnMappingStore trait.

The stub implementation satisfies the trait bounds for MockBackend in tests. Returning Ok(None) / Ok(vec![]) is appropriate since these tests focus on app state sync, not LID/PN mapping functionality.

wacore/src/types/jid.rs (2)

67-134: LGTM! Comprehensive test coverage for signal address formatting.

Tests cover key scenarios including LID JIDs with/without devices, phone numbers with server mapping, and the edge case of dots in LID user IDs. The protocol address format tests verify the .0 suffix pattern.


36-39: No special handling needed for group JIDs.

Group JIDs (g.us) are used to identify groups in the system, but to_protocol_address() and to_signal_address_string() are only called on individual participant JIDs or LID JIDs—never on group JIDs themselves. Group JIDs are passed as strings to SenderKeyName for group identification, not converted to protocol addresses. The current server mapping (which converts s.whatsapp.netc.us for phone numbers and leaves other servers like g.us unchanged) is correct and handles all necessary cases.

storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/up.sql (1)

18-20: ALTER TABLE ... ADD COLUMN edge_routing_info BLOB looks fine (nullable / non-breaking).

src/send.rs (1)

11-20: Passing &wa::Message through the async send path is fine as long as you only serialize/clone it.

Please double-check Client::add_recent_message(...) and any downstream usage doesn’t retain the reference beyond the call (it should eagerly serialize/clone).

src/retry.rs (3)

59-71: Retry now cleanly “take once” from cache; good early-return behavior.


137-156: Passing &original_msg into send_message_impl is a good simplification (no Arc churn).


295-332: Test updates look correct for the new “insert then take” API.

src/handlers/notification.rs (1)

30-33: Arc-based handler signature looks consistent with the broader refactor.

wacore/src/store/traits.rs (1)

147-176: The concern about breaking no_std compatibility is not applicable here. wacore currently depends unconditionally on async-trait and tokio, and all existing Backend store traits (IdentityStore, SessionStore, AppStateKeyStore, AppStateStore, SenderKeyStoreHelper, SenderKeyDistributionStore, DevicePersistence) already use #[async_trait]. Adding LidPnMappingStore follows the established pattern with no new dependencies or compatibility implications. Existing backends (SqliteStore, MockBackend) already implement this trait without issue.

src/message.rs (3)

338-346: Per-sender session locking implementation looks correct.

The use of session_locks cache with get_with to lazily create per-sender mutexes is a good pattern for preventing race conditions during concurrent message processing from the same sender. This aligns with the coding guideline to serialize per-chat operations using appropriate locks.


64-165: LID-based session resolution logic is well-documented and correctly implements WhatsApp Web behavior.

The detailed comments explaining WhatsApp Web's WAWebSignalAddress.toString() behavior provide excellent context. The three-branch logic (LID sender, PN sender with LID mapping, PN sender without mapping) correctly mirrors the expected behavior. The bidirectional cache population (LID→PN and PN→LID) ensures consistency.


2517-2757: Comprehensive test coverage for LID-PN cache integration.

The tests thoroughly cover the key scenarios:

  • Cache population on message reception with sender_lid
  • No cache pollution without sender_lid
  • Bidirectional caching for LID senders with participant_pn
  • Repeated message handling
  • PN messages using cached LID for session lookup

This provides good confidence in the cache behavior.

src/handlers/ib.rs (3)

37-41: Handler signature correctly updated to Arc<Node>.

The migration from NodeRef<'_> to Arc<Node> aligns with the broader refactor across handlers. Delegating to handle_ib_impl with a reference (&node) is efficient as it avoids unnecessary cloning.


67-94: Edge routing info handling is well-implemented with proper validation.

The implementation:

  1. Validates presence of routing_info child node
  2. Checks for NodeContent::Bytes content type
  3. Validates non-empty bytes before storing
  4. Logs appropriately at each failure point

The async closure in modify_device correctly captures and moves routing_bytes.


126-130: Offline sync signaling correctly implemented.

Using Ordering::Relaxed is appropriate here since the atomic flag is coordinated with notify_waiters() which provides the necessary synchronization for waiters. The pattern correctly mimics WhatsApp Web's offlineDeliveryEnd event as noted in the comment.

src/handlers/message.rs (1)

35-96: Per-chat mailbox pattern correctly prevents race conditions.

The two-phase locking approach (enqueue lock → queue access → send) ensures messages are enqueued in arrival order even under concurrent access. This is a good solution for the PreKey message ordering problem described in the comments.

A few observations:

  1. The channel capacity of 10,000 is generous for backpressure
  2. Workers are spawned lazily and cleaned up via the cache TTL (5 minutes per the relevant snippet from src/client.rs)
  3. When the channel receiver is dropped (cache eviction), the worker task will exit gracefully when recv() returns None

As per coding guidelines, this correctly uses Client::chat_locks semantics (now message_queues + message_enqueue_locks) to serialize per-chat operations.

src/pair.rs (3)

26-36: Node API migration looks correct.

The switch from attr_parser().get("from") to node.attrs.get("from").map(|s| s.as_str()) correctly accesses the attribute using the new Node API. The comparison against SERVER_JID is appropriate.


226-237: Self LID-PN mapping addition is critical for self-messaging.

This correctly adds the bot's own LID-to-PN mapping after successful pairing. The guard condition ensures both JID fields are non-empty before caching. Using LearningSource::Pairing provides good provenance for debugging.

The comment clearly explains why this is needed: when sending DMs to self, the existing LID-based session should be found instead of creating a new PN-based one.


40-46: Verify PairUtils::build_ack_node handles the new Node type.

The call to PairUtils::build_ack_node(node) now passes &Node instead of the previous type. Based on the relevant snippet from wacore/src/pair.rs, this function accepts &Node, so the migration is correct.

wacore/src/send.rs (5)

113-170: LID session resolution logic is well-designed.

The three-phase session resolution:

  1. Check for existing session under PN address → use PN
  2. No PN session + LID mapping exists → check for LID session → use LID if found
  3. No session under either → need prekeys, default to PN

This correctly handles the case where a session was established via a received message with sender_lid, ensuring replies reuse that session instead of creating a new PN-based one.

The separation of device_jid (for <to> attribute) from encryption_jid (for actual encryption) is correct - the server expects the original addressing while we use the appropriate session internally.


607-648: LID conversion and hosted device filtering for groups is correct.

The logic:

  1. Converts phone-based device JIDs to LID format for LID groups (lines 607-619)
  2. Deduplicates after conversion to handle overlapping queries (lines 621-625)
  3. Filters out hosted devices (device 99, @HosteD server) from SKDM distribution (lines 627-648)

The comment at lines 627-633 clearly explains the filtering rationale:

  • Exact sending device is excluded (we have our own sender key)
  • Other own devices are kept (they need SKDM to decrypt our messages)
  • Hosted devices are excluded (don't participate in group E2EE)

This matches WhatsApp Web's getFanOutList behavior as documented in the test comments.


739-742: Addressing mode attribute added for LID groups.

Adding addressing_mode="lid" for LID groups aligns with WhatsApp Web behavior and ensures proper handling by the server.


1036-1163: Comprehensive tests for hosted device identification and filtering.

The tests clearly document:

  1. What hosted devices are (Cloud API / Meta Business API)
  2. How they're identified (device 99, @HosteD server)
  3. Why they're filtered from groups (don't use Signal protocol)
  4. The expected behavior for 1:1 chats vs groups

The test_hosted_devices_filtered_from_group_skdm test provides good coverage of the filtering logic with various device types.


1210-1401: LID-PN session mismatch fix tests are thorough.

The tests validate the critical scenario that caused the original bug:

  • test_lid_session_lookup_scenario: Verifies the lookup flow
  • test_lid_jid_preserves_companion_device_id: Ensures device ID 33 (WhatsApp Web) is preserved
  • test_lid_lookup_only_for_pn_jids: Confirms LID lookup only applies to s.whatsapp.net JIDs

These tests provide confidence that the fix handles edge cases correctly.

src/handlers/basic.rs (2)

25-28: LGTM!

The handler correctly uses the new Arc<Node> signature and passes a reference to client.handle_success. This is consistent with the trait definition and other handlers.


97-105: LGTM!

The Arc::try_unwrap with unwrap_or_else fallback is the correct pattern for obtaining an owned Node when handle_ack_response requires ownership. This avoids unnecessary cloning when the Arc has a single reference.

src/lid_pn_cache.rs (3)

186-205: Two separate lock acquisitions create a brief inconsistency window.

The add() method releases the lid_to_entry lock before acquiring pn_to_entry. During this window, a concurrent reader could see an inconsistent state (LID present but PN not yet updated). This is mitigated by timestamp-based conflict resolution, so it's not a correctness bug, but worth noting.

For the current use case (message processing where exact consistency isn't critical), this is acceptable.


78-89: LGTM!

The LidPnEntry struct is well-documented with clear field descriptions. The design supports bidirectional lookups and timestamp-based conflict resolution as documented.


44-76: LGTM!

The as_str/parse roundtrip is clean and the fallback to Other for unknown strings is a safe default.

storages/sqlite-storage/src/sqlite_store.rs (3)

844-879: Background writes trade durability for performance.

The cache-first, fire-and-forget DB write pattern means put_identity_for_device returns Ok(()) even if the DB write eventually fails. This is intentional for Signal protocol hot paths, and failures are logged. However, a process crash before the DB write completes would lose the identity.

This tradeoff is appropriate for the use case. The warning logs at lines 874-875 ensure operators can detect persistent storage issues.


1097-1179: LGTM - good batch optimization for group messaging.

The get_addresses_with_sessions method efficiently combines cache lookups with a single batched DB query for cache misses. The negative caching (storing None for non-existent sessions) prevents repeated DB queries.


139-161: LGTM - cache configuration looks reasonable.

100k capacity with 1-hour TTL is appropriate for identity/session caches. The moka cache handles eviction automatically.

src/client.rs (8)

545-563: LGTM - critical node inline processing prevents race conditions.

Processing success, failure, and stream:error inline ensures login state is set before checking expected_disconnect or spawning other tasks. This prevents subtle races during reconnection.


906-929: Solid guard against duplicate <success> stanzas and reconnect races.

The atomic swap on is_logged_in (line 917) combined with expected_disconnect check (line 910) ensures only the first <success> per connection is processed. The connection_generation increment (line 924) invalidates stale post-login tasks from previous connections.


258-265: Fire-and-forget warm-up is acceptable but consider handling errors more explicitly.

The warm-up is spawned without awaiting, so errors are only logged. This is fine since cache warm-up is best-effort and shouldn't block client creation. However, if warm-up consistently fails, callers won't know.

For now this is acceptable since the cache degrades gracefully (just misses).


1520-1527: Good: transport disconnect spawned in background to avoid blocking message loop.

Cloning the Arc<Transport> before spawning ensures the lock isn't held during the async disconnect. This prevents deadlocks if disconnect needs to access other client state.


298-326: LGTM - cache + background persistence pattern.

The method correctly:

  1. Updates in-memory cache synchronously for immediate availability
  2. Spawns background task for persistence to avoid blocking message processing
  3. Logs warnings on persistence failures

This matches the pattern used in sqlite_store.rs for identity/session writes.


90-109: New concurrency primitives look well-designed.

  • session_locks: Per-device Signal session locks prevent races when processing messages from the same sender across chats
  • message_queues: Per-chat queues ensure message ordering (critical for PreKey message ordering)
  • message_enqueue_locks: Serialize enqueue operations to prevent race during queue initialization

The 5-minute TTL and 10k capacity are reasonable bounds.

Based on learnings, these align with the guideline to "Use Client::chat_locks to serialize per-chat operations" - these caches provide that serialization.


952-961: Clean macro pattern for generation checking.

The check_generation! macro provides a concise way to bail out of stale post-login tasks. Using a macro avoids repetitive boilerplate while keeping the early-return pattern explicit.


2079-2261: LGTM - comprehensive test coverage for LID-PN cache integration.

Tests cover:

  • Basic cache operations (insert, lookup)
  • Timestamp-based conflict resolution
  • Integration with SendContextResolver trait

This validates the critical path for LID-PN session reuse.

Comment thread src/handshake.rs
Comment thread src/handshake.rs
Comment thread src/send.rs Outdated
Comment thread src/usync.rs
Comment on lines +1 to +12
-- Remove edge_routing_info column from device table
-- SQLite doesn't support DROP COLUMN directly in older versions, but newer SQLite (3.35+) does
-- For compatibility, we create a new table without the column and migrate data
CREATE TABLE device_backup AS SELECT
id, lid, pn, registration_id, noise_key, identity_key, signed_pre_key,
signed_pre_key_id, signed_pre_key_signature, adv_secret_key, account,
push_name, app_version_primary, app_version_secondary, app_version_tertiary,
app_version_last_fetched_ms
FROM device;
DROP TABLE device;
ALTER TABLE device_backup RENAME TO device;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Down migration is unsafe: CREATE TABLE ... AS SELECT drops constraints/types/indexes.
This rollback will recreate device without PK/FK/NOT NULL/defaults (and any indexes/triggers), which can break the app after a downgrade. Rework to: (1) CREATE TABLE device_backup (...full schema without edge_routing_info...), (2) INSERT INTO device_backup(cols...) SELECT cols... FROM device, (3) drop/rename, and (4) re-create the original indexes/constraints.

🤖 Prompt for AI Agents
In
storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/down.sql
around lines 1-12, the current use of "CREATE TABLE device_backup AS SELECT ..."
will drop constraints, types, defaults, PK/FK and indexes; replace this with an
explicit safe migration: (1) create device_backup with the full original device
table schema but without the edge_routing_info column (include PK, NOT NULLs,
defaults, FK clauses, and any triggers), (2) run an INSERT INTO
device_backup(col1, col2, ...) SELECT col1, col2, ... FROM device to copy data
explicitly (list columns in the same order), (3) DROP TABLE device and RENAME
device_backup TO device, and (4) re-create any indexes, triggers and foreign key
constraints that existed on device (if not included in the CREATE), ensuring the
final schema matches the original except for the removed column.

Comment on lines +4 to +16
CREATE TABLE lid_pn_mapping (
lid TEXT NOT NULL, -- LID user part (e.g., "100000012345678")
phone_number TEXT NOT NULL, -- Phone number user part (e.g., "559980000001")
created_at INTEGER NOT NULL, -- Unix timestamp when mapping was first learned
learning_source TEXT NOT NULL, -- Source of the mapping (usync, peer_pn_message, etc.)
updated_at INTEGER NOT NULL, -- Unix timestamp of last update
device_id INTEGER NOT NULL, -- Device ID for multi-account support
PRIMARY KEY (lid, device_id),
FOREIGN KEY(device_id) REFERENCES device(id) ON DELETE CASCADE
);

-- Index for reverse lookup (phone number -> LID)
CREATE INDEX idx_lid_pn_mapping_phone ON lid_pn_mapping(phone_number, device_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reverse lookup “most recent” isn’t guaranteed without an ordering/index strategy.

idx_lid_pn_mapping_phone (phone_number, device_id) won’t help pick the “most recent” mapping if multiple rows exist per phone. If callers expect “most recent”, consider either enforcing uniqueness or indexing for ORDER BY updated_at DESC:

-CREATE INDEX idx_lid_pn_mapping_phone ON lid_pn_mapping(phone_number, device_id);
+CREATE INDEX idx_lid_pn_mapping_phone_updated
+  ON lid_pn_mapping(phone_number, device_id, updated_at DESC);

Also consider whether you want a UNIQUE(phone_number, device_id) constraint (if the mapping is intended to be 1:1 per device).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE lid_pn_mapping (
lid TEXT NOT NULL, -- LID user part (e.g., "100000012345678")
phone_number TEXT NOT NULL, -- Phone number user part (e.g., "559980000001")
created_at INTEGER NOT NULL, -- Unix timestamp when mapping was first learned
learning_source TEXT NOT NULL, -- Source of the mapping (usync, peer_pn_message, etc.)
updated_at INTEGER NOT NULL, -- Unix timestamp of last update
device_id INTEGER NOT NULL, -- Device ID for multi-account support
PRIMARY KEY (lid, device_id),
FOREIGN KEY(device_id) REFERENCES device(id) ON DELETE CASCADE
);
-- Index for reverse lookup (phone number -> LID)
CREATE INDEX idx_lid_pn_mapping_phone ON lid_pn_mapping(phone_number, device_id);
CREATE TABLE lid_pn_mapping (
lid TEXT NOT NULL, -- LID user part (e.g., "100000012345678")
phone_number TEXT NOT NULL, -- Phone number user part (e.g., "559980000001")
created_at INTEGER NOT NULL, -- Unix timestamp when mapping was first learned
learning_source TEXT NOT NULL, -- Source of the mapping (usync, peer_pn_message, etc.)
updated_at INTEGER NOT NULL, -- Unix timestamp of last update
device_id INTEGER NOT NULL, -- Device ID for multi-account support
PRIMARY KEY (lid, device_id),
FOREIGN KEY(device_id) REFERENCES device(id) ON DELETE CASCADE
);
-- Index for reverse lookup (phone number -> LID)
CREATE INDEX idx_lid_pn_mapping_phone_updated
ON lid_pn_mapping(phone_number, device_id, updated_at DESC);
🤖 Prompt for AI Agents
In
storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/up.sql
around lines 4-16, the current index idx_lid_pn_mapping_phone(phone_number,
device_id) does not guarantee efficient selection of the “most recent” mapping
when multiple rows exist per phone; either enforce a 1:1 mapping by adding a
UNIQUE(phone_number, device_id) constraint to the table definition or add an
index that covers ordering by timestamp (e.g., a composite index including
updated_at such as ON (phone_number, device_id, updated_at)) so ORDER BY
updated_at DESC can be satisfied by the index — choose and apply one of these
fixes and update migration text accordingly.

Comment thread storages/sqlite-storage/src/sqlite_store.rs Outdated
Comment on lines +21 to +111
/// Creates a TLS connector based on feature flags
fn create_tls_connector() -> Connector {
// Install rustls crypto provider (only once)
CRYPTO_PROVIDER_INIT.call_once(|| {
let _ = rustls::crypto::ring::default_provider().install_default();
});

#[cfg(feature = "danger-skip-tls-verify")]
{
use std::sync::Arc as StdArc;
use tokio_rustls::TlsConnector;

warn!("TLS certificate verification is DISABLED - this is insecure!");

// Create a custom verifier that accepts any certificate
#[derive(Debug)]
struct NoVerifier;

impl rustls::client::danger::ServerCertVerifier for NoVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}

fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
{
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}

fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
{
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}

fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::ED25519,
]
}
}

let config = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(StdArc::new(NoVerifier))
.with_no_client_auth();

let tls_connector = TlsConnector::from(StdArc::new(config));
Connector::Rustls(tls_connector)
}

#[cfg(not(feature = "danger-skip-tls-verify"))]
{
use std::sync::Arc as StdArc;
use tokio_rustls::TlsConnector;

let mut root_store = rustls::RootCertStore::empty();
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());

let config = rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();

let tls_connector = TlsConnector::from(StdArc::new(config));
Connector::Rustls(tls_connector)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add additional friction to danger-skip-tls-verify and confirm rustls version compatibility.

The feature-gating provides a first layer of protection, but this accepts any certificate without verification—a high-risk operation. Consider restricting activation further by:

  • Adding cfg(any(test, debug_assertions)) in addition to the feature flag, or
  • Requiring an explicit runtime opt-in (environment variable).

This prevents accidental insecure releases in production builds. Additionally, confirm this implementation compiles and functions correctly with your pinned versions of rustls, tokio-rustls, and webpki-roots. While the ServerCertVerifier trait and RootCertStore APIs are relatively stable, version mismatches can still surface issues. Verify against your specific dependency lock.

🤖 Prompt for AI Agents
transports/tokio-transport/src/lib.rs lines 21-111: the danger-skip-tls-verify
block currently unconditionally enables an insecure verifier by feature flag
alone; restrict activation by either making the compile-time gate stricter
(e.g., require both the feature and test/debug_assertions) or require an
explicit runtime opt-in (check an env var and refuse/emit a loud warning unless
set), and then ensure the custom ServerCertVerifier and RootCertStore usage
matches the exact rustls/tokio-rustls/webpki-roots versions in Cargo.lock
(adjust trait method names and types or imports if needed) so the code compiles
against your pinned dependencies.

Comment on lines +236 to +263
///
/// # Arguments
/// * `patch` - The patch to validate
/// * `state` - The hash state AFTER applying the patch mutations
/// * `keys` - The expanded app state keys for MAC computation
/// * `collection_name` - The collection name
/// * `had_no_prior_state` - If true, skip ALL MAC validation. This should be true
/// when processing patches without a prior local state (e.g., first sync without snapshot).
/// WhatsApp Web handles this case by throwing a retryable error ("empty lthash"), but we
/// can safely skip validation and process the mutations for usability. The state will be
/// corrected on the next proper sync with a snapshot.
pub fn validate_patch_macs(
patch: &wa::SyncdPatch,
state: &HashState,
keys: &ExpandedAppStateKeys,
collection_name: &str,
had_no_prior_state: bool,
) -> Result<(), AppStateError> {
// Skip ALL MAC validation if we had no prior state.
// When we receive patches without a snapshot for a never-synced collection,
// WhatsApp Web throws a retryable "empty lthash" error. We can't properly validate
// either the snapshotMac (computed from wrong baseline) or the patchMac (which
// includes the snapshotMac). Instead, we process the mutations and rely on
// future syncs with snapshots to correct the state.
if had_no_prior_state {
return Ok(());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t skip patch_mac validation when had_no_prior_state

You can’t validate snapshot_mac against a missing baseline, but you can still validate patch_mac (it authenticates the patch + its snapshot_mac field). Skipping it reduces integrity even with validate_macs=true (e.g., dropped/reordered mutations won’t be detected at the patch level).

Suggested change (skip only the snapshot-mac check; keep patch-mac check):

 pub fn validate_patch_macs(
     patch: &wa::SyncdPatch,
     state: &HashState,
     keys: &ExpandedAppStateKeys,
     collection_name: &str,
     had_no_prior_state: bool,
 ) -> Result<(), AppStateError> {
-    // Skip ALL MAC validation if we had no prior state.
-    // When we receive patches without a snapshot for a never-synced collection,
-    // WhatsApp Web throws a retryable "empty lthash" error. We can't properly validate
-    // either the snapshotMac (computed from wrong baseline) or the patchMac (which
-    // includes the snapshotMac). Instead, we process the mutations and rely on
-    // future syncs with snapshots to correct the state.
-    if had_no_prior_state {
-        return Ok(());
-    }
-
-    if let Some(snap_mac) = patch.snapshot_mac.as_ref() {
-        let computed_snap = state.generate_snapshot_mac(collection_name, &keys.snapshot_mac);
-        if computed_snap != *snap_mac {
-            return Err(AppStateError::PatchSnapshotMACMismatch);
-        }
-    }
+    // If we have no baseline state, we can't validate snapshot_mac against our local state,
+    // but we can still validate patch_mac (authenticates the patch contents).
+    if !had_no_prior_state {
+        if let Some(snap_mac) = patch.snapshot_mac.as_ref() {
+            let computed_snap = state.generate_snapshot_mac(collection_name, &keys.snapshot_mac);
+            if computed_snap != *snap_mac {
+                return Err(AppStateError::PatchSnapshotMACMismatch);
+            }
+        }
+    }
 
     if let Some(patch_mac) = patch.patch_mac.as_ref() {
         let version = patch.version.as_ref().and_then(|v| v.version).unwrap_or(0);
         let computed_patch = generate_patch_mac(patch, collection_name, &keys.patch_mac, version);
         if computed_patch != *patch_mac {
             return Err(AppStateError::PatchMACMismatch);
         }
     }
 
     Ok(())
 }

Also applies to: 271-277

🤖 Prompt for AI Agents
In wacore/appstate/src/processor.rs around lines 236-263 (and also update lines
271-277), don’t return early when had_no_prior_state; instead only skip
snapshot_mac validation but still compute and verify the patch_mac. Replace the
current if had_no_prior_state { return Ok(()) } with a boolean (e.g.,
skip_snapshot_mac_check = had_no_prior_state) and use that flag where
snapshot_mac is validated to bypass just that check; ensure the patch_mac path
always runs (compute expected patch_mac from the patch, state, and keys and
compare it to the provided patch_mac, returning an error on mismatch). Update
the later block at 271-277 to remove any early-exit that skips patch_mac
validation and use the new flag to conditionally skip only the snapshot_mac
verification.

Comment thread wacore/src/store/traits.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: 7

Caution

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

⚠️ Outside diff range comments (3)
storages/sqlite-storage/src/sqlite_store.rs (1)

787-839: Fire-and-forget identity/session writes lack graceful shutdown; validate application-level durability guarantees.

put_identity_for_device and put_session_for_device (and put_session_for_device at 918–968) spawn background DB writes and return Ok(()) immediately without awaiting completion. Errors are silently logged. Additionally:

  • No Drop impl or graceful shutdown mechanism waits for spawned background tasks; pending writes are lost if the process exits before the tasks complete.
  • The comment "Update cache first to keep the hot path off disk" is misleading—SqliteStore has no cache field; the store queries the database directly on every read.
  • PRAGMA synchronous = NORMAL means writes are not immediately durable to disk; with PRAGMA journal_mode = WAL, durability depends on checkpoint timing.

Confirm that the application has explicit shutdown handling to flush pending writes (e.g., draining tokio task joiners or ensuring sufficient delay before process exit) and that callers do not assume writes are persisted immediately after the method returns.

src/client.rs (1)

1646-1650: is_connected() can return false spuriously due to try_lock() contention.
Because noise_socket is a tokio::sync::Mutex, try_lock() fails if any task is holding it briefly (e.g., send_node), which can cause post-login tasks to incorrectly treat the client as disconnected.

Suggested direction: track connection state via an AtomicBool (set when noise_socket is set/cleared), or make is_connected async and use lock().await.

src/message.rs (1)

362-408: The unwrap_err() pattern is idiomatic for rand 0.9 but violates error handling guidelines. The pattern let rng = rand::rngs::OsRng; ... &mut rng.unwrap_err() is the documented, correct approach in rand 0.9 to convert TryRngCore to RngCore for APIs requiring the infallible trait. However, unwrap_err() will silently panic if OS RNG fails, which violates the guideline to avoid .unwrap() and .expect() outside of truly unrecoverable paths. OS RNG failure in this async context is not necessarily unrecoverable—consider either handling the error explicitly or documenting why panicking is acceptable here.

🧹 Nitpick comments (4)
src/socket/noise_socket.rs (1)

103-109: Consider mutex contention impact with spawn_blocking.

The lock is held across the spawn_blocking call (lines 103-107) and subsequent .await. While this is necessary for correctness (to maintain counter-to-send ordering), it means large message encryption fully serializes concurrent sends—other tasks block even though the actual CPU work runs on a separate thread.

This trade-off is acceptable for correctness, but worth noting if throughput of concurrent large messages becomes a concern in the future.

wacore/src/store/traits.rs (1)

86-118: Good addition; consider deriving PartialEq/Eq for LidPnMappingEntry to ease testing and cache comparisons.

-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct LidPnMappingEntry {
src/handshake.rs (1)

28-50: Remove redundant length checks + unreachable RoutingInfoTooLarge match arm.
You already pre-check routing_info.len() > MAX_EDGE_ROUTING_LEN (Lines 65-73), so Err(HandshakeError::RoutingInfoTooLarge) from build_edge_routing_preintro is effectively unreachable (Lines 84-91). Either rely on the helper only, or keep the call-site check and make the helper infallible.

-        } else {
-            match build_edge_routing_preintro(routing_info) {
+        } else {
+            match build_edge_routing_preintro(routing_info) {
                 Ok(mut header) => {
@@
-                Err(HandshakeError::RoutingInfoTooLarge) => {
-                    warn!(
-                        target: "Client",
-                        "Routing info unexpectedly exceeds {} bytes; skipping pre-intro",
-                        MAX_EDGE_ROUTING_LEN
-                    );
-                    wacore_binary::consts::WA_CONN_HEADER.to_vec()
-                }
                 Err(err) => return Err(err),
             }
         }

Also applies to: 63-97

src/client.rs (1)

90-110: New cache/lock infrastructure is reasonable; consider reconciling with existing per-chat serialization guidance.
Given the repo guidance to serialize per-chat operations (via Client::chat_locks), please double-check the new message_queues/message_enqueue_locks strategy doesn’t diverge in a way that can deadlock with other locks (sender/session/chat).

Also applies to: 201-213

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 132a591 and 8ec84e9.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • src/client.rs (33 hunks)
  • src/handshake.rs (3 hunks)
  • src/message.rs (9 hunks)
  • src/pair.rs (6 hunks)
  • src/socket/noise_socket.rs (4 hunks)
  • src/usync.rs (2 hunks)
  • storages/sqlite-storage/Cargo.toml (1 hunks)
  • storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/down.sql (1 hunks)
  • storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/up.sql (1 hunks)
  • storages/sqlite-storage/src/schema.rs (3 hunks)
  • storages/sqlite-storage/src/sqlite_store.rs (22 hunks)
  • wacore/src/store/traits.rs (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/down.sql
  • src/usync.rs
  • storages/sqlite-storage/Cargo.toml
  • storages/sqlite-storage/src/schema.rs
  • storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/up.sql
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: All I/O in the main crate must use Tokio; be mindful of race conditions
Wrap blocking I/O (e.g., ureq) and heavy CPU-bound tasks (e.g., media encryption) in tokio::task::spawn_blocking
Avoid .unwrap() and .expect() outside of tests and truly unrecoverable paths

Files:

  • src/pair.rs
  • src/socket/noise_socket.rs
  • src/handshake.rs
  • src/message.rs
  • src/client.rs
{src,wacore}/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

{src,wacore}/**/*.rs: Use thiserror for custom error types (e.g., SocketError)
Use anyhow::Error for functions with multiple failure modes

Files:

  • src/pair.rs
  • src/socket/noise_socket.rs
  • wacore/src/store/traits.rs
  • src/handshake.rs
  • src/message.rs
  • src/client.rs
wacore/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases

Files:

  • wacore/src/store/traits.rs
src/{client,send,message}.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use Client::chat_locks to serialize per-chat operations

Files:

  • src/message.rs
  • src/client.rs
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: The whatsapp-rust (main) crate integrates wacore with Tokio for async and Diesel for SQLite persistence

Applied to files:

  • src/pair.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations

Applied to files:

  • src/socket/noise_socket.rs
  • src/message.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/**/*.rs : All I/O in the main crate must use Tokio; be mindful of race conditions

Applied to files:

  • src/socket/noise_socket.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to wacore/**/*.rs : wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases

Applied to files:

  • src/socket/noise_socket.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • src/client.rs
🧬 Code graph analysis (2)
wacore/src/store/traits.rs (2)
storages/sqlite-storage/src/sqlite_store.rs (5)
  • get_lid_pn_mapping_by_lid (2116-2121)
  • get_lid_pn_mapping_by_phone (2123-2128)
  • put_lid_pn_mapping (2130-2132)
  • get_all_lid_pn_mappings (2134-2136)
  • delete_lid_pn_mapping (2138-2140)
src/appstate_sync.rs (5)
  • get_lid_pn_mapping_by_lid (522-527)
  • get_lid_pn_mapping_by_phone (529-534)
  • put_lid_pn_mapping (536-541)
  • get_all_lid_pn_mappings (543-547)
  • delete_lid_pn_mapping (549-551)
storages/sqlite-storage/src/sqlite_store.rs (1)
wacore/src/store/traits.rs (5)
  • get_lid_pn_mapping_by_lid (105-105)
  • get_lid_pn_mapping_by_phone (108-108)
  • put_lid_pn_mapping (111-111)
  • get_all_lid_pn_mappings (114-114)
  • delete_lid_pn_mapping (117-117)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build & Test
🔇 Additional comments (12)
src/socket/noise_socket.rs (1)

20-24: Well-designed concurrency control for frame ordering.

The mutex addition with clear documentation correctly addresses the race condition where concurrent encrypt_and_send calls could result in frames being sent out-of-order relative to their counter values. Using tokio::sync::Mutex is appropriate for async code per project guidelines.

Also applies to: 35-35

wacore/src/store/traits.rs (2)

86-118: Verify wacore no_std compatibility for the new serde/async_trait usage (alloc-backed String fields).

This file stays “no Tokio / no DB”, but serde derives + String/Vec require the crate feature set to be correct for no_std + alloc. Please confirm wacore still compiles in its no_std configuration after this change (feature flags for serde/alloc, etc.). As per coding guidelines, wacore must remain no_std-compatible.


149-178: Backend bound update looks correct; ensure downstream backends implement LidPnMappingStore in multi-account scenarios.

Given DevicePersistence has explicit *_for_device APIs but LidPnMappingStore does not, please sanity-check the intended scoping (single-device-only vs “current device”) for multi-account mode to avoid cross-device mapping leakage.

storages/sqlite-storage/src/sqlite_store.rs (4)

55-82: Enabling PRAGMA foreign_keys = ON per connection is the right move.


165-188: with_semaphore pattern looks fine (permits held across blocking work).


213-305: edge_routing_info persistence wiring looks consistent across save/load/create paths.

Please double-check the schema/migrations for device.edge_routing_info (type/nullability) match Option<Vec<u8>> end-to-end (Diesel schema + migration).

Also applies to: 307-396, 398-500, 688-784, 536-555


1921-2140: LID↔PN mapping helpers + LidPnMappingStore impl look consistent (device_id defaults to 1).

src/handshake.rs (1)

10-24: Good: panic/DoS risk replaced with typed error + size limit.
This looks like a solid fix vs the prior assert! approach, and the MAX_EDGE_ROUTING_LEN guard is clear.

src/pair.rs (1)

26-36: Server-JID gate looks correct with new Node attrs access.
The attrs.get(...).map(|s| s.as_str()) pattern reads cleanly and avoids panics.

src/message.rs (2)

914-926: DM sender_alt extraction by server type looks correct.
Using sender_pn for LID senders and sender_lid for PN senders is a clean split and should help session resolution.


350-359: Moka single-flight initialization and Jid key choice are sound; however, verify the locking strategy aligns with architecture.

Moka 0.12.11 guarantees that Cache::get_with() coalesces concurrent initialization calls per key into a single execution, preventing redundant mutex creation. The Jid type derives Hash and Eq using all fields (user, server, agent, device, integrator), so lock granularity matches sender identity precisely.

One clarification: AGENTS.md references Client::chat_locks for per-chat operations, but the actual per-chat message ordering is handled by message_queues in src/handlers/message.rs, while session_locks serializes Signal protocol operations per sender/recipient. Confirm this two-level locking pattern (per-chat message queues + per-sender Signal operations) is the intended design.

src/client.rs (1)

544-565: Concurrent node processing after sequential decryption is sensible—verify ordering assumptions.
Spawning non-critical stanza handlers can change inter-stanza ordering vs prior behavior; please verify that any handlers relying on ordering are either “critical” or internally serialized (e.g., per-chat/per-sender).

Comment thread src/client.rs
Comment on lines +298 to 327
/// Add a LID-PN mapping to both the in-memory cache and persistent storage.
/// This is called when we learn about a mapping from messages, usync, etc.
pub(crate) async fn add_lid_pn_mapping(
&self,
lid: &str,
phone_number: &str,
source: LearningSource,
) -> Result<()> {
use wacore::store::traits::LidPnMappingEntry;

// Add to in-memory cache
let entry = LidPnEntry::new(lid.to_string(), phone_number.to_string(), source);
self.lid_pn_cache.add(entry.clone()).await;

// Persist to storage in background (don't block message processing)
let backend = self.persistence_manager.backend();
let storage_entry = LidPnMappingEntry {
lid: entry.lid,
phone_number: entry.phone_number,
created_at: entry.created_at,
updated_at: entry.created_at,
learning_source: entry.learning_source.as_str().to_string(),
};

backend
.put_lid_pn_mapping(&storage_entry)
.await
.map_err(|e| anyhow!("persisting LID-PN mapping: {e}"))?;
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hot-path DB write: add_lid_pn_mapping claims “background” but awaits the write.
This can put SQLite writes on the message/pairing critical path and increase “database is locked” churn. Either update the comment and accept the sync write, or actually spawn/batch persistence (and make failures best-effort).

-        // Persist to storage in background (don't block message processing)
+        // Persist to storage (NOTE: this currently awaits; consider spawning if this is hot-path)
         let backend = self.persistence_manager.backend();
@@
-        backend
-            .put_lid_pn_mapping(&storage_entry)
-            .await
-            .map_err(|e| anyhow!("persisting LID-PN mapping: {e}"))?;
+        backend
+            .put_lid_pn_mapping(&storage_entry)
+            .await
+            .map_err(|e| anyhow!("persisting LID-PN mapping: {e}"))?;

(If you want the true “background” behavior, I can sketch a minimal queue-based persister that preserves ordering and de-dupes.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Add a LID-PN mapping to both the in-memory cache and persistent storage.
/// This is called when we learn about a mapping from messages, usync, etc.
pub(crate) async fn add_lid_pn_mapping(
&self,
lid: &str,
phone_number: &str,
source: LearningSource,
) -> Result<()> {
use wacore::store::traits::LidPnMappingEntry;
// Add to in-memory cache
let entry = LidPnEntry::new(lid.to_string(), phone_number.to_string(), source);
self.lid_pn_cache.add(entry.clone()).await;
// Persist to storage in background (don't block message processing)
let backend = self.persistence_manager.backend();
let storage_entry = LidPnMappingEntry {
lid: entry.lid,
phone_number: entry.phone_number,
created_at: entry.created_at,
updated_at: entry.created_at,
learning_source: entry.learning_source.as_str().to_string(),
};
backend
.put_lid_pn_mapping(&storage_entry)
.await
.map_err(|e| anyhow!("persisting LID-PN mapping: {e}"))?;
Ok(())
}
/// Add a LID-PN mapping to both the in-memory cache and persistent storage.
/// This is called when we learn about a mapping from messages, usync, etc.
pub(crate) async fn add_lid_pn_mapping(
&self,
lid: &str,
phone_number: &str,
source: LearningSource,
) -> Result<()> {
use wacore::store::traits::LidPnMappingEntry;
// Add to in-memory cache
let entry = LidPnEntry::new(lid.to_string(), phone_number.to_string(), source);
self.lid_pn_cache.add(entry.clone()).await;
// Persist to storage (NOTE: this currently awaits; consider spawning if this is hot-path)
let backend = self.persistence_manager.backend();
let storage_entry = LidPnMappingEntry {
lid: entry.lid,
phone_number: entry.phone_number,
created_at: entry.created_at,
updated_at: entry.created_at,
learning_source: entry.learning_source.as_str().to_string(),
};
backend
.put_lid_pn_mapping(&storage_entry)
.await
.map_err(|e| anyhow!("persisting LID-PN mapping: {e}"))?;
Ok(())
}

Comment thread src/message.rs
Comment on lines +64 to 177
// Determine the JID to use for end-to-end decryption.
//
// CRITICAL: WhatsApp Web ALWAYS uses LID-based addresses for Signal sessions when
// a LID mapping is known. This is implemented in WAWebSignalAddress.toString():
//
// var n = o("WAWebWidFactory").asUserWidOrThrow(this.wid);
// var a = !n.isLid() && n.isUser(); // true if PN
// var i = a ? o("WAWebApiContact").getCurrentLid(n) : n; // Get LID if PN
// if (i == null) {
// return [this.wid.user, t, "@c.us"].join(""); // No LID, use PN
// } else {
// return [i.user, t, "@lid"].join(""); // Use LID
// }
//
// This means sessions are stored under the LID address, not the PN address.
// When we receive a PN-addressed message, we must look up the session using
// the LID address (if a LID mapping is known) to match WhatsApp Web's behavior.
let sender_encryption_jid = {
let sender = &info.source.sender;
let alt = info.source.sender_alt.as_ref();
let pn_server = wacore_binary::jid::DEFAULT_USER_SERVER;
let lid_server = wacore_binary::jid::HIDDEN_USER_SERVER;

if sender.server == lid_server {
if let Some(alt_jid) = alt {
if alt_jid.server == pn_server {
alt_jid.clone()
} else {
// Alt is another LID variant; stick with the original LID sender.
sender.clone()
// Sender is already LID - use it directly for session lookup.
// Also cache the LID-to-PN mapping if PN alt is available.
if let Some(alt_jid) = alt
&& alt_jid.server == pn_server
{
if let Err(err) = self
.add_lid_pn_mapping(
&sender.user,
&alt_jid.user,
crate::lid_pn_cache::LearningSource::PeerLidMessage,
)
.await
{
warn!(
"Failed to persist LID-to-PN mapping {} -> {}: {err}",
sender.user, alt_jid.user
);
}
} else if info.source.is_from_me {
// Self-sent LID message without PN alt — try to fall back to our PN identity.
if let Some(own_pn) = self.get_pn().await {
log::debug!(
"Self-sent message from LID {}, using own phone number {}:{} for decryption",
sender,
own_pn.user,
sender.device
debug!(
"Cached LID-to-PN mapping: {} -> {}",
sender.user, alt_jid.user
);
}
sender.clone()
} else if sender.server == pn_server {
// Sender is PN - check if we have a LID mapping.
// WhatsApp Web uses LID for sessions when available.

// First, cache/update the mapping if sender_lid attribute is present
if let Some(alt_jid) = alt
&& alt_jid.server == lid_server
{
if let Err(err) = self
.add_lid_pn_mapping(
&alt_jid.user,
&sender.user,
crate::lid_pn_cache::LearningSource::PeerPnMessage,
)
.await
{
warn!(
"Failed to persist PN-to-LID mapping {} -> {}: {err}",
sender.user, alt_jid.user
);
Jid {
user: own_pn.user,
server: own_pn.server,
agent: own_pn.agent,
device: sender.device,
integrator: own_pn.integrator,
}
} else {
log::warn!("Self-sent message from LID but own phone number not available");
sender.clone()
}
debug!(
"Cached PN-to-LID mapping: {} -> {}",
sender.user, alt_jid.user
);

// Use the LID from the message attribute for session lookup
let lid_jid = Jid {
user: alt_jid.user.clone(),
server: lid_server.to_string(),
device: sender.device,
agent: sender.agent,
integrator: sender.integrator,
};
log::debug!(
"Using LID {} for session lookup (sender was PN {})",
lid_jid,
sender
);
lid_jid
} else if let Some(lid_user) = self.lid_pn_cache.get_current_lid(&sender.user).await
{
// No sender_lid attribute, but we have a cached LID mapping
let lid_jid = Jid {
user: lid_user.clone(),
server: lid_server.to_string(),
device: sender.device,
agent: sender.agent,
integrator: sender.integrator,
};
log::debug!(
"Using cached LID {} for session lookup (sender was PN {})",
lid_jid,
sender
);
lid_jid
} else {
// No PN alt provided and not self-sent. Keep the original LID sender.
// No LID mapping known - use PN address
log::debug!("No LID mapping for {}, using PN for session lookup", sender);
sender.clone()
}
} else {
// Sender already uses PN (or another stable server). Never upgrade to LID.
// Other server type (e.g., bot, hosted) - use as-is
sender.clone()
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix misleading “Cached mapping” debug logs (they run even on persistence failure).
Both branches log “Cached … mapping” unconditionally after add_lid_pn_mapping even if it returned Err (Lines 93-110, 120-137). Gate the debug log on success.

-                    if let Err(err) = self
+                    match self
                         .add_lid_pn_mapping(
@@
-                        .await
-                    {
-                        warn!(
-                            "Failed to persist LID-to-PN mapping {} -> {}: {err}",
-                            sender.user, alt_jid.user
-                        );
-                    }
-                    debug!(
-                        "Cached LID-to-PN mapping: {} -> {}",
-                        sender.user, alt_jid.user
-                    );
+                        .await
+                    {
+                        Ok(()) => debug!("Cached LID-to-PN mapping: {} -> {}", sender.user, alt_jid.user),
+                        Err(err) => warn!("Failed to persist LID-to-PN mapping {} -> {}: {err}", sender.user, alt_jid.user),
+                    }
🤖 Prompt for AI Agents
In src/message.rs around lines 64 to 177, the debug logs that say "Cached
LID-to-PN mapping" and "Cached PN-to-LID mapping" run even when
add_lid_pn_mapping returned Err; change the flow to only emit those debug logs
on success: capture the Result from add_lid_pn_mapping, if it is Ok(...) then
call debug!(...) to log the cached mapping, otherwise keep the existing
warn!(...) for the Err path and do not log the success message; ensure both
similar branches (the LID branch and the PN branch) follow this pattern so the
debug message is gated on successful persistence.

Comment thread src/pair.rs
Comment on lines +226 to +244
// Add the own LID-PN mapping to the cache so that when sending DMs to self,
// we can find the existing LID-based session instead of creating a new PN-based one.
// This is critical for self-messaging to work correctly.
if !jid.user.is_empty() && !lid.user.is_empty() {
if let Err(err) = client
.add_lid_pn_mapping(&lid.user, &jid.user, LearningSource::Pairing)
.await
{
warn!(
"Failed to persist own LID-PN mapping {} <-> {}: {err}",
lid.user, jid.user
);
} else {
info!(
"Added own LID-PN mapping to cache: {} <-> {}",
lid.user, jid.user
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid logging full phone/LID identifiers (PII) in pairing logs.
The warn!/info! messages currently include lid.user and jid.user verbatim. Consider masking (e.g., last 2–4 digits) or logging only that a mapping was stored.

-                    warn!(
-                        "Failed to persist own LID-PN mapping {} <-> {}: {err}",
-                        lid.user, jid.user
-                    );
+                    warn!("Failed to persist own LID-PN mapping (redacted): {err}");
@@
-                    info!(
-                        "Added own LID-PN mapping to cache: {} <-> {}",
-                        lid.user, jid.user
-                    );
+                    info!("Added own LID-PN mapping to cache (redacted)");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Add the own LID-PN mapping to the cache so that when sending DMs to self,
// we can find the existing LID-based session instead of creating a new PN-based one.
// This is critical for self-messaging to work correctly.
if !jid.user.is_empty() && !lid.user.is_empty() {
if let Err(err) = client
.add_lid_pn_mapping(&lid.user, &jid.user, LearningSource::Pairing)
.await
{
warn!(
"Failed to persist own LID-PN mapping {} <-> {}: {err}",
lid.user, jid.user
);
} else {
info!(
"Added own LID-PN mapping to cache: {} <-> {}",
lid.user, jid.user
);
}
}
// Add the own LID-PN mapping to the cache so that when sending DMs to self,
// we can find the existing LID-based session instead of creating a new PN-based one.
// This is critical for self-messaging to work correctly.
if !jid.user.is_empty() && !lid.user.is_empty() {
if let Err(err) = client
.add_lid_pn_mapping(&lid.user, &jid.user, LearningSource::Pairing)
.await
{
warn!("Failed to persist own LID-PN mapping (redacted): {err}");
} else {
info!("Added own LID-PN mapping to cache (redacted)");
}
}
🤖 Prompt for AI Agents
In src/pair.rs around lines 226 to 244, the warn! and info! log calls print full
PII (lid.user and jid.user); change them to avoid logging raw phone/LID values
by masking or redacting the identifiers before logging (e.g., derive a safe
string that keeps only the last 2–4 characters or replace with "<redacted>" or a
short hash). Create a small helper or inline logic to produce masked_lid and
masked_jid, use those in both warn! and info! messages, and ensure no other code
paths in this block log the raw values.

Comment thread src/socket/noise_socket.rs Outdated
Comment thread storages/sqlite-storage/src/sqlite_store.rs Outdated
Comment thread storages/sqlite-storage/src/sqlite_store.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)
Cargo.toml (1)

35-43: Document danger-skip-tls-verify feature in README to prevent production misuse

The feature has solid code-level guardrails (proper #[cfg] gating, not in defaults, and a runtime warn!() log), but it's not mentioned in the README. Add a prominent warning in both the main README and the tokio-transport crate README documenting the feature's testing-only purpose and the runtime warning that fires when enabled.

Suggested addition to README.md or a "Features" section:

### Danger: TLS Certificate Verification Bypass

The `danger-skip-tls-verify` feature disables TLS certificate verification for testing with mock servers only. **Do not use in production.** When enabled, a `WARN` log message is emitted at startup.
storages/sqlite-storage/src/sqlite_store.rs (1)

787-839: Update misleading comment about cache behavior and document fire-and-forget durability semantics

The comment "Update cache first to keep the hot path off disk" is inaccurate—this function has no cache mechanism. The actual behavior is that put_identity_for_device returns Ok(()) before the database write completes (fire-and-forget via tokio::spawn), which differs from the synchronous semantics of read operations like load_identity_for_device that use with_semaphore(...).await. This durability trade-off should be documented clearly to prevent callers from assuming writes are durable on return.

Suggested change:

-        // Update cache first to keep the hot path off disk
+        // Fire-and-forget write: returns before DB persistence completes

Consider documenting this fire-and-forget semantics at the function level or offering durable variants if callers require guaranteed durability before return.

Also applies to: put_session_for_device (918-968)

♻️ Duplicate comments (2)
storages/sqlite-storage/src/sqlite_store.rs (2)

89-96: Pool/semaphore mismatch + misleading comments
pool_size = 32 but db_semaphore is 4, while comments imply they “match” / “reduce lock contention” in a way that no longer describes reality.

-            .max_size(pool_size) // Limit concurrent connections to reduce memory and lock contention
+            .max_size(pool_size) // Allow up to `pool_size` conns; concurrency is gated by `db_semaphore`

-            db_semaphore: Arc::new(tokio::sync::Semaphore::new(4)), // Match pool max_size
+            db_semaphore: Arc::new(tokio::sync::Semaphore::new(4)), // Limit concurrent DB ops

Also applies to: 120-123


1963-2003: LID↔PN persistence: stale comment + created_at overwritten on update
Comment says “created_at DESC” but the query orders by updated_at.desc(). Also, upsert updates created_at, which breaks “first learned” semantics unless that’s intended.

-            // Get the most recent mapping for this phone number (by created_at DESC)
+            // Get the most recent mapping for this phone number (by updated_at DESC)
...
                 .on_conflict((lid_pn_mapping::lid, lid_pn_mapping::device_id))
                 .do_update()
                 .set((
                     lid_pn_mapping::phone_number.eq(&phone_number),
-                    lid_pn_mapping::created_at.eq(created_at),
                     lid_pn_mapping::learning_source.eq(&learning_source),
                     lid_pn_mapping::updated_at.eq(now),
                 ))

Also applies to: 2005-2051

🧹 Nitpick comments (2)
storages/sqlite-storage/src/sqlite_store.rs (2)

213-305: Semaphore is bypassed on device upserts (can reintroduce lock contention)
save_device_data{,_for_device} runs blocking writes without with_semaphore / db_semaphore, so these can run at full pool concurrency even though other hot paths are gated. Consider routing these through with_semaphore for consistency.

Also applies to: 307-396


841-862: Deletes should probably be semaphore-gated too
delete_identity_for_device / delete_session_for_device don’t use with_semaphore, which can still contribute to “database is locked” under load.

Also applies to: 970-991

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ec84e9 and b7a1dde.

📒 Files selected for processing (3)
  • Cargo.toml (1 hunks)
  • src/socket/noise_socket.rs (3 hunks)
  • storages/sqlite-storage/src/sqlite_store.rs (22 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/socket/noise_socket.rs
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: The whatsapp-rust (main) crate integrates wacore with Tokio for async and Diesel for SQLite persistence

Applied to files:

  • Cargo.toml
  • storages/sqlite-storage/src/sqlite_store.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to waproto/src/whatsapp.proto : Protocol Buffers definitions reside in waproto/src/whatsapp.proto

Applied to files:

  • Cargo.toml
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to waproto/build.rs : In waproto, use prost in build.rs to compile Protocol Buffers into Rust structs

Applied to files:

  • Cargo.toml
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to wacore/**/*.rs : wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases

Applied to files:

  • Cargo.toml
  • storages/sqlite-storage/src/sqlite_store.rs
🧬 Code graph analysis (1)
storages/sqlite-storage/src/sqlite_store.rs (2)
storages/sqlite-storage/src/device_aware_store.rs (1)
  • new (21-23)
wacore/src/store/traits.rs (5)
  • get_lid_pn_mapping_by_lid (105-105)
  • get_lid_pn_mapping_by_phone (108-108)
  • put_lid_pn_mapping (111-111)
  • get_all_lid_pn_mappings (114-114)
  • delete_lid_pn_mapping (117-117)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build & Test
🔇 Additional comments (1)
storages/sqlite-storage/src/sqlite_store.rs (1)

55-82: Good: enforce PRAGMA foreign_keys = ON per connection
This is the right place to do it (SQLite FK enforcement is per-connection), and you’re propagating failures so broken connections don’t enter the pool.

Comment thread storages/sqlite-storage/src/sqlite_store.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: 0

Caution

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

⚠️ Outside diff range comments (2)
src/client.rs (1)

560-635: Unbounded tokio::spawn per stanza risks runaway memory/CPU under floods.
Non-critical nodes are spawned without any concurrency limit/backpressure. A receipt/notification flood can create thousands of tasks and starve the runtime. Consider a bounded work queue / Semaphore / JoinSet with max in-flight tasks, or route only specific heavy handlers into spawned tasks while processing others inline.

src/message.rs (1)

366-412: Fix RNG initialization to avoid creating temporary wrappers on each use.

Line 366 should initialize the RNG once as a mutable variable: let mut rng = rand::rngs::OsRng.unwrap_err();, then pass &mut rng to message_decrypt at lines 409 and 485, rather than creating new temporary wrappers with &mut rng.unwrap_err() on each call.

-        let rng = rand::rngs::OsRng;
+        let mut rng = rand::rngs::OsRng.unwrap_err();

Then replace both occurrences of &mut rng.unwrap_err() with &mut rng:

  • Line 409: &mut rng
  • Line 485: &mut rng
♻️ Duplicate comments (5)
storages/sqlite-storage/src/sqlite_store.rs (1)

2042-2042: Stale comment and created_at mutation on update remain unaddressed.

Two issues from previous reviews persist:

  1. Line 2042: Comment says "by created_at DESC" but Line 2053 orders by updated_at.desc().
  2. Line 2108: created_at is updated on conflict, contradicting "time of first creation" semantics.

Apply this diff to fix both issues:

-            // Get the most recent mapping for this phone number (by created_at DESC)
+            // Get the most recent mapping for this phone number (by updated_at DESC)

                 .on_conflict((lid_pn_mapping::lid, lid_pn_mapping::device_id))
                 .do_update()
                 .set((
                     lid_pn_mapping::phone_number.eq(&phone_number),
-                    lid_pn_mapping::created_at.eq(created_at),
                     lid_pn_mapping::learning_source.eq(&learning_source),
                     lid_pn_mapping::updated_at.eq(now),
                 ))

If created_at is intended to track "time of last re-learning", rename it (e.g., last_learned_at) to avoid confusion.

Also applies to: 2053-2053, 2108-2108

src/client.rs (2)

90-116: session_locks must not be evictable (Signal mutual exclusion can break).
session_locks is a moka Cache with TTL + max_capacity, so it can evict while an old Arc<Mutex<()>> is still held, allowing a new mutex to be created for the same key and breaking exclusivity. Consider a non-evicting map (e.g., DashMap<String, Arc<Mutex<()>>>), or implement Weak-based cleanup so “in-use” locks can’t be replaced.

Also applies to: 203-216


300-329: LID↔PN persistence isn’t “background”, and cache+storage can diverge on failure.
The comment says “background”, but the code awaits the DB write. Also, you insert into the in-memory cache before persistence; if the write fails, memory now contains a mapping that storage does not. Either (a) update the comment and treat persistence as required (and consider rolling back the in-memory insert on failure), or (b) actually background the write and make it best-effort (log/metric on failure).

src/send.rs (1)

31-47: Lock key must be non-evictable + should match the actual encryption target.

  1. The session_locks cache eviction hazard still applies here (see prior notes).
  2. You lock on encryption_jid, but in the peer path you still pass to into prepare_peer_stanza(), which encrypts using to_jid.to_protocol_address() (so PN vs LID can diverge). That can reintroduce session mismatch / ratchet races. Prefer passing encryption_jid into the peer encrypt call.
-        let stanza_to_send: wacore_binary::Node = if peer && !to.is_group() {
+        let stanza_to_send: wacore_binary::Node = if peer && !to.is_group() {
             // Peer messages are only valid for individual users, not groups
             let device_store_arc = self.persistence_manager.get_device_arc().await;
             let mut store_adapter = SignalProtocolStoreAdapter::new(device_store_arc);

             wacore::send::prepare_peer_stanza(
                 &mut store_adapter.session_store,
                 &mut store_adapter.identity_store,
-                to,
+                encryption_jid,
                 message,
                 request_id,
             )
             .await?
src/message.rs (1)

64-176: “Cached mapping” debug logs still fire even when persistence fails (misleading).
This is the same issue called out previously: both mapping branches unconditionally debug!("Cached ...") after an Err from add_lid_pn_mapping (Line 93-110, Line 120-137). Gate the “Cached …” log on success.

-                    if let Err(err) = self
-                        .add_lid_pn_mapping(
-                            &sender.user,
-                            &alt_jid.user,
-                            crate::lid_pn_cache::LearningSource::PeerLidMessage,
-                        )
-                        .await
-                    {
-                        warn!(
-                            "Failed to persist LID-to-PN mapping {} -> {}: {err}",
-                            sender.user, alt_jid.user
-                        );
-                    }
-                    debug!(
-                        "Cached LID-to-PN mapping: {} -> {}",
-                        sender.user, alt_jid.user
-                    );
+                    match self
+                        .add_lid_pn_mapping(
+                            &sender.user,
+                            &alt_jid.user,
+                            crate::lid_pn_cache::LearningSource::PeerLidMessage,
+                        )
+                        .await
+                    {
+                        Ok(()) => debug!(
+                            "Cached LID-to-PN mapping: {} -> {}",
+                            sender.user, alt_jid.user
+                        ),
+                        Err(err) => warn!(
+                            "Failed to persist LID-to-PN mapping {} -> {}: {err}",
+                            sender.user, alt_jid.user
+                        ),
+                    }
-                    if let Err(err) = self
-                        .add_lid_pn_mapping(
-                            &alt_jid.user,
-                            &sender.user,
-                            crate::lid_pn_cache::LearningSource::PeerPnMessage,
-                        )
-                        .await
-                    {
-                        warn!(
-                            "Failed to persist PN-to-LID mapping {} -> {}: {err}",
-                            sender.user, alt_jid.user
-                        );
-                    }
-                    debug!(
-                        "Cached PN-to-LID mapping: {} -> {}",
-                        sender.user, alt_jid.user
-                    );
+                    match self
+                        .add_lid_pn_mapping(
+                            &alt_jid.user,
+                            &sender.user,
+                            crate::lid_pn_cache::LearningSource::PeerPnMessage,
+                        )
+                        .await
+                    {
+                        Ok(()) => debug!(
+                            "Cached PN-to-LID mapping: {} -> {}",
+                            sender.user, alt_jid.user
+                        ),
+                        Err(err) => warn!(
+                            "Failed to persist PN-to-LID mapping {} -> {}: {err}",
+                            sender.user, alt_jid.user
+                        ),
+                    }
🧹 Nitpick comments (7)
src/client.rs (3)

259-269: Warm-up task is fire-and-forget; consider test/runtime determinism hooks.
Spawning warm-up during Client::new() is fine, but it can introduce timing nondeterminism (especially in tests/benchmarks). Consider gating via config/feature flag, or exposing an async “await warm-up” hook used by tests.

Also applies to: 272-298


331-369: resolve_encryption_jid is the right direction; avoid extra clones + centralize JID construction.
Minor: lid_user.clone() is unnecessary, and manually rebuilding Jid in multiple places is easy to drift. Consider a small helper like target.with_server_user(lid_server, lid_user) (or similar) to avoid field copy mistakes.


707-758: Ack/dispatch refactor to Arc<Node> looks clean; reduce double-locking for IQ waiters.
contains_key() under lock followed by handle_iq_response() can lead to extra lock churn and a small TOCTOU window. If feasible, consider a single “try-handle-response-waiter” path that removes/handles atomically.

src/send.rs (1)

80-83: Retry cache insertion happens before encryption; verify consumers handle “failed-to-send” entries.
You cache the message bytes before stanza preparation. If encryption fails, you’ll retain a retry entry that may never be valid to retry. If that’s intended, OK; otherwise consider caching only after successful stanza build (or tagging entries with a state).

Also applies to: 279-282

src/message.rs (3)

350-363: Per-sender protocol-address lock is a solid race fix; verify lock ordering + bounded growth.
Using the full protocol address string for the lock key should help avoid ratchet counter races (Line 350-363). Two follow-ups:

  1. Ensure you don’t deadlock with other locks (notably the per-chat serialization guidance for src/{client,send,message}.rs). Consider documenting the intended lock ordering if both can be held. Based on learnings, use Client::chat_locks to serialize per-chat operations.
  2. Confirm self.session_locks has TTL/eviction; otherwise this can grow unbounded in long-running bots.

445-590: UntrustedIdentity path is safer now; one comment is misleading.
Behavior change (clear identity, keep session so libsignal can archive old state) looks right (Line 445-590). Minor: the retry-path comment says “we cleared the session” (Line 515-517) but the code clears identity, not the session.

-                                // The first attempt triggered UntrustedIdentity, we cleared the session, but meanwhile
+                                // The first attempt triggered UntrustedIdentity, we cleared the old identity, but meanwhile

2534-3108: Test coverage for LID↔PN cache population and PN→LID session lookup behavior is strong.
Only suggestion: there’s a lot of repeated node construction + “encryption JID” derivation; consider a small test helper to reduce duplication and make future format changes cheaper.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50f942e and 0ab77b3.

📒 Files selected for processing (4)
  • src/client.rs (33 hunks)
  • src/message.rs (9 hunks)
  • src/send.rs (6 hunks)
  • storages/sqlite-storage/src/sqlite_store.rs (24 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: All I/O in the main crate must use Tokio; be mindful of race conditions
Wrap blocking I/O (e.g., ureq) and heavy CPU-bound tasks (e.g., media encryption) in tokio::task::spawn_blocking
Avoid .unwrap() and .expect() outside of tests and truly unrecoverable paths

Files:

  • src/message.rs
  • src/send.rs
  • src/client.rs
src/{client,send,message}.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use Client::chat_locks to serialize per-chat operations

Files:

  • src/message.rs
  • src/send.rs
  • src/client.rs
{src,wacore}/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

{src,wacore}/**/*.rs: Use thiserror for custom error types (e.g., SocketError)
Use anyhow::Error for functions with multiple failure modes

Files:

  • src/message.rs
  • src/send.rs
  • src/client.rs
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: The whatsapp-rust (main) crate integrates wacore with Tokio for async and Diesel for SQLite persistence

Applied to files:

  • storages/sqlite-storage/src/sqlite_store.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to wacore/**/*.rs : wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases

Applied to files:

  • storages/sqlite-storage/src/sqlite_store.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations

Applied to files:

  • src/message.rs
  • src/send.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to {src,wacore}/**/*.rs : Use anyhow::Error for functions with multiple failure modes

Applied to files:

  • src/send.rs
🧬 Code graph analysis (4)
storages/sqlite-storage/src/sqlite_store.rs (1)
wacore/src/store/traits.rs (5)
  • get_lid_pn_mapping_by_lid (105-105)
  • get_lid_pn_mapping_by_phone (108-108)
  • put_lid_pn_mapping (111-111)
  • get_all_lid_pn_mappings (114-114)
  • delete_lid_pn_mapping (117-117)
src/message.rs (2)
wacore/binary/src/jid.rs (6)
  • user (219-219)
  • user (290-292)
  • user (347-349)
  • device (221-221)
  • device (296-298)
  • device (353-355)
src/lid_pn_cache.rs (1)
  • get_current_lid (156-159)
src/send.rs (1)
wacore/src/send.rs (2)
  • new (850-856)
  • prepare_peer_stanza (463-502)
src/client.rs (1)
src/lid_pn_cache.rs (4)
  • with_timestamp (108-120)
  • parse (62-75)
  • get_current_lid (156-159)
  • get_phone_number (164-167)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build & Test
🔇 Additional comments (18)
storages/sqlite-storage/src/sqlite_store.rs (9)

8-8: LGTM! Imports and DeviceRow extension look good.

The new imports support retry logic (warn), concurrency primitives (Arc), and LID-PN mapping features. The edge_routing_info field extends the device data model as expected.

Also applies to: 10-10, 15-15, 17-17, 41-41


65-65: LGTM! PRAGMA settings are appropriate.

Doubling busy_timeout to 30 seconds and enabling foreign_keys improves lock contention handling and ensures referential integrity.

Also applies to: 77-80


89-93: Pool and semaphore sizing increased for higher concurrency.

Pool size raised to 32 with semaphore at 16 to handle higher load. The semaphore effectively limits concurrent DB operations while the pool provides connection availability.

Also applies to: 122-122


231-231: LGTM! Edge routing info propagation is consistent.

The edge_routing_info field is properly threaded through all device persistence paths (save/load for both single and multi-device modes).

Also applies to: 274-274, 294-294, 330-330, 365-365, 385-385, 430-430, 495-495, 553-553, 724-724, 779-779


794-859: LGTM! Identity write with retry logic is well-implemented.

The retry mechanism with exponential backoff (up to 5 retries) gracefully handles SQLite lock contention. Semaphore coordination and error logging provide good observability.


889-909: LGTM! Identity load refactored to use with_semaphore helper.

Using the with_semaphore helper improves consistency and maintainability for read operations.


916-950: LGTM! Session methods improved with retry logic and semaphore coordination.

Session operations now use consistent patterns: reads with with_semaphore, writes with retry logic and exponential backoff. Debug logging aids troubleshooting.

Also applies to: 959-1031, 1033-1054


1063-1108: LGTM! Batch session checking with proper SQLite parameter limit handling.

The method chunks addresses into groups of 900, staying well under SQLite's ~999 bind parameter limit. Returns a HashSet for efficient membership checking in group messaging scenarios.


2182-2209: LGTM! LidPnMappingStore trait implementation follows established patterns.

The trait delegates to device-specific methods with device_id = 1, consistent with other single-device trait implementations in this file.

src/client.rs (3)

658-706: Sequential decrypt + owned Node return looks correct; watch CPU cost of decrypt/decompress.
This keeps Noise counter ordering correct. If decrypt_frame/decompress ever becomes materially CPU-heavy, consider spawn_blocking for decompress/unmarshal (Tokio guideline), but current structure is reasonable.


949-1065: Offline-sync gating needs strict “set flag then notify” ordering.
The waiting logic is fine if the offline stanza handler does offline_sync_completed.store(true, …) before offline_sync_notifier.notify_waiters(). If notify happens first, the waiter can miss it and stall until timeout. Please verify handler ordering.


1546-1571: 515 handling: proactive transport disconnect is a good resilience tweak.
Cloning the transport outside the lock and disconnecting in a spawned task is the right shape to avoid blocking the recv loop.

src/send.rs (2)

11-20: Signature change to &wa::Message is a nice win (less Arc churn).
Call-site update in send_message() looks correct.

Also applies to: 22-30


53-66: Peer retry behavior: confirm whether peer messages should be cached too.
Peer path doesn’t call add_recent_message(), while DM/group paths do. If peer messages can also go through retry flows, you likely want consistent caching (or an explicit comment why not).

src/message.rs (4)

6-6: Good logging import split (debug + warn).


579-586: Good: retry receipt on UntrustedIdentity retry failure.


919-930: DM sender_alt extraction by sender type is clear and matches the intent.


1648-1705: Nice coverage for LID protocol address formatting; keep it aligned to the actual implementation.
These assertions will be brittle if to_protocol_address() formatting changes; if that API is from an external crate/workspace dependency, confirm version pinning and behavior.

@jlucaso1
jlucaso1 force-pushed the feat-lid-pn-mapping branch from 0ab77b3 to f2fd83c Compare December 13, 2025 06:36

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

Caution

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

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

13-17: Critical: Thread-local storage breaks no_std compatibility.

The thread_local! macro requires the standard library and violates the coding guideline that wacore must remain no_std-compatible. This will break compilation in no_std environments.

Consider alternatives such as:

  • Using a mutex-protected static buffer pool
  • Passing buffers as parameters through the call stack
  • Using stack-allocated arrays with const generics for smaller buffers

As per coding guidelines, wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases.

wacore/src/pair.rs (1)

134-154: Restore HMAC verification: outer container integrity check is currently disabled.

The commented-out mac.verify_slice(hmac_bytes) check (lines 147–153) bypasses verification of the outer AdvSignedDeviceIdentityHmac container. While the inner account signature is still verified, it covers a different scope (inner_details_bytes) and assumes the outer container's details_bytes field is trustworthy. Without HMAC verification, a modified details_bytes at the outer level is not detected, creating a security gap.

Uncomment the verification block and rename _hmac_bytes to hmac_bytes to fix this.

-        let _hmac_bytes = hmac_container
+        let hmac_bytes = hmac_container
             .hmac
             .as_deref()
             .ok_or_else(|| PairCryptoError {
                 code: 500,
                 text: "internal-error",
                 source: anyhow::anyhow!("HMAC container missing hmac"),
             })?;

         if is_hosted_account {
             mac.update(ADV_HOSTED_PREFIX_ACCOUNT_SIGNATURE);
         }
         mac.update(details_bytes);
-        // if mac.verify_slice(hmac_bytes).is_err() {
-        //     return Err(PairCryptoError {
-        //         code: 401,
-        //         text: "hmac-mismatch",
-        //         source: anyhow::anyhow!("HMAC mismatch"),
-        //     });
-        // }
+        if mac.verify_slice(hmac_bytes).is_err() {
+            return Err(PairCryptoError {
+                code: 401,
+                text: "hmac-mismatch",
+                source: anyhow::anyhow!("HMAC mismatch"),
+            });
+        }
♻️ Duplicate comments (8)
wacore/appstate/src/processor.rs (1)

254-262: Don't skip patch_mac validation when had_no_prior_state.

This implementation still skips ALL MAC validation (including patch_mac) when had_no_prior_state is true, which directly contradicts the previous review feedback. The earlier comment explicitly stated:

"You can't validate snapshot_mac against a missing baseline, but you can still validate patch_mac (it authenticates the patch + its snapshot_mac field). Skipping it reduces integrity even with validate_macs=true."

The patch_mac authenticates the patch contents and doesn't depend on having a prior state baseline—only the snapshot_mac check requires the baseline.

Apply this diff to skip only snapshot_mac validation while preserving patch_mac validation:

-    // Skip ALL MAC validation if we had no prior state.
-    // When we receive patches without a snapshot for a never-synced collection,
-    // WhatsApp Web throws a retryable "empty lthash" error. We can't properly validate
-    // either the snapshotMac (computed from wrong baseline) or the patchMac (which
-    // includes the snapshotMac). Instead, we process the mutations and rely on
-    // future syncs with snapshots to correct the state.
-    if had_no_prior_state {
-        return Ok(());
-    }
-
-    if let Some(snap_mac) = patch.snapshot_mac.as_ref() {
-        let computed_snap = state.generate_snapshot_mac(collection_name, &keys.snapshot_mac);
-        if computed_snap != *snap_mac {
-            return Err(AppStateError::PatchSnapshotMACMismatch);
-        }
-    }
+    // If we have no baseline state, we can't validate snapshot_mac against our local state,
+    // but we can still validate patch_mac (authenticates the patch contents).
+    if !had_no_prior_state {
+        if let Some(snap_mac) = patch.snapshot_mac.as_ref() {
+            let computed_snap = state.generate_snapshot_mac(collection_name, &keys.snapshot_mac);
+            if computed_snap != *snap_mac {
+                return Err(AppStateError::PatchSnapshotMACMismatch);
+            }
+        }
+    }
transports/tokio-transport/src/lib.rs (1)

21-111: TLS connector centralization is well-structured.

The create_tls_connector() function cleanly separates TLS configuration from transport creation. The feature-gated danger-skip-tls-verify path with warning log is appropriate.

Note: A previous review already flagged concerns about adding additional friction to the danger-skip-tls-verify feature (e.g., restricting to test/debug builds or requiring runtime opt-in).

src/message.rs (1)

93-110: Fix misleading "Cached mapping" debug logs.

The debug!() logs on lines 106-109 and 133-136 run unconditionally after add_lid_pn_mapping, even when persistence fails. This was flagged in a previous review.

Apply this fix to both branches:

-                    if let Err(err) = self
+                    match self
                         .add_lid_pn_mapping(
                             &sender.user,
                             &alt_jid.user,
                             crate::lid_pn_cache::LearningSource::PeerLidMessage,
                         )
                         .await
                     {
-                        warn!(
-                            "Failed to persist LID-to-PN mapping {} -> {}: {err}",
-                            sender.user, alt_jid.user
-                        );
+                        Ok(()) => debug!(
+                            "Cached LID-to-PN mapping: {} -> {}",
+                            sender.user, alt_jid.user
+                        ),
+                        Err(err) => warn!(
+                            "Failed to persist LID-to-PN mapping {} -> {}: {err}",
+                            sender.user, alt_jid.user
+                        ),
                     }
-                    debug!(
-                        "Cached LID-to-PN mapping: {} -> {}",
-                        sender.user, alt_jid.user
-                    );

Also applies to: 120-137

src/pair.rs (1)

234-243: PII (phone numbers/LIDs) logged in pairing flow.

The warn! and info! messages include lid.user and jid.user verbatim, which are sensitive identifiers. Consider masking or redacting these values in logs.

src/send.rs (1)

40-46: Session lock cache eviction race condition already flagged.

This concern was identified in a previous review: using a moka Cache with TTL and max_capacity for session_locks can evict entries while Arc clones are still held, allowing new mutexes to be created and breaking Signal session mutual exclusion.

storages/sqlite-storage/src/sqlite_store.rs (2)

2104-2110: created_at update on conflict still present.

A previous review noted that updating created_at in the conflict clause contradicts "first learned" semantics. If created_at should represent when the mapping was first learned, remove it from the update set:

                 .do_update()
                 .set((
                     lid_pn_mapping::phone_number.eq(&phone_number),
-                    lid_pn_mapping::created_at.eq(created_at),
                     lid_pn_mapping::learning_source.eq(&learning_source),
                     lid_pn_mapping::updated_at.eq(now),
                 ))

If the current behavior (updating created_at) is intentional, consider renaming the field or updating documentation to clarify its semantics.


2042-2053: Stale comment: says "created_at DESC" but orders by updated_at.

The comment on line 2042 states "by created_at DESC" but the actual query orders by updated_at.desc(). Update the comment to match the implementation:

-            // Get the most recent mapping for this phone number (by created_at DESC)
+            // Get the most recent mapping for this phone number (by updated_at DESC)
src/client.rs (1)

300-329: Hot-path DB write: add_lid_pn_mapping claims "background" but awaits the write.

The comment on line 314 states "Persist to storage in background (don't block message processing)", but the implementation awaits the database write (lines 324-327), which blocks the caller. This can add SQLite write latency to the message processing path and increase "database is locked" contention.

Either update the comment to reflect the synchronous behavior, or spawn the persistence as a true background task with best-effort error handling.

🧹 Nitpick comments (8)
wacore/src/client/context.rs (1)

7-19: Consider using Jid::new constructor for cleaner code.

The manual Jid construction could be simplified by using the Jid::new constructor. Additionally, consider extracting the hardcoded "lid" server name as a constant for better maintainability.

Apply this diff to use the constructor:

 fn build_pn_to_lid_map(lid_to_pn_map: &HashMap<String, Jid>) -> HashMap<String, Jid> {
     lid_to_pn_map
         .iter()
         .map(|(lid_user, phone_jid)| {
-            let lid_jid = Jid {
-                user: lid_user.clone(),
-                server: "lid".to_string(),
-                ..Default::default()
-            };
+            let lid_jid = Jid::new(lid_user, "lid");
             (phone_jid.user.clone(), lid_jid)
         })
         .collect()
 }

For the hardcoded server name, consider adding at module level:

const LID_SERVER: &str = "lid";
const PHONE_SERVER: &str = "s.whatsapp.net";
wacore/binary/src/jid.rs (1)

177-259: Consider folding HOSTED_LID_SERVER into existing classification helpers (is_ad, fallback known servers).
Right now HOSTED_LID_SERVER exists and is_hosted() checks it, but:

  • is_ad() doesn’t include it (Line 224-229), which may cause inconsistent behavior for @hosted.lid AD-style JIDs.
  • The fallback known_servers list in FromStr doesn’t include it (Line 404-416), so server-only "hosted.lid" would be rejected while "s.whatsapp.net" is allowed.
 pub const HOSTED_SERVER: &str = "hosted";
 pub const HOSTED_LID_SERVER: &str = "hosted.lid";
     fn is_ad(&self) -> bool {
         self.device() > 0
             && (self.server() == DEFAULT_USER_SERVER
                 || self.server() == HIDDEN_USER_SERVER
-                || self.server() == HOSTED_SERVER)
+                || self.server() == HOSTED_SERVER
+                || self.server() == HOSTED_LID_SERVER)
     }
             let known_servers = [
                 DEFAULT_USER_SERVER,
                 GROUP_SERVER,
                 LEGACY_USER_SERVER,
                 BROADCAST_SERVER,
                 HIDDEN_USER_SERVER,
                 NEWSLETTER_SERVER,
                 HOSTED_SERVER,
+                HOSTED_LID_SERVER,
                 MESSENGER_SERVER,
                 INTEROP_SERVER,
                 BOT_SERVER,
                 STATUS_BROADCAST_USER,
             ];
src/handlers/notification.rs (2)

52-62: version is computed but never used.

The version variable is extracted from attributes but only logged indirectly via the {version} format string. If this is intentional (placeholder for future sync scheduling), consider adding a TODO comment. If sync scheduling should use this value, the implementation is incomplete.

                     let name = collection_node.attrs().string("name");
                     let mut attrs = collection_node.attrs();
                     let version = attrs.optional_u64("version").unwrap_or(0);
                     info!(
                         target: "Client/AppState",
                         "scheduling sync for collection '{name}' from version {version}."
                     );
+                    // TODO: Actually schedule the sync using name and version

30-34: Consider passing &Arc<Node> to preserve cheap cloning.

handle_notification_impl takes &Node, so node.clone() on lines 75 and 83 performs a deep clone of the Node structure. If you pass &Arc<Node> instead, event dispatch can clone the Arc (cheap) rather than the Node (expensive).

-async fn handle_notification_impl(client: &Arc<Client>, node: &Node) {
+async fn handle_notification_impl(client: &Arc<Client>, node: &Arc<Node>) {
src/handlers/ib.rs (1)

46-60: Minor: Redundant string conversion.

timestamp is converted from Option<&str>Option<String>Option<&str> via as_deref(). This works but is slightly inefficient.

                 let mut attrs = child.attrs();
                 let dirty_type = attrs.string("type");
-                let timestamp = attrs.optional_string("timestamp").map(|s| s.to_string());
+                let timestamp = attrs.optional_string("timestamp");
 ...
                     if let Err(e) = client_clone
-                        .clean_dirty_bits(&dirty_type, timestamp.as_deref())
+                        .clean_dirty_bits(&dirty_type, timestamp)
                         .await

Note: This requires timestamp to be moved into the spawned task or cloned as String if clean_dirty_bits needs ownership.

src/handshake.rs (1)

84-91: Unreachable match arm due to prior length check.

The Err(HandshakeError::RoutingInfoTooLarge) arm is unreachable because line 65 already validates routing_info.len() > MAX_EDGE_ROUTING_LEN and returns early. Consider simplifying with unwrap() (safe here due to prior check) or keeping as defensive code with a comment.

             match build_edge_routing_preintro(routing_info) {
                 Ok(mut header) => {
                     debug!(
                         target: "Client",
                         "Sending edge routing pre-intro ({} bytes) for optimized reconnection",
                         routing_info.len()
                     );
                     header.extend_from_slice(&wacore_binary::consts::WA_CONN_HEADER);
                     header
                 }
-                Err(HandshakeError::RoutingInfoTooLarge) => {
-                    warn!(
-                        target: "Client",
-                        "Routing info unexpectedly exceeds {} bytes; skipping pre-intro",
-                        MAX_EDGE_ROUTING_LEN
-                    );
-                    wacore_binary::consts::WA_CONN_HEADER.to_vec()
-                }
-                Err(err) => return Err(err),
+                // SAFETY: Length already validated above; RoutingInfoTooLarge is the only error variant
+                Err(_) => unreachable!("Length validated at line 65"),
             }

Alternatively, keep the current code as defensive programming against future changes to build_edge_routing_preintro.

src/lid_pn_cache.rs (1)

211-224: Consider batch insertion for warm_up optimization.

Each add() call acquires two write locks. For large caches, this could be optimized to acquire locks once and batch insert:

pub async fn warm_up(&self, entries: Vec<LidPnEntry>) {
    let count = entries.len();
    let start = std::time::Instant::now();
    
    // Batch insert with single lock acquisition per map
    {
        let mut lid_map = self.lid_to_entry.write().await;
        let mut pn_map = self.pn_to_entry.write().await;
        
        for entry in entries {
            lid_map.insert(entry.lid.clone(), entry.clone());
            let should_update = match pn_map.get(&entry.phone_number) {
                Some(existing) => existing.created_at <= entry.created_at,
                None => true,
            };
            if should_update {
                pn_map.insert(entry.phone_number.clone(), entry);
            }
        }
    }
    
    log::info!("LID-PN cache warmed up with {} entries in {:?}", count, start.elapsed());
}

The timing log will help identify if this becomes a bottleneck in practice.

src/client.rs (1)

588-607: Good critical node handling with inline processing.

Processing critical nodes (success, failure, stream:error) inline ensures state consistency before spawning concurrent tasks. The comment correctly explains that is_logged_in must be set before checking expected_disconnect or spawning other tasks.

Optional: Consider extracting the critical node check to a helper function:

fn is_critical_node(tag: &str) -> bool {
    matches!(tag, "success" | "failure" | "stream:error")
}

Then use: if is_critical_node(node.tag.as_str())

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab77b3 and f2fd83c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (47)
  • Cargo.toml (1 hunks)
  • src/appstate_sync.rs (1 hunks)
  • src/client.rs (33 hunks)
  • src/client/context_impl.rs (1 hunks)
  • src/handlers/basic.rs (5 hunks)
  • src/handlers/ib.rs (3 hunks)
  • src/handlers/iq.rs (2 hunks)
  • src/handlers/message.rs (3 hunks)
  • src/handlers/notification.rs (3 hunks)
  • src/handlers/receipt.rs (2 hunks)
  • src/handlers/router.rs (8 hunks)
  • src/handlers/traits.rs (2 hunks)
  • src/handlers/unimplemented.rs (2 hunks)
  • src/handshake.rs (3 hunks)
  • src/lib.rs (1 hunks)
  • src/lid_pn_cache.rs (1 hunks)
  • src/message.rs (9 hunks)
  • src/pair.rs (6 hunks)
  • src/pdo.rs (2 hunks)
  • src/receipt.rs (2 hunks)
  • src/request.rs (2 hunks)
  • src/retry.rs (4 hunks)
  • src/send.rs (6 hunks)
  • src/socket/noise_socket.rs (3 hunks)
  • src/store/signal_adapter.rs (2 hunks)
  • src/usync.rs (2 hunks)
  • storages/sqlite-storage/Cargo.toml (1 hunks)
  • storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/down.sql (1 hunks)
  • storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/up.sql (1 hunks)
  • storages/sqlite-storage/src/schema.rs (3 hunks)
  • storages/sqlite-storage/src/sqlite_store.rs (24 hunks)
  • transports/tokio-transport/Cargo.toml (2 hunks)
  • transports/tokio-transport/src/lib.rs (2 hunks)
  • wacore/appstate/src/processor.rs (3 hunks)
  • wacore/binary/Cargo.toml (1 hunks)
  • wacore/binary/src/jid.rs (6 hunks)
  • wacore/binary/src/node.rs (2 hunks)
  • wacore/libsignal/src/protocol/session_cipher.rs (10 hunks)
  • wacore/libsignal/src/protocol/state/session.rs (5 hunks)
  • wacore/src/client/context.rs (6 hunks)
  • wacore/src/pair.rs (2 hunks)
  • wacore/src/send.rs (11 hunks)
  • wacore/src/store/device.rs (2 hunks)
  • wacore/src/store/traits.rs (3 hunks)
  • wacore/src/types/jid.rs (1 hunks)
  • wacore/src/usync.rs (2 hunks)
  • wacore/tests/jid_test.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (16)
  • src/pdo.rs
  • src/lib.rs
  • src/client/context_impl.rs
  • storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/down.sql
  • wacore/binary/Cargo.toml
  • wacore/src/store/device.rs
  • src/handlers/traits.rs
  • Cargo.toml
  • transports/tokio-transport/Cargo.toml
  • src/handlers/iq.rs
  • src/handlers/unimplemented.rs
  • storages/sqlite-storage/src/schema.rs
  • wacore/tests/jid_test.rs
  • storages/sqlite-storage/migrations/2025-12-11-000000_add_lid_pn_mapping/up.sql
  • src/receipt.rs
  • wacore/src/usync.rs
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: All I/O in the main crate must use Tokio; be mindful of race conditions
Wrap blocking I/O (e.g., ureq) and heavy CPU-bound tasks (e.g., media encryption) in tokio::task::spawn_blocking
Avoid .unwrap() and .expect() outside of tests and truly unrecoverable paths

Files:

  • src/handlers/receipt.rs
  • src/handlers/router.rs
  • src/retry.rs
  • src/usync.rs
  • src/request.rs
  • src/handlers/ib.rs
  • src/handlers/notification.rs
  • src/socket/noise_socket.rs
  • src/handlers/message.rs
  • src/send.rs
  • src/handshake.rs
  • src/message.rs
  • src/lid_pn_cache.rs
  • src/pair.rs
  • src/appstate_sync.rs
  • src/handlers/basic.rs
  • src/client.rs
  • src/store/signal_adapter.rs
{src,wacore}/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

{src,wacore}/**/*.rs: Use thiserror for custom error types (e.g., SocketError)
Use anyhow::Error for functions with multiple failure modes

Files:

  • src/handlers/receipt.rs
  • wacore/binary/src/jid.rs
  • src/handlers/router.rs
  • src/retry.rs
  • src/usync.rs
  • wacore/appstate/src/processor.rs
  • src/request.rs
  • src/handlers/ib.rs
  • wacore/binary/src/node.rs
  • wacore/src/pair.rs
  • wacore/src/types/jid.rs
  • src/handlers/notification.rs
  • src/socket/noise_socket.rs
  • src/handlers/message.rs
  • wacore/src/store/traits.rs
  • src/send.rs
  • wacore/src/send.rs
  • src/handshake.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • wacore/src/client/context.rs
  • src/message.rs
  • src/lid_pn_cache.rs
  • src/pair.rs
  • src/appstate_sync.rs
  • src/handlers/basic.rs
  • wacore/libsignal/src/protocol/state/session.rs
  • src/client.rs
  • src/store/signal_adapter.rs
wacore/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases

Files:

  • wacore/binary/src/jid.rs
  • wacore/appstate/src/processor.rs
  • wacore/binary/src/node.rs
  • wacore/src/pair.rs
  • wacore/src/types/jid.rs
  • wacore/src/store/traits.rs
  • wacore/src/send.rs
  • wacore/libsignal/src/protocol/session_cipher.rs
  • wacore/src/client/context.rs
  • wacore/libsignal/src/protocol/state/session.rs
src/{client,send,message}.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use Client::chat_locks to serialize per-chat operations

Files:

  • src/send.rs
  • src/message.rs
  • src/client.rs
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: The whatsapp-rust (main) crate integrates wacore with Tokio for async and Diesel for SQLite persistence

Applied to files:

  • storages/sqlite-storage/Cargo.toml
  • storages/sqlite-storage/src/sqlite_store.rs
  • src/pair.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to wacore/**/*.rs : wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases

Applied to files:

  • storages/sqlite-storage/Cargo.toml
  • src/request.rs
  • wacore/src/pair.rs
  • src/socket/noise_socket.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • src/appstate_sync.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations

Applied to files:

  • src/retry.rs
  • src/usync.rs
  • src/socket/noise_socket.rs
  • src/handlers/message.rs
  • src/send.rs
  • src/message.rs
  • src/client.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to waproto/src/whatsapp.proto : Protocol Buffers definitions reside in waproto/src/whatsapp.proto

Applied to files:

  • wacore/src/types/jid.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/**/*.rs : All I/O in the main crate must use Tokio; be mindful of race conditions

Applied to files:

  • src/socket/noise_socket.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to {src,wacore}/**/*.rs : Use anyhow::Error for functions with multiple failure modes

Applied to files:

  • src/send.rs
🧬 Code graph analysis (17)
src/handlers/router.rs (1)
wacore/binary/src/node.rs (2)
  • attrs (83-85)
  • attrs (181-185)
src/usync.rs (1)
wacore/src/usync.rs (1)
  • parse_lid_mappings_from_response (71-109)
src/handlers/ib.rs (1)
wacore/binary/src/node.rs (2)
  • attrs (83-85)
  • attrs (181-185)
wacore/src/types/jid.rs (2)
wacore/binary/src/jid.rs (15)
  • device (221-221)
  • device (296-298)
  • device (353-355)
  • new (305-311)
  • new (362-370)
  • server (220-220)
  • server (293-295)
  • server (350-352)
  • user (219-219)
  • user (290-292)
  • user (347-349)
  • from_str (385-485)
  • from (213-215)
  • from (557-559)
  • from (563-565)
wacore/libsignal/src/core/address.rs (1)
  • name (296-298)
src/handlers/notification.rs (4)
wacore/binary/src/attrs.rs (2)
  • jid (75-78)
  • jid (201-204)
src/handlers/traits.rs (1)
  • handle (26-26)
wacore/binary/src/node.rs (4)
  • children (76-81)
  • children (134-139)
  • attrs (83-85)
  • attrs (181-185)
wacore/binary/src/builder.rs (2)
  • children (36-39)
  • attrs (24-34)
wacore/src/store/traits.rs (2)
storages/sqlite-storage/src/sqlite_store.rs (5)
  • get_lid_pn_mapping_by_lid (2184-2189)
  • get_lid_pn_mapping_by_phone (2191-2196)
  • put_lid_pn_mapping (2198-2200)
  • get_all_lid_pn_mappings (2202-2204)
  • delete_lid_pn_mapping (2206-2208)
src/appstate_sync.rs (5)
  • get_lid_pn_mapping_by_lid (522-527)
  • get_lid_pn_mapping_by_phone (529-534)
  • put_lid_pn_mapping (536-541)
  • get_all_lid_pn_mappings (543-547)
  • delete_lid_pn_mapping (549-551)
src/send.rs (1)
wacore/src/send.rs (2)
  • new (850-856)
  • prepare_peer_stanza (463-502)
wacore/libsignal/src/protocol/session_cipher.rs (7)
wacore/libsignal/src/protocol/protocol.rs (2)
  • ciphertext (456-458)
  • counter (126-128)
wacore/libsignal/src/protocol/ratchet/params.rs (2)
  • their_identity_key (76-78)
  • their_identity_key (155-157)
wacore/libsignal/src/protocol/identity_key.rs (2)
  • public_key (39-41)
  • public_key (115-117)
wacore/libsignal/src/protocol/state/prekey.rs (1)
  • public_key (72-79)
wacore/libsignal/src/core/curve.rs (2)
  • public_key (237-245)
  • public_key_bytes (93-97)
wacore/libsignal/src/protocol/ratchet/keys.rs (5)
  • counter (106-108)
  • mac_key (96-98)
  • key (126-128)
  • key (164-166)
  • index (131-133)
wacore/libsignal/src/protocol/state/session.rs (1)
  • root_key (159-165)
wacore/src/client/context.rs (3)
wacore/binary/src/jid.rs (2)
  • new (305-311)
  • new (362-370)
wacore/src/send.rs (2)
  • new (850-856)
  • get_lid_for_phone (917-919)
src/client/context_impl.rs (1)
  • get_lid_for_phone (32-34)
src/lid_pn_cache.rs (1)
src/client.rs (1)
  • new (171-270)
storages/sqlite-storage/src/sqlite_store.rs (1)
wacore/src/store/traits.rs (5)
  • get_lid_pn_mapping_by_lid (105-105)
  • get_lid_pn_mapping_by_phone (108-108)
  • put_lid_pn_mapping (111-111)
  • get_all_lid_pn_mappings (114-114)
  • delete_lid_pn_mapping (117-117)
src/pair.rs (5)
src/client.rs (1)
  • handle_iq (1659-1686)
wacore/binary/src/node.rs (2)
  • children (76-81)
  • children (134-139)
wacore/binary/src/builder.rs (2)
  • children (36-39)
  • bytes (41-44)
wacore/src/pair.rs (1)
  • build_ack_node (68-83)
wacore/binary/src/jid.rs (3)
  • user (219-219)
  • user (290-292)
  • user (347-349)
src/appstate_sync.rs (2)
storages/sqlite-storage/src/sqlite_store.rs (7)
  • get_lid_pn_mapping_by_lid (2184-2189)
  • None (547-547)
  • None (553-553)
  • get_lid_pn_mapping_by_phone (2191-2196)
  • put_lid_pn_mapping (2198-2200)
  • get_all_lid_pn_mappings (2202-2204)
  • delete_lid_pn_mapping (2206-2208)
wacore/src/store/traits.rs (5)
  • get_lid_pn_mapping_by_lid (105-105)
  • get_lid_pn_mapping_by_phone (108-108)
  • put_lid_pn_mapping (111-111)
  • get_all_lid_pn_mappings (114-114)
  • delete_lid_pn_mapping (117-117)
src/handlers/basic.rs (6)
src/handlers/ib.rs (1)
  • handle (37-40)
src/handlers/iq.rs (1)
  • handle (31-36)
src/handlers/notification.rs (1)
  • handle (30-33)
src/handlers/receipt.rs (1)
  • handle (28-31)
src/handlers/traits.rs (1)
  • handle (26-26)
src/handlers/unimplemented.rs (1)
  • handle (49-52)
wacore/libsignal/src/protocol/state/session.rs (2)
wacore/libsignal/src/protocol/protocol.rs (2)
  • chain_key (572-574)
  • counter (126-128)
wacore/libsignal/src/protocol/ratchet/keys.rs (4)
  • index (131-133)
  • new (121-123)
  • new (160-162)
  • counter (106-108)
src/client.rs (1)
src/lid_pn_cache.rs (5)
  • with_timestamp (108-120)
  • parse (62-75)
  • as_str (46-59)
  • get_current_lid (156-159)
  • get_phone_number (164-167)
src/store/signal_adapter.rs (1)
wacore/libsignal/src/protocol/state/session.rs (1)
  • deserialize (575-594)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build & Test
🔇 Additional comments (67)
wacore/appstate/src/processor.rs (1)

154-160: LGTM: Correct detection of empty initial state.

The logic correctly identifies when there's no prior state by checking both version == 0 and an all-zeros hash. This is the proper baseline for determining MAC validation behavior.

wacore/src/client/context.rs (4)

29-31: LGTM! Proper maintenance of bidirectional mapping.

The pn_to_lid_map field is correctly initialized in all constructors and properly synchronized with lid_to_pn_map updates. The reverse mapping will improve lookup performance for phone-to-LID conversions.

Also applies to: 45-45, 55-56, 61-61, 67-67


81-84: LGTM! Clean reverse lookup implementation.

The method provides symmetric functionality to phone_jid_for_lid_user and enables efficient phone-to-LID lookups using the maintained reverse map.


86-101: LGTM! Correct device JID conversion logic.

The method properly handles the conversion from phone-based to LID-based device JIDs while preserving the device-specific fields (device, agent, integrator). The fallback to return the original JID when no mapping exists is appropriate and well-documented.


119-129: LGTM! Well-designed trait extension for LID lookup.

The default implementation provides backward compatibility while allowing implementations to override with LID-aware session resolution. The async signature appropriately supports cache or database lookups, as seen in the context_impl.rs implementation that delegates to lid_pn_cache.

src/socket/noise_socket.rs (6)

5-5: Correct use of tokio::sync::Mutex.

Using tokio::sync::Mutex is appropriate here since the lock will be held across .await points (both spawn_blocking and transport.send). A std::sync::Mutex would cause issues when held across async boundaries.


20-24: Clear documentation and correct mutex pattern.

The inline documentation effectively explains the ordering requirement. Using Mutex<()> is idiomatic when synchronization is needed without protecting shared data.


44-46: LGTM!

The mutex acquisition at the start ensures the entire operation (counter increment → encryption → framing → send) is serialized, preventing out-of-order frame delivery.


82-96: Good use of spawn_blocking for CPU-bound encryption.

Offloading large message encryption to the blocking thread pool follows the coding guidelines. The Arc pattern correctly handles ownership transfer into the blocking task.

Note: The mutex remains held during the blocking task, which is intentional for ordering. This could cause contention under high concurrent load with large messages, but that's the necessary tradeoff for correctness.


141-189: Test covers buffer reuse semantics.

The test validates that buffers are returned with preserved capacity and cleared contents. Uses .unwrap() appropriately in test context per coding guidelines.

Consider adding a concurrency test in the future to verify the mutex ordering guarantees under parallel encrypt_and_send calls.


127-133: Read path is architecturally single-threaded; concurrent decryption is not a concern.

The decrypt_frame method is called sequentially from a single while loop in client.rs (line 588), and the comment explicitly states "Decrypt the frame synchronously (required for noise protocol counter ordering)". Node processing is spawned concurrently after decryption, not during. The synchronous nature of decrypt_frame and single-threaded read loop make the counter ordering safe without additional synchronization. The asymmetry with encrypt_and_send (which uses send_mutex to serialize concurrent writes) is intentional: writes can arrive from multiple async tasks, reads arrive from a single event loop.

wacore/libsignal/src/protocol/session_cipher.rs (4)

453-460: LGTM!

The DecryptionResult struct is well-designed with clear documentation explaining the purpose of the used_previous_session flag.


263-286: LGTM!

The changes correctly adapt to the new DecryptionResult return type. The used_previous_session flag is appropriately unused here since PreKey messages establish new sessions rather than using previous ones.


519-521: LGTM: DuplicatedMessage errors now propagate immediately.

The change to return DuplicatedMessage errors immediately (rather than collecting them and trying other sessions) is correct. When a duplicate is detected on a specific chain, it's definitive—no other session will change that outcome.

Also applies to: 584-586


317-353: No action required – this implementation correctly follows Signal Protocol specifications.

The code properly handles archived sessions per the Signal Protocol Sesame session-management specification. When a message is successfully decrypted with an archived (previous) session, that session is promoted to active. Since the archived session was established when the identity was trusted, and the Double Ratchet algorithm provides forward secrecy for those messages independent of future identity changes, skipping the trust check for archived sessions is correct and aligned with the protocol's design.

Signal's identity verification (safety numbers) is intended to detect key changes and authenticate future messages, not retroactively invalidate past messages from sessions that were trusted at establishment time. The implementation's distinction between archived and current sessions follows this model properly.

storages/sqlite-storage/Cargo.toml (1)

13-24: LGTM!

The addition of 32-column-tables Diesel feature is appropriate for supporting wider schema requirements (likely the new lid_pn_mapping tables), and the log dependency enables standard logging support for the storage backend.

wacore/binary/src/node.rs (2)

25-36: LGTM!

The as_content_ref method correctly converts owned NodeContent variants to their borrowed NodeContentRef counterparts, properly borrowing primitives and recursively mapping nested nodes.


62-74: LGTM!

The as_node_ref method provides a clean borrow-based conversion from owned Node to NodeRef, correctly borrowing the tag, converting attrs entries, and boxing the content reference. This maintains wacore's no_std compatibility.

transports/tokio-transport/src/lib.rs (1)

209-209: Verify the 100x increase in event channel capacity.

The channel capacity increased from 100 to 10,000. This significantly increases memory footprint per transport instance. Ensure this is intentional and necessary for the expected message throughput, and consider documenting the rationale.

src/request.rs (1)

167-182: LGTM! Clean migration to Arc-based ownership.

The handle_iq_response method correctly transitions to Arc<Node> ownership:

  • Uses Arc::try_unwrap to avoid cloning when there are no other references
  • Falls back to cloning the inner Node only when necessary
  • The waiter lookup and send logic remains correct

This aligns with the broader PR-wide shift to Arc-based node handling for the per-chat worker queue architecture.

wacore/libsignal/src/protocol/state/session.rs (3)

227-247: LGTM! Efficient index-based lookup for receiver chains.

The new get_receiver_chain_index helper enables index-based access patterns throughout the session state, reducing unnecessary cloning when only the index or in-place mutation is needed.


391-423: LGTM! Efficient in-place message key removal.

The refactored get_message_keys now:

  1. Finds the message key position without cloning the chain
  2. Only mutates when a matching key is found
  3. Uses direct index access for the removal

This avoids cloning the entire chain just to find and remove a single message key.


430-432: The expect() calls are safe and the invariant holds across all call sites.

Both set_message_keys (line 430-432) and set_receiver_chain_key (line 449-451) safely call .expect() on get_receiver_chain_index(). The only call sites for these methods are in get_or_create_message_key(), which is always preceded by a call to get_or_create_chain_key(). This function guarantees that the receiver chain for the given sender already exists in state—either by retrieving an existing chain or by creating and adding a new one before returning. Therefore, when these methods attempt to look up the chain index, it is guaranteed to exist.

wacore/src/types/jid.rs (3)

15-42: LGTM! WhatsApp Web-compatible Signal address formatting.

The to_signal_address_string implementation correctly:

  • Includes device suffix only when device != 0
  • Maps s.whatsapp.net to c.us to match WhatsApp Web's internal format
  • Is well-documented with the corresponding JavaScript implementation for reference

44-64: LGTM! Protocol address generation matches WhatsApp Web's createSignalLikeAddress.

The to_protocol_address implementation correctly:

  • Encodes the device in the name portion via to_signal_address_string
  • Uses device_id = 0 for the ProtocolAddress (matching WA Web behavior)
  • Results in the {SignalAddress}.0 format expected by the protocol

67-134: LGTM! Comprehensive test coverage.

Tests thoroughly cover:

  • LID JIDs with and without device numbers
  • LID JIDs with dots in the user portion
  • Phone number JIDs with server mapping
  • Protocol address string formatting consistency
src/store/signal_adapter.rs (2)

44-53: LGTM! Simplified adapter without in-memory session cache.

The removal of the Moka session cache simplifies the implementation. This aligns with the PR's architectural changes introducing centralized caching via lid_pn_cache module. SQLite's built-in caching should provide adequate performance for session lookups.


56-93: The review comment references methods (with_cache_capacity, invalidate_session, clear_session_cache) that do not exist in the current codebase. A comprehensive search of the entire repository found no definitions or usages of these methods. The SessionAdapter struct in src/store/signal_adapter.rs contains only the trait implementations shown in the provided snippet and no cache management APIs. This breaking change claim cannot be verified against the actual codebase.

Likely an incorrect or invalid review comment.

src/message.rs (4)

350-362: LGTM! Per-sender session locking prevents race conditions.

The session locking correctly:

  • Uses the full Signal protocol address string as the lock key
  • Matches the SignalProtocolStoreAdapter's per-session semantics
  • Prevents ratchet counter races when processing concurrent messages from the same sender

This aligns with the coding guideline to serialize per-chat operations. Based on learnings, this is the correct approach.


445-586: LGTM! Improved UntrustedIdentity handling preserves in-flight messages.

The updated handling correctly:

  • Clears only the old identity (not the session)
  • Allows the session to be archived when the new PreKeySignalMessage is processed
  • Enables decryption of any in-flight messages encrypted with the old session
  • Spawns retry receipts when retry fails for various error conditions

The documentation clearly explains the recovery flow and rationale.


919-930: LGTM! Bidirectional sender_alt extraction.

The updated logic correctly extracts alternate JID based on sender type:

  • LID sender → look for sender_pn attribute
  • PN sender → look for sender_lid attribute

This enables proper session resolution regardless of which identity format the message uses.


2533-3109: Excellent test coverage for LID-PN cache and session behavior.

The new tests comprehensively cover:

  • Cache population on receiving messages with sender_lid
  • Bidirectional LID↔PN mappings
  • PN messages using LID for session lookup when mapping is known
  • Fallback to PN when no LID mapping exists
  • Cache handling for repeated messages and group contexts
src/usync.rs (1)

3-3: Nice: persistence failure is handled (no silent drop).
Warn + continue is the right tradeoff for “best-effort learning” here. (This also addresses the earlier review note.)

Also applies to: 48-69

src/handlers/receipt.rs (1)

28-30: Arc migration looks consistent.
Signature + call site update matches the new handler routing flow.

src/handlers/router.rs (2)

49-60: Dispatch signature update to Arc<Node> is clean.
Using node.tag.as_str() for lookup and passing the same Arc into the handler keeps ownership simple and avoids extra cloning.


155-218: Tests updated appropriately for owned Node construction.
The test setup (IndexMap attrs + NodeContent) matches the new API and validates routing behavior.

wacore/binary/src/jid.rs (1)

488-527: Server-only JID display change is clear and well-covered by tests.
The “no leading @” behavior is explicit and validated.

Also applies to: 659-666

src/appstate_sync.rs (1)

520-552: Good test scaffolding to satisfy new Backend: LidPnMappingStore bound.
Benign defaults are appropriate for these unit tests.

src/retry.rs (2)

59-71: Retry flow simplification looks good (early-return on cache miss, no Arc churn).
The updated control flow is easier to follow and matches the updated send_message_impl signature.

Also applies to: 137-155


317-332: Tests correctly validate take-once semantics after API change.
First take returns the message; second take returns None.

wacore/src/store/traits.rs (1)

86-118: LID↔phone mapping store API is well-shaped (includes updated_at + integrates into Backend).
This provides the needed metadata for “most recent by phone” lookups and makes the capability uniformly available via Backend.

Also applies to: 149-178

src/handlers/message.rs (1)

35-43: LGTM on the ordering guarantee approach.

The double-lock pattern (enqueue_mutex before queue creation) correctly ensures FIFO ordering even when the queue is created lazily. The comment documentation clearly explains the concurrency reasoning. Based on learnings, this aligns with the per-chat serialization requirement.

src/handlers/ib.rs (2)

127-131: LGTM on offline sync completion signaling.

The atomic store with Ordering::Relaxed followed by notify_waiters() correctly signals completion to waiting post-login tasks. The Notify ensures proper synchronization for tasks that call notified().await.


71-85: This comment is incorrect. The modify_device() call is part of a fire-and-forget pattern where in-memory state is modified immediately, and a background saver task handles persistence asynchronously. Errors during persistence are logged via the background task: if let Err(e) = self.save_to_disk().await { error!("Error saving device state in background: {e}"); }. The design is intentional and appropriate for this use case.

Likely an incorrect or invalid review comment.

src/pair.rs (2)

26-36: LGTM on Node API migration.

The migration from NodeRef to Node is clean. The attrs.get("from").map(|s| s.as_str()) pattern correctly handles the optional attribute comparison against SERVER_JID.


161-175: Good defensive parsing with parser.finish() check.

The pattern of using optional_jid() with a subsequent finish() check properly handles attribute parsing errors while still providing default values. This is a solid approach for handling potentially malformed pair-success messages.

src/handshake.rs (2)

63-97: LGTM on edge routing validation and fallback.

The validation at line 65 correctly prevents oversized routing info before calling build_edge_routing_preintro. The fallback to WA_CONN_HEADER ensures graceful degradation. The past review concerns about assert! panics have been properly addressed.


31-50: LGTM on pre-intro encoding.

The 3-byte big-endian length encoding is correct: (len >> 16) for high byte, (len >> 8) for middle byte, len for low byte. The capacity pre-allocation avoids reallocations.

src/handlers/basic.rs (1)

25-28: Consistent Arc migration across all handlers.

The signature changes align with the updated StanzaHandler trait. The AckHandler correctly uses Arc::try_unwrap to attempt zero-copy ownership transfer, falling back to clone when other references exist—this is the idiomatic pattern when an owned value is required.

Also applies to: 49-52, 73-76, 97-105

src/send.rs (2)

22-25: Clean migration from Arcwa::Message to borrowed reference.

Passing the message by reference is appropriate since it's read-only during the send operation. This reduces unnecessary Arc overhead throughout the sending path.


53-54: Good defensive check for peer message path.

Adding !to.is_group() ensures peer stanzas are only used for individual recipients, preventing incorrect path selection.

wacore/src/send.rs (4)

113-170: Well-designed LID-aware session resolution.

The logic correctly handles the scenario where a session was established under a LID address (from receiving a message with sender_lid) but the reply is addressed to a phone number. The mapping preserves the original device_jid for the XML to attribute while using the LID session for encryption.


290-333: Correct separation of encryption and addressing JIDs.

The code properly uses the LID-mapped JID for Signal encryption while preserving the original phone number JID for the XML to attribute. The unwrap_or(device_jid) is defensive since all devices should be in the map.


1036-1163: Excellent test coverage for LID-PN session handling and hosted device filtering.

The tests comprehensively cover:

  • Hosted device identification (is_hosted() behavior)
  • SKDM distribution filtering logic
  • LID session lookup scenarios
  • Device ID preservation in LID JID construction

The detailed documentation in test comments (explaining what hosted devices are and why they're filtered) adds valuable context.

Also applies to: 1210-1401


607-648: Correct LID conversion and hosted device filtering for group SKDM.

The implementation correctly:

  1. Converts phone-based JIDs to LID format for LID-addressed groups
  2. Deduplicates after conversion to handle users appearing under both formats
  3. Filters hosted/Cloud API devices (device ID 99 or @hosted/@hosted.lid server) which don't participate in group E2EE

The is_hosted() method is properly defined in the JidExt trait and matches the filtering requirements. This matches WhatsApp Web's getFanOutList behavior.

src/lid_pn_cache.rs (3)

18-76: Clean serialization pattern for LearningSource.

The as_str()/parse() pattern is appropriate for database storage. Defaulting unknown strings to Other provides forward compatibility when new sources are added.


186-205: Correct timestamp-based conflict resolution for PN mappings.

The logic correctly implements "newest LID wins" for phone number lookups while preserving all LID entries. The sequential locking pattern is acceptable for the expected low-frequency write operations.


254-437: Comprehensive test coverage for cache operations.

Tests cover basic CRUD, timestamp-based conflict resolution (both directions), warm-up, clearing, and serialization roundtrip. Good coverage of the core functionality.

storages/sqlite-storage/src/sqlite_store.rs (5)

65-80: Good PRAGMA configuration for SQLite reliability.

  • busy_timeout = 30000 (30s) is appropriate for WAL mode under concurrent load
  • foreign_keys = ON ensures referential integrity for the new lid_pn_mapping table

Both correctly applied per-connection.


89-93: Reasonable pool and semaphore configuration.

Pool size 32 with semaphore 16 allows connection reuse while limiting concurrent operations. The updated comment accurately describes the intent.

Also applies to: 120-123


787-859: Robust retry logic for identity and session writes.

The exponential backoff (10-160ms over 5 retries) is appropriate for SQLite lock contention. The warn! logging helps diagnose contention issues in production.

Also applies to: 952-1031


1063-1108: SQLite bind parameter limit properly handled with chunking.

Chunking by 900 stays well under SQLite's typical 999 variable limit. This addresses the concern from a previous review.


41-41: Consistent edge_routing_info propagation across device operations.

The new field is correctly handled across all device CRUD paths: save, load, and create operations for both single-device and multi-device modes.

Also applies to: 231-231, 274-274, 294-294, 330-330, 365-365, 385-385, 430-430, 495-495, 553-553, 724-724, 779-779

src/client.rs (4)

90-111: Excellent concurrency design for per-device and per-chat synchronization.

The three-layer locking strategy effectively prevents race conditions:

  • session_locks: Prevents Signal protocol races when multiple messages from the same sender arrive concurrently across different chats
  • message_queues: Ensures sequential processing within each chat (critical for PreKey messages)
  • message_enqueue_locks: Serializes queue creation to prevent initialization races

The 5-minute TTL is reasonable and allows idle resources to be reclaimed while message_enqueue_locks ensures safe queue recreation when needed.

Based on learnings, this implements the Client::chat_locks pattern mentioned in the coding guidelines for serializing per-chat operations.

Also applies to: 203-215


949-1004: Excellent generation guard pattern to prevent stale task races.

The connection generation counter and check_generation! macro effectively handle reconnection races, particularly during the 515 pairing flow. When a new connection is established, any stale post-login tasks from the previous connection are properly cancelled before performing network operations.

This prevents race conditions where:

  • Old connection tasks might try to send on a closed socket
  • Duplicate passive tasks run after reconnection
  • App state sync from old connection interferes with new connection

The macro makes the check consistent and readable across the post-login sequence.

Also applies to: 1037-1037, 1057-1057, 1069-1069, 1075-1075, 1090-1090, 1097-1097, 1138-1138, 1162-1162, 1177-1177


1042-1064: Correct implementation of offline sync flow matching WhatsApp Web.

The post-login sequence correctly follows WhatsApp Web's behavior:

  1. Send active IQ to exit passive mode (line 1038)
  2. Server responds with <ib><offline count="X"/></ib>
  3. Wait for offline sync completion before proceeding with passive tasks (prekey upload, presence)
  4. Timeout (5s) handles edge cases where server doesn't send offline ib

The generation check after the wait (line 1057) properly handles connection replacement during the wait period.


2123-2310: Comprehensive test coverage for LID-PN cache functionality.

The tests validate the critical aspects of the LID-PN cache:

  • Basic bidirectional mappings (phone↔LID lookup)
  • Timestamp-based conflict resolution (newer mapping wins for phone→LID lookup)
  • Integration with SendContextResolver trait (used by wacore during message encryption)

This ensures the cache correctly prevents LID-PN session mismatches when sending replies to messages received with sender_lid attributes.

Comment thread src/handlers/message.rs
Comment thread src/usync.rs
Comment on lines +683 to +686
log::debug!(
"{remote_address} derived message_keys: mac_key={}",
hex::encode(message_keys.mac_key())
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Caution: Debug logs expose cryptographic key material.

The new debug logs output sensitive cryptographic keys (MAC keys, root keys, ephemeral keys) in hexadecimal. While these are debug-level logs typically disabled in production, if debug logging is inadvertently enabled in production environments, this could leak sensitive cryptographic material to log files.

Consider:

  1. Adding a compile-time feature flag to disable cryptographic material logging in release builds
  2. Documenting that debug logging must never be enabled in production
  3. Using log filtering to redact these specific keys even at debug level

Also applies to: 755-760, 763-768

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

Caution

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

⚠️ Outside diff range comments (4)
src/prekeys.rs (3)

113-125: Remove or complete the unused key lookup logic.

This loop serves no purpose in its current state:

  • found_count is initialized but never incremented
  • The condition on line 115 will never be met
  • Line 123 breaks immediately when a key is found

The comment acknowledges this is incomplete. Either implement proper key reuse logic or remove this dead code to avoid unnecessary iterations.

Apply this diff to remove the dead code:

-        // Check if we have existing unuploaded keys by trying IDs sequentially
-        // We'll check a reasonable range to find existing keys
-        let found_count = 0;
-        for id in 1..=1000u32 {
-            if found_count >= WANTED_PRE_KEY_COUNT {
-                break;
-            }
-
-            if let Ok(Some(_record)) = backend.load_prekey(id).await {
-                // Check if this key was already uploaded by seeing if it exists on server
-                // For simplicity, assume unuploaded keys have a specific pattern or we track separately
-                // For now, we'll use existing keys if available but generate new ones with sequential IDs
-                break; // We'll generate new ones with better tracking
-            }
-        }
-

128-137: Critical performance issue: Loop may iterate millions of times.

This loop can iterate up to 16,777,215 times with an async backend call per iteration, causing severe performance degradation when many pre-keys exist. Additionally, the logic is incorrect—it breaks at the first gap (line 135) rather than finding the highest existing ID.

Consider maintaining a high-water mark in the device store or using a more efficient backend query:

-        // Step 2: Generate new keys with sequential IDs to avoid collisions
-        let mut highest_existing_id = 0u32;
-
-        // Find the highest existing pre-key ID to start from
-        for id in 1..=16777215u32 {
-            if backend.contains_prekey(id).await.unwrap_or(false) {
-                highest_existing_id = id;
-            } else {
-                break; // Found first gap
-            }
-        }
-
-        let start_id = highest_existing_id + 1;
+        // Step 2: Generate new keys with sequential IDs to avoid collisions
+        // TODO: Implement backend.get_highest_prekey_id() or maintain a counter in device store
+        let start_id = {
+            let device_guard = device_store.read().await;
+            device_guard.next_prekey_id.unwrap_or(1)
+        };

Alternatively, if you must scan for gaps, limit the search range to a reasonable window (e.g., last 1000 IDs).


153-153: Critical: Code will not compile—OsRng.unwrap_err() is invalid.

OsRng is a struct, not a Result. Calling .unwrap_err() on it will cause a compilation error. Based on the rand_core API, you should pass &mut OsRng directly to the key generation function.

Apply this diff:

-            let key_pair = KeyPair::generate(&mut OsRng.unwrap_err());
+            let key_pair = KeyPair::generate(&mut OsRng);
src/socket/error.rs (1)

18-30: Public enum variant addition is a breaking change—call it out / mitigate match breakage.
EncryptSendErrorKind is pub; adding ChannelClosed can break downstream exhaustive matches. If you want to reduce future breakage, consider making the enum #[non_exhaustive] (likely requires a major bump anyway) and documenting the expectation that callers include a wildcard arm.

♻️ Duplicate comments (4)
storages/sqlite-storage/src/sqlite_store.rs (2)

2099-2115: created_at overwritten on conflict: semantic ambiguity.

Line 2112 updates created_at during conflict resolution, which contradicts "first created" semantics. If created_at should reflect the original insertion time, remove it from the conflict-update path:

                 .on_conflict((lid_pn_mapping::lid, lid_pn_mapping::device_id))
                 .do_update()
                 .set((
                     lid_pn_mapping::phone_number.eq(&phone_number),
-                    lid_pn_mapping::created_at.eq(created_at),
                     lid_pn_mapping::learning_source.eq(&learning_source),
                     lid_pn_mapping::updated_at.eq(now),
                 ))

If you intend created_at to reflect "time this (lid, phone) pair was (re)learned," rename it or document that behavior to avoid confusion.


2046-2057: Comment mismatch: says "created_at DESC" but query orders by "updated_at DESC".

Line 2046's comment is outdated.

-            // Get the most recent mapping for this phone number (by created_at DESC)
+            // Get the most recent mapping for this phone number (by updated_at DESC)
src/send.rs (1)

37-51: Re-verify session_locks cannot “recreate” locks via eviction (prior critical issue).
This still uses .session_locks.get_with(... Arc<Mutex<()>> ...); if the backing map is an evicting cache, eviction while clones exist can allow a new mutex to be created later, breaking session mutual exclusion. This looks similar to a previously-addressed review thread—please confirm it’s still fixed at current head.

#!/bin/bash
set -euo pipefail

echo "=== Locate Client::session_locks definition / type ==="
rg -n --type rust 'session_locks\s*:' -C 3

echo "=== Locate session_locks builder/config (TTL/capacity/eviction) ==="
rg -n --type rust 'session_locks.*Cache::builder|Cache::builder\(\).*session_locks|time_to_live|max_capacity' -C 5

echo "=== Locate chat_locks (guideline expectation) ==="
rg -n --type rust '\bchat_locks\b' -C 3

echo "=== Locate resolve_encryption_jid usage + lock scope ==="
rg -n --type rust 'resolve_encryption_jid|to_protocol_address\(\)|session_locks\.get_with' -C 3

Also applies to: 299-307

src/message.rs (1)

64-177: Fix misleading “Cached … mapping” logs (they currently log success even on persist failure) + consider redacting PII in warn logs.
Right now both branches emit debug!("Cached … mapping") even if add_lid_pn_mapping(...).await returns Err (same issue as prior feedback). Also, warn! logs include raw phone/LID identifiers (PII).

-                    if let Err(err) = self
+                    match self
                         .add_lid_pn_mapping(
@@
-                        .await
-                    {
-                        warn!(
-                            "Failed to persist LID-to-PN mapping {} -> {}: {err}",
-                            sender.user, alt_jid.user
-                        );
-                    }
-                    debug!(
-                        "Cached LID-to-PN mapping: {} -> {}",
-                        sender.user, alt_jid.user
-                    );
+                        .await
+                    {
+                        Ok(()) => debug!("Cached LID-to-PN mapping: {} -> {}", sender.user, alt_jid.user),
+                        Err(err) => warn!("Failed to persist LID-to-PN mapping {} -> {}: {err}", sender.user, alt_jid.user),
+                    }

(Apply the same pattern to the PN→LID block.)
For the PII angle, consider masking (e.g., last 4) or logging a stable hash in warn! paths.

🧹 Nitpick comments (4)
storages/sqlite-storage/src/sqlite_store.rs (2)

89-93: Comment is misleading: increasing pool size increases memory usage.

The comment claims "reduce memory and lock contention," but raising max_size from 4 to 64 increases memory footprint (64 connections vs. 4). The intent appears to be increasing concurrency, not reducing memory.

-            .max_size(pool_size) // Limit concurrent connections to reduce memory and lock contention
+            .max_size(pool_size) // Increased to support higher concurrency

794-859: Approve retry logic; consider minor efficiency improvement.

The exponential backoff pattern correctly handles SQLite contention. The semaphore-per-attempt approach is sound.

Minor efficiency: address_owned and key_vec are cloned on every retry iteration (lines 807-808). Since they're already owned outside the loop, you could move the .clone() calls outside and reuse them:

+        let address_owned = address.to_string();
+        let key_vec = key.to_vec();
+
         for attempt in 0..=MAX_RETRIES {
             // ...
-            let address_clone = address_owned.clone();
-            let key_clone = key_vec.clone();
+            let address_clone = address_owned.clone(); // Still needed per spawn_blocking
+            let key_clone = key_vec.clone();

Actually, on second look, this is already optimal since spawn_blocking moves ownership. The current pattern is fine.

src/message.rs (2)

367-380: Session lock is a good direction, but watch unbounded growth + ensure this complements Client::chat_locks (per repo guidance).
self.session_locks.get_with(signal_addr_str, …) will grow without eviction as new senders/devices appear. Also, repo guidance for src/message.rs is to serialize per-chat operations via Client::chat_locks; this adds per-session serialization, but shouldn’t replace per-chat ordering guarantees if callers rely on them.

Consider an eviction strategy (TTL/LRU) for session_locks keys, and double-check chat-level serialization where needed. Based on learnings, use Client::chat_locks for per-chat serialization.


2551-3125: New cache population tests: watch flakiness from spawned background tasks.
These tests call handle_encrypted_message, which spawns tasks (delivery receipts, retry receipts, PDO requests). That can lead to nondeterministic logs/timing in CI.

If you see flakes, consider injecting a “test mode” that disables side-effect spawns, or await/join spawned tasks via a task tracker.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2fd83c and c32bdcc.

📒 Files selected for processing (6)
  • src/message.rs (10 hunks)
  • src/prekeys.rs (4 hunks)
  • src/send.rs (8 hunks)
  • src/socket/error.rs (2 hunks)
  • src/socket/noise_socket.rs (6 hunks)
  • storages/sqlite-storage/src/sqlite_store.rs (24 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: All I/O in the main crate must use Tokio; be mindful of race conditions
Wrap blocking I/O (e.g., ureq) and heavy CPU-bound tasks (e.g., media encryption) in tokio::task::spawn_blocking
Avoid .unwrap() and .expect() outside of tests and truly unrecoverable paths

Files:

  • src/send.rs
  • src/socket/error.rs
  • src/prekeys.rs
  • src/socket/noise_socket.rs
  • src/message.rs
src/{client,send,message}.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use Client::chat_locks to serialize per-chat operations

Files:

  • src/send.rs
  • src/message.rs
{src,wacore}/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

{src,wacore}/**/*.rs: Use thiserror for custom error types (e.g., SocketError)
Use anyhow::Error for functions with multiple failure modes

Files:

  • src/send.rs
  • src/socket/error.rs
  • src/prekeys.rs
  • src/socket/noise_socket.rs
  • src/message.rs
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations

Applied to files:

  • src/send.rs
  • src/socket/noise_socket.rs
  • src/message.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to {src,wacore}/**/*.rs : Use anyhow::Error for functions with multiple failure modes

Applied to files:

  • src/send.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to {src,wacore}/**/*.rs : Use thiserror for custom error types (e.g., SocketError)

Applied to files:

  • src/socket/error.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: The whatsapp-rust (main) crate integrates wacore with Tokio for async and Diesel for SQLite persistence

Applied to files:

  • storages/sqlite-storage/src/sqlite_store.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to wacore/**/*.rs : wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases

Applied to files:

  • storages/sqlite-storage/src/sqlite_store.rs
🧬 Code graph analysis (3)
src/send.rs (3)
src/client.rs (1)
  • new (171-270)
wacore/src/send.rs (2)
  • new (850-856)
  • prepare_peer_stanza (463-502)
src/store/persistence_manager.rs (1)
  • new (20-62)
src/prekeys.rs (1)
src/store/persistence_manager.rs (1)
  • backend (120-122)
src/socket/noise_socket.rs (2)
src/socket/error.rs (2)
  • transport (69-80)
  • channel_closed (91-98)
wacore/src/handshake/utils.rs (1)
  • generate_iv (37-41)
🔇 Additional comments (11)
src/prekeys.rs (1)

98-105: LGTM: Good pattern for reducing lock contention.

Cloning the backend Arc and dropping the guard early is a sound approach for the potentially long-running pre-key operations that follow. This allows concurrent access to the device store while maintaining safe access to the backend.

storages/sqlite-storage/src/sqlite_store.rs (4)

932-948: LGTM! Clear debug logging with helpful context.

The logging and comments effectively explain the normal concurrent behavior during session creation, which helps prevent false alarms.


1067-1112: LGTM! Batch query correctly chunked to avoid SQLite parameter limits.

The 900-item chunks safely stay under SQLite's bind parameter limit (typically 999), and returning a HashSet enables efficient lookup.


41-41: LGTM! edge_routing_info properly propagated through device persistence.

The Option<Vec<u8>> field is consistently saved, loaded, and defaulted across all device data paths.

Also applies to: 231-231, 274-274, 294-294, 430-430, 495-495


2186-2213: LGTM! Trait implementation correctly delegates to device-scoped methods.

The single-device mode (device_id = 1) delegation is consistent with other trait implementations in this file.

src/socket/error.rs (1)

91-98: Good: dedicated channel_closed constructor preserves buffers and gives a clear discriminant.
Having a first-class kind for “sender task died / channel closed” should make higher-level retries/telemetry cleaner than overloading Transport.

src/send.rs (2)

11-20: LGTM: switching send path to &wa::Message reduces Arc churn and aligns with wacore APIs.
Just ensure no spawned tasks capture message by reference beyond the send_message await boundary.

Also applies to: 22-35


83-86: The add_recent_message implementation correctly handles borrowed message references.

With message: &wa::Message, the method serializes immediately to owned Vec<u8> via msg.encode_to_vec() at line 654, then stores this in the moka cache at line 655. No dangling references. The moka cache itself handles thread-safety for concurrent access, so no additional per-chat locks are needed for this caching operation.

src/socket/noise_socket.rs (1)

38-90: Nice improvement: single sender task guarantees frame order without a mutex.
Sequential job processing + spawn_blocking for large encryptions matches the Tokio I/O guidance well.

Also applies to: 91-175

src/message.rs (2)

251-273: Replace substring matching for group/broadcast detection with explicit server equality checks.

The is_group_sender heuristic uses server.contains(".us") which is problematic. This matches any server ending in ".us" (including "c.us" for regular users), not just group servers ("g.us") and broadcast servers. If user JIDs can have a server value of "c.us", session decryption will be incorrectly skipped.

Replace with explicit equality checks: server == "g.us" || server == "broadcast" or, if available, use semantic methods like sender_encryption_jid.is_group() or sender_encryption_jid.is_broadcast().


381-429: The code is correct for rand 0.9 with TryRngCore. The .unwrap_err() call is a valid method from the TryRngCore trait (properly imported at the top of the file), which is the idiomatic way to use OsRng in rand 0.9. The pattern &mut rng.unwrap_err() and &mut rand::rngs::OsRng.unwrap_err() are both valid and compile correctly. No changes needed.

Likely an incorrect or invalid review comment.

Comment thread src/message.rs Outdated
Comment thread src/socket/noise_socket.rs
Comment thread src/socket/noise_socket.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

♻️ Duplicate comments (1)
src/message.rs (1)

93-110: Debug logs incorrectly report success even when persistence fails.

The debug log at lines 106-109 executes unconditionally after the if let Err block, so it will log "Cached LID-to-PN mapping" even if add_lid_pn_mapping returned an error. The same issue occurs at lines 120-136.

Apply this diff to gate the debug log on success:

-                    if let Err(err) = self
+                    match self
                         .add_lid_pn_mapping(
                             &sender.user,
                             &alt_jid.user,
                             crate::lid_pn_cache::LearningSource::PeerLidMessage,
                         )
                         .await
                     {
-                        warn!(
-                            "Failed to persist LID-to-PN mapping {} -> {}: {err}",
-                            sender.user, alt_jid.user
-                        );
+                        Ok(()) => debug!(
+                            "Cached LID-to-PN mapping: {} -> {}",
+                            sender.user, alt_jid.user
+                        ),
+                        Err(err) => warn!(
+                            "Failed to persist LID-to-PN mapping {} -> {}: {err}",
+                            sender.user, alt_jid.user
+                        ),
                     }
-                    debug!(
-                        "Cached LID-to-PN mapping: {} -> {}",
-                        sender.user, alt_jid.user
-                    );

Apply the same pattern to lines 120-136.

🧹 Nitpick comments (3)
src/socket/noise_socket.rs (1)

38-62: Constructor correctly spawns the sender task.

The initialization properly uses Arc for shared ownership and spawns the sender task with appropriate Tokio primitives. No error handling issues.

Optional: Consider documenting the rationale for the channel buffer size of 32 (line 45):

 // Create channel for send jobs. Buffer size of 32 allows multiple
-// callers to enqueue work without blocking on channel capacity.
+// callers to enqueue work without blocking on channel capacity.
+// 32 is chosen to balance memory usage with throughput for typical workloads.
 let (send_job_tx, send_job_rx) = mpsc::channel::<SendJob>(32);
src/handlers/message.rs (1)

79-84: Remove redundant .clone() call.

client_for_worker is already an Arc<Client> clone, so calling .clone() again on line 82 is redundant (though harmless).

Apply this diff to simplify:

                 tokio::spawn(async move {
                     while let Some(msg_node) = rx.recv().await {
-                        client_for_worker
-                            .clone()
-                            .handle_encrypted_message(msg_node)
+                        client_for_worker.handle_encrypted_message(msg_node)
                             .await;
                     }
src/message.rs (1)

251-254: Fragile string matching for server type detection.

The check sender_encryption_jid.server.contains(".us") could theoretically match unintended server names. Consider using exact server constant comparisons (e.g., wacore_binary::jid::GROUP_SERVER, wacore_binary::jid::BROADCAST_SERVER) instead of substring matching for robustness.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c32bdcc and c70fe02.

📒 Files selected for processing (5)
  • src/handlers/message.rs (3 hunks)
  • src/message.rs (10 hunks)
  • src/socket/noise_socket.rs (6 hunks)
  • src/usync.rs (2 hunks)
  • storages/sqlite-storage/src/sqlite_store.rs (24 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/usync.rs
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: All I/O in the main crate must use Tokio; be mindful of race conditions
Wrap blocking I/O (e.g., ureq) and heavy CPU-bound tasks (e.g., media encryption) in tokio::task::spawn_blocking
Avoid .unwrap() and .expect() outside of tests and truly unrecoverable paths

Files:

  • src/handlers/message.rs
  • src/socket/noise_socket.rs
  • src/message.rs
{src,wacore}/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

{src,wacore}/**/*.rs: Use thiserror for custom error types (e.g., SocketError)
Use anyhow::Error for functions with multiple failure modes

Files:

  • src/handlers/message.rs
  • src/socket/noise_socket.rs
  • src/message.rs
src/{client,send,message}.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use Client::chat_locks to serialize per-chat operations

Files:

  • src/message.rs
🧠 Learnings (3)
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to src/{client,send,message}.rs : Use Client::chat_locks to serialize per-chat operations

Applied to files:

  • src/handlers/message.rs
  • src/message.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: The whatsapp-rust (main) crate integrates wacore with Tokio for async and Diesel for SQLite persistence

Applied to files:

  • storages/sqlite-storage/src/sqlite_store.rs
📚 Learning: 2025-09-20T02:07:36.999Z
Learnt from: CR
Repo: jlucaso1/whatsapp-rust PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-09-20T02:07:36.999Z
Learning: Applies to wacore/**/*.rs : wacore must remain no_std-compatible and have no dependencies on Tokio or specific databases

Applied to files:

  • storages/sqlite-storage/src/sqlite_store.rs
🧬 Code graph analysis (3)
src/handlers/message.rs (2)
src/handlers/traits.rs (1)
  • handle (26-26)
src/client.rs (1)
  • new (171-270)
src/socket/noise_socket.rs (3)
src/socket/error.rs (2)
  • transport (69-80)
  • channel_closed (91-98)
wacore/src/handshake/utils.rs (1)
  • generate_iv (37-41)
src/transport.rs (2)
  • send (22-24)
  • disconnect (26-26)
src/message.rs (3)
wacore/binary/src/attrs.rs (2)
  • jid (75-78)
  • jid (201-204)
wacore/binary/src/jid.rs (9)
  • user (219-219)
  • user (290-292)
  • user (347-349)
  • from (213-215)
  • from (557-559)
  • from (563-565)
  • server (220-220)
  • server (293-295)
  • server (350-352)
src/lid_pn_cache.rs (1)
  • get_current_lid (156-159)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build & Test
🔇 Additional comments (27)
storages/sqlite-storage/src/sqlite_store.rs (12)

8-8: LGTM: Imports and type changes support new functionality.

The new imports (warn, Arc) and the edge_routing_info field addition to DeviceRow are appropriate for the retry logic and edge routing feature additions.

Also applies to: 10-10, 15-15, 17-17, 41-42


89-93: Pool and semaphore sizing aligned at 64.

The pool and semaphore are now consistently set to 64, which addresses previous review feedback. However, 64 concurrent SQLite connections is quite aggressive and may stress the database under certain workloads.

Consider monitoring for "database is locked" errors or high memory usage under peak load.

Also applies to: 122-122


65-65: LGTM: PRAGMA tuning and foreign key enforcement.

  • Doubling busy_timeout to 30 seconds provides more tolerance for write contention.
  • Enabling foreign_keys is essential for referential integrity, especially with the new lid_pn_mapping table.

Also applies to: 77-80


231-231: LGTM: Consistent edge_routing_info propagation.

The edge_routing_info field is consistently propagated through all device save/load paths with appropriate handling of the Option<Vec<u8>> type.

Also applies to: 274-274, 294-294, 330-330, 365-365, 385-385, 430-430, 495-495, 553-553, 724-724, 779-779


794-859: LGTM: Retry logic with exponential backoff for identity writes.

The retry logic appropriately handles SQLite write contention with exponential backoff (10→20→40→80→160ms) and warning logs for debugging.


963-1035: LGTM: Retry logic with exponential backoff for session writes.

Consistent with the identity write retry pattern, with appropriate debug logging for successful session persistence.


863-880: LGTM: Refactored read/delete operations with consistent semaphore usage.

The read operations now use the with_semaphore helper for consistent concurrency control. The extensive debug logging in get_session_for_device (lines 932-948) helpfully explains that missing sessions during concurrent message processing is normal behavior.

Also applies to: 889-909, 916-954, 1039-1057


1067-1112: LGTM: Batch session checking with proper chunking.

The batch API correctly chunks queries at 900 addresses to stay well below SQLite's bind parameter limit (~999), addressing previous review feedback.


1995-2033: LGTM: LID-PN mapping lookups handle both directions.

The lookup methods properly query by LID or phone number, with the phone lookup correctly ordering by updated_at DESC to retrieve the most recent mapping (line 2057), and the comment at line 2046 accurately describes this behavior.

Also applies to: 2035-2075


2077-2122: LGTM: LID-PN mapping upsert correctly preserves created_at.

The upsert logic properly:

  • Sets created_at only on initial insert (line 2103)
  • Updates only phone_number, learning_source, and updated_at on conflict (lines 2110-2113)

This addresses previous review feedback about not overwriting created_at on updates.


2124-2161: LGTM: LID-PN mapping retrieval and deletion.

The get_all and delete operations follow the established pattern for device-scoped queries.

Also applies to: 2163-2183


2185-2212: LGTM: LidPnMappingStore trait delegates to device_id=1.

The trait implementation follows the established pattern of delegating to device-specific methods with device_id=1 for single-device mode, consistent with other trait implementations in this file.

src/socket/noise_socket.rs (7)

1-23: LGTM: Clean async primitives and type definitions.

The imports and type definitions properly set up the task-based send pipeline. SendJob encapsulates the work unit with buffers and a response channel, while SendResult clearly defines the success/error return signature.


25-36: LGTM: Well-structured ownership model.

The refactored struct properly delegates write operations to a dedicated sender task via an MPSC channel, avoiding lock contention while maintaining sequential ordering guarantees.


64-89: LGTM: Sequential processing ensures ordering.

The sender task correctly owns write_counter and processes jobs one at a time, guaranteeing FIFO ordering and monotonic counter increments without synchronization overhead. The graceful shutdown on channel closure is proper.


91-174: LGTM: Correct encryption and framing logic with proper async handling.

The function correctly:

  • Owns and increments the write counter before encryption
  • Uses in-place encryption for small messages to avoid allocations
  • Offloads large-message encryption to spawn_blocking (complies with coding guidelines)
  • Preserves buffer ownership through all error paths for caller reuse
  • Uses async transport correctly

Line 145's unwrap_or_else fallback is defensive—try_unwrap should always succeed since the spawned blocking task has completed—but the defensive clone is harmless.


176-203: LGTM: Buffer recovery now correctly implemented.

Lines 186-193 properly recover the buffers from the SendError when the channel send fails, addressing the previous review concern. The caller can now reuse their allocated capacity even on channel-closed errors.

Lines 198-200: Returning empty Vecs when the oneshot receive fails is unavoidable—the sender task holds the buffers and has either panicked or been aborted (e.g., during Drop). This is an acceptable edge case for abnormal shutdown scenarios.


214-221: LGTM: Proper cleanup via task abort.

Aborting the sender task on drop prevents resource leaks if the transport hangs. In-flight encrypt_and_send calls will fail gracefully with empty buffers (acceptable for abnormal shutdown).


278-354: LGTM: Test now correctly verifies FIFO ordering.

The test addresses the previous review concern by:

  1. Embedding a unique index in each message's plaintext (line 337)
  2. Using a RecordingTransport that decrypts and extracts the index from each sent frame (lines 304-310)
  3. Asserting the recorded send order matches the expected sequence [0, 1, 2, ..., 9] (lines 350-353)

This validates that the sender task maintains FIFO ordering across concurrent encrypt_and_send calls.

src/handlers/message.rs (2)

48-62: Well-designed concurrency control.

The enqueue lock pattern correctly ensures message ordering even when the queue is created concurrently. The lock is held for the entire enqueue operation (including queue creation), preventing race conditions.


94-97: Message loss on enqueue failure is logged but not recovered.

If tx.send(node).await fails (e.g., channel closed), the message is logged but lost. This is acceptable if the channel should never close during normal operation, but ensure upstream error handling or monitoring can detect this condition.

src/message.rs (6)

367-379: Well-designed per-sender session locking.

Using the full Signal protocol address string as the lock key correctly prevents race conditions when multiple messages from the same sender are processed concurrently, and aligns with SignalProtocolStoreAdapter's internal locking.


472-486: Correctly avoids holding lock across async I/O.

The backend handle is properly extracted while holding the lock (lines 472-478), then the lock is dropped before the async delete_identity call (line 486). This avoids the lock contention issue flagged in the previous review.


938-950: Correct bidirectional sender_alt extraction.

The logic properly handles both LID senders (looking for sender_pn) and PN senders (looking for sender_lid), enabling bidirectional session resolution regardless of which address format is used.


1668-1725: Comprehensive test coverage for LID protocol address format.

This test validates that the protocol address generation matches WhatsApp Web's format, which is critical for session compatibility. The test cases cover various edge cases including multiple dots and missing device numbers.


2554-2794: Excellent test coverage for LID-PN cache behavior.

These test cases thoroughly validate the cache population logic for various scenarios:

  • DM messages with sender_lid attribute
  • Messages without sender_lid (negative case)
  • LID senders with participant_pn in groups
  • Repeated messages (idempotency)

The documentation clearly explains the purpose and expected behavior of each test.


2813-3128: Thorough validation of session resolution logic.

These tests cover all three session resolution paths comprehensively:

  1. Using LID from sender_lid attribute (lines 2813-2926)
  2. Using cached LID mapping when attribute absent (lines 2928-3035)
  3. Falling back to PN when no mapping exists (lines 3037-3128)

The tests validate the critical fix for MAC verification failures by ensuring the Rust client matches WhatsApp Web's session lookup behavior.

Comment thread src/handlers/message.rs
.get_with_by_ref(&chat_id, async {
// Create a channel with backpressure
// Increased capacity to handle high message rates without blocking
let (tx, mut rx) = mpsc::channel::<Arc<Node>>(10000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider potential memory pressure from large channel capacity.

The capacity of 10,000 messages per chat, combined with up to 10,000 cached chats, could result in significant memory usage (potentially millions of messages in memory) under high load scenarios. While the 5-minute TTL helps expire idle queues and each message is just an Arc<Node>, consider whether this capacity is necessary or if a lower value (e.g., 1000) with appropriate backpressure handling would be safer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants