Skip to content

PoC: extension reward reporting + expansion (Daml) - #8

Open
timwu20 wants to merge 4 commits into
multi-sync-poc-buy-trafficfrom
multi-sync-poc-reward-reporting
Open

PoC: extension reward reporting + expansion (Daml)#8
timwu20 wants to merge 4 commits into
multi-sync-poc-buy-trafficfrom
multi-sync-poc-reward-reporting

Conversation

@timwu20

@timwu20 timwu20 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Daml PoC of the extension reward integration sketched in the design doc's Section H, stacked on the buy rung (#2). On-ledger pieces only — the SV-side rate computation (tranche split by burn share, the voted per-synchronizer weight, the report timeout) and the operator-side automation stay off-ledger and are not part of this PR.

What this does

Per round, a registered synchronizer's operator commits its activity on the decentralized synchronizer, and later expands that commitment into reward coupons through the existing CIP-104 machinery:

  • DedicatedSynchronizerState (new, splice-amulet): per-synchronizer reporting state. The operator's ReportActivity choice records a Merkle root hash over its activity records plus the round total, enforces strictly increasing rounds, accumulates a running total-ever-reported, and creates a per-round ExtensionActivityReport.
  • AmuletRules_StartProcessingExtensionRewardsV2 (new choice, vote-dispatched via appended CRARC_StartProcessingExtensionRewardsV2): consumes a report and creates the root ProcessRewardsV2 carrying the report's root hash, the issuance rate computed by DSO automation, the operator as expander, and the report's total as the root weight budget. The vote also attests the synchronizer's purchased traffic total, and the choice rejects a report exceeding it.
  • ProcessRewardsV2 (extended, per Section H "noting that party on the ProcessRewardsV2 contracts" + "push issuance per activity record into process rewards"): appended expander : Optional Party, issuancePerActivityUnit : Optional Decimal, and weightBudget : Optional Decimal; the expander is an observer (it must see the contracts to act) and the controller of ProcessBatch when set; leaf expansion mints coupons at activity weight × rate. Nones everywhere (all existing contracts and the gsync path) is byte-for-byte today's behavior — including that the delegate-based DsoRules_ProcessRewardsV2_ProcessBatch wrapper cannot touch extension contracts.
  • BatchOfWeightedBatches (appended Batch constructor): internal nodes of an extension tree declare each child's subtree weight inside the hashed commitment. Expansion checks the declared weights against the node's budget and endows each child with its declared weight; leaves check their allowances against theirs. Weighted nodes require a rate and unweighted ones reject it, so the two tree shapes cannot mix within one round's expansion.

Economic bound

The decentralized synchronizer's own flow needs none of this because every SV recomputes the full tree before confirming the root — expansion is transcription. An extension tree has a single author the SVs cannot recompute; what stands in is a chain of on-ledger checks, each anchored one level up:

minted  ≤  Σ committed tree weights  ≤  reported totalActivity  ≤  purchased traffic
        (B: budget threading)      (B: root budget)          (A: vote-attested cap)

A binds the report to reality: the confirming vote carries the purchased total (from MemberTraffic, which SVs already see), and AmuletRules_StartProcessingExtensionRewardsV2 requires totalActivity ≤ purchasedTotal. B binds the tree to the report: the root's budget is the reported total, every internal node's children must fit its budget, and every leaf's allowances must fit its own — so no expansion order or tree shape can mint past what was reported. An operator can still distribute weight among its own parties as it pleases; what it cannot do is mint more than the round's attested activity.

