feat: read peer packages from the synchronizer, not Noise - #468
Conversation
`GET /packages/compare-peers` opened a Noise connection to every peer and asked it to list its packages. The synchronizer already replicates each participant's `VettedPackages` mapping to every member, so this node can read a peer's vetting from its own topology store and contact nobody. `fetch_vetted_packages` did that read for this participant only. It now takes the participant as an argument, so the same call answers for a peer, and the self-read goes through it unchanged. Three things improve. A peer that is up but unreachable over Noise no longer reports as having no packages. A peer whose Noise public key this node does not hold is now compared, where the old fan-out skipped it. And the answer describes what the peer has vetted, which is what decides whether it can run a package, rather than what it has uploaded. Two new `PeerErrorKind` variants say why a row is empty: `TopologyReadFailed` and `NoVettedPackages`. The Noise variants stay on the wire for the callers that still use the transport. The comparison also re-checks the participant uid on each result, because Canton treats `filter_participant` as a prefix match.
The handler reports an empty vetting set as NoVettedPackages with reachable: false, so the old message named a decode failure that can no longer happen. The assertion now matches the message the classifier emits.
49944c7 to
7921d87
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The new wire values are backward-incompatible, empty successful reads conflict with reachable semantics, and peer reads are serialized.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Moves peer package comparison from Noise requests to local synchronizer topology reads.
Changes:
- Adds topology-backed peer vetting lookup and result construction.
- Adds topology-specific peer error variants and CLI labels.
- Updates unit and integration-test expectations.
File summaries
| File | Description |
|---|---|
crates/decman/src/server/package_inventory.rs |
Reads and validates peer-vetted package IDs. |
crates/decman/src/server/handlers/parties.rs |
Replaces Noise fan-out with topology reads. |
crates/common/src/types.rs |
Adds topology-related error variants. |
crates/decman-cli/src/ui.rs |
Labels the new error variants. |
crates/decman/tests/common/phases/check_peer_dars.rs |
Updates comparison expectations. |
crates/decman/tests/common/phases/concurrent_sibling_cancel.rs |
Updates obsolete transport commentary. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
schronck
left a comment
There was a problem hiding this comment.
Checked the Canton side against the source. filter_participant is split on :: and both halves go into the store as LIKE prefixes, so the exact uid check is right. The proto participant_uid is the full uid string, which matches what CantonId prints. CI is green including the IT run.
Nothing here blocks. I would still fix the first point in this PR, because the PR changes what the icon means.
-
The UI reads only
reachableand nevererror_kind(crates/decman/frontend/src/components/PackagesPanel.tsx:171). A peer that vetted nothing and a failed local topology read both render as the WiFi-off icon with tooltip "Unreachable". After a live upgrade with a stale synchronizer-id cache, every peer on this page will show as unreachable. Smallest fix: build the tooltip fromerror_kind. Copilot's third comment is the same point. -
The exact uid filter is the one new correctness rule and has no test. Inline.
-
Local rows are the uploaded set, peer cells are the vetted set. Inline.
Rest inline. Copilot's serde point only bites an old decman-cli against a new node, which we do not ship. Copilot's serial-await point matters little here: the endpoint is a button click with under ten peers.
The peer loop reconnected per peer. `TopologyReader` now resolves the synchronizer id and opens the admin channel once, and the loop reuses it. The reads stay sequential: this endpoint is a button click over a handful of peers, and each read is local. The exact-uid check is the one new correctness rule and nothing covered it. The fold moves into `vetted_ids_of`, a pure function over the response, with a regression case where a prefix-colliding participant must contribute nothing. The UI read only `reachable`, so a failed topology read and a peer that has vetted nothing both rendered as "Unreachable". A stale synchronizer-id cache would have shown every peer as down with no way to tell. The tooltip now comes from `error_kind` and names which side the fault is on. Two doc comments overclaimed. No caller emits the Noise `PeerErrorKind` variants any more; they stay only so an existing client keeps deserializing the enum. And the comparison's two sides measure different things: local rows are the uploaded set, peer cells are the vetted set.
What this changes
GET /packages/compare-peerstold you which packages each peer node holds.To answer it, this node opened a Noise connection to every peer and asked
each one to list its packages.
It no longer does. The synchronizer already replicates every participant's
VettedPackagestopology mapping to every member, so this node reads apeer's vetting from its own topology store. No peer is contacted.
Why
This is the first step of moving decman off the Noise transport. Package
checking is the easiest one to move, because Canton already publishes the
answer and decman was asking for it a second time over its own network.
The change stands on its own. It adds no Daml package, no new identity, and
no migration.
How it works
fetch_vetted_packagesalready read this participant's vetted packages fromthe topology store. It now takes the participant as an argument, so the same
call answers for a peer:
The self-read goes through the same function, so both paths agree by
construction.
One correctness detail: Canton treats
filter_participantas a prefixmatch, so one participant's id can select another participant's mapping. The
read now confirms the exact uid on each result.
What improves
a closed port or a proxy in the way used to read as "this peer has no
packages". The topology store answers whether or not the two nodes can
reach each other.
skipped those rows entirely.
what decides whether that peer can run a package. The old answer was what
the peer had uploaded.
API
PeerErrorKindgains two variants, and loses none:topology_read_failed— this node's topology read failed.no_vetted_packages— the read succeeded and the peer has vetted nothing.The comparison emits only these two now, and after this PR nothing emits the
Noise variants at all —
peer_error_kind_from_noise_errwas their onlyproducer and it is deleted here. They stay on the wire so an existing client
keeps deserializing the enum, not because another caller still sets them.
They go when the transport goes.
reachablekeeps its field name and its meaning to a client — "this nodehas that peer's package list". Nothing is reached any more, so the name is
now wrong. Renaming it is a breaking change to the wire type and to the UI,
so it belongs in its own PR, not this one.
What this does NOT do
Noise is still here, and still carries everything else: owner-key requests,
participant status, and every workflow.
MessageType::ListPackagesstillhas a server handler, so a peer on an older build can still ask this node
for its packages and get an answer. Only the caller is gone.
How this was tested
cargo fmt --all -- --checkcargo clippy --all-targets --all-features --no-deps -- -D warningsonthe CI toolchain
cargo test --workspace --lib— 820 testscargo test -p decman-cli— 74 teststhis node holds and one it does not, the empty-vetting case, a failed
read, and stable ordering
check_peer_dars(all threenodes see both peers with packages) and in
concurrent_sibling_cancel(the comparison still answers while a workflow is live)
Review follow-ups
Addressed in
222fe308:TopologyReaderresolves the synchronizer id and opens the admin channelonce for the whole peer set, instead of reconnecting per peer. The reads
stay sequential — every read is local and this endpoint is a button click.
vetted_ids_of, a pure function, witha regression case where a prefix-colliding participant contributes nothing.
error_kind. It used to read "Unreachable"for both a failed topology read and a peer that has vetted nothing, so a
stale synchronizer-id cache would have shown every peer as down.
Still open, deliberately:
local_packagesis the uploaded set while each peer's list is the vettedset, so a package uploaded here but not vetted here reads as a mismatch on
every peer. Documented on
fetch_peer_packages. Switching the local side tothe vetted set would drop those rows from the operator's table, which is a
UI decision.
reachablekeeps its name although nothing is reached.Review notes
Two test comments described the old transport and are corrected here. One of
them, in
concurrent_sibling_cancel, guarded a Noise chunk-routing bug thatthis change removes outright; the assertion stays, because a live run must
still not hide a peer's packages.