docs(design): operator-prepaid attestation storage - #4011
Conversation
575f9ab to
4a16377
Compare
Design for #3972: move the storage cost of a stored attestation entry off the contract's balance and onto whoever onboards the node, by adding a single operational step in which the operator funds that storage. Payment and submission have to be separate transactions. A function-call access key cannot attach a deposit -- not even inside a meta-transaction, since validate_delegate_action_key applies the same check to inner actions -- and report_data binds the quote to env::signer_account_pk(), so an operator cannot submit on the node's behalf. The operator therefore credits a per-account balance with a deposit-capable key, and the node keeps self-submitting with its own restricted key. The charging rules are evaluated read-only at the top of submit_participant_info, so an unfunded submission is rejected before any verification or cross-contract round trip, and debited authoritatively at insert, where a failed attestation stores nothing and so charges nothing. That also keeps #3991 unblocked: no charge or refund ever has to survive a failing callback. Migration and key rotation stay free and bounded by state that already exists -- ongoing_migrations holds one declared destination per participant, so a participant can hold at most two entries and a non-participant none -- rather than by a new cap constant. Also records the alternatives that were rejected with their evidence, and states the one-node-per-account assumption explicitly, since the contract does not enforce it.
4a16377 to
6d38389
Compare
Reynaldo and Marten pushed back on the storage-credit accounting as more general than the problem needs, and the follow-up meeting settled on a counter: one prepayment buys one grant to store one attestation entry, operators prepay again per extra node, no refunds. The counter is not just a substitute for the balance -- it removes three things from the previous draft. Migration no longer needs a free-entry rule gated on ongoing_migrations, there is no participant-status check, and the one-node-per-account assumption is gone entirely, since an operator wanting two nodes simply prepays twice. That also fixes the case Reynaldo raised of an operator testing several nodes at once, which the migration rule handled badly. Grants are denominated in entries rather than NEAR, so re-pricing after a storage-price change cannot strand an existing operator -- a balance model has to handle exactly that. Moves the storage-credit design to Alternatives with the reasons it was rejected, marks every operator step as existing except the prepayment (the off-chain attestation check read as a new requirement, which it is not), and records the open question of whether a reclaimed entry should return its grant.
Settle the three decisions left open after review: - A grant is capacity, not a consumable ticket: reclaiming an entry returns the grant to the account that owned it. Without this an operator testing several nodes in sequence pays per node even though the contract reclaimed each slot (raised by gilcu3). One increment at the contract's single removal site, and no NEAR moves, so the no-refunds decision is untouched. - Delete a counter's row when it reaches zero so the map does not accumulate rows for accounts holding nothing. - Do not confiscate grants when a node leaves the participant set. Kicks are often temporary, and taking prepaid capacity would force an operator to pay again merely to rejoin. Also state what the fee actually has to cover, which was missing: the worst-case attestation entry (604 bytes) plus the grants-map row it creates (~130 bytes), about 0.0073 NEAR together, plus headroom for future layout growth so a schema change cannot leave issued grants under-funded. 0.02 NEAR gives ~2.7x. And state plainly that no NEAR is ever returned -- recycling gives back capacity, never money, and there is no withdrawal path. The security argument is extended to show recycling does not weaken the bound: a grant returns only once the entry it paid for is gone, so entries held never exceed grants bought.
'An operator running two nodes prepays twice' did not say why anyone would. Give the two real cases: a backup node, and the second node during a migration.
…ragraph The background implied contract-funded storage started with #3940. It did not: the deployed 3.13.0 contains charging code that never collects, because it reads the storage delta before the write is flushed, and #3714 -- the only version that genuinely required a deposit -- never shipped. So this has been true of every deployed version, and #3940 only made it intentional. That matters because it means the drain is not a regression to be reverted but a gap that was always there. Also split the run-on sentence about the security implication into three bullets: unlimited entries, roughly 7x cost asymmetry, and the contract being unable to write state at all once its balance is gone.
…otal attestation_grants(account_id) -> u32 did not say which number it returned. It is available grants -- bought minus those currently backing an entry -- since the counter is incremented on prepay, decremented on insert and incremented again on reclaim. Rename it available_attestation_grants and spell out the relation. Lifetime total is deliberately not stored: the contract enforces the invariant by keeping the counter non-negative, so neither 'bought' nor 'entries held' has to be tracked separately and nothing in the design reads them.
Three paragraphs to say: check the grant before verifying, consume it on success, consume nothing on failure. Cut to two, keeping only the two things that are not self-evident -- why both steps exist (a Dstack callback lands later, so the early check cannot be the enforcement) and the #3991 consequence.
…ionale Recycling was stated in passing inside the state description and the charging rules, which under-sold the single property that most changes how the scheme feels to use. Put it in the opening paragraph, add it as a goal, and give it two named reasons instead of one: - fairness, the original argument -- the contract got the storage back, so charging again charges twice for the same thing; - operator experience -- prepay for the capacity you actually run, three grants for three nodes, and then stop thinking about it. Nodes can be torn down, re-provisioned, rotated and migrated indefinitely on the same grants. Without recycling every re-provisioned node is a fresh purchase and a fresh thing to remember.
There was a problem hiding this comment.
Pull request overview
Adds a design-draft document describing an “operator prepays attestation storage” model intended to address #3972 by shifting attestation-entry storage costs off the contract balance while preserving deposit-less self-attestation from function-call keys.
Changes:
- Introduces a grant-based prepayment model (
prepay_attestation_storage) and outlines the intended charging, reclaim, and UX flows. - Documents API surface, state additions, sizing rationale, rollout steps, and testing plan.
- Records alternatives considered (meta-tx, full-access keys, credit accounting, caps, governance gating) and why they were rejected.
Comments suppressed due to low confidence (2)
docs/design/operator-prepaid-attestation-storage.md:76
- This table entry says any remainder of the attached deposit is kept. That conflicts with #3972’s acceptance criteria calling for any excess over the required flat deposit to be refunded (even if the refund happens in the prepay step rather than on
submit_participant_info). Either update the design/issue to match, or change the spec here to refund the remainder.
| `prepay_attestation_storage(account_id: AccountId)` | `#[payable]` | Grants `floor(attached_deposit / fee)` entries to `account_id`. Rejects below one fee. Any remainder is kept. Permissionless — anyone may prepay for any account. |
docs/design/operator-prepaid-attestation-storage.md:120
- The PR description says the design assumes “one node per NEAR account” (not enforced by the contract), but the design text here says it makes “no assumption about how many nodes share an account.” Please reconcile this by either explicitly stating the operational assumption (while noting the contract doesn’t enforce it) or updating the PR description.
Migration and multi-node operators need no special rule: an operator who wants a second live node prepays a second grant. That is the whole mechanism, and it is why this design needs no cap constant, no participant-status check, and no assumption about how many nodes share an account.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - **Fairness** — the fee bought storage. Once the contract has that storage back, charging again for the next entry would be charging twice for the same thing. | ||
| - **Operator experience** — an operator prepays for the capacity they actually run (three grants for three nodes, say) and is then done with it. Nodes can be torn down, re-provisioned, rotated and migrated indefinitely without another prepayment, as long as no more than three entries are held at once. Without recycling, every re-provisioned node is a fresh purchase and a fresh thing to remember. | ||
|
|
||
| **No NEAR is ever returned.** Recycling gives back capacity, never money: a deposit is consumed permanently the moment it is made, and the only thing that ever comes back is the right to store another entry. There is no withdrawal path and none is planned. |
There was a problem hiding this comment.
Consider tightening the wording to distinguish "no withdrawals / redeem" from "refund any overpayment in the prepay call", so the document is internally consistent and aligned with #3972.
Done — the two were conflated. The doc now states that "No NEAR is ever returned" covers both cases: no withdrawal method, and no refund of an overpayment. It also says why keeping the remainder deviates from the contract's usual require_deposit + refund_to pattern — a grant is a discrete unit, so the leftover is at most one fee short of the next grant, and a transfer path for sub-0.02 NEAR dust is not worth it.
On the #3972 alignment: the issue was the stale side, not the doc. Its first acceptance criterion still required refunding excess, which the design meeting overrode — that AC is now updated, along with three others that had drifted from the agreed model.
There was a problem hiding this comment.
Correction to my reply above: the design changed rather than the wording.
prepay_attestation_storage now takes an explicit grant count and requires an attached deposit of exactly fee × grants, rejecting anything else:
prepay_attestation_storage(account_id, grants) #[payable]
So there is no remainder to keep, and nothing to reconcile against the require_deposit + refund_to convention — the case your finding was about no longer exists. My earlier reply justified keeping the remainder; disregard that reasoning. "No NEAR is ever returned" now means only that there is no withdrawal method.
Pull request overviewDoc-only PR adding a design draft for #3972: shift the cost of a stored attestation entry from the contract's balance onto the operator, without touching the node's deposit-less function-call key. The mechanism is a per-account grant counter — the operator prepays with a deposit-capable key in a separate transaction, Changes:
Reviewed changesPer-file summary
Verified against the tree, and accurate: FindingsBlocking (must fix before merge):
Non-blocking (nits, follow-ups, suggestions):
No embedded instructions to the reviewer, secrets, or injection attempts in the diff or PR body. |
Resolves the open questions and the findings from the bot review on #4011. Legacy entries: keep working with no grant (rule 1) and yield a grant when swept (rule 3), both without extra code. The second is a deliberate grandfather -- 14 entries on mainnet, 31 on testnet -- but the exposure is precisely "entries present at deploy time become permanent capacity", so Rollout now says to check the count at deploy instead of assuming it. 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. Re-provisioning: a grant returns only once its entry expires, which takes 7 days, so the earlier claim that an operator could prepay once and forget it was wrong. Softened throughout: prepay for the nodes you run plus a spare, typically two. An owner-callable release method would remove the need for the spare and is recorded as an alternative, declined for now. Fee is 0.02 NEAR (~2.7x the ~0.0073 floor). Multiple grants per call are supported. The views are contract-only; operators read them with the NEAR CLI, so nothing is added to the DTO/ABI surface. Also: state that all fee figures are charged bytes rather than borsh sizes, which was mixing 604 and 450; note the early check rejects a TLS key owned by another account so it does not burn verification gas; and record the price risk on outstanding grants as an accepted bounded residual.
- Rule 3 costs gas in a gas-bounded sweep. Every removed entry adds a grants-map write, and that write is a row insert rather than an update whenever the owner's row was deleted at zero. clean_invalid_attestations_tera_gas and the RESHARE_CLEAN_INVALID_ATTESTATIONS_MAX_SCAN default were sized for removals alone and need re-validating, with a budget guard of the kind #3936 adds. This was missed entirely in the earlier drafts. - Rule 1 turns on ownership, not key presence. Say so explicitly: an early check that only tests whether the TLS key exists would classify a submission for someone else's key as "existing entry, no grant needed", pass it, verify it, and only then fail on TlsKeyOwnedByOtherAccount. - WORST_CASE_ENTRY_BYTES lives in a #[cfg(test)] module and the fee no longer derives from it, so stop referencing it as though it were a production constant; name the test that pins 604 instead. - available_attestation_grants returns 0 both for "never prepaid" and for "prepaid, now backing an entry", since the row is deleted at zero. Explain how to disambiguate with get_tee_accounts.
netrome
left a comment
There was a problem hiding this comment.
The doc feels a bit verbose, and I think it could be improved if we slimmed it down and put more emphasis on the core model earlier on in the doc, combined with some sequence diagrams.
But since this is transient, and we have alignment on the core idea I don't think it's critical to polish this doc.
Marten approved but asked for it to be slimmed; Reynaldo has not been able to read it because of the length, which makes trimming the blocker on his review rather than a nicety. 259 lines -> 160. No decisions changed. The cuts are duplication and background: - Goals and Non-goals removed; the opening paragraph and Alternatives already carried both. - Background compressed: the nearcore snippet and the FunctionCallPermission / meta-transaction detail become one line each, keeping only why payment and submission cannot be done by the same party. - "No NEAR is ever refunded" appeared three times, the 7-day spare-grant point three times, and the legacy grandfather in three sections. Each now has one home, cross-referenced. - Decisions and Alternatives tables keep every row but lose the prose; each is now a clause rather than a paragraph. - Operator UX drops the seven-row status table, which restated the guide's own contents, and says instead that only one step is added and nothing else changes. - Rollout renamed to Implementation notes, since that is what it is now that the design is settled.
…ependency Two facts from the full bot review that the earlier summary did not surface: - #3785 is merged (2026-07-29), so "land it first or alongside" described a dependency that no longer exists. Replaced with what actually remains: mock entries are sweepable now, but 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 or granted. - The legacy grandfather does not work the way we assumed when deciding to accept it. A live node re-attests under rule 1, so its entry never fails re-verification, is never swept, and yields no grant -- operators currently running nodes get nothing and need nothing. Only *abandoned* entries convert to grants. The number is still negligible today (14 mainnet, 31 testnet, nearly all live) and the deploy-time count check still bounds it, but the rationale is "abandoned entries are rare", not "it rewards our existing operators". Also: say why prepay keeps the remainder instead of following the contract's require_deposit + refund_to convention, and note that making the fee votable needs the ConfigExt DTO plumbing and a borsh-schema snapshot, not just the migration.
|
Thanks — went through all ten. Nine addressed, one accepted as a bounded residual. Also slimmed the doc from 259 to 162 lines, per @netrome and @gilcu3 in Slack.
Accepted deliberately rather than fixed, but your analysis corrected our reasoning for accepting it, so the doc now says something different from what we decided in the meeting. We had reasoned "it's a small bounded gift to the operators already running nodes". That was wrong, and your note is why: a live node re-attests under rule 1, so its entry never fails re-verification, is never swept, and yields no grant. Live operators get nothing — and need nothing. Only abandoned entries convert, which is the opposite of the population we thought we were rewarding. The bound still holds: 14 entries on mainnet and 31 on testnet, nearly all live, so the abandoned set is ~0 today. What changed is that the rationale is now "abandoned entries are rare", not "this rewards our operators" — and because that is only true at deploy time, the doc makes it a release-checklist item: check the stored-entry count before shipping, and purge rather than grant if it is in the thousands, which would mean the still-open drain was exploited first. Adding a grant-backed marker would cost ~110 bytes per entry on the fee and a second map, which we judged not worth it at this scale.
Agreed and now stated rather than implied. The security section records it as an accepted residual: exposure is bounded by outstanding grants, the contract stores counts rather than purchase prices so it cannot even identify which grants are underwater, governance can re-price only future grants, and a future update can address it if the gap becomes material. We took the operator-certainty side of that trade knowingly.
Correct, and the claim is dropped. A fresh CVM means a new TLS key, so the new submission is rule 2 while the old entry stays valid for up to
Fixed. All figures are now explicitly charged bytes, and the mock-is-larger point is restated as 604 vs 599 with the borsh pair removed from the sizing guidance entirely.
Fixed — this one the earlier summary I was working from had omitted, so thanks for the specificity. Step 1 now records that mock entries are already sweepable and that reclaimability is still not universal: Non-blocking
Fixed. The constant is no longer cited; the doc names the test that pins 604 and notes the fee is a
Fixed: the doc now says
Good catch, this was missed entirely. Now an implementation note: re-validate
Fixed. Rule 1 now says explicitly that it keys on ownership rather than presence, and why: a presence-only precondition would classify someone else's entry as "existing, no grant needed" and still reach
Fixed — one sentence on why: a grant is a discrete unit, so the leftover is at most one fee short of the next grant, and a transfer path for sub-0.02 NEAR dust is not worth it.
Fixed, both named. Also updated #3972's acceptance criteria, which had drifted from the agreed model — it still required refunding excess and still claimed an operator could pay the first submission on the node's behalf, which the |
- 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.
prepay_attestation_storage(account_id, grants) now requires exactly fee x grants and rejects anything else. That is clearer about intent than floor(attached / fee) and it removes the remainder question altogether -- there is nothing left over to keep or refund, so the justification for keeping it goes too, along with the Copilot finding it was answering. The operator reads attestation_storage_fee() first and attaches the exact multiple; one call now covers every node they run. Also, per review: "a count (not an amount)"; drop the line about views being contract-only; drop the note that 604 is pinned by a test; drop the stale-guide note, which belongs in the implementation PR; and shorten the pre-upgrade-grant residual to the decision and the deploy-time check.
|
|
||
| ### Fee | ||
|
|
||
| 0.02 NEAR, about 2.7× the floor. Figures are **charged** bytes — key and record overhead included — not borsh sizes: |
There was a problem hiding this comment.
Implementation note: I think this should be configurable on the contract and not hard-coded.
There was a problem hiding this comment.
Agreed, and that's the design already — it's a governance-votable Config field, not a constant. The doc said so eight lines below your comment (and again in the Decisions table), which is too far from the number to count.
Moved it onto the number itself: "0.02 NEAR — a governance-votable Config field, not a constant — about 2.7× the floor."
|
Addressed all open review comments in db9badf — replies are on each thread. Two are design changes, not wording: Prepay moves to node-account creation (@gilcu3, flow + operator UX). We were both anchoring it too late. Existing entries are grandfathered by the migration (@gilcu3 on the residual). One grant credited per account already holding an entry, so rule 3 returns grants that were genuinely issued. Removes an accepted residual and the rule-3 asymmetry, without the per-entry marker I'd rejected on cost. Same 14/31 free slots, issued explicitly up front; the deploy-time count check stays. Rest: fee is now labelled a votable |
|
PR title type suggestion: This PR includes changes to source code files ( Suggested title: |
…on crediting db9badf had the state migration credit one grant per existing entry so that rule 3 only ever returns grants that were genuinely issued. Reverting that: it is more code, not less -- an iteration plus a per-account write inside From<MpcContract> -- and it buys only the tidiness of the invariant reading uniformly, which is a documentation nicety rather than a functional gain. The intended behaviour is that nothing happens for existing entries. An entry that predates the fee already holds a slot and no grant was ever bought for it, so it is in effect a grant already spent. Re-attestation is free under rule 1, so those operators need no grant and no action, and the contract needs no migration step, no per-entry marker and no second map. Moved out of Security into its own "Existing nodes" section and written plainly, so it reads as a decision taken on purpose rather than a residual risk discovered later. The deploy-time count check stays. Security keeps the one real residual, the price risk on sold grants.
Four paragraphs to three, same three facts: nothing is owed to already-attested nodes, nothing in the contract handles them specially, and the one consequence is accepted on purpose.
gilcu3
left a comment
There was a problem hiding this comment.
Thanks for the updates
Replied to some comments. We still don't fully agree on some of them, but this is approved either way as none of them are blockers to me
| /// TODO(#3972): the flat onboarding deposit will be derived from these sizes too. | ||
| /// TODO(#4015): the prepaid-storage fee is sized from these numbers. | ||
| #[rstest] | ||
| #[case::dstack(599, worst_case_dstack_attestation())] | ||
| #[case::mock(604, worst_case_mock_attestation())] | ||
| fn stored_attestation_entry__should_have_the_pinned_size( | ||
| #[case] expected_bytes: u64, | ||
| #[case] verified_attestation: VerifiedAttestation, | ||
| ) { | ||
| // Given / When | ||
| let bytes_stored = measure_stored_entry_bytes(verified_attestation); |
There was a problem hiding this comment.
Thanks for the reference, but this does not measure real deposit costs, as it only inserts one attestation into an empty state
There was a problem hiding this comment.
see here some measurement results #4011 (comment)
| Two accepted residuals: | ||
|
|
||
| - **Pre-upgrade entries yield a grant when swept**, since rule 3 cannot distinguish them from paid ones. Accepted: there are 14 such entries on mainnet and 31 on testnet, so the free capacity is negligible (and init-time mock sentinels never expire, so they are never swept and never yield one). Check the count at deploy though — a count in the thousands would mean the still-open drain was exploited first, and those entries should be purged rather than granted. |
There was a problem hiding this comment.
but with_mocked_participant_attestations still inserts non-expiring MockAttestation::Valid sentinels at init (crates/contract/src/tee/tee_state.rs:136-142), so those are never swept. Grandfathering handles them cleanly: the sentinel's account gets its grant, and a real submission for the same TLS key is rule 1, which consumes nothing.
oh, if that is true, why didn't we close that gap in #3785 ?
| | Component | Charged bytes | Cost | | ||
| |---|---|---| | ||
| | Worst-case entry: a `Mock` one at 604 (`Dstack` is 599) | 604 | 0.00604 NEAR | | ||
| | Grants-map row | ~130 | ~0.0013 NEAR | | ||
| | **Floor** | **~734** | **~0.0073 NEAR** | |
There was a problem hiding this comment.
This benchmark is still not so accurate for what we need. The underlying data structure is a hash map, so its total size "per entry" is not as easy to compute. To really figure this out we would need to insert many attestations and then get the average or maximum of the cost per attestation. Therefore, we still don't have proper numbers, but we could do it as part of the PR following this design. No need to mention an amount here, except as a rough estimate
| |---|---|---| | ||
| | `prepay_attestation_storage(account_id, grants)` | `#[payable]` | Adds `grants` to `account_id`. Requires an attached deposit of exactly `fee × grants` and rejects anything else, so there is no remainder to keep or refund. Permissionless — anyone may prepay for any account. | | ||
| | `available_attestation_grants(account_id) -> u32` | view | Grants available. | | ||
| | `attestation_storage_fee() -> NearToken` | view | Current fee. | |
There was a problem hiding this comment.
The fee is votable, so it changes without a release
Right, we are making it votable :) Probably we didn't need that though, I don't see operators ever changing that, but that's fine. But again, if the operator can already get the value from config() then we don't need this convenience. Extracting the value from the config blob is trivial, and we do not expect any operator to do this automatically anyway, as this is meant to be a manual step after all, specially for a value that would almost never change.
c46da65 to
93c9ab5
Compare
|
PR title type suggestion: This PR changes only configuration files, font assets, and build artifacts—not documentation. The type prefix should probably be Suggested title: |
93c9ab5 to
c46da65
Compare
|
PR title type suggestion: This PR modifies both source code ( Suggested title: |
Ran that experiment — 200 sequential inserts into Per-entry cost is constant in map size. The 2-byte drift is my probe's own fault: I named the accounts The reason is that That means Two caveats I do want to keep, and the doc says both:
|
@gilcu3: config() already returns the fee, and reading it is a manual step on a value that almost never changes, so a dedicated view may not earn a permanent place in the API. Marked optional and left out of the implementation until we decide -- adding it later is cheaper than removing it. The operator UX now reads attestation_storage_fee_millinear from config() instead.
@gilcu3 on #4011: config() already returns attestation_storage_fee_millinear, and an operator reads it once by hand on a value that almost never changes, so a dedicated view does not earn a permanent place in the API or the ABI. Both test harnesses now read the fee the way an operator does -- the in-process one through config(), the sandbox one through the config view -- so nothing depends on a contract method existing purely for convenience. The internal helper stays, since prepay_attestation_storage needs the value to check the deposit. Left a TODO(#4015) noting the view can be added later if operators ask; adding is cheaper than removing.
|
PR title type suggestion: This PR adds new contract implementation code in addition to design documentation. The type prefix should be Suggested title: |
|
PR title type suggestion: This PR changes source code ( Suggested format: |
|
PR title type suggestion: This PR adds a new source code file (8K+ lines in Suggested title: |
|
|
||
| ### Charging rules | ||
|
|
||
| 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. |
There was a problem hiding this comment.
Nit: Don't see the point in distinguishing the different verifications we have here
| 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. | |
| Evaluated read-only at the top of `submit_participant_info` — before any verification, so an ungranted or unauthorised call never reaches attestation verification checks. |
|
|
||
| Accepted deliberately: if such an entry is later swept, rule 3 hands its owner a grant they never bought. Negligible at 14 entries on mainnet and 31 on testnet, but check the count before deploying — thousands would mean the drain was exploited first, and those entries should be purged rather than left to become grants. | ||
|
|
||
| ## Operator UX |
There was a problem hiding this comment.
This feels a bit strange but sure, it's worth keeping as a reminder to update the operator guide - although we should always remember to do this for operator-facing changes.
Closes #3972.
Design doc only — no code. Implementation is tracked in #4015.
validate_delegate_action_keyapplies the same check to inner actions — andreport_databinds the quote toenv::signer_account_pk(), so an operator cannot submit on the node's behalf.Mockchecks or averify_quoteround trip, and consumed at insert, so a failed attestation consumes nothing. That also keeps refactor(contract): drop fail_attestation_submission now that the deposit is gone #3991 unblocked: no charge has to survive a failing callback.Attestation::Mock, per-account entry caps, the governance gate, refundable bonds, and an owner-callable release method.Fee is 0.02 NEAR, ~2.7× the ~0.0073 floor (604-byte worst-case entry plus its ~130-byte grants-map row).
Implementation notes captured for whoever picks this up: it needs a state migration for both the grants map and the
Configfee field, and rule 3's per-entry write meansclean_invalid_attestations's gas budget has to be re-validated.Separately noted in the doc:
docs/running-an-mpc-node-in-tdx-external-guide.mdstill tells operators the call "will incur a cost (TBD, XXX NEAR)" and points at the now-closed #903. Stale onmaintoday, independent of this design.