Skip to content

Commit b655db6

Browse files
committed
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.
1 parent 0705397 commit b655db6

1 file changed

Lines changed: 5 additions & 23 deletions

File tree

docs/design/operator-prepaid-attestation-storage.md

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# Operator-Prepaid Attestation Storage
22

3-
**Status:** Draft — for team review
4-
53
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.
64

75
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
3634
| Method | Kind | Purpose |
3735
|---|---|---|
3836
| `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.
4137
| `available_attestation_grants(account_id) -> u32` | view | Grants available. |
4238
| `attestation_storage_fee() -> NearToken` | view | Current fee. |
4339

4440
Views are contract-only; operators read them with the NEAR CLI.
4541

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+
4644
### Charging rules
4745

4846
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.
4947

5048
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".
5149
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.
5351

5452
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.
5553

@@ -120,7 +118,7 @@ Note the guide's [Submitting Participant Info](../running-an-mpc-node-in-tdx-ext
120118
| 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. |
121119
| 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. |
122120
| 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. |
124122
| Consumption point | At insert | Up front, charging failures too | Nothing is stored on failure, so nothing is owed; keeps #3991 unblocked. |
125123
| 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. |
126124

@@ -142,21 +140,5 @@ Every new entry consumes a paid grant, so the contract funds no new attestation
142140

143141
Two accepted residuals:
144142

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.
146144
- **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

Comments
 (0)