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
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.
There was a problem hiding this comment.
Done in 7ce9282: DsoRules_RegisterSynchronizerResult now returns dedicatedSynchronizerStateCid alongside the registration cid.
| -- (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.
There was a problem hiding this comment.
Took your suggestion, landed in #8 since that's the choice's home: AmuletRules_StartProcessingExtensionRewardsV2 now takes the RegisteredSynchronizer and requires it to match the report's synchronizer id and operator (both fields, for the duplicate-registration case — same lesson as the offboard check here). An archived registration cannot be fetched, so offboarding closes the reward path structurally, the same way it closes the buy path. Commit f6a9a6a; test_StartProcessing_registrationGate covers the offboarded, wrong-operator, and wrong-synchronizer cases.
This also sets up the answer to your cid-churn and Optional comments — with the reward path gated on the registration, the offboard vote may not need to touch the state at all. Replies on those threads once that rework is in.
| -- 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.
There was a problem hiding this comment.
Fixed by taking the state out of the vote entirely (7ce9282): the offboard action now archives only the registration, so there is no pinned state cid for a report to invalidate — the blocking lever you found is gone. The leftover state can't do damage: with the registration archived, #8's gate means its reports can never mint. Cleanup happens after the fact — the DSO archives the state directly, and if the id changed under it, it just retries with the current one; no vote is riding on it. (The contract-key option isn't available — Daml 3.x removed keys — and splitting the archive into two vote actions would leave the state action with the same pinning problem.)
| 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.
There was a problem hiding this comment.
Both points taken, and the Optional disappeared along with the state argument (7ce9282). You're right on the version question: everything here ships as one package version, so a registration without a state can't actually exist — the Optional was defending against a case that can't happen while creating the orphan case that can. The orphan story is now deliberate instead of accidental: the state survives offboarding, it's harmless (see the gate), and DSO cleanup archives it out-of-band. The one real interaction — re-registering while an old state still exists — is spelled out in the description.
…ate wiring [ci] Registration (DsoRules_RegisterSynchronizer) now also creates the DedicatedSynchronizerState and returns both contract ids, so an operator can report from its first round. Offboarding (DsoRules_ArchiveSynchronizerRegistration, vote-dispatched via appended SRARC_ArchiveSynchronizerRegistration) archives only the registration. That closes both economic paths: the buy needs the disclosed registration, and reward processing requires the live registration (the start-processing gate). The reporting state is deliberately not pinned by the vote - its contract id changes with every report, so a vote naming it could be invalidated by the operator reporting while the vote is open. The leftover state is inert and is archived by DSO automation off the vote's critical path. Signed-off-by: Timothy Wu <tim.wu@chainsafe.io>
070f5aa to
7ce9282
Compare
Daml PoC, stacked on the reward-reporting rung (#8). Completes the registration story create-to-offboard: the offboard vote requested in review (#1 thread), plus the production wiring for the reporting state. Reworked per review: the vote archives only the registration.
What this does
DsoRules_ArchiveSynchronizerRegistration(new choice, vote-dispatched via appendedSRARC_ArchiveSynchronizerRegistration): archives theRegisteredSynchronizer. Archiving the registration alone shuts down everything that matters: traffic can no longer be bought (the buy choice needs the registration disclosed to it), and rewards can no longer be processed (PoC: extension reward reporting + expansion (Daml) #8's start-processing choice requires the live registration). It is also the recovery path for a duplicate registration that slips past the SV-UI uniqueness check.DsoRules_RegisterSynchronizercreates theDedicatedSynchronizerStatealongside the registration and returns both cids, 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)
The vote archives the registration and nothing else. The reporting state is deliberately left alone, for a reason found in review: every report replaces the state contract with a new contract id, and a vote pins exact contract ids when it is created. Votes stay open for days. If the operator files one routine report while the offboard vote is open, the pinned id goes stale and the vote fails when it closes — so a vote that touches the state hands the operator a way to block its own offboarding indefinitely. Keeping the state out of the vote removes that lever.
The leftover state is harmless. The operator can still file reports against it, but a report can never be turned into Amulet: minting requires the start-processing choice, and that choice requires the live registration, which is gone. Cleanup is simple: the state is DSO-signed, so DSO automation archives it any time after the offboard — and if a report has changed the id in the meantime, the archive just retries against the current one. Nothing else is waiting on it.
One ordering rule: clean up the old state before re-registering the same synchronizer. Re-registration creates a fresh state that starts with no reporting history, and if the old state were still around, the operator would hold two live states and could report the same round twice. Per-report processing votes remain the backstop either way.
Deliberately untouched by offboarding:
MemberTrafficrecords (history) and reward processing already authorized for past rounds (ProcessRewardsV2contracts complete) — authorized work finishes, new work stops.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; the registration is gone, the state survives, the operator can still file a report — and the report is worthless (the processing gate is pinned bytest_StartProcessing_registrationGatein PoC: extension reward reporting + expansion (Daml) #8) — then DSO-side cleanup archives the state.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.