How it's verified (Daml Script)

  • TestExtensionRewardReporting (splice-amulet-test): full report → start → expand flow with a two-level weighted tree, coupon amounts = weight × rate; report monotonicity/negative-total/wrong-party negatives; expansion security (tampered pre-image fails the hash check, the DSO party cannot expand extension batches, a leaf cannot be replayed against the wrong hash); economic bound (a report exceeding the attested purchased total is rejected; an internal node over-declaring its children's weights and a leaf minting past its declared weight both fail; a plain unweighted batch cannot smuggle a rate).
  • TestExtensionRewardVote (splice-dso-governance-test): the governance path — register via vote, report as operator, start processing via a 4-SV supermajority vote, expand, assert the coupon.

Full amulet, wallet, and dso-governance suites pass.

Assumptions (Section H implies but does not pin — flagging for review)

  1. Activity weights are Decimal, reusing MintingAllowance as the leaf type, with the weight × rate multiplication in Numeric 10 at expansion. The committed tree is in activity units because the rate does not exist at report time (it depends on all synchronizers' reports and burns).
  2. The report lives on a new DedicatedSynchronizerState rather than on RegisteredSynchronizer (Section H says "possibly in registry contract"); kept separate to leave the registration stable. Production wiring of the state's creation (at registration) follows the report-home decision.
  3. CalculateRewardsV2 is untouched: the extension flow starts from the report contract; the timeout / count-as-zero behavior is SV-side.
  4. providersWithWrongVettingState remains caller-supplied at expansion; extension expansions pass what the operator knows, with the unhide trigger as the correction path.

Tracked in

Implements E10-1 (ChainSafe/canton-extending-mainnet#47); part of Epic E10 (#22). The SV-side integration and operator-side automation are tracked as follow-ups (#57 / #58).

timwu20 added 2 commits July 31, 2026 15:43
…) [ci]

The registered synchronizer's operator commits its per-round activity on the
decentralized synchronizer and expands the commitment into reward coupons
through the CIP-104 machinery:

- DedicatedSynchronizerState: per-synchronizer reporting state; the operator's
  ReportActivity choice records a Merkle root hash + round total, enforces
  strictly increasing rounds, accumulates the total ever reported, and creates
  a per-round ExtensionActivityReport
- AmuletRules_StartProcessingExtensionRewardsV2 (vote-dispatched via appended
  CRARC constructor): consumes a report and creates the root ProcessRewardsV2
  with the report's root hash, the issuance rate, and the operator as expander
- ProcessRewardsV2: appended expander + issuancePerActivityUnit; the expander
  is observer and ProcessBatch controller when set; children inherit both;
  leaves mint activity weight * rate. None/None is unchanged gsync behavior
- Daml Script tests: report/expand happy path with coupon amounts, report
  monotonicity + authorization negatives, hash-binding negatives, and the
  4-SV vote path end to end

Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
…artifacts [ci]

Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
timwu20 added 2 commits August 3, 2026 17:31
CI's output scanner fails jobs on any DsWarning even when sbt succeeds:
- replace the 8-tuple setup return with a ReportedActivitySetup record
  (Daml has no instances for tuples of size > 5)
Review-pass fixes bundled in:
- StartProcessingExtensionRewardsV2 doc comment moved to the -- ^ form
  after the choice declaration
- roundClosedAt field doc covers the extension case (start-of-processing
  time; reports carry no round-close time)
- new dry-run expansion test (contracts consumed, no coupons minted)

Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
…als [ci]

Close the gap between an extension's reported activity total and what its
committed tree can mint:

- StartProcessingExtensionRewardsV2 takes the vote-attested purchasedTotal
  and requires reported activity <= purchased traffic
- BatchOfWeightedBatches (appended Batch constructor): internal nodes of an
  extension tree declare each child subtree's weight inside the hashed
  commitment; ProcessRewardsV2 carries a weightBudget threaded from the
  reported total, and expansion requires each level's declared weights to
  fit its budget, bounding total minting by the reported total
- path exclusivity: unweighted internal nodes only without a rate (the
  decentralized synchronizer's own flow, which is unchanged - its trees are
  recomputed and vote-confirmed by every SV, so no budget is needed);
  weighted nodes only with one
- tests: over-declared internal node, over-weight leaf, report exceeding
  purchased, plain-batch smuggling

Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
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.

1 participant