@@ -161,19 +161,29 @@ fn refund_to(account_id: &AccountId, amount: NearToken) {
161161 }
162162}
163163
164- /// Charges the storage growth since `initial_storage` and refunds the rest. The
165- /// [`MINIMUM_ATTESTATION_STORAGE_DEPOSIT`] floor guarantees the refund never underflows.
166- fn keep_storage_delta_and_refund_rest ( account_id : & AccountId , initial_storage : u64 ) {
164+ /// Requires at least [`MINIMUM_ATTESTATION_STORAGE_DEPOSIT`], then keeps the storage delta since
165+ /// `initial_storage` and refunds the excess.
166+ fn charge_storage ( account_id : & AccountId , initial_storage : u64 ) -> Result < ( ) , Error > {
167+ let attached = env:: attached_deposit ( ) ;
168+ if attached < MINIMUM_ATTESTATION_STORAGE_DEPOSIT {
169+ return Err ( InvalidParameters :: InsufficientDeposit {
170+ attached : attached. as_yoctonear ( ) ,
171+ required : MINIMUM_ATTESTATION_STORAGE_DEPOSIT . as_yoctonear ( ) ,
172+ }
173+ . into ( ) ) ;
174+ }
175+
167176 // saturating_sub: a shrink charges nothing rather than underflowing.
168177 let bytes_grown = env:: storage_usage ( ) . saturating_sub ( initial_storage) ;
169178 let cost = env:: storage_byte_cost ( ) . saturating_mul ( u128:: from ( bytes_grown) ) ;
170- match env :: attached_deposit ( ) . checked_sub ( cost) {
179+ match attached . checked_sub ( cost) {
171180 Some ( refund) => refund_to ( account_id, refund) ,
172181 // Unreachable given the MINIMUM_ATTESTATION_STORAGE_DEPOSIT floor, which
173182 // minimum_attestation_storage_deposit__should_cover_worst_case_entry pins to the
174183 // worst-case entry cost.
175184 None => log ! ( "attestation storage cost {cost} exceeded deposit for {account_id}" ) ,
176185 }
186+ Ok ( ( ) )
177187}
178188
179189impl Default for MpcContract {
@@ -798,11 +808,12 @@ impl MpcContract {
798808 /// `verify_quote` call, with [`Self::resolve_verification`] chained as its
799809 /// callback to run the post-DCAP checks and store the attestation.
800810 ///
801- /// The caller must attach at least [`MINIMUM_ATTESTATION_STORAGE_DEPOSIT`],
802- /// enough to cover the worst-case stored entry. On success only the actual
803- /// storage delta is kept and the excess is refunded, so a re-submission that
804- /// changes no stored bytes is charged nothing. The full deposit is refunded if
805- /// the attestation is not accepted.
811+ /// Storage is charged to the caller only when a new entry is stored or the caller is not a
812+ /// current participant. A participant re-attesting an existing entry is charged nothing and
813+ /// need not attach any deposit, so the node's function-call access key can re-attest. When a
814+ /// charge applies, the caller must attach at least [`MINIMUM_ATTESTATION_STORAGE_DEPOSIT`]
815+ /// (enough to cover the worst-case stored entry); only the actual storage delta is kept and
816+ /// the excess is refunded. The full deposit is refunded if the attestation is not accepted.
806817 #[ payable]
807818 #[ handle_result]
808819 pub fn submit_participant_info (
@@ -839,30 +850,30 @@ impl MpcContract {
839850 account_public_key,
840851 } ;
841852
842- let attached = env:: attached_deposit ( ) ;
843- if attached < MINIMUM_ATTESTATION_STORAGE_DEPOSIT {
844- return Err ( InvalidParameters :: InsufficientDeposit {
845- attached : attached. as_yoctonear ( ) ,
846- required : MINIMUM_ATTESTATION_STORAGE_DEPOSIT . as_yoctonear ( ) ,
847- }
848- . into ( ) ) ;
849- }
853+ // Non-participants pay per entry so an outsider cannot drain the contract; participants
854+ // re-attest for free.
855+ let caller_is_not_participant = !self
856+ . protocol_state
857+ . is_existing_or_prospective_participant ( & account_id)
858+ . unwrap_or ( false ) ;
850859
851860 match proposed_participant_attestation {
852861 Attestation :: Mock ( mock) => {
853862 let tee_upgrade_deadline_duration =
854863 Duration :: from_secs ( self . config . tee_upgrade_deadline_duration_seconds ) ;
855864 let initial_storage = env:: storage_usage ( ) ;
856- self . tee_state . verify_and_store_mock (
865+ let insertion = self . tee_state . verify_and_store_mock (
857866 node_id,
858867 mock,
859868 tee_upgrade_deadline_duration,
860869 ) ?;
861- keep_storage_delta_and_refund_rest ( & account_id, initial_storage) ;
870+ if insertion. is_new ( ) || caller_is_not_participant {
871+ charge_storage ( & account_id, initial_storage) ?;
872+ }
862873 Ok ( PromiseOrValue :: Value ( ( ) ) )
863874 }
864875 Attestation :: Dstack ( attestation) => Ok ( PromiseOrValue :: Promise (
865- self . submit_dstack_attestation ( node_id, attestation) ?,
876+ self . submit_dstack_attestation ( node_id, attestation, caller_is_not_participant ) ?,
866877 ) ) ,
867878 }
868879 }
@@ -874,6 +885,7 @@ impl MpcContract {
874885 & mut self ,
875886 node_id : NodeId ,
876887 attestation : DstackAttestation ,
888+ caller_is_not_participant : bool ,
877889 ) -> Result < Promise , Error > {
878890 let Some ( verifier_account_id) = self . tee_verifier_account_id . clone ( ) else {
879891 return Err ( TeeError :: VerifierNotConfigured . into ( ) ) ;
@@ -894,6 +906,7 @@ impl MpcContract {
894906 . resolve_verification ( VerificationContext {
895907 node_id,
896908 attestation,
909+ caller_is_not_participant,
897910 } ) ,
898911 ) )
899912 }
@@ -2383,8 +2396,8 @@ impl MpcContract {
23832396 }
23842397
23852398 /// Runs the post-DCAP checks and stores the attestation for a
2386- /// [`VerificationResult::Verified`] response, then keeps the storage delta
2387- /// and refunds the excess deposit .
2399+ /// [`VerificationResult::Verified`] response, then charges the storage delta unless a
2400+ /// participant re-attested an existing entry .
23882401 fn verify_post_dcap_and_store (
23892402 & mut self ,
23902403 context : & VerificationContext ,
@@ -2395,17 +2408,22 @@ impl MpcContract {
23952408 Duration :: from_secs ( self . config . tee_upgrade_deadline_duration_seconds ) ;
23962409
23972410 let initial_storage = env:: storage_usage ( ) ;
2398- if let Err ( err ) = self . tee_state . verify_and_store_dstack (
2411+ let insertion = match self . tee_state . verify_and_store_dstack (
23992412 context. node_id . clone ( ) ,
24002413 & context. attestation ,
24012414 report,
24022415 tee_upgrade_deadline_duration,
24032416 ) {
2404- log ! ( "post-DCAP check failed for {account_id}: {err}" ) ;
2405- return Err ( err. into ( ) ) ;
2406- }
2417+ Ok ( insertion) => insertion,
2418+ Err ( err) => {
2419+ log ! ( "post-DCAP check failed for {account_id}: {err}" ) ;
2420+ return Err ( err. into ( ) ) ;
2421+ }
2422+ } ;
24072423
2408- keep_storage_delta_and_refund_rest ( account_id, initial_storage) ;
2424+ if insertion. is_new ( ) || context. caller_is_not_participant {
2425+ charge_storage ( account_id, initial_storage) ?;
2426+ }
24092427 Ok ( ( ) )
24102428 }
24112429
@@ -4679,6 +4697,9 @@ mod tests {
46794697 VerificationContext {
46804698 node_id,
46814699 attestation,
4700+ // Fixed, not parametrized: this flag only affects charging, which the mock VM
4701+ // can't observe, so true and false would store identical state here.
4702+ caller_is_not_participant : true ,
46824703 } ,
46834704 )
46844705 }
@@ -8116,19 +8137,21 @@ mod tests {
81168137 assert ! ( configs. contains_key( & tls_key_b) , "node B config must exist" ) ;
81178138 }
81188139
8140+ const MAX_HASH : [ u8 ; 32 ] = [ 0xff ; 32 ] ;
8141+
81198142 // Catches entry-size growth: fails if a schema change makes the largest storable entry
81208143 // cost more than the deposit at today's storage_byte_cost. It cannot see a future
81218144 // storage_byte_cost increase on a live contract; the deposit's margin covers that.
81228145 #[ rstest]
81238146 #[ case:: dstack( VerifiedAttestation :: Dstack ( ValidatedDstackAttestation {
8124- mpc_image_hash: [ 0xff ; 32 ] . into( ) ,
8125- launcher_compose_hash: [ 0xff ; 32 ] . into( ) ,
8147+ mpc_image_hash: MAX_HASH . into( ) ,
8148+ launcher_compose_hash: MAX_HASH . into( ) ,
81268149 expiry_timestamp_seconds: u64 :: MAX ,
81278150 measurements: default_measurements( ) [ 0 ] ,
81288151 } ) ) ]
81298152 #[ case:: mock( VerifiedAttestation :: Mock ( MpcMockAttestation :: WithConstraints {
8130- mpc_docker_image_hash: Some ( [ 0xff ; 32 ] . into( ) ) ,
8131- launcher_docker_compose_hash: Some ( [ 0xff ; 32 ] . into( ) ) ,
8153+ mpc_docker_image_hash: Some ( MAX_HASH . into( ) ) ,
8154+ launcher_docker_compose_hash: Some ( MAX_HASH . into( ) ) ,
81328155 expiry_timestamp_seconds: Some ( u64 :: MAX ) ,
81338156 expected_measurements: Some ( default_measurements( ) [ 0 ] ) ,
81348157 } ) ) ]
0 commit comments