Tenant API: the add-hosts prepare and onboard endpoints - #381
Open
schronck wants to merge 2 commits into
Open
Conversation
The serial-N+1 builder had no caller. These are it: POST /v0/tenant/add-hosts/prepare returns the unsigned topology plus the hashes to sign, and POST /v0/tenant/add-hosts/onboard co-signs the wallet-signed bundle with this node's topology key and submits it. Prepare reads head state before it builds, so a wallet whose pinned base serial has gone stale gets a 409 it can act on — re-read and retry — rather than a 500 that reads as a broken host. Onboard returns 400 rather than 500 when validation refuses the bundle, because that is the caller's bytes failing against this host's own view, not this host malfunctioning. Both report only this host's view. The wallet calls every host itself and aggregates, exactly as onboarding already works.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR wires the previously-added “add hosts to an existing external party” topology builder/validator into the Tenant API by adding prepare/onboard HTTP endpoints, associated request/response DTOs, and the workflow submission function that co-signs and submits the wallet-provided topology bundle.
Changes:
- Added
POST /v0/tenant/add-hosts/prepareandPOST /v0/tenant/add-hosts/onboardhandlers and registered them in the server. - Introduced wallet-facing DTOs for add-hosts prepare/onboard requests and responses (and re-exported them via server types).
- Added
submit_add_hostsworkflow function to validate, co-sign, and submit the wallet-signed add-hosts topology bundle.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/decman/src/workflow/external_party/add_hosts.rs | Adds submit_add_hosts for co-signing + submission of wallet-signed add-hosts topology. |
| crates/decman/src/server/types.rs | Re-exports new Tenant add-hosts DTOs for handler use. |
| crates/decman/src/server/mod.rs | Registers the new tenant add-hosts endpoints on the Actix server. |
| crates/decman/src/server/handlers/tenant.rs | Implements the new /v0/tenant/add-hosts/* endpoints plus shared base64 decode helper/tests. |
| crates/decman/src/server/handlers/mod.rs | Re-exports the new tenant add-hosts handlers. |
| crates/common/src/api.rs | Defines new API DTOs for add-hosts prepare/onboard request/response payloads. |
Suppressed comments (1)
crates/decman/src/server/handlers/tenant.rs:443
- All
submit_add_hosts(...)errors are mapped to HTTP 400, butsubmit_add_hostscan also fail due to internal RPC issues (signing/submission/topology reads) that should be 500 per the endpoint’s documented responses. This makes client retry/diagnostics harder.
Err(e) => {
tracing::error!("tenant add-hosts onboard: submission failed: {e:#}");
return HttpResponse::BadRequest().json(ErrorResponse {
error: format!("Failed to submit the add-hosts topology on this host: {e}"),
});
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The wire-shape test used a truncated namespace, which CantonId refuses, so the unit-test job failed. Fixed with a full fingerprint. The rest is Copilot's review, which was right. submit_add_hosts and prepare_add_hosts both returned anyhow, so the handlers could not tell a caller error from a Canton failure and answered 400 to everything — including the stale-serial race that the docs promised a 409 for, and RPC failures that are not the caller's fault at all. AddHostsError names the four cases and the handlers map them: 404 unknown party, 409 stale pin, 400 refused bundle, 500 Canton failure. Each is a different remedy for the wallet, so collapsing them sent it to the wrong one. Dropped the pre-read in the prepare handler: prepare_add_hosts reads head state itself and now reports why it refused, so the second read only widened the window in which the serial could move between check and build. Also documented the 404 and 409 the handlers actually return.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #379. Base is
feat/external-party/add-hosts-topology, so review that first — the diff here is only this commit.Item 3 of the scoping study's §06. #379 built the serial-N+1 topology write and its validator; nothing called them. These are the callers.
Endpoints
POST /v0/tenant/add-hosts/preparePOST /v0/tenant/add-hosts/onboardSame shape as onboarding: every host prepares independently, the wallet compares the bytes before it signs, and each host submits only its own authorization. Canton accumulates the rest and promotes the mapping when the last one lands. No host relays to another, and a retry converges because re-submitting an identical transaction is a no-op.
Status codes worth arguing about
409 on a stale pin. Prepare reads head state before it builds. If the wallet's
base_serialno longer matches, that is not a malfunction — the party moved between the wallet's read and this call — so it returns 409 with both serials and the wallet can re-read and retry. A 500 would read as "this host is broken" and send the wallet to the wrong remedy.400 when validation refuses the bundle.
validate_add_hosts_topologyrejecting a submission means the caller's bytes failed against this host's own head-state read. That is the caller's problem, not the host's.Both endpoints report only this host's view, matching
/v0/tenant/onboard.Completedmeans this host's authorized mapping has already advanced past the base serial;InProgressmeans the change is still a proposal here.New in
add_hosts.rssubmit_add_hosts— the add-hosts counterpart toallocate_party. It re-reads head state, runs the validator, co-signs with an emptysigned_byso the node picks its own key, and submits. Validation happens before this node's key touches the bytes, which is the whole point: the party already exists and already holds contracts, so a forged serial N+1 could evict its current hosts rather than merely create something unwanted.Tests
Five unit tests:
decode_all(including that one bad entry fails the whole batch, since a silently dropped entry would break the index alignment the signatures depend on), and wire-shape round-trips for both request DTOs —decman-walletmirrors these types, so a renamed field would fail to deserialize on the other end.The endpoint behavior itself needs a live Canton, so an integration-test phase is the honest coverage. That plus the wallet-side flow (§06 item 4) is the natural next PR.
cargo clippy --workspace --all-targets --all-features -- -D warningsis clean. Per the usual rule I have not runcargo testlocally.