Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions advanced/signal-protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ When sending a direct message, the library resolves all known devices for both t
- **Recipient devices** receive the actual message content
- **Own other devices** (your other linked devices) receive a `DeviceSentMessage` wrapper containing the message plus the destination JID, so your other devices can display the sent message in the correct chat

<Note>
**Destination JID encoding via `DsmDestination` ([#1137](https://github.com/oxidezap/whatsapp-rust/pull/1137)).** The `DeviceSentMessage` wrapper writes its destination JID as a length-prefixed protobuf field, which needs the encoded length before the bytes. `wacore::messages::MessageUtils::encode_dm_plaintexts` and `dm_plaintexts_from_encoded` used to take `destination_jid: &str`, so the caller rendered the `Jid` into a `String` purely to measure and copy it. Both now take `impl DsmDestination` instead. `DsmDestination` is implemented directly on `Jid`, so it can measure and write its own wire form without an intermediate `String`. It's also implemented on `str` and the standard string wrappers (`String`, `Box<str>`, `Rc<str>`, `Arc<str>`, `Cow<str>`), carried through references of any depth via two blanket impls. The DM send path now passes `to_jid: &Jid` directly instead of `&to_jid.to_string()`.
</Note>

### Device resolution

The DM send path builds the full device list in a WA Web-compliant manner (matching `WAWebSendUserMsgJob` and `WAWebDBDeviceListFanout`):
Expand Down Expand Up @@ -1723,6 +1727,28 @@ pub struct SessionRecord {

Only rare operations (archive current session, promote previous session, take/restore during session setup) trigger `Arc::make_mut` and a deep copy.

### Chain key buffer reuse

As of [#1137](https://github.com/oxidezap/whatsapp-rust/pull/1137), advancing a chain key no longer allocates a fresh buffer for its persisted 32-byte key material on every step. `SessionState` stores each chain key's bytes as `Option<bytes::Bytes>` on the underlying protobuf `ChainKey` field. `Bytes` is immutable, so writing the ratcheted key used to be an unconditional `Bytes::copy_from_slice(..)` on every send or receive that advances a session's chain key. In the harness benchmark that motivated this change — a single-device 1:1 pingpong session — a full message round trip advances chain keys three times (twice sending, once receiving); a real send can touch more sessions than that, since [DM device fanout](#dm-device-fanout) encrypts separately for every resolved recipient and own-device session.

`write_chain_key` (`wacore/libsignal/src/protocol/state/session.rs`) instead reuses the existing buffer in place when it safely can:

```rust
fn write_chain_key(field: &mut Option<bytes::Bytes>, key: &[u8]) {
if let Some(existing) = field.take()
&& existing.len() == key.len()
&& let Ok(mut owned) = existing.try_into_mut()
{
owned.copy_from_slice(key);
*field = Some(owned.freeze());
return;
}
*field = Some(bytes::Bytes::copy_from_slice(key));
}
```

Reuse only happens when both guards pass: `try_into_mut()` succeeds solely when the `Bytes` is uniquely owned (no other clone observing the old key), and the length check keeps a differently-sized buffer (e.g. from a legacy record) from reaching `copy_from_slice` at all — like the slice method it resolves to via `DerefMut`, a length mismatch there panics rather than writing anything. Either guard failing falls back to the original allocating behavior. In steady state a checked-out session record is uniquely owned — the cache takes it out of its `Arc` via `try_unwrap` (see [Session object cache](#session-object-cache) above) — so the fallback is rare.

### Redundant signal store write elimination

The `SignalStoreCache` uses targeted deduplication strategies per store type. For identities (which rarely change), `put_dedup()` compares incoming bytes against the cached value and skips if identical:
Expand Down