Skip to content
Merged
Show file tree
Hide file tree
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
17 changes: 14 additions & 3 deletions api/store.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -292,16 +292,27 @@ async fn touch_tc_token_sender_timestamp(&self, jid: &str, sender_timestamp: i64
/// `sender_timestamp`. The symmetric counterpart of
/// `touch_tc_token_sender_timestamp` — each writer owns its own field, so the
/// notification path never drops a sender bucket the issuance path wrote
/// concurrently. Same atomicity requirement as above.
/// concurrently.
///
/// **Newer-wins**: the stored `(token, token_timestamp)` pair is overwritten
/// only when the existing token is a byte-less placeholder, or the incoming
/// `token_timestamp` is at least as new as the stored one — a stale write must
/// never clobber a fresher real token. `SqliteStore` enforces this atomically
/// inside an `IMMEDIATE` transaction; `InMemoryBackend` under its state lock.
/// This is what lets concurrent history-sync chunks and the
/// privacy-notification path converge on the same row without an external
/// lock. The default read-modify-write below is a best-effort for
/// third-party backends — same atomicity caveat as
/// `touch_tc_token_sender_timestamp`.
async fn store_received_tc_token(&self, jid: &str, token: &[u8], token_timestamp: i64) -> Result<()> {
// default: read-modify-write via get_tc_token + put_tc_token
// default: read-modify-write via get_tc_token + put_tc_token, newer-wins
}
```

<Note>
`delete_expired_tc_tokens` gained a second `sender_cutoff` parameter — **this is a breaking change** for any custom backend that overrides it; update the signature to `(&self, token_cutoff: i64, sender_cutoff: i64) -> Result<u32>` and prune on both windows independently (see the built-in `SqliteStore`/`InMemoryBackend` implementations for the two-filter pattern).

`touch_tc_token_sender_timestamp` and `store_received_tc_token` are new defaulted methods — existing custom backends compile and work unchanged, but should override both with an atomic upsert if the backend supports one, since the default read-modify-write can race a concurrent writer touching the same row (post-send issuance vs. an incoming `privacy_token` notification).
`touch_tc_token_sender_timestamp` and `store_received_tc_token` are defaulted methods — existing custom backends compile and work unchanged, but should override both with an atomic upsert if the backend supports one, since the default read-modify-write can race a concurrent writer touching the same row (post-send issuance vs. an incoming `privacy_token` notification). For `store_received_tc_token` specifically, a non-atomic override that races two callers can let an older token's write land last and clobber a fresher one — the built-in backends close this by making the newer-wins check part of the same read+write.
</Note>

### Sent message store
Expand Down
8 changes: 5 additions & 3 deletions api/tctoken.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,14 @@ When a contact sends you a privacy token, the library handles it automatically:

1. Parses the `<notification type="privacy_token">` stanza
2. Resolves the sender to a LID for storage (using `sender_lid` attribute or LID-PN cache)
3. Applies a timestamp monotonicity guardan older token doesn't overwrite a newer *real* token
3. Applies a timestamp monotonicity pre-filterif the incoming token is older than a stored *real* token, storage and the presence re-subscribe are both skipped
4. Stores the token in the backend, preserving any `sender_timestamp` already recorded by the post-send issuance path
5. Re-subscribes presence for the sender to pick up the updated token

The same newer-wins rule (older writes rejected, a byte-less placeholder always accepts the first real token) is also enforced **atomically inside the store itself** — see [`store_received_tc_token`](/api/store#tctoken-storage). This is what closes the cross-source race between this notification path and history-sync's tc-token candidates: both call the same store method, so whichever call lands last can never clobber a fresher token, without needing a lock shared across the two paths.

<Note>
A byte-less placeholder (written by post-send issuance before any real token has been received) is always replaced by the contact's first real token, even if the placeholder's own timestamp is newer — the monotonicity guard only applies once a real token is on record.
A byte-less placeholder (written by post-send issuance before any real token has been received) is always replaced by the contact's first real token, even if the placeholder's own timestamp is newer — the newer-wins rule only blocks a stale write once a real token is on record.
</Note>

### Startup pruning
Expand Down Expand Up @@ -384,7 +386,7 @@ sequenceDiagram
Note over Client,Server: Incoming token notification
Server->>Client: notification type="privacy_token"
Client->>Client: Resolve sender LID
Client->>Backend: store_received_tc_token (monotonicity guard, preserves sender_timestamp)
Client->>Backend: store_received_tc_token (atomic newer-wins, preserves sender_timestamp)
Client->>Server: Re-subscribe presence

Note over App,Server: Outgoing 1:1 call offer (voip feature, unconditional — no AB prop gate)
Expand Down
4 changes: 3 additions & 1 deletion concepts/storage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,9 @@ pub trait ProtocolStore: Send + Sync {
// byte-less placeholder if no entry exists yet. Defaulted (read-modify-write).
async fn touch_tc_token_sender_timestamp(&self, jid: &str, sender_timestamp: i64) -> Result<()> { /* default */ }
// Store a token received from a contact without clobbering an existing
// sender_timestamp. Defaulted (read-modify-write).
// sender_timestamp. Newer-wins: a stale write (older token_timestamp,
// unless the existing token is a byte-less placeholder) is a no-op.
// Defaulted (read-modify-write); SqliteStore/InMemoryBackend enforce it atomically.
async fn store_received_tc_token(&self, jid: &str, token: &[u8], token_timestamp: i64) -> Result<()> { /* default */ }

// Sent Message Store (retry support, matches WA Web's getMessageTable)
Expand Down