PoC: registration lifecycle (offboard vote + reporting-state wiring) - #12
PoC: registration lifecycle (offboard vote + reporting-state wiring)#12timwu20 wants to merge 1 commit into
Conversation
40d8b4a to
f5a7cb0
Compare
2bb23d4 to
7ae0c9b
Compare
f5a7cb0 to
c85a34a
Compare
7ae0c9b to
39cfab1
Compare
…ate wiring [ci] - DsoRules_ArchiveSynchronizerRegistration (vote-dispatched via appended SRARC constructor): archives a RegisteredSynchronizer and its DedicatedSynchronizerState in one governed action, validating the state belongs to the registration; recovery path for duplicate registrations. Hard archive by design: an archived registration cannot be disclosed, so the buy gate closes with no new checks; authorized reward processing for past rounds completes, new work stops. - DsoRules_RegisterSynchronizer now creates the reporting state, so the operator can report from its first round. - Tests: archive-via-vote, mismatched-state negative, registration-creates- state assertion; vote test reports against the vote-created state. Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
39cfab1 to
e002dad
Compare
c85a34a to
070f5aa
Compare
| registeredSynchronizerCid <- create RegisteredSynchronizer with dso; synchronizerId; operator | ||
| -- reporting state for the extension reward flow (see DedicatedSynchronizerState), | ||
| -- created at registration so the operator can report from its first round | ||
| void $ create DedicatedSynchronizerState with |
There was a problem hiding this comment.
Registration now creates two contracts, but DsoRules_RegisterSynchronizerResult still returns only registeredSynchronizerCid. The offboard choice below needs this state's cid, so callers have to go and find it. Cheap to return now, awkward to add later.
| -- (an archived RegisteredSynchronizer cannot be disclosed to the buy choice) and future | ||
| -- activity reports become impossible (the reporting state is gone). Records of past | ||
| -- purchases (MemberTraffic) and reward processing already authorized for past rounds are | ||
| -- untouched: authorized work completes, new work stops. |
There was a problem hiding this comment.
This boundary misses a middle state: reported but not yet authorized.
Offboarding archives the registration and the state, but not outstanding ExtensionActivityReport contracts. And AmuletRules_StartProcessingExtensionRewardsV2 never checks the registration:
require "issuance rate is positive" (issuancePerActivityUnit > 0.0)
report <- fetchAndArchive (ForDso with dso) reportCid
require "reported activity does not exceed purchased traffic" ...
So a report filed shortly before offboarding can still be turned into new minting afterwards. E10-2's 1-tick timeout does not help here, since counting a report as zero does not remove the contract.
Suggestion, using this PR's own argument: have start-processing take the disclosed RegisteredSynchronizer and check it matches the report's synchronizerId and operator. An archived registration cannot be disclosed, so the reward path would close automatically, exactly the way the buy gate does.
| -- untouched: authorized work completes, new work stops. | ||
| with | ||
| registeredSynchronizerCid : ContractId RegisteredSynchronizer | ||
| optStateCid : Optional (ContractId DedicatedSynchronizerState) |
There was a problem hiding this comment.
DedicatedSynchronizerState_ReportActivity is consuming and does create this with ..., so the state gets a new contract id on every report. This pins one, and vote-close no longer has a try/catch (see the VRO_AcceptedButActionFailed note at DsoRules.daml:446), so a stale cid fails the entire vote-close transaction rather than just this action.
Net effect: the operator being offboarded can block its own offboarding indefinitely by reporting once per round while the vote is open, using an action that looks completely routine.
This is the same concern that motivated keeping the state off RegisteredSynchronizer in the first place (E10-1 assumption 2, "kept separate to leave RegisteredSynchronizer stable"): a frequently re-created contract should not be the thing something else identifies by cid. The offboard vote is now doing that.
Options: give DedicatedSynchronizerState a contract key and archive by key, or split this into two actions.
| with | ||
| registeredSynchronizerCid : ContractId RegisteredSynchronizer | ||
| optStateCid : Optional (ContractId DedicatedSynchronizerState) | ||
| -- reporting state of the synchronizer; None only for registrations created before |
There was a problem hiding this comment.
Two things on this Optional.
The orphan has a consequence worth stating. The PR body notes that passing None for a registration that does have a state leaves that state active. ReportActivity needs only the state contract and never fetches the registration, so an offboarded operator with an orphaned state can keep filing reports indefinitely, and combined with the gap noted above those reports can still be processed. The offboard would look like it succeeded without actually offboarding.
Can this case even arise? splice-dso-governance is 0.1.28 on feat/dedicated-sync, on multi-sync-poc-reward-reporting, and on this branch, so there is no version bump anywhere in the stack. If the whole stack ships as one package version, no registration can exist without a state, the Optional defends against nothing, and it is the only thing creating the orphan hole. Making stateCid mandatory would remove that case entirely.
It only earns its keep if DA might cut a release with #1/#2 but without #8/#12. Worth confirming rather than assuming.
Daml PoC, stacked on the reward-reporting rung (#8). Completes the registration story create-to-archive: the offboard vote requested in review (#1 thread), plus the production wiring for the reporting state.
What this does
DsoRules_ArchiveSynchronizerRegistration(new choice, vote-dispatched via appendedSRARC_ArchiveSynchronizerRegistration): archives aRegisteredSynchronizerand itsDedicatedSynchronizerStatein one governed action, validating that the state belongs to the registration. This is also the recovery path for a duplicate registration that slips past the SV-UI uniqueness check.DsoRules_RegisterSynchronizercreates theDedicatedSynchronizerStatealongside the registration, so an operator can report from its first round (production wiring for PoC: extension reward reporting + expansion (Daml) #8's report flow, following its assumption 2).Offboarding semantics (design position — flagging for review)
Hard archive, no lifecycle-state field. Archival is the state: an archived registration cannot be disclosed, so the buy gate closes with no new checks anywhere; the archived state contract ends reporting the same way. Deliberately untouched:
MemberTrafficrecords (history), and reward processing already authorized for past rounds (ProcessRewardsV2contracts complete) — authorized work finishes, new work stops. A suspend-style status field stays addable later if wanted; it would put a new check into every reader, so it is not included here.Two edges of this shape, named rather than found:
optStateCid = Nonefor a registration that has a state leaves that state active (Daml cannot check contract absence). No choice in this PR archives a state standalone; the cleanup path is the state's implicit signatoryArchiveexercised as the DSO party — the usual admin-archive precedent.lastReportedRound = None), so rounds already reported under the old registration could be reported again. Processing stays vote-gated per report, which is the backstop; a re-registration vote can also carry the old high-water mark forward if the SVs prefer.How it's verified (Daml Script)
test_RegisterSynchronizer_viaVote(extended): registration creates the reporting state.test_ArchiveSynchronizerRegistration_viaVote: register then offboard via 4-SV votes; both contracts gone.test_ArchiveSynchronizerRegistration_mismatchedState: archiving with another synchronizer's state is rejected; the same exercise with the matching state then succeeds (positive control).test_ArchiveSynchronizerRegistration_duplicateSynchronizerId: two registrations sharing a synchronizer id under different operators (the duplicate-recovery scenario) — archiving one cannot take the other operator's state, so the state check matches on operator as well as synchronizer id.test_ArchiveSynchronizerRegistration_noState: a registration predating the reporting state offboards withoptStateCid = None.TestExtensionRewardVote(updated): reports against the vote-created state instead of a test-created one.Full amulet, wallet, and dso-governance suites pass.
Tracked in
Implements E1-4 (ChainSafe/canton-extending-mainnet#30); the SV-UI duplicate check that pairs with this as the uniqueness mechanism is #54.