diff --git a/advanced/signal-protocol.mdx b/advanced/signal-protocol.mdx
index 1aba4b6..6a2a9fa 100644
--- a/advanced/signal-protocol.mdx
+++ b/advanced/signal-protocol.mdx
@@ -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
+
+**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`, `Rc`, `Arc`, `Cow`), 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()`.
+
+
### Device resolution
The DM send path builds the full device list in a WA Web-compliant manner (matching `WAWebSendUserMsgJob` and `WAWebDBDeviceListFanout`):
@@ -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` 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, 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: