Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
46 changes: 46 additions & 0 deletions agent_docs/adv_account_identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# ADV account anchors and hosted devices

The ADV account anchor and the Signal device address are different identities.
For a `user:99@hosted` device the anchor is `user@s.whatsapp.net`; for
`user:99@hosted.lid` it is `user@lid`. The device session/identity retains its
original hosted namespace. `adv_account_jid` is restricted to selecting the
anchor; do not replace general JID normalization or Signal address rendering.

Static reference: whatspec commit
`ae3880925bacd23863cc30ec480c9f82bfd869f1`, schema `4.5.0`, WA
`2.3000.1052559453`, 676 bundles, set hash
`d6ab7ae601eda281cfac570a68de1d787adde8f4d778f0bb994416f661a21abf`.
The `bundle-store` asset `bundles-2.3000.1052559453-<set hash>.tar.xz` has
SHA256 `2958d99a3d84141fa8479f1f991e06246e68c063ca8f618a8d5d361287387f62`.
The archive and all 676 bundle hashes/sizes were checked against the pinned
lock. Complete modules were extracted using an AST, without executing them.

Relevant original byte offsets (start inclusive, end exclusive):

| Bundle | Module | Offsets |
| --- | --- | --- |
| `0RwvWNH8_qZ.js` | `WAWebAdvSignatureApi` | 767477–775940 |
| `0RwvWNH8_qZ.js` | `WAWebHandleAdvDeviceNotificationUtils` | 731532–733850 |
| `331d6f8883e8cd73ff267508b8e5889920b0219c7fd88325ee56ee22eefdcb00.js` | `WAWebWidFactory` | 545429–549200 |
| same | `WAWebWid` | 535329–545312 |
| same | `WAWebSignalAddress` | 3107699–3108968 |

`validateADVwithIdentityKey` reads the original device identity separately
from the anchor selected with `asUserWidOrThrow`. That helper maps Hosted to
the ordinary PN family and HostedLid to LID. SignalAddress preserves hosted
device addressing; Wid rejects a hosted primary with device 0. This is static
Web evidence, not a claim about the server's response to malformed IQs.

Account fences must select the same anchor as verification. Multi-lock
acquisition groups by account family, then user/device, retaining the original
server as a tie-breaker. Plain lexical server ordering would acquire a hosted
LID device before its ordinary LID anchor, opposite to inbound processing.
The comparator is only for Signal locks; phash and device-list ordering retain
their existing rules.

The paired own-account fallback remains trusted only for an exact account
family/user match. Embedded certificate keys cannot replace an existing
anchor, and both signatures still use the original device's hosted semantics.
Cold bootstrap retains bounded singleflight, generation cancellation and owned
durable publication. A hosted USync list follows its existing separately
authenticated policy and cannot replace our paired primary identity.
2 changes: 1 addition & 1 deletion src/client/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ impl Client {
if jid.device == 0 {
return None;
}
let primary = jid.with_device(0).to_protocol_address();
let primary = wacore::types::jid::adv_account_jid(jid).to_protocol_address();
Some(
self.session_lock_for(primary.as_str())
.await
Expand Down
4 changes: 2 additions & 2 deletions src/client/lid_pn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1144,8 +1144,8 @@ impl Client {
// must not publish new trust while this iteration retains old
// session material. Use the send path's order for the whole set.
let mut keys = vec![
from.with_device(0),
to.with_device(0),
wacore::types::jid::adv_account_jid(from),
wacore::types::jid::adv_account_jid(to),
pn_jid.clone(),
lid_jid.clone(),
];
Expand Down
48 changes: 48 additions & 0 deletions src/client/node_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2820,6 +2820,54 @@ impl Client {

#[cfg(test)]
mod tests {
/// Compare this diagnostic on the exact base and head with identical flags.
/// It constructs futures without polling them or opening a network session.
#[tokio::test]
#[ignore = "layout diagnostic: run on base and head with --ignored --nocapture"]
async fn audit_connected_retention_future_layouts() {
use std::mem::{size_of, size_of_val};
let client = crate::test_utils::create_test_client().await;
let shutdown = client.connection_shutdown_signal();
let node = crate::test_utils::node_to_owned_ref(&NodeBuilder::new("iq").build());
println!(
"QueueBudgets: {} bytes",
size_of::<crate::inbound_queue::QueueBudgets>()
);
println!(
"read_messages_loop: {} bytes",
size_of_val(&client.read_messages_loop())
);
println!(
"read_messages_loop_inner: {} bytes",
size_of_val(&client.read_messages_loop_inner())
);
println!(
"process_node_scoped: {} bytes",
size_of_val(&client.process_node_scoped(node.clone(), &shutdown, false))
);
println!(
"dispatch_node_scoped: {} bytes",
size_of_val(&client.dispatch_node_scoped(node, &shutdown, Some(StanzaTag::Iq), 0))
);
let reader_client = client.clone();
let reader = async move {
let connection = reader_client.connect().await?;
connection.read_until_disconnected().await;
Ok::<(), ConnectError>(())
};
println!(
"connected fixture reader task: {} bytes",
size_of_val(&reader)
);
drop(reader);
let primary = Jid::pn("12025550189");
println!(
"ensure_adv_account_identity: {} bytes",
size_of_val(&client.ensure_adv_account_identity(&primary))
);
client.signal_shutdown_sync();
}

#[cfg(feature = "mobile-core")]
use crate::test_utils::primary_startup_client;

Expand Down
8 changes: 7 additions & 1 deletion src/inbound_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pub(crate) struct QueueBudgets {
pub chat: Arc<ByteBudget>,
pub ack: Arc<ByteBudget>,
pub work: Arc<crate::flush_scope::FlushScope>,
pub adv_bootstrap: std::sync::OnceLock<Arc<crate::prekeys::AdvBootstrapRegistry>>,
per_chat: Arc<BudgetRegistry>,
pub draining: std::sync::atomic::AtomicBool,
}
Expand All @@ -137,6 +138,7 @@ impl Default for QueueBudgets {
chat: Arc::new(ByteBudget::new(GLOBAL_CHAT_BYTES)),
ack: Arc::new(ByteBudget::new(ACK_BYTES)),
work: Arc::default(),
adv_bootstrap: std::sync::OnceLock::new(),
per_chat: Arc::default(),
draining: false.into(),
}
Expand All @@ -162,7 +164,11 @@ impl QueueBudgets {
// Controller, global counters and work scope are counted once.
+ size_of::<Self>()
+ 2 * (size_of::<ByteBudget>() + 2 * size_of::<usize>())
+ size_of::<crate::flush_scope::FlushScope>() + 2 * size_of::<usize>();
+ size_of::<crate::flush_scope::FlushScope>() + 2 * size_of::<usize>()
+ self
.adv_bootstrap
.get()
.map_or(0, |registry| registry.retained_bytes());
(
wacore::stats::CollectionStats::new(entries.len() as u64, registry_bytes as u64),
GLOBAL_CHAT_BYTES.saturating_sub(self.chat.available()) as u64,
Expand Down
Loading
Loading