CIP-104 Mode A coupon-reassignment automation#255
Open
gyorgybalazsi wants to merge 73 commits into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
This PR lays the M1+M2 foundation for Mode A CIP-104 coupon reassignment by introducing a new on-ledger governable action to assign RewardCouponV2 beneficiaries and wiring it through DecMan’s Rust proposal plumbing, plus adding DAML tests and design/plan documentation.
Changes:
- Add DAML
AssignRewardBeneficiariesGovernableActionthat exercisesRewardCoupon_AssignBeneficiarieswith an “empty”ExtraArgs. - Add a dedicated DAML test package (
governance-rewards-assign-test) to validate happy-path and key negative cases for coupon assignment. - Add Rust backend support: new
ProposalType::AssignRewardBeneficiaries, boundary validation, action serialization mapping, and serializer shape test.
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/coupon-reassignment/m1-m2-implementation-plan.md | Adds an M1+M2 implementation plan and task breakdown for coupon reassignment foundations. |
| docs/coupon-reassignment/design.md | Adds a Mode A design doc describing the end-to-end automation architecture and constraints. |
| daml/multi-package.yaml | Registers the new DAML test package in the multi-package build. |
| daml/governance-rewards/daml/Governance/Rewards/AssignRewardBeneficiaries.daml | Implements the new governable action wrapping RewardCoupon_AssignBeneficiaries. |
| daml/governance-rewards/daml.yaml | Adds the reward-assignment and token-metadata DARs as data-dependencies for compilation/imports. |
| daml/governance-rewards-assign-test/daml/Governance/Rewards/TestAssignRewardBeneficiaries.daml | Adds DAML scripts covering happy-path and failure modes for assignment. |
| daml/governance-rewards-assign-test/daml/Governance/Rewards/AssignTestUtils.daml | Adds test utilities to create governance and drive propose/confirm/execute in scripts. |
| daml/governance-rewards-assign-test/daml.yaml | Defines the new DAML test package and its DAR dependencies (incl. amulet 0.1.19). |
| crates/decman/src/server/types.rs | Introduces RewardBeneficiary, ProposalType::AssignRewardBeneficiaries, validation, and unit tests. |
| crates/decman/src/server/action_serializer.rs | Adds serialization helper + proposal args mapping + a round-trip shape unit test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
gyorgybalazsi
added a commit
that referenced
this pull request
Jul 20, 2026
Adds the M3+M4 implementation plan (5 verification passes, 17 findings fixed) to the shared branch so it's part of PR #255, and re-syncs design.md with the cip-104 spec (governance-vs-topology threshold fix, §6.1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gyorgybalazsi
added a commit
that referenced
this pull request
Jul 20, 2026
Adds the M3+M4 implementation plan (5 verification passes, 17 findings fixed) to the shared branch so it's part of PR #255, and re-syncs design.md with the cip-104 spec (governance-vs-topology threshold fix, §6.1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gyorgybalazsi
force-pushed
the
feat/governance/coupon-reassignment-automation
branch
from
July 20, 2026 11:47
a163d29 to
d0364fc
Compare
gyorgybalazsi
changed the base branch from
main
to
feat/accept-external-party-setup
July 20, 2026 11:48
…1.19 (devnet-matched)
Adds AssignRewardBeneficiaries, a GovernableAction that lets governance assign beneficiaries to CIP-104 RewardCouponV2 coupons via the RewardCoupon_AssignBeneficiaries choice, so coupons can be minted before they expire. Ships with its own test package (governance-rewards-assign-test) depending on amulet 0.1.19 (for RewardCouponV2) alongside governance-rewards-v1, since the amulet 0.1.17/0.1.19 version split anticipated in the plan did not in fact conflict. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dation Adds the RewardBeneficiary struct and AssignRewardBeneficiaries variant (primary_coupon + additional_coupons + new_beneficiaries, mirroring the DAML template so "at least one coupon" is a type guarantee) plus a validate() arm enforcing non-empty beneficiaries, <= 20 entries, each percentage in (0.0, 1.0], and an exact sum of 1.0. Backend-only (Task 2 of 4); the serializer arm lands in Task 3.
…Strings, not CantonId primary_coupon/additional_coupons hold ledger contract-ids, not parties. CantonId::parse requires a prefix::hexNamespace shape and rejects bare contract-id strings, so a real coupon cid could never be submitted through the JSON API. Type them as String like every other cid-bearing field in this file; RewardBeneficiary.beneficiary stays CantonId since it is genuinely a party.
Design (L1/L2/L3 actor model; Mode A assign-&-self-mint cranker) and the M1/M2 implementation plan, so reviewers can see the full scope. M3 (auto-confirm engine + cranker) and M4 (devnet IT) land in the same PR once the split-source question in the PR description is settled.
Move docs/reward-cranker → docs/coupon-reassignment and re-copy the swept spec + plan (name change "reward cranker" → "coupon-reassignment automation"). Mirrors the cip-104 source docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the dir rename: the prior commit captured only the move (old wording). This applies the swept content, matching the cip-104 source docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the M3+M4 implementation plan (5 verification passes, 17 findings fixed) to the shared branch so it's part of PR #255, and re-syncs design.md with the cip-104 spec (governance-vs-topology threshold fix, §6.1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebased onto Robert's feat/accept-external-party-setup (#256). Fixups so the combined branch builds green: - governance-rewards -> 0.1.2 (both AcceptExternalPartySetup [#256] and AssignRewardBeneficiaries [M1] in one package); update both test packages' dar refs; ship the 0.1.2 DAR in releases/v1. - GovernanceSection.tsx: add a default case to the propose switch so the automation-only proposal types (assign_reward_beneficiaries, and the coming set_reward_split) — which have no UI form by design — don't leave `proposal` possibly-unassigned (TS2454). Surfaced once gen-types emits those variants into the TS union. Verified: cargo test -p decman green; frontend tsc clean; dpm build --all clean; all reward DAML scripts (mine + Robert's) pass at 0.1.2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 1 of the Mode A M3+M4 plan. On-ledger governance-configured reward split (keyless singleton, replace-by-cid) + the GovernableAction that sets/replaces it, with execute-time guards (non-empty, <=20, exact sum 1.0). Tests: creates, replaces (singleton), empty-rejected. Refresh the 0.1.2 DAR. All reward DAML scripts (mine + Robert's) pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 2 of the Mode A M3+M4 plan. New ProposalType::SetRewardSplit
{ new_beneficiaries, prior_config } reusing the existing exact-Decimal
validate_reward_beneficiaries helper; serializer arm (governanceParty,
proposer, priorConfig, beneficiaries) with a make_optional_contract_id
helper. Validation + round-trip unit tests. fmt/clippy/tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_governance_rules Task 3 of the Mode A M3+M4 plan. Behavior-preserving: propose_action now calls submit_proposal; the active-GovernanceRules resolution is a reusable helper. Widen execute_confirm_action/get_party_credentials/packages() to pub(crate) for the reward-automation module. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 4 of the Mode A M3+M4 plan. New reward_automation module: shared active_created_records (GetActiveContracts, decoded via the fetch_proposal_infos pattern), OnLedgerSplitSource (defensive singleton RewardSplitConfig read), and unassigned_coupons (RewardCoupon interface view). parse_split_record unit-tested; I/O verified later by the devnet IT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The codebase deliberately avoids async-trait and there is one split source today, so replace the single-impl SplitSource trait + OnLedgerSplitSource with a plain pub(crate) async fn effective_split (same single swap point for a future shared reward-config template). Removes the async-trait direct dependency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tasks 5-6 of the Mode A M3+M4 plan. split_matches (exact-Decimal set equality) + default-deny is_confirmable; parse_assign_record + read_pending_assign to read a pending AssignRewardBeneficiaries proposal's coupons + split. Pure logic unit-tested; the read is verified by the devnet IT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tasks 7-8 of the Mode A M3+M4 plan. select_batch (TTL-watermark + minting-margin + cap) + run_proposer_once (dedupe via covered coupons, propose AssignRewardBeneficiaries); already_confirmed_by + run_confirmer_once (validate each pending proposal against the on-ledger split, auto-confirm via CoreDomain). Pure logic unit-tested; the I/O is verified by the devnet IT. Also re-exports submit_proposal + execute_confirm_action (pub(crate), Task 3) from handlers::mod so they are reachable through the private governance submodule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 9 of the Mode A M3+M4 plan. run_reward_automation_loop + run_once_for_party tie the reader/proposer/confirmer together, fetching governance confirmations once per tick and sharing them for dedupe + confirm. Registered in start_server by cloning the existing web::Data<AppState> (shared state, not a fresh AppState). New NodeConfig.reward_automation_interval_secs (default 300s); enablement is on-ledger (RewardSplitConfig presence). Re-export the governance helpers; drop the now-unnecessary dead_code allows. clippy/fmt/tests/tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 24, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation interval Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…imeout Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Localnet builds DecMan with --features test-mode, where /contracts/query falls back to a WildcardFilter and matches by concrete template name, so an interface query (RewardCoupon) never matches and returns empty. Query the concrete Splice.Amulet:RewardCouponV2 template on localnet; devnet keeps the interface query. Also drop the stale 'devnet-only' wording (the phase now runs on localnet every CI run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ole pending The helper assumed the cycle's proposal was the only pending domain action (len()==1 / is_empty()). A prior phase (notification_feed) can leave an unrelated proposal pending, which broke the first propose->confirm->execute run after it (the reward phase). Match this cycle's proposal by action label / cid instead. Behavior-preserving for clean-slate phases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extracting submit_proposal out of the propose_action handler collapsed every failure into HTTP 500. Restore the pre-extraction semantics via a SubmitProposalError that carries the intended status: 400 for bad input (invalid admin party, no fundable holdings, bad create args, package not configured), 502 for upstream registry-context fetch failures, 500 for true internal errors (ledger connect/submit, missing contract id). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Splice.Api.Token.MetadataV1 exports no ready-made emptyExtraArgs; the implementation builds ExtraArgs locally from emptyChoiceContext + emptyMetadata. Fix the design/plan docs that claimed it was exported. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…guard The pre-extraction handler mapped "package not configured" to 400, but the request is valid — the node just isn't provisioned to serve it — so correct it to 503. Also document 502/503 in the propose_action OpenAPI responses and add two network-free regression tests that pin distinct submit_proposal failure classes (503 unconfigured, 400 bad input) to distinct statuses, guarding against another collapse-to-500 refactor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
validate_reward_beneficiaries now rejects a split that repeats a beneficiary, matching the on-ledger RewardCoupon_AssignBeneficiaries `require "Beneficaries are unique"`. Without this a duplicated split passes the governance vote and then fails every Delegation_Assign, leaving a permanently unusable delegation. Add edge-case coverage for the per-percentage bound, the 20/21 count boundary, and the duplicate case (from the independent PR review, finding #1/#5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
select_batch caps a tick at MAX_BATCH and defers the rest; a sustained backlog can let coupons expire unassigned. Extract the ripeness predicate (is_ripe) and log a warning from run_reassign_once when the ripe set exceeds MAX_BATCH, so silent truncation is observable (PR review #2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The setup/revoke shape tests only checked field labels. Descend into values so a make_party<->make_contract_id swap, a renamed nested `percentage`, or a wrong Optional shape fails CI instead of at ledger submission. Adds an as_list test helper (PR review #4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only the empty-split reject was tested. Add a reusable negative driver and cases for percentages not summing to 1.0, a percentage outside (0,1], and more than 20 beneficiaries (PR review #5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lter A configured reward_automation_interval_secs of 0 reaches tokio::time::interval, which panics and silently kills the background loop task; clamp 0 to 1s with a warning. Also extract the unassigned- coupon fail-safe filter (provider == decparty, beneficiary is None) into a pure parse_unassigned_coupon and unit-test it, so the security-relevant skip logic is covered without the localnet IT (PR review low findings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RevokeCouponReassignmentDelegation::validate() accepted an empty `delegation` cid, deferring the failure to ledger submission; reject it at the boundary, consistent with AcceptExternalPartySetup (PR review). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The README covered only the minting-delegation actions. Add the three coupon-reassignment templates, the 1-of-n trust model and split security boundary, propose/revoke examples, and the Mode-A/Mode-B exclusivity caveat (PR review). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cargo fmt --check flagged hand-written formatting in the review-fix edits (governance.rs, action_serializer.rs, types.rs). No semantic change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What this PR does
Old behavior
A consortium's decentralized party (decparty — e.g.
cbtc-network) earns CIP-104 app rewards asRewardCouponV2coupons, but in that on-chain capacity it has no wallet: it can act only through slow m-of-n threshold governance. Nobody assigns each coupon's beneficiaries in time, so the coupons expire unclaimed every round. (Devnet: 118 coupons, ~43k CC, allbeneficiary = null, 0 collected.)New behavior
The consortium votes once to create an on-ledger delegation that carries the decparty's authority with the beneficiary split baked in. Thereafter any single member's node reassigns each round's unassigned coupons directly — a plain 1-of-n ledger command, no per-round vote — but only to that pre-voted split. Beneficiaries then self-mint. One vote up front, no human clicks per round, and any one live member keeps it going. The reassignment and the 0.8/0.2 split are now regression-tested on localnet every CI run (the test harness seeds its own coupons), not just verified once by hand on devnet.
The change that enables it
CouponReassignmentDelegation(signatory = decparty,assigners= members,splitbaked in) with anonconsumingDelegation_Assignchoice — controller = any singleassigner(1-of-n) — that reassigns caller-supplied coupons to the contract'ssplit. The split is never a caller argument (the security boundary). Created / replaced / revoked viaSetupCouponReassignmentDelegation/RevokeCouponReassignmentDelegationgovernance actions (the only step that takes a vote).reward_automation): reads the active delegation + the decparty's unassigned coupons, batches by TTL-watermark with a minting margin, and exercisesDelegation_Assignas a plain command on a timer tick.ProposalTypeplumbing for the setup/revoke actions.RewardCouponV2coupons and asserts the automation reassigns them to the voted split — turning the previously devnet-only, manually-verified path into deterministic CI coverage.Caveats
cbtc-networkfor the devnet exercise.)DECPM_IT_REWARD; the driven non-assigner negative remains covered only by a DAML unit test.--features test-mode)/contracts/querycan't use interface filters, so the IT queries the concreteSplice.Amulet:RewardCouponV2template rather than theRewardCouponinterface production uses. Known, tracked in #272 (likely a deletable stale workaround — the automation's own interface read does work in test-mode).Details
What changed
governance-rewards→ 0.1.3):CouponReassignmentDelegation(+Delegation_Assign/Delegation_Revoke),SetupCouponReassignmentDelegation,RevokeCouponReassignmentDelegation; GWT (TestHarness) tests.ProposalType::{Setup,Revoke}CouponReassignmentDelegation+validate+ serializer;reward_automationmodule —active_delegationreader,select_batch,run_reassign_onceexecutor (submit_delegation_assign→Delegation_Assignexercise,act_as = [assigner]/read_as = [decparty]), wired into the per-node looprun_once_for_party.crates/decman/tests/): aseed_reward_couponsphase that creates unassignedRewardCouponV2coupons via the JSON Ledger API as the localnet DSO stand-in; aledger_apihelper module (command builder + decoded ACS read tolerant of array/NDJSON); a poll-then-hard-fail precondition and a 0.8/0.2 split-by-value assertion incoupon_reassignment; thegovernance-rewardsDAR added to the localnet DAR distribution; wired to run every CI run (localnet unconditional, devnet opt-in). Also made the sharedpropose_confirm_executehelper target its own proposal (by label/cid) instead of assuming it's the only pending action — a prior phase (notification_feed) leaves an unrelated proposal pending.Security
The split lives on-ledger in the delegation and
Delegation_Assignreads it (newBeneficiaries = split) — never from the caller; the choice iscontroller assigner+assert (assigner \elem` assigners)(a genuine 1-of-n gate, not escalatable).SetupCouponReassignmentDelegation.executeImpl` re-validates the split at execute time, so a direct ledger submit can't bake in a bad split. Fairness is enforced in L3 DAML by construction, not by trusting any node.Verification
cargo fmt --checkclean;cargo clippy --all-targets --all-features -- -D warnings= 0;cargo test -p decman— 287 lib + 38 integration green.dpm build --allgreen; DAML test packages green.Delegation_Assigntemplate_idmodule path (the core runtime action) is locked by a dedicated unit test.governance_workflowssuite passes including the reward phase — it seeds an unassigned coupon, creates the delegation with one vote, the per-node automation reassigns it (Delegation_Assign, coupon archived), and the test asserts the 0.8/0.2 split by value (beneficiary 80.0 vs operator 20.0, 4:1).test result: ok. 1 passed.DECPM_IT_REWARD-gated; devnet target wiring is a follow-up (IT: wire TestTarget::Devnet for CIP-104 coupon-reassignment (Option B) #271).Devnet exercise (pre-merge) — ✅ PASSED
Deployed image
f6a716ftodec-party-manager-1/2/4;governance-rewards-v1DAR uploaded + vetted; Mode-B paused oncbtc-network.One governance vote (2-of-2) created the on-ledger
CouponReassignmentDelegation; the per-node loop then reassigned each ripe coupon automatically (attestor-2), no errors, no per-round vote.Split verified on-ledger by value (PQS
archives()), across 91 coupon pairs:cbtc-beneficiaryauth0_007c65…)Ratio 4.000 : 1 — exactly the voted 0.8 / 0.2.
Batching confirmed: batch size scales with the loop tick interval (
batch ≈ inflow × interval). At the exercise's60stick each coupon was assigned individually (count=1); raising the interval to30 minproducedcount=3batches, as predicted.Tick-interval tuning
DECPM_REWARD_AUTOMATION_INTERVAL_SECS=21600, dlc-infra bug(parties): list_my_owner_keys times out on devnet — RequestOwnerKeys Noise round trip never returns peer's keys #149): devnet inflow ~6 coupons/hr → ~36/batch (underMAX_BATCH=50), ~4 assigns/day, ~22h slack to the 34h assign deadline. The binding constraint isMAX_BATCH=50, not the 36h TTL.RewardCouponV2: app rewards are the olderAppRewardCouponwith the beneficiary already set at mint (no reassignment gap), so the target inflow isn't measurable there yet. Volume proxy: mainnet app-reward flow is ~14× devnet (~84/hr). If mainnet later adopts theRewardCouponV2deferred-assignment model,MAX_BATCH=50caps the interval at ~36 min (use ~20 min for headroom) and/orMAX_BATCHmust be raised — the devnet 6h would be catastrophic there (~500/tick vs a 50 cap). Production interval +MAX_BATCHare deferred until the mainnet coupon model is confirmed.Notes
governance-rewardspackage → 0.1.3.main).Design doc lives in
docs/coupon-reassignment/design.mdon this branch. Companion: #256 (Mode B).Follow-ups: #271 (devnet target for the reward IT) · #272 (test-mode
/contracts/queryinterface-filter cleanup).