Fix merged-pull query: include posting identities, not network id

Phase 3 merged-pull was shipped when network_id == posting_id (pre-
v0.6.1), so adding our_node_id to the pull query's follows list was
enough to trigger recipient-match on DMs. v0.6.1 broke that:

- Fresh installs generate independent network + posting keys; DMs are
  encrypted to posting id, but the pull query carried network id.
  Recipient-match never fired. Non-follower DMs never reached.
- Upgraders rotated the network key; DMs addressed to the old key
  (now the default posting id) never matched either.

Fix: pull-request builders now include every entry from
list_posting_identities() in query_list. Network id is omitted — it
is never an author and never a wrapped_key.recipient, so adding it
would only leak the boundary without ever matching.

Four call sites fixed: Network::pull_from_peer, and
ConnectionManager::{pull_from_peer, pull_from_peer_unlocked, and the
post-notification-triggered pull path}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-04-22 20:29:47 -04:00
parent 3c5b80d017
commit 4da6a8dc85
2 changed files with 38 additions and 19 deletions

View file

@ -1346,18 +1346,25 @@ impl ConnectionManager {
conn: iroh::endpoint::Connection, conn: iroh::endpoint::Connection,
storage: &Arc<StoragePool>, storage: &Arc<StoragePool>,
peer_id: &NodeId, peer_id: &NodeId,
our_node_id: NodeId, _our_node_id: NodeId,
) -> anyhow::Result<PullSyncStats> { ) -> anyhow::Result<PullSyncStats> {
let (our_follows, follows_sync) = { let (our_follows, follows_sync, our_personas) = {
let s = storage.get().await; let s = storage.get().await;
(s.list_follows()?, s.get_follows_with_last_sync().unwrap_or_default()) (
s.list_follows()?,
s.get_follows_with_last_sync().unwrap_or_default(),
s.list_posting_identities().unwrap_or_default(),
)
}; };
// Merged pull: include our own NodeId in the query so the peer returns // Merged pull: include every posting identity we hold so DMs to any
// posts where we're either a followed author OR a recipient (DM). // of our personas match via wrapped_key.recipient. Network NodeId is
// never an author or recipient and would never match.
let mut query_list = our_follows; let mut query_list = our_follows;
if !query_list.contains(&our_node_id) { for pi in &our_personas {
query_list.push(our_node_id); if !query_list.contains(&pi.node_id) {
query_list.push(pi.node_id);
}
} }
let request = PullSyncRequestPayload { let request = PullSyncRequestPayload {
@ -1911,18 +1918,21 @@ impl ConnectionManager {
}, },
}; };
let (our_follows, follows_sync) = { let (our_follows, follows_sync, our_personas) = {
let storage = self.storage.get().await; let storage = self.storage.get().await;
( (
storage.list_follows()?, storage.list_follows()?,
storage.get_follows_with_last_sync().unwrap_or_default(), storage.get_follows_with_last_sync().unwrap_or_default(),
storage.list_posting_identities().unwrap_or_default(),
) )
}; };
// Merged pull: include our own NodeId in the query list. // Merged pull: include every posting identity so DMs match recipient.
let mut query_list = our_follows; let mut query_list = our_follows;
if !query_list.contains(&self.our_node_id) { for pi in &our_personas {
query_list.push(self.our_node_id); if !query_list.contains(&pi.node_id) {
query_list.push(pi.node_id);
}
} }
let (mut send, mut recv) = pull_conn.open_bi().await?; let (mut send, mut recv) = pull_conn.open_bi().await?;
@ -2009,18 +2019,21 @@ impl ConnectionManager {
.get(peer_id) .get(peer_id)
.ok_or_else(|| anyhow::anyhow!("not connected to {}", hex::encode(peer_id)))?; .ok_or_else(|| anyhow::anyhow!("not connected to {}", hex::encode(peer_id)))?;
let (our_follows, follows_sync) = { let (our_follows, follows_sync, our_personas) = {
let storage = self.storage.get().await; let storage = self.storage.get().await;
( (
storage.list_follows()?, storage.list_follows()?,
storage.get_follows_with_last_sync().unwrap_or_default(), storage.get_follows_with_last_sync().unwrap_or_default(),
storage.list_posting_identities().unwrap_or_default(),
) )
}; };
// Merged pull: include our own NodeId in the query list. // Merged pull: include every posting identity so DMs match recipient.
let mut query_list = our_follows; let mut query_list = our_follows;
if !query_list.contains(&self.our_node_id) { for pi in &our_personas {
query_list.push(self.our_node_id); if !query_list.contains(&pi.node_id) {
query_list.push(pi.node_id);
}
} }
let request = PullSyncRequestPayload { let request = PullSyncRequestPayload {

View file

@ -1713,17 +1713,23 @@ impl Network {
/// Pull posts from a peer (persistent if available, ephemeral otherwise). /// Pull posts from a peer (persistent if available, ephemeral otherwise).
pub async fn pull_from_peer(&self, peer_id: &NodeId) -> anyhow::Result<PullStats> { pub async fn pull_from_peer(&self, peer_id: &NodeId) -> anyhow::Result<PullStats> {
let conn = self.get_connection(peer_id).await?; let conn = self.get_connection(peer_id).await?;
let (our_follows, follows_sync) = { let (our_follows, follows_sync, our_personas) = {
let storage = self.storage.get().await; let storage = self.storage.get().await;
( (
storage.list_follows()?, storage.list_follows()?,
storage.get_follows_with_last_sync().unwrap_or_default(), storage.get_follows_with_last_sync().unwrap_or_default(),
storage.list_posting_identities().unwrap_or_default(),
) )
}; };
// Merged pull: include our own NodeId so DMs addressed to us match. // Merged pull: include every posting identity we hold so DMs addressed
// to any of our personas match on recipient. Our network NodeId is
// never an author nor a wrapped_key recipient — including it would
// never match and would leak the network↔posting boundary.
let mut query_list = our_follows; let mut query_list = our_follows;
if !query_list.contains(&self.our_node_id) { for pi in &our_personas {
query_list.push(self.our_node_id); if !query_list.contains(&pi.node_id) {
query_list.push(pi.node_id);
}
} }
let (mut send, mut recv) = conn.open_bi().await?; let (mut send, mut recv) = conn.open_bi().await?;
write_typed_message( write_typed_message(