Skip to content

feat: read peer packages from the synchronizer, not Noise - #468

Merged
schronck merged 3 commits into
mainfrom
feature/package-check-via-synchronizer
Sep 22, 2026
Merged

schronck merged 3 commits into
mainfrom
feature/package-check-via-synchronizer

Conversation

@scolear

@scolear scolear commented Sep 17, 2026 •

Copy link
Copy Markdown
Member

What this changes

GET /packages/compare-peers told 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
VettedPackages topology mapping to every member, so this node reads a
peer'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_packages already read this participant's vetted packages from
the topology store. It now takes the participant as an argument, so the same
call answers for a peer:

list_vetted_packages(ListVettedPackagesRequest {
    base_query: Some(BaseQuery { operation: AddReplace, ..head_state_query(&sync_id) }),
    filter_participant: peer_participant_id,   // any member, not just me
})

The self-read goes through the same function, so both paths agree by
construction.

One correctness detail: Canton treats filter_participant as a prefix
match, so one participant's id can select another participant's mapping. The
read now confirms the exact uid on each result.

What improves

  • A peer that is up stops reporting as empty. A failed Noise handshake,
    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.
  • A peer with no Noise public key is now compared. The old fan-out
    skipped those rows entirely.
  • The answer means more. It reports what a peer has vetted, which is
    what decides whether that peer can run a package. The old answer was what
    the peer had uploaded.

API

PeerErrorKind gains 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_err was their only
producer 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.

reachable keeps its field name and its meaning to a client — "this node
has 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::ListPackages still
has 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 -- --check
  • cargo clippy --all-targets --all-features --no-deps -- -D warnings on
    the CI toolchain
  • cargo test --workspace --lib — 820 tests
  • cargo test -p decman-cli — 74 tests
  • four new unit tests over the pure row builder: the name join for a package
    this node holds and one it does not, the empty-vetting case, a failed
    read, and stable ordering
  • the integration suite covers this endpoint in check_peer_dars (all three
    nodes 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:

  • TopologyReader resolves the synchronizer id and opens the admin channel
    once 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.
  • The exact-uid check is extracted as vetted_ids_of, a pure function, with
    a regression case where a prefix-colliding participant contributes nothing.
  • The UI tooltip is built from 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.
  • Two doc comments that overclaimed are corrected.

Still open, deliberately:

  • local_packages is the uploaded set while each peer's list is the vetted
    set, 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 to
    the vetted set would drop those rows from the operator's table, which is a
    UI decision.
  • reachable keeps 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 that
this change removes outright; the assertion stays, because a live run must
still not hide a peer's packages.

`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.
@scolear
scolear force-pushed the feature/package-check-via-synchronizer branch from 49944c7 to 7921d87 Compare September 17, 2026 20:58
@scolear
scolear marked this pull request as ready for review September 17, 2026 20:58
@scolear
scolear requested review from a team and sosaucily and a balanced review from Copilot September 17, 2026 20:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread crates/common/src/types.rs
Comment thread crates/decman/src/server/handlers/parties.rs
Comment thread crates/decman/src/server/handlers/parties.rs
Comment thread crates/decman/src/server/package_inventory.rs Outdated

@schronck schronck left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. The UI reads only reachable and never error_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 from error_kind. Copilot's third comment is the same point.

  2. The exact uid filter is the one new correctness rule and has no test. Inline.

  3. 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.

Comment thread crates/decman/src/server/package_inventory.rs
Comment thread crates/common/src/types.rs Outdated
Comment thread crates/decman/src/server/handlers/parties.rs
Comment thread crates/decman/src/server/handlers/parties.rs Outdated
Comment thread crates/decman/src/server/handlers/parties.rs Outdated
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.

@schronck schronck left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@schronck
schronck merged commit 516299d into main Sep 22, 2026
10 checks passed
@schronck
schronck deleted the feature/package-check-via-synchronizer branch September 22, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants