# Type-Safe Protocol Node Architecture
All protocol stanza builders use the declarative, type-safe pattern defined in `wacore/src/iq/`. This architecture provides compile-time safety, validation, and clear separation between request building and response parsing.
## Core Traits
### `ProtocolNode` (`wacore/src/protocol.rs`)
Maps Rust structs to WhatsApp protocol nodes:
```rust
pub trait ProtocolNode: Sized {
fn tag(&self) -> &'static str;
fn into_node(self) -> Node;
fn try_from_node(node: &Node) -> Result<Self>;
}
```
### `IqSpec` (`wacore/src/iq/spec.rs`)
Pairs IQ requests with their typed responses:
```rust
pub trait IqSpec {
type Response;
fn build_iq(&self) -> InfoQuery<'static>;
fn parse_response(&self, response: &Node) -> Result<Self::Response>;
}
```
## Derive Macros (Recommended)
Use the derive macros from `wacore-derive` (re-exported via `wacore`):
```rust
use wacore::{ProtocolNode, EmptyNode, StringEnum};
// Empty node (tag only)
#[derive(EmptyNode)]
#[protocol(tag = "participants")]
pub struct ParticipantsRequest;
// Node with string attributes
#[derive(ProtocolNode)]
#[protocol(tag = "query")]
pub struct QueryRequest {
#[attr(name = "request", default = "interactive")]
pub request_type: String,
}
// Enum with string representations
#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
pub enum BlocklistAction {
#[str = "block"]
Block,
#[str = "unblock"]
Unblock,
}
```
**Available derive macros:**
- `EmptyNode` - For nodes with only a tag (no attributes)
- `ProtocolNode` - For nodes with string attributes
- `StringEnum` - For enums with string representations (generates `as_str()`, `Display`, `TryFrom<&str>`, `Default`)
For enums where the default should not be the first variant, use `#[string_default]`.
### Legacy Declarative Macros
Prefer derive macros for new code. Declarative macros in `wacore/src/protocol.rs` (`define_empty_node!`, `define_simple_node!`) are also available but considered legacy.
## Implementation Pattern
1. Define request struct with `ProtocolNode` (or derive macro)
2. Define response struct with `ProtocolNode`
3. Create `IqSpec` implementation pairing them
4. Use `client.execute(Spec::new(&jid)).await?` in feature code
See `wacore/src/iq/groups.rs` and `wacore/src/iq/blocklist.rs` for complete examples.
## IQ Executor
Use `Client::execute()` for simplified IQ request/response handling:
```rust
// Single call replaces manual build + send + parse
let response = client.execute(GroupQueryIq::new(&jid)).await?;
```
**API Design Note**: IqSpec constructors should take `&Jid` (not `Jid`) to avoid forcing callers to clone.
## Node Parsing Helpers
Use helpers from `wacore/src/iq/node.rs`:
```rust
use crate::iq::node::{required_child, required_attr, optional_attr, optional_jid};
fn try_from_node(node: &Node) -> Result<Self> {
let id = required_attr(node, "id")?;
let name = optional_attr(node, "name");
let jid = optional_jid(node, "jid")?;
let child = required_child(node, "group")?;
}
```
## Validated Newtypes
Use newtypes to enforce protocol constraints at compile time. See `GroupSubject` in `wacore/src/iq/groups.rs` for examples.
Constants from WhatsApp Web A/B props:
- `GROUP_SUBJECT_MAX_LENGTH`: 100 characters
- `GROUP_DESCRIPTION_MAX_LENGTH`: 2048 characters
- `GROUP_SIZE_LIMIT`: 257 participants
## File Organization
```text
wacore/src/iq/
├── mod.rs # Re-exports
├── spec.rs # IqSpec trait definition
├── node.rs # Helper functions
├── groups.rs # Group types + IqSpec impls
└── blocklist.rs # Blocklist types + IqSpec impls
```
Each feature file contains: constants, enums (`StringEnum`), request/response structs (`ProtocolNode`), `IqSpec` impls, and unit tests.
## Noise Handshake Patterns
Three Noise patterns coexist, mirroring WA Web's `WAWebOpenChatSocket`:
| **XX** | First connect / pairing / forced fallback | `XxHandshakeState` | 1.5 RTT |
| **IK** | Reconnect with valid cached `serverStaticPub` | `IkHandshakeState` | 1 RTT, ships 0-RTT login payload |
| **XXfallback** | Server rejects in-flight IK (reply has `static != null`) | `XxFallbackHandshakeState` | 1 RTT (reuses already-sent eph.) |
### Selection (`src/handshake.rs::select_pattern`)
```text
ik_failures >= 1 ───────────────────────────────────────► XX
no cached server_cert_chain ─────────────────────────────► XX
leaf.not_after < now OR intermediate.not_after < now ────► XX
otherwise ──────────────────────────────────────────────► IK with leaf.key
```
The counter `Client.ik_handshake_failures: AtomicU32` is per-process and
not persisted (matches WA Web's `K = 0` reset on process start).
### Invalidation policy
| Transient (timeout, disconnect, transport) | unchanged | unchanged |
| Crypto-fatal during IK (cert MAC, decrypt, proto) | `+= 1` | cleared via `DeviceCommand::ClearServerCertChain`|
| XX or XX-fallback failure | unchanged | unchanged (XX never reads the cache) |
| Any successful handshake | reset to `0` | repopulated (XX, XX-fallback) or kept (IK Continue)|
Distinguishing transient from crypto-fatal is via `HandshakeError::is_transient()`
and `HandshakeError::is_crypto_fatal()`. Getting the classification wrong leads
to either oscillating back to XX needlessly or looping on a stale cache.
### Persisted state (`Device.server_cert_chain`)
`CachedServerCertChain { intermediate, leaf }` with each cert reduced to
`{ key: [u8; 32], not_before: i64, not_after: i64 }`. Mirrors the
storage layout WA Web uses in `PrefsInfoStore.js:setCertificateChain` —
only those fields end up on disk.
`verify_server_cert` checks structural shape, the issuer-serial pin, the
chain link, and that `leaf.key` matches the decrypted Noise static.
Ed25519 signature verification against `WA_CERT_PUB_KEY` is intentionally
skipped (would break the e2e mock server). Same posture as whatsmeow.
### Logs (matching WA Web's `[socket]` lines)
```text
[socket] doFullHandshake: openChatSocket send hello
[socket] resumeNoiseHandshake started
[socket] resumeNoiseHandshake send hello
[socket] resumeNoiseHandshake rcv hello
[socket] resumeNoiseHandshake deriving secrets
[socket] resumeNoiseHandshake failed: serverStaticCiphertext not null —
doFallbackHandshake continuing handshake with given server hello
[socket] continueFullHandshakeCore client finish and deriving secrets
```