You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(design): drop the status banner, Testing and Implementation notes
- Status line removed; the design is settled, not a draft for review.
- Testing removed outright: the test plan belongs in the implementation PR.
- Implementation notes removed, but two of its items were costs of design
decisions rather than implementation chores, so they moved next to the decisions
that cause them instead of disappearing: rule 3 now carries its own sweep
gas-budget caveat, and the votable-Config row in Decisions carries the state
migration, ConfigExt plumbing and snapshot regeneration. The non-universal
sweepability caveat moved to the Security residual about swept entries, which is
what it qualifies.
Dropped entirely: the #3785 dependency note (merged, so there is nothing to
sequence) and the runbook-coordination reminder (Operator UX already says
operators must prepay first).
Also repaired the API table, which an earlier edit had split by inserting a
paragraph between its rows.
259 -> 144 lines across this and the previous trim.
Copy file name to clipboardExpand all lines: docs/design/operator-prepaid-attestation-storage.md
+5-23Lines changed: 5 additions & 23 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,5 @@
1
1
# Operator-Prepaid Attestation Storage
2
2
3
-
**Status:** Draft — for team review
4
-
5
3
Funding model for [#3972](https://github.com/near/mpc/issues/3972): the storage cost of an attestation entry moves off the contract's balance and onto whoever onboards the node, at the cost of one new operator step.
6
4
7
5
One prepayment buys one **grant** — permission for a node account to hold one attestation entry. The grant returns when that entry is reclaimed, so it is a slot the operator keeps rather than a per-attestation charge. No NEAR is ever refunded. Fee: **0.02 NEAR**.
@@ -36,20 +34,20 @@ The counter is **available** grants, not lifetime total, so `0` means either "ne
36
34
| Method | Kind | Purpose |
37
35
|---|---|---|
38
36
|`prepay_attestation_storage(account_id)`|`#[payable]`| Grants `floor(attached / fee)`. Rejects below one fee, keeps any remainder. Permissionless — anyone may prepay for any account. |
39
-
40
-
Keeping the remainder deviates from the contract's usual `require_deposit` + `refund_to` pattern, deliberately: a grant is a discrete unit, so the leftover is at most one fee short of the next grant, and adding a transfer path to return sub-0.02 NEAR dust is not worth it. "No NEAR is ever returned" means exactly that — no withdrawal method, and no refund of an overpayment.
|`attestation_storage_fee() -> NearToken`| view | Current fee. |
43
39
44
40
Views are contract-only; operators read them with the NEAR CLI.
45
41
42
+
Keeping the remainder deviates from the contract's usual `require_deposit` + `refund_to` pattern, deliberately: a grant is a discrete unit, so the leftover is at most one fee short of the next grant, and adding a transfer path to return sub-0.02 NEAR dust is not worth it. "No NEAR is ever returned" means exactly that — no withdrawal method, and no refund of an overpayment.
43
+
46
44
### Charging rules
47
45
48
46
Evaluated read-only at the top of `submit_participant_info` — before any verification, so an ungranted or unauthorised call never reaches the `Mock` checks or a `verify_quote` round trip — and applied at insert.
49
47
50
48
1.**Entry exists and this account owns it** — re-attestation. Updates in place, consumes nothing. Keys on *ownership*, not key presence: a presence-only test would classify someone else's key as "no grant needed".
51
49
2.**New entry** — consume one grant; reject if the account has none.
52
-
3.**Entry reclaimed** by `clean_invalid_attestations` — return one grant to its owner. There is exactly one removal site, so the counter cannot drift.
50
+
3.**Entry reclaimed** by `clean_invalid_attestations` — return one grant to its owner. There is exactly one removal site, so the counter cannot drift. This adds a grants-map write per removed entry — a row *insert*, not an update, whenever the owner's row was deleted at zero — inside a gas-bounded sweep, so `clean_invalid_attestations_tera_gas` and `RESHARE_CLEAN_INVALID_ATTESTATIONS_MAX_SCAN` need re-validating against the heavier per-entry cost.
53
51
54
52
Consuming at insert means a failed attestation consumes nothing — nothing was stored, so nothing is owed — and keeps [#3991](https://github.com/near/mpc/issues/3991) unblocked, since no charge has to survive a failing callback.
55
53
@@ -120,7 +118,7 @@ Note the guide's [Submitting Participant Info](../running-an-mpc-node-in-tdx-ext
120
118
| Multi-node / migration | Prepay per node | Free extra entry gated on `ongoing_migrations`; or a hard cap of N | Both needed a participant check or a magic number; repeating the prepayment needs neither. |
121
119
| Grant semantics | Capacity — reclaiming returns it | Consumable ticket | The contract got the storage back, so charging again charges twice. Also lets an operator re-provision on the grants they hold. |
122
120
| Refunds | None | Redeem unconsumed grants |~0.02 NEAR; recycling covers the real need without moving money. |
123
-
| Fee source | Votable `Config` field | Derived from `storage_byte_cost` at call time; or a constant | Re-priceable without a release. Costs a state-schema change. |
121
+
| Fee source | Votable `Config` field | Derived from `storage_byte_cost` at call time; or a constant | Re-priceable without a release. Costs a state migration — the fee field and the grants map both change `MpcContract`'s borsh layout — plus `ConfigExt` DTO plumbing and regenerated borsh-schema and ABI snapshots. |
124
122
| Consumption point | At insert | Up front, charging failures too | Nothing is stored on failure, so nothing is owed; keeps #3991 unblocked. |
125
123
| Map hygiene | Delete row at zero; keep grants when a node is kicked | Keep zero rows; or confiscate on removal from the set | Kicks are often temporary, so confiscating would force paying again to rejoin. |
126
124
@@ -142,21 +140,5 @@ Every new entry consumes a paid grant, so the contract funds no new attestation
142
140
143
141
Two accepted residuals:
144
142
145
-
-**Abandoned pre-upgrade entries yield a grant when swept**, since rule 3 cannot distinguish them from paid ones. Note who this does *not* benefit: an operator running a live node re-attests under rule 1, so its entry never fails re-verification, is never swept, and yields nothing — live operators get no grant and need none. The grants go to whoever *abandoned* an entry. Today that is a negligible set (14 entries on mainnet and 31 on testnet, nearly all live), but the exposure is precisely "abandoned entries present at deploy become permanent capacity", so the count must be checked at deploy rather than assumed. A count in the thousands would mean the still-open drain was exploited before the release, and those entries should be purged rather than granted.
143
+
-**Abandoned pre-upgrade entries yield a grant when swept**, since rule 3 cannot distinguish them from paid ones. Note who this does *not* benefit: an operator running a live node re-attests under rule 1, so its entry never fails re-verification, is never swept, and yields nothing — live operators get no grant and need none. The grants go to whoever *abandoned* an entry. Not everything is sweepable either: `TeeState::with_mocked_participant_attestations` stores bare, non-expiring `Mock::Valid` sentinels at init, which never fail re-verification, so they are never swept and never yield a grant. Today the abandoned set is negligible (14 entries on mainnet and 31 on testnet, nearly all live), but the exposure is precisely "abandoned entries present at deploy become permanent capacity", so the count must be checked at deploy rather than assumed. A count in the thousands would mean the still-open drain was exploited before the release, and those entries should be purged rather than granted.
146
144
-**The contract carries the price risk on sold grants**, bounded by the number outstanding. It stores counts, not purchase prices, so it cannot retro-price them; a future update can address this if the gap becomes material.
147
-
148
-
## Implementation notes
149
-
150
-
1.[#3785](https://github.com/near/mpc/pull/3785) has landed (`stamp_expiry_on_legacy_mocks`, `v3_13_0_state.rs:122`), so abandoned mock entries are already sweepable — no dependency to wait on. Reclaimability is not universal though: `TeeState::with_mocked_participant_attestations` still stores bare, non-expiring `Mock::Valid` sentinels at init, which never fail re-verification and so are never swept. Those stay contract-funded and never yield a grant.
151
-
2.**A state migration is required.** The grants map and the `Config` fee field both change `MpcContract`'s borsh layout: frozen snapshot module plus a `migrate()` arm the way `crates/contract/src/v3_13_0_state.rs` does it. Making the fee votable also needs the `ConfigExt` DTO plumbing in `crates/contract/src/dto_mapping.rs`, and both the borsh-schema and ABI snapshots regenerated.
152
-
3.**Re-validate the sweep gas budget.** Rule 3 adds a grants-map write per removed entry — a row *insert*, not an update, whenever the owner's row was deleted at zero — inside gas-bounded `clean_invalid_attestations`. Re-check `clean_invalid_attestations_tera_gas` and `RESHARE_CLEAN_INVALID_ATTESTATIONS_MAX_SCAN`, with a guard test of the kind [#3936](https://github.com/near/mpc/pull/3936) adds.
153
-
4. Check the stored-entry count at deploy, per [Security](#security).
154
-
5. Coordinate the runbook change with the release: operators onboarding after the upgrade must prepay or their node's submissions fail in a loop.
155
-
156
-
## Testing
157
-
158
-
- Re-attestation consumes nothing; a new entry consumes exactly one; no grant rejects; a TLS key owned by another account is rejected by the early check, before verification.
159
-
- A failed attestation consumes nothing and stores nothing, on both the sync and async paths.
160
-
- A reclaimed entry returns one grant to its owner, the row is deleted at zero, a kicked node keeps its grants, and the recycled grant is spendable.
161
-
- Sandbox, with a real function-call key: ungranted submission rejected, a second account prepays, the node's own zero-deposit submission succeeds; two prepayments allow two entries.
162
-
- Gas guard for the `clean_invalid_attestations` worst case: every scanned entry removed, every owner's row absent so each write is an insert.
0 commit comments