Skip to content

Commit 3efa6e4

Browse files
committed
fix(contract): fund attestation storage from the contract balance
submit_participant_info takes no deposit; the contract's own balance stakes the bounded attestation entry. A node self-submits with its function-call access key for its first attestation and re-attestations alike, fixing onboarding: #3714 required a deposit, which a function-call key cannot attach. This makes intentional what pre-#3714 did by accident. Pre-#3714 measured the storage delta right after inserting into the IterableMap, before the deferred write was flushed, so the delta read as 0 and the caller was never charged. #3714 added the flush (kept here), which exposed the real cost and started rejecting the node's zero-deposit submit. Storage is bounded and reclaimed by clean_invalid_attestations, so the contract-funded cost is bounded and self-healing. Removes the charge path, the payable modifiers, deposit forwarding, and the e2e funding workaround; updates tests and the ABI snapshot.
1 parent c8d62f9 commit 3efa6e4

11 files changed

Lines changed: 51 additions & 365 deletions

File tree

crates/contract/src/lib.rs

Lines changed: 13 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -156,26 +156,6 @@ fn refund_to(account_id: &AccountId, amount: NearToken) {
156156
}
157157
}
158158

159-
/// Charges `account_id` the storage growth since `initial_storage` and refunds the excess. A
160-
/// participant re-attesting an unchanged entry grows nothing and pays nothing, so the node's
161-
/// function-call access key can re-attest; a new entry costs the measured delta, funded by the
162-
/// caller (an operator's full-access key for a first submission).
163-
fn charge_storage(account_id: &AccountId, initial_storage: u64) -> Result<(), Error> {
164-
// saturating_sub: a shrink charges nothing rather than underflowing.
165-
let bytes_grown = env::storage_usage().saturating_sub(initial_storage);
166-
let cost = env::storage_byte_cost().saturating_mul(u128::from(bytes_grown));
167-
let attached = env::attached_deposit();
168-
if attached < cost {
169-
return Err(InvalidParameters::InsufficientDeposit {
170-
attached: attached.as_yoctonear(),
171-
required: cost.as_yoctonear(),
172-
}
173-
.into());
174-
}
175-
refund_to(account_id, attached.saturating_sub(cost));
176-
Ok(())
177-
}
178-
179159
impl Default for MpcContract {
180160
fn default() -> Self {
181161
env::panic_str("Calling default not allowed.");
@@ -798,13 +778,8 @@ impl MpcContract {
798778
/// `verify_quote` call, with [`Self::resolve_verification`] chained as its
799779
/// callback to run the post-DCAP checks and store the attestation.
800780
///
801-
/// Storage is charged to the caller only when a new entry is stored or the caller is not a
802-
/// current participant. A participant re-attesting an existing entry grows nothing and is charged
803-
/// nothing, so the node's function-call access key can re-attest with no deposit. A new entry
804-
/// costs the measured storage delta, which the caller must cover (an operator's full-access key
805-
/// funds a first submission); the excess is refunded, as is the full deposit on a rejected
806-
/// attestation.
807-
#[payable]
781+
/// Storage is funded by the contract's own balance, so a node submits with no deposit via its
782+
/// function-call access key, for its first attestation and re-attestations alike.
808783
#[handle_result]
809784
pub fn submit_participant_info(
810785
&mut self,
@@ -840,30 +815,19 @@ impl MpcContract {
840815
account_public_key,
841816
};
842817

843-
// Non-participants pay per entry so an outsider cannot drain the contract; participants
844-
// re-attest for free.
845-
let caller_is_not_participant = !self
846-
.protocol_state
847-
.is_existing_or_prospective_participant(&account_id)
848-
.unwrap_or(false);
849-
850818
match proposed_participant_attestation {
851819
Attestation::Mock(mock) => {
852820
let tee_upgrade_deadline_duration =
853821
Duration::from_secs(self.config.tee_upgrade_deadline_duration_seconds);
854-
let initial_storage = env::storage_usage();
855-
let insertion = self.tee_state.verify_and_store_mock(
822+
self.tee_state.verify_and_store_mock(
856823
node_id,
857824
mock,
858825
tee_upgrade_deadline_duration,
859826
)?;
860-
if insertion.is_new() || caller_is_not_participant {
861-
charge_storage(&account_id, initial_storage)?;
862-
}
863827
Ok(PromiseOrValue::Value(()))
864828
}
865829
Attestation::Dstack(attestation) => Ok(PromiseOrValue::Promise(
866-
self.submit_dstack_attestation(node_id, attestation, caller_is_not_participant)?,
830+
self.submit_dstack_attestation(node_id, attestation)?,
867831
)),
868832
}
869833
}
@@ -875,7 +839,6 @@ impl MpcContract {
875839
&mut self,
876840
node_id: NodeId,
877841
attestation: DstackAttestation,
878-
caller_is_not_participant: bool,
879842
) -> Result<Promise, Error> {
880843
let Some(verifier_account_id) = self.tee_verifier_account_id.clone() else {
881844
return Err(TeeError::VerifierNotConfigured.into());
@@ -892,11 +855,9 @@ impl MpcContract {
892855
.then(
893856
Self::ext(env::current_account_id())
894857
.with_static_gas(Gas::from_tgas(self.config.resolve_verification_tera_gas))
895-
.with_attached_deposit(env::attached_deposit())
896858
.resolve_verification(VerificationContext {
897859
node_id,
898860
attestation,
899-
caller_is_not_participant,
900861
}),
901862
))
902863
}
@@ -2333,12 +2294,10 @@ impl MpcContract {
23332294
}
23342295
}
23352296

2336-
/// Verify-quote callback: on a verifier verdict it runs the post-DCAP
2337-
/// checks and stores the attestation, keeping the storage delta and
2338-
/// refunding the excess. Refunds the full deposit if the attestation is not
2339-
/// accepted.
2297+
/// Verify-quote callback: on a verifier verdict it runs the post-DCAP checks and stores the
2298+
/// attestation (storage funded by the contract's balance). On any failure it fails the
2299+
/// submitter's transaction.
23402300
#[private]
2341-
#[payable]
23422301
pub fn resolve_verification(
23432302
&mut self,
23442303
#[serializer(borsh)] context: VerificationContext,
@@ -2370,9 +2329,8 @@ impl MpcContract {
23702329
match attestation_result {
23712330
Ok(()) => PromiseOrValue::Value(()),
23722331
Err(err) => {
2373-
refund_to(&account_id, env::attached_deposit());
2374-
// Fail the submitter's transaction from a separate receipt so
2375-
// the refund above commits (a panic here would roll it back)
2332+
// Fail the submitter's transaction from a separate receipt so any prior state
2333+
// commits (a panic here would roll it back)
23762334
let promise = Promise::new(env::current_account_id()).function_call(
23772335
method_names::FAIL_ATTESTATION_SUBMISSION.to_string(),
23782336
borsh::to_vec(&err.to_string())
@@ -2386,8 +2344,7 @@ impl MpcContract {
23862344
}
23872345

23882346
/// Runs the post-DCAP checks and stores the attestation for a
2389-
/// [`VerificationResult::Verified`] response, then charges the storage delta unless a
2390-
/// participant re-attested an existing entry.
2347+
/// [`VerificationResult::Verified`] response. Storage is funded by the contract's balance.
23912348
fn verify_post_dcap_and_store(
23922349
&mut self,
23932350
context: &VerificationContext,
@@ -2397,22 +2354,14 @@ impl MpcContract {
23972354
let tee_upgrade_deadline_duration =
23982355
Duration::from_secs(self.config.tee_upgrade_deadline_duration_seconds);
23992356

2400-
let initial_storage = env::storage_usage();
2401-
let insertion = match self.tee_state.verify_and_store_dstack(
2357+
if let Err(err) = self.tee_state.verify_and_store_dstack(
24022358
context.node_id.clone(),
24032359
&context.attestation,
24042360
report,
24052361
tee_upgrade_deadline_duration,
24062362
) {
2407-
Ok(insertion) => insertion,
2408-
Err(err) => {
2409-
log!("post-DCAP check failed for {account_id}: {err}");
2410-
return Err(err.into());
2411-
}
2412-
};
2413-
2414-
if insertion.is_new() || context.caller_is_not_participant {
2415-
charge_storage(account_id, initial_storage)?;
2363+
log!("post-DCAP check failed for {account_id}: {err}");
2364+
return Err(err.into());
24162365
}
24172366
Ok(())
24182367
}
@@ -4687,9 +4636,6 @@ mod tests {
46874636
VerificationContext {
46884637
node_id,
46894638
attestation,
4690-
// Fixed, not parametrized: this flag only affects charging, which the mock VM
4691-
// can't observe, so true and false would store identical state here.
4692-
caller_is_not_participant: true,
46934639
},
46944640
)
46954641
}

crates/contract/src/tee/tee_state.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,6 @@ pub(crate) enum ParticipantInsertion {
5353
UpdatedExistingParticipant,
5454
}
5555

56-
impl ParticipantInsertion {
57-
pub(crate) fn is_new(&self) -> bool {
58-
matches!(self, Self::NewlyInsertedParticipant)
59-
}
60-
}
61-
6256
#[derive(Debug)]
6357
pub enum TeeValidationResult {
6458
/// All participants are valid

crates/contract/src/tee/verification_context.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,4 @@ use super::tee_state::NodeId;
1111
pub struct VerificationContext {
1212
pub(crate) node_id: NodeId,
1313
pub(crate) attestation: DstackAttestation,
14-
pub(crate) caller_is_not_participant: bool,
1514
}

crates/contract/tests/inprocess/attestation_submission.rs

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use super::common;
44
use mpc_contract::{
55
MpcContract,
6-
errors::{Error, InvalidParameters, InvalidState, TeeError},
6+
errors::{Error, InvalidState, TeeError},
77
primitives::{
88
key_state::EpochId,
99
participants::{ParticipantId, ParticipantInfo},
@@ -15,25 +15,21 @@ use mpc_contract::{
1515
},
1616
tee::tee_state::{AttestationSubmissionError, NodeId},
1717
};
18-
use near_mpc_contract_interface::{
19-
deposits::SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR,
20-
types::{Attestation, InitConfig, MockAttestation, ProtocolContractState},
18+
use near_mpc_contract_interface::types::{
19+
Attestation, InitConfig, MockAttestation, ProtocolContractState,
2120
};
2221
use std::collections::BTreeMap;
2322

2423
use assert_matches::assert_matches;
2524
use near_account_id::AccountId;
26-
use near_sdk::{NearToken, test_utils::VMContextBuilder, testing_env};
25+
use near_sdk::{test_utils::VMContextBuilder, testing_env};
2726
use rstest::rstest;
2827
use std::time::Duration;
2928
use test_utils::attestation::mock_dto_dstack_attestation;
3029

3130
const SECOND: Duration = Duration::from_secs(1);
3231
const NANOS_IN_SECOND: u64 = SECOND.as_nanos() as u64;
3332

34-
const ATTESTATION_STORAGE_DEPOSIT: NearToken =
35-
NearToken::from_millinear(SUBMIT_PARTICIPANT_INFO_DEPOSIT_MILLINEAR);
36-
3733
const DEFAULT_PARTICIPANT_COUNT: usize = 3;
3834
const DEFAULT_THRESHOLD_SIZE: u64 = 2;
3935
const DEFAULT_CONTRACT_PROTOCOL_STATE: ContractProtocolState = ContractProtocolState::Running;
@@ -163,16 +159,7 @@ impl TestSetup {
163159
node_id: &NodeId,
164160
attestation: Attestation,
165161
) -> Result<(), mpc_contract::errors::Error> {
166-
// `submit_participant_info` requires the flat storage fee, unlike the
167-
// deposit-free calls `common::participant_context` is built for.
168-
testing_env!(
169-
VMContextBuilder::new()
170-
.signer_account_id(node_id.account_id.clone())
171-
.predecessor_account_id(node_id.account_id.clone())
172-
.block_timestamp(near_sdk::env::block_timestamp())
173-
.attached_deposit(ATTESTATION_STORAGE_DEPOSIT)
174-
.build()
175-
);
162+
testing_env!(common::participant_context(&node_id.account_id));
176163
self.contract
177164
.submit_participant_info(attestation, node_id.tls_public_key.clone())
178165
.map(|_| ())
@@ -278,21 +265,14 @@ fn submit_participant_info__should_reject_overwrite_from_other_account() {
278265
assert_eq!(stored_before, stored_after);
279266
}
280267

281-
/// A non-participant storing a new entry must cover its storage cost, so an outsider cannot
282-
/// store an attestation without paying for it.
268+
/// A newcomer stores its first attestation with no deposit (contract-funded storage), so the
269+
/// node's function-call access key can self-onboard.
283270
#[test]
284-
fn submit_participant_info__should_reject_when_non_participant_deposit_is_below_storage_cost() {
271+
fn submit_participant_info__should_store_new_entry_with_zero_deposit() {
285272
// Given
286273
let mut setup = TestSetupBuilder::new().build();
287274
let newcomer = node_id_for(&"newcomer.near".parse().unwrap());
288-
let attached_deposit = NearToken::from_yoctonear(1);
289-
testing_env!(
290-
VMContextBuilder::new()
291-
.signer_account_id(newcomer.account_id.clone())
292-
.predecessor_account_id(newcomer.account_id.clone())
293-
.attached_deposit(attached_deposit)
294-
.build()
295-
);
275+
testing_env!(common::participant_context(&newcomer.account_id));
296276

297277
// When
298278
let result = setup
@@ -304,17 +284,20 @@ fn submit_participant_info__should_reject_when_non_participant_deposit_is_below_
304284
.map(|_| ());
305285

306286
// Then
307-
assert_matches!(
308-
&result,
309-
Err(Error::InvalidParameters(InvalidParameters::InsufficientDeposit { attached, required }))
310-
if *attached == attached_deposit.as_yoctonear() && required > attached
287+
assert_matches!(&result, Ok(()));
288+
assert!(
289+
setup
290+
.contract
291+
.get_attestation(newcomer.tls_public_key)
292+
.unwrap()
293+
.is_some()
311294
);
312295
}
313296

314-
/// A current participant re-attesting an existing entry is charged nothing and need not attach
315-
/// any deposit, so the node's function-call access key can re-attest.
297+
/// A current participant re-attesting an existing entry succeeds with no attached deposit, so the
298+
/// node's function-call access key can re-attest.
316299
#[test]
317-
fn submit_participant_info__should_charge_nothing_when_participant_reattests_with_zero_deposit() {
300+
fn submit_participant_info__should_reattest_with_zero_deposit() {
318301
// Given
319302
let mut setup = TestSetupBuilder::new().build();
320303
let node = setup.get_participant_node_ids()[0].clone();

0 commit comments

Comments
 (0)