You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix: pinned contract deployments, retired legacy keys, and renamed tenant execute API - #521
Contracts peers could sign substituted ledger effects, migrated parties retained obsolete signing authority in a shadowed mapping, and the tenant submission API called its execute step onboard.
This batch addresses three issues:
Contracts workflow: peer signs any transaction acting as the accepted party #423 (P1 / big): Contracts invitations now commit to ordered package/module/template IDs and normalized create-argument hashes. Acceptance persists the commitments, the UI shows them, and peers verify them alongside the prepared-transaction hash and acting party. Only one matching create per commitment is allowed; changed arguments/templates, exercises, and extra nodes are rejected. Migration 000020 persists pending commitments.
A migrated party's PartyToKeyMapping is shadowed, not removed #453 (P1 / medium): Add-member, remove-member, and threshold-change workflows retire the legacy PartyToKeyMapping after inline signing keys are effective. Members authorize the exact next-serial Remove under the current namespace authority. Cleanup requires usable inline keys and waits for Canton to confirm removal; retries resume at cleanup. Parties without a legacy mapping skip it.
The v0 tenant rename is breaking; callers must update. Contracts resolve package-name selectors to exact IDs before persisting the workflow; those IDs stay pinned through preparation and retries. Old invitations without deployment commitments must be restarted with an upgraded coordinator. Noise protocol 0xD3 rejects pre-enforcement peers in both directions, including health probes and resumed signing commands. Upgrade the whole mesh before these workflows; legacy-key retirement needs enough upgraded namespace owners online. Argument commitments bind the intended creates before ledger preparation, so submission preparation remains in its existing workflow position.
Validation
DECMAN_SKIP_FRONTEND=1 cargo test --workspace: 1,088 passed, 3 ignored.
cargo fmt --all --check and git diff --check: passed.
After npm ci, frontend build passed and 164 tests passed with NODE_OPTIONS=--no-experimental-webstorage (Node 26).
Regression coverage includes substituted templates/arguments, non-create and extra nodes, ledger annotation/map-order/trailing-None normalization, persisted commitments, inline-key safety, exact removal serial/operation, and workflow command restrictions.
Frontend lint still reports 60 existing errors; the error in the edited NotificationsView file was reproduced against origin/main. No new lint rule violations were introduced.
Added a live Canton regression to the existing threshold-change integration phase: seed a shadowed legacy mapping with an unclaimed key, require the workflow to remove it after raising the namespace threshold to two, assert the exact next-serial removal on all three participants, and interrupt/retry a partially authorized removal. The full Canton integration suite, including this regression, passed on 992c6d1: https://github.com/DLC-link/decentralization-manager/actions/runs/37681183536/job/112997338595.
The coordinator regenerates these intents from mutable dec_parties cache state instead of reusing the commitments sent in the invitation. In particular, GovernanceThreshold { value: None } is resolved from the cached party threshold each time deployment_intents runs. If that cache changes while operators are accepting the invite or before a retry, the coordinator validates its persisted prepared submissions against a different hash than peers accepted, causing an otherwise resumable workflow to fail. Persist the resolved intent/context once before invitation and use that same value for preparation, coordinator signing, and retries.
Incorrect SemVer ordering may select prerelease over stable package
This newly uses compare_versions to choose which package is actually deployed, but that helper is not a correct SemVer ordering for prereleases: for example, it compares the nonnumeric segment 0-alpha lexicographically greater than 0, so descending order selects 1.0.0-alpha ahead of stable 1.0.0. A #name selector can therefore pin a prerelease even when the newer stable release is installed. Use a proper package-version/SemVer comparison (with a deterministic tie-breaker) before selecting the first match.
Empty deployments are misclassified as outdated coordinator invitations
This emptiness check also rejects invitations produced by the current coordinator when ContractsRequest.contracts is empty. That is still a supported request shape: prepare_submissions explicitly treats it as “skip submission preparation,” and ContractsDialog.tsx tells users they may leave the list empty to skip creation. Such a run now fails after acceptance with the inaccurate claim that the coordinator is outdated. Either reject empty deployments synchronously in start_contracts with an appropriate 4xx response, or carry an explicit commitment-format marker so an intentional empty deployment is distinguishable from a legacy invitation.
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
POST /contracts accepted an empty contracts list. The coordinator then
skipped preparation, and every invited peer failed at signing, which
now reports an outdated coordinator. The request is refused with 400
before anything is persisted or any peer is invited.
The coordinator rebuilt its deployment commitments from the cached
party threshold each time it needed them: for the invitation, for
preparation and for its own signature. A threshold that changed in
between made the prepared creates differ from what peers accepted, and
the run failed late at signing without saying why.
A run now records its commitments, and the threshold they used, when
it starts. Preparation refuses to run if the cached threshold changed
since then, and names the old and new values. The coordinator signs
against the recorded commitments, the same ones the peers hold.
Copilot's review of e81fd48 listed three earlier-missed findings in its summary. Here is what happened to each.
Commitments rebuilt from the mutable cache (contracts/coordinator.rs:136). Fixed in 8044586. A run now records its commitments, and the party threshold they used, when it starts. Preparation refuses to run if the cached threshold changed since then, and the error names both values. The coordinator signs against the recorded commitments, which are the ones the peers hold. A changed threshold still fails the run. That is deliberate, because the alternative would deploy a threshold the party no longer has.
Prerelease sorted above stable (contracts/steps/prepare.rs:56). Won't fix. Daml package versions are dot-separated integers only. SDK 3.4.11 refuses to build a package versioned 1.0.0-alpha, so no prerelease can reach a participant's package list.
Empty deployment reported as an outdated coordinator (contracts/steps/sign.rs:60). Fixed in f323b82. POST /contracts now refuses an empty contracts list with 400. Such a run failed on every peer at signing even before this PR. This PR only changed the error message. Note that ContractsDialog.tsx does not tell users to leave the list empty.
Tests: three new unit tests in contracts/steps/prepare.rs and one handler test in handlers/workflows.rs. With the threshold check removed, a_threshold_change_after_the_commit_stops_preparation fails. All 780 decman unit tests pass, and clippy is clean with all targets and features.
Legacy key retirement (#453). The retirement step runs after the submit step in all three workflows. submit_kick now waits for the exact serial and membership of the DNS and P2P mappings before the step advances. Each peer reads the live mapping from the head state and signs the Remove at the next serial. validate_legacy_retirement refuses to sign when the head-state P2P mapping has no usable inline keys. That check makes the step safe even if a coordinator sends the command early. The coordinator persists the step before cleanup, so a crash after the topology change resumes at RetireLegacyKeys. A peer that finds no mapping returns Ok, so a retry is idempotent. signing_keys.rs and sign.rs read the legacy mapping only when inline keys are empty, so a retired key cannot sign after removal. The code supports the reply to Copilot.
Pinned deployments (#423).start_contracts resolves package aliases to exact IDs and records the intents before it persists the run or invites anyone. Peers verify each prepared create against the intents stored on their own run row at accept time, not against a later payload. The check pins the package ID, module, entity, and the normalized argument hash. It also rejects exercises, extra nodes, and a non-root create. committed_submission_context fails preparation when the cached threshold differs from the recorded one. An empty contracts list now gets a 400, and sign_submissions refuses empty intents, so no empty deploy can pass as intentional.
Tenant rename (#431). The four /onboard routes are removed with no alias. require_admin and the other checks in the handlers are unchanged. The wallet crate, the integration phases, and the docs use the new paths. I found no caller in the repo on the old paths.
Noise. The PR adds MessageType::RetireLegacyKeys and bumps WIRE_VERSION to 0xD3 in noise/mod.rs. This is Noise-specific logic. It is fine for this PR, but the Noise sunset work (#451) must carry the version gate over.
I did not run the Canton integration suite locally. I rely on the CI run linked in the description for the live retirement regression.
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
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.
Contracts peers could sign substituted ledger effects, migrated parties retained obsolete signing authority in a shadowed mapping, and the tenant submission API called its execute step
onboard.This batch addresses three issues:
/onboardroutes to/execute, including shared DTOs, handlers, wallet methods, callers, and documentation.Compatibility
The v0 tenant rename is breaking; callers must update. Contracts resolve package-name selectors to exact IDs before persisting the workflow; those IDs stay pinned through preparation and retries. Old invitations without deployment commitments must be restarted with an upgraded coordinator. Noise protocol 0xD3 rejects pre-enforcement peers in both directions, including health probes and resumed signing commands. Upgrade the whole mesh before these workflows; legacy-key retirement needs enough upgraded namespace owners online. Argument commitments bind the intended creates before ledger preparation, so submission preparation remains in its existing workflow position.
Validation
DECMAN_SKIP_FRONTEND=1 cargo test --workspace: 1,088 passed, 3 ignored.DECMAN_SKIP_FRONTEND=1 cargo clippy --workspace --all-targets --all-features -- -D warnings: passed.cargo fmt --all --checkandgit diff --check: passed.npm ci, frontend build passed and 164 tests passed withNODE_OPTIONS=--no-experimental-webstorage(Node 26).Closes #423
Closes #453
Closes #431