Prototype SV governance voter flow#5533
Conversation
Define the initial Phase 1 allowlist for governance-voter eligible actions and cover the proposed taxonomy plus represented-SV vote-slot semantics in Daml tests. Signed-off-by: Eric Mann <eric@avrofi.com>
Add explicit vote-cast role metadata so the vote record can identify the represented SV and operator signing path without changing tally semantics. Signed-off-by: Eric Mann <eric@avrofi.com>
Introduce the SV-declared governance-voter binding lifecycle without a contract key so the Phase 1 authority model can be reviewed independently of submit-path mechanics. Signed-off-by: Eric Mann <eric@avrofi.com>
Use the SV governance-voter binding to authorize alternate signing while recording the vote in the represented SV's existing vote slot. Signed-off-by: Eric Mann <eric@avrofi.com>
Keep the consolidated prototype to a single governance package version bump so intermediate stack-only DAR versions do not leak into the final review branch. Signed-off-by: Eric Mann <eric@avrofi.com>
Use represented SV parties as vote map keys and prevent governance-voter submissions from replacing prior operator votes, making the shared vote-slot semantics explicit for review. Signed-off-by: Eric Mann <eric@avrofi.com>
State that explicit disclosure is the supported submission path for unaffiliated governance voters, keep SV-hosted relay as an optional deployment choice, and flag CIP-0103 alignment as the remaining review item before this prototype is promoted out of draft. Signed-off-by: Eric Mann <eric@avrofi.com>
CIP-0103 governs the dApp/Wallet API rather than on-ledger contract shape; the cast choice is already compatible with an external-party prepareExecute flow with disclosed contracts. The remaining alignment work lives in the governance-voter dApp client, not in this PR. Signed-off-by: Eric Mann <eric@avrofi.com>
Address review feedback: drop the operator override on governance-voter eligible actions, route their vote requests through the governance-voter path, and replace clearing with rotation back to the represented SV (with onboarding default of self-voting). - Remove the ClearGovernanceVoter choice from SvGovernanceVoter; the represented SV always has a binding, and "return to operator" is RotateGovernanceVoter back to self. - Add DsoRules_RequestGovernanceVote (governance-voter controlled) and reject governance-voter eligible actions on DsoRules_RequestVote / DsoRules_CastVote. DsoRules_CastGovernanceVote no longer needs the operator-overwrite guard. - Update DsoTestUtils helpers to dispatch by action type and to auto-create self-bindings; rewrite the prototype lifecycle/cast tests for the new policy; port existing direct request/cast call sites to the appropriate path. - Refresh the bundled splice-dso-governance 0.1.25 DAR, dars.lock, and the SV governance-voter doc. Signed-off-by: Eric Mann <eric@avrofi.com>
Phase 1 does not enforce a one-active-binding-per-SV invariant: the represented SV may keep more than one SvGovernanceVoter contract alive at the same time, and any active binding can authorize a cast for the represented SV. All such casts land in the represented SV's single vote slot under the same per-SV cooldown, so additional bindings broaden the set of authorized signers without changing the one-vote-per-SV tally. - Document the multi-binding semantics in the SV governance-voter doc. - Add testGovernanceVoterMultipleBindings exercising two concurrent bindings for sv1 (delegateA, delegateB), validating that both can cast into the same represented-SV slot, that the second cast overwrites the first via the cooldown path, and that a delegate cannot cast against the other delegate's binding (signer-must-match-binding check). Signed-off-by: Eric Mann <eric@avrofi.com>
DsoRules_CastVote and DsoRules_CastGovernanceVote previously accepted votes regardless of how much time had elapsed since the request was opened. The voting period is meaningful only if both the request choices and the cast choices enforce it. Add a deadline check on both cast paths that mirrors the existing close-vote semantics: when targetEffectiveAt is set the voting period extends to the effective time (matching the documented behavior that SVs can keep voting between voteBefore and targetEffectiveAt); when it is not set the deadline is voteBefore. Add testCastDeadlineExpiry to lock down both branches. Signed-off-by: Eric Mann <eric@avrofi.com>
The operator cast path tells the caller which choice to use when the
action does not belong on that path ("use DsoRules_CastGovernanceVote").
The governance-voter cast path was missing the equivalent hint and read
as a flat statement of fact. Match the operator path's wording so the
two messages are symmetric and point the caller at the right choice.
Signed-off-by: Eric Mann <eric@avrofi.com>
DsoRules_CastVote previously overwrote the caller-supplied castBy and castByRole silently, mirroring the operator authority but masking any caller-side mismatch. DsoRules_CastGovernanceVote instead validates the caller-supplied values against the binding before overwriting; the operator path should follow the same fail-loud pattern so caller bugs surface immediately. Add two require checks on the operator cast path: castBy must equal vote.sv, and castByRole must be VCR_Operator. The silent overwrite remains for forward-compatibility with future timestamping but no longer hides incorrect callers. Add testOperatorCastAttributionGuards to exercise both guards. Signed-off-by: Eric Mann <eric@avrofi.com>
The binding template's module comment claims "there is intentionally no Clear choice," but the implicit per-signatory Archive choice still lets the represented SV unilaterally archive any of its bindings (the SV is the sole signatory). Reviewers reading the no-Clear claim shouldn't mistake it for a hard invariant. Acknowledge the escape hatch in the template's module comment and in the SV governance-voter doc, and frame it as self-harm only that is trivially recoverable via a new self-binding. Hard enforcement of "an SV always has at least one active binding" is deferred. Signed-off-by: Eric Mann <eric@avrofi.com>
The SV dApp PoC discussion landed on a single governance-voter party per represented SV: accountability is hard enough with one voting party to declare, and multi-user assignment lives at the dApp/UI layer rather than via multiple ledger bindings. This reverses the earlier prototype note that explicitly permitted multiple concurrent bindings. - Update the SV governance-voter doc to state the single-binding intent and flag that Phase 1 enforces it through the workflow (RotateGovernanceVoter exclusively) rather than at the template level. Promotion to a contract key or registry is left as an open question for Splice maintainers. - Drop testGovernanceVoterMultipleBindings, which exercised behavior the design no longer endorses. - Add an explicit "exactly one active binding for sv1" assertion at each step of testSvGovernanceVoterBindingLifecycle so the invariant is visible in the test, not just implied by the consuming choice. Signed-off-by: Eric Mann <eric@avrofi.com>
Code review on the prototype draft pointed out that the single-active-binding-per-SV invariant is workflow-only (onboarding default plus consuming RotateGovernanceVoter), not enforced by the SvGovernanceVoter template itself. If a represented SV does bare-create parallel bindings, both delegates can cast against the same represented- SV slot under last-writer-wins. This commit captures that boundary as a test so future work (contract key, single-binding registry, or onboarding-side enforcement) has a concrete behavior to compare against: - testGovernanceVoterDuplicateBindingsAmbiguity exercises sv1 bare- creating two SvGovernanceVoter contracts in parallel, has each delegate cast against the same represented-SV slot, and asserts the slot count stays at one with last-writer-wins on castBy and castByRole. No template or choice changes; the test pins existing behavior the CIP also flags as an open review question. Signed-off-by: Eric Mann <eric@avrofi.com>
DsoRules_CastGovernanceVote used fetchChecked + archive because it needs to validate that the request action is governance-voter eligible before consuming it (unlike DsoRules_CastVote, which uses fetchAndArchive after the operator-path eligibility check). The cooldown check sat after the explicit archive, which is functionally correct because the transaction aborts on failure, but is non-idiomatic: validation should precede destructive operations. Move enforceCooldown above archive requestCid in DsoRules_CastGovernanceVote so all gate checks (binding validity, role, SV membership, action eligibility, deadline, cooldown) run before the request is consumed. The operator path already follows this order via fetchAndArchive; this aligns the governance-voter path stylistically. Bump splice-dso-governance to 0.1.26 and refresh dars.lock. Signed-off-by: Eric Mann <eric@avrofi.com>
A re-review pointed out that the prototype design doc described the single-active-binding shape as preserved "by construction" through the consuming RotateGovernanceVoter lifecycle, and as "enforced through the workflow", both of which overstate what the template actually guarantees. The represented SV is the sole signatory and can bare- create additional bindings; the consuming rotation only enforces that one specific binding is consumed when a rotation happens, not that no parallel binding ever exists. Soften the wording in docs/src/sv_operator/sv_governance_voter.rst to match the language in the companion CIP draft: - The intended Phase 1 workflow has one active governance-voter party per represented SV, shaped by the consuming rotation and self- binding onboarding default, rather than preserved as a contract- level invariant. - Spell out the residual behavior: if two bindings do coexist, both delegates can cast under last-writer-wins; the tally is still one vote per represented SV, but the cast log becomes ambiguous. - Reference testGovernanceVoterDuplicateBindingsAmbiguity, which pins that behavior so any future tightening has a baseline. No Daml code or test changes; the template comments and the new test already use the workflow-intent framing. Signed-off-by: Eric Mann <eric@avrofi.com>
| isGovernanceVoterAction : ActionRequiringConfirmation -> Bool | ||
| isGovernanceVoterAction action = case action of | ||
| ARC_DsoRules with dsoAction -> case dsoAction of | ||
| SRARC_OffboardSv _ -> True |
There was a problem hiding this comment.
Is that deliberate? I was expecting this to be an operational concern as it often needs to be acted upon quickly.
There was a problem hiding this comment.
Yes, we discussed this. offboarding is one of those where it is very unclear what the right choice is.
Some offboardings are because the node is broken, some because of a governance change.
For now, let's ignore which action is governance vs operational, that will be a separate discussion in parallel to the design here, that should not affect design choices.
| SRARC_SetConfig _ -> True | ||
| SRARC_UpdateSvRewardWeight _ -> True | ||
| SRARC_CreateUnallocatedUnclaimedActivityRecord _ -> True | ||
| _ -> False |
There was a problem hiding this comment.
I would still recommend to not have a wildcard here and and instead explicitly match on all constructors. That way when a new one gets added you're forced to think about how it should be treated which is much better than getting the wrong default.
| -- ^ The time before which votes are accepted, and SHOULD be submitted. | ||
| votes : Map.Map Text Vote | ||
| -- ^ The votes cast by current or previous SVs. These may be previous SVs in case | ||
| votes : Map.Map Party Vote |
There was a problem hiding this comment.
That doesn't work, it's not a backwards compatible change.
| data Vote = Vote with | ||
| sv : Party -- ^ The SV party used to submit the vote. | ||
| sv : Party -- ^ The represented SV whose vote slot is updated. | ||
| castBy : Party -- ^ The party that signed the vote command. |
There was a problem hiding this comment.
these also need to be optional
| -- Controlled by the governance-voter party authenticated via the binding; | ||
| -- the represented SV is taken from the binding. The initial vote is | ||
| -- recorded against the represented SV's slot with VCR_GovernanceVoter. | ||
| nonconsuming choice DsoRules_RequestGovernanceVote : DsoRules_RequestGovernanceVoteResult |
There was a problem hiding this comment.
do we really need a new choice or can we just add biningCid as an optional argument to the vote choice and then in the choice branch on whether it's a governance or non-governance choice?
| -- Note that this choice can be used to both cast the initial vote, and update a vote. | ||
| -- | ||
| -- Operator path: only for *operational* actions. Governance-voter eligible | ||
| -- actions are cast exclusively via DsoRules_CastGovernanceVote. |
There was a problem hiding this comment.
same question here, it's not clear to me why we need two separate choices
| observer dso, governanceVoter | ||
|
|
||
| ensure | ||
| sv /= dso |
There was a problem hiding this comment.
not sure why we need those assertions, I don't think we do that anywhere else.
| -- self-binding. Hard enforcement of the "every SV has at least one active | ||
| -- binding" invariant is deferred; the Phase 1 expectation is that the SV | ||
| -- workflow uses RotateGovernanceVoter rather than direct archive. | ||
| template SvGovernanceVoter |
There was a problem hiding this comment.
I'm rather nervous about the signatories on that. Having the SV as the signatory means we also need the SV as a confirmer for any transaction involving this which is often not what you want. If you look at the exiting contracts for votes, prices, … you see that they are all very careful to only have the DSO as a signatory. Is there anything that stops us from doing the same here and create this through dso rules? You could even then do the initial creation with confirmation and deduplication to avoid duplicates.
| governanceVoter : Party | ||
| where | ||
| signatory sv | ||
| observer dso, governanceVoter |
There was a problem hiding this comment.
I'm wondering if you really need the governanceVoter as an observer or whether you can read it through scan or something like that. That way you would fully avoid any potential issues with the node potentially unvetting the dar and breaking something. It could be that we're ok with that but if we are we need a careful analysis of that.
| nonconsuming choice DsoRules_RequestGovernanceVote : DsoRules_RequestGovernanceVoteResult | ||
| with | ||
| governanceVoter : Party | ||
| bindingCid : ContractId SvGovernanceVoter |
There was a problem hiding this comment.
What staleness are we willing to accept? Let's say at time X someone creates a vote at this point SvGovernanceVoter is still pointing to party A. When the vote gets executed it now points to party B.
I'd lean towards saying this needs to get revalidated similarly to how we don't allow the votes of an offboarded SV to still have an effect.
Summary
This PR is a prototype for maintainer review and discussion, not necessarily a final upstream design. It makes the proposed Phase 1 SV governance-voter model concrete for discussion under canton-foundation/canton-dev-fund#223, especially Milestone 1: Governance-Voting Identity and CIP.
Phase 1 preserves the existing one-vote-per-SV semantics. A governance voter is modeled as an alternate signer for a represented SV's vote, not as a new voting unit or source of additional vote weight.
Changes included:
isGovernanceVoterAction, with newActionRequiringConfirmationconstructors rejected by default until explicitly reviewed.VotewithcastByandcastByRoleso vote records distinguish the represented SV from the party/authority path that signed the vote.SvGovernanceVoter, an SV-declared binding template without a contract key; downstream paths fetch it by contract ID and validate the one-active-binding assumption at the workflow level.DsoRules_CastGovernanceVote, which validates the binding, represented SV, governance-voter signer, signer role, and action allowlist before writing the vote into the represented SV's existing vote slot.(Internal) Design IDs covered: GV-001, GV-002, GV-003, GV-004, GV-005, GV-006.
Notes For Reviewers
SRARC_OffboardSvis included in the proposed Phase 1 allowlist so reviewers can evaluate it explicitly; this should be validated through maintainer/CIP review because it is a high-impact membership action.governanceVoter == svis allowed for bootstrap/self-voting, whilegovernanceVoter == dsois rejected.Test Plan
direnv exec . sbt "splice-dso-governance-test-daml/damlTest"direnv exec . sbt damlDarsLockFileUpdate