Skip to content

Commit 16ce226

Browse files
committed
fix(contract): address review on async attestation flow
- refund the attached deposit when a participant refreshes an existing attestation (charge_attestation_storage early-return kept it silently) - add v3_13_0_state migration shadow so migrate() can upgrade from a deployed 3.13.0 layout, not just 3.12.0 - collapse the two per-arm debug_asserts in on_attestation_verified into one guarding both resolved paths, with a note on why cleanup stays in resolve_verification (its remove-before-store gates the timeout race) - nits: from_yoctonear(0) on the fail-call; drop a redundant yield comment; reframe the promise_yield_resume comment; PendingAttestation fields pub(crate); document the verify_quote wire args; benchmark TODOs on the new gas defaults - TODO(#3720): stash only tcb_info in PendingAttestation (follow-up)
1 parent 29dc047 commit 16ce226

3 files changed

Lines changed: 177 additions & 15 deletions

File tree

crates/contract/src/lib.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod update;
1717
pub mod utils;
1818

1919
pub mod v3_12_0_state;
20+
pub mod v3_13_0_state;
2021

2122
#[cfg(feature = "bench-contract-methods")]
2223
mod bench;
@@ -904,9 +905,6 @@ impl MpcContract {
904905
},
905906
);
906907

907-
// The yield is the method's return value: `enqueue_yield_request` called
908-
// `promise_return` as the final host call, so returning unit here adds no
909-
// `value_return` that would override it.
910908
Ok(())
911909
}
912910

@@ -920,8 +918,9 @@ impl MpcContract {
920918
) -> Result<(), Error> {
921919
let is_new_attestation =
922920
matches!(insertion, ParticipantInsertion::NewlyInsertedParticipant);
923-
// A participant refreshing an existing attestation is not charged.
921+
924922
if caller_is_participant && !is_new_attestation {
923+
refund_attestation_deposit(account_id, attached);
925924
return Ok(());
926925
}
927926

@@ -2161,6 +2160,14 @@ impl MpcContract {
21612160
pub fn migrate() -> Result<Self, Error> {
21622161
log!("migrating contract");
21632162

2163+
match try_state_read::<v3_13_0_state::MpcContract>() {
2164+
Ok(Some(state)) => return Ok(state.into()),
2165+
Ok(None) => return Err(InvalidState::ContractStateIsMissing.into()),
2166+
Err(err) => {
2167+
log!("failed to deserialize state into 3.13.0 state: {:?}", err);
2168+
}
2169+
};
2170+
21642171
match try_state_read::<v3_12_0_state::MpcContract>() {
21652172
Ok(Some(state)) => return Ok(state.into()),
21662173
Ok(None) => return Err(InvalidState::ContractStateIsMissing.into()),
@@ -2415,7 +2422,7 @@ impl MpcContract {
24152422
refund_attestation_deposit(&account_id, pending.attached_deposit);
24162423
}
24172424
// MUST be the last host call: anything after could panic and roll back
2418-
// the state mutations above.
2425+
// the state mutations above
24192426
env::promise_yield_resume(
24202427
&pending.data_id,
24212428
serde_json::to_vec(&attestation_result)
@@ -2482,8 +2489,17 @@ impl MpcContract {
24822489
#[callback_result] result: Result<AttestationResult, PromiseError>,
24832490
) -> PromiseOrValue<()> {
24842491
let reason = match result {
2485-
Ok(AttestationResult::Ok) => return PromiseOrValue::Value(()),
2486-
Ok(AttestationResult::Err(reason)) => reason,
2492+
Ok(resolved) => {
2493+
// resolve_verification already removed the entry and refunded on failure. It
2494+
// removes before storing, so a verifier reply that arrives after the
2495+
// ~200-block yield-resume timeout (handled by the Err arm below) bails
2496+
// instead of storing an attestation whose deposit was already refunded.
2497+
debug_assert!(!self.pending_attestations.contains_key(&account_id));
2498+
match resolved {
2499+
AttestationResult::Ok => return PromiseOrValue::Value(()),
2500+
AttestationResult::Err(reason) => reason,
2501+
}
2502+
}
24872503
Err(_promise_err) => {
24882504
// Timeout: the resolution callback never resumed us, so the
24892505
// pending entry is still here. Clean it up and refund.
@@ -2500,7 +2516,7 @@ impl MpcContract {
25002516
let promise = Promise::new(env::current_account_id()).function_call(
25012517
method_names::FAIL_ATTESTATION_SUBMISSION.to_string(),
25022518
borsh::to_vec(&reason).expect("borsh serialization of reason must succeed"),
2503-
NearToken::from_near(0),
2519+
NearToken::from_yoctonear(0),
25042520
Gas::from_tgas(self.config.fail_attestation_submission_tera_gas),
25052521
);
25062522
PromiseOrValue::Promise(promise.as_return())

crates/contract/src/tee/pending_attestation.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,22 @@ use near_sdk::{CryptoHash, NearToken, near};
1313
/// One in-flight verification per submitter account.
1414
#[derive(Debug, BorshSerialize, BorshDeserialize)]
1515
pub struct PendingAttestation {
16-
/// The submitted payload the post-DCAP checks consume once the verifier
17-
/// returns its report.
18-
pub dstack: DstackAttestation,
16+
/// The submitted payload. Only `tcb_info` is read after the verifier callback;
17+
/// `quote`/`collateral` are consumed once for the pre-callback `verify_quote`
18+
/// call, so storing the whole struct holds ~2-8 KiB of dead bytes per in-flight
19+
/// entry. TODO(#3720): stash only `tcb_info`.
20+
pub(crate) dstack: DstackAttestation,
1921
/// Checked against the quote's report-data during the post-DCAP checks.
20-
pub tls_public_key: Ed25519PublicKey,
22+
pub(crate) tls_public_key: Ed25519PublicKey,
2123
/// Stashed because the deposit is not visible from the callback receipt:
2224
/// consumed for storage on success, refunded on failure.
23-
pub attached_deposit: NearToken,
25+
pub(crate) attached_deposit: NearToken,
2426
/// Participant status at submit time, which decides whether the caller pays
2527
/// for storage. Captured because the callback receipt is no longer the
2628
/// caller, so it can no longer be re-derived.
27-
pub caller_is_participant: bool,
29+
pub(crate) caller_is_participant: bool,
2830
/// Yield handle, read back by the callback to resume the yield.
29-
pub data_id: CryptoHash,
31+
pub(crate) data_id: CryptoHash,
3032
}
3133

3234
#[near(serializers = [json])]
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//! ## Overview
2+
//! Shadows the contract state written by the `3.13.0` release so [`crate::migrate`]
3+
//! can upgrade from it. See [`crate::v3_12_0_state`] for the rationale and guideline.
4+
//!
5+
//! `3.13.0` differs from the live layout only by the two fields this version adds:
6+
//! `Config::fail_attestation_submission_tera_gas` (and the three verifier gas knobs,
7+
//! all defaulted here) and the `MpcContract::pending_attestations` map.
8+
9+
use borsh::{BorshDeserialize, BorshSerialize};
10+
use near_mpc_contract_interface::types::{Metrics, VerifyForeignTransactionRequest};
11+
use near_sdk::{
12+
AccountId, env,
13+
store::{Lazy, LookupMap},
14+
};
15+
16+
use crate::{
17+
SupportedForeignChainsByNode,
18+
foreign_chains_metadata::ForeignChainsMetadata,
19+
node_migrations::NodeMigrations,
20+
primitives::{
21+
ckd::CKDRequest,
22+
domain::max_reconstruction_threshold,
23+
signature::{SignatureRequest, YieldIndex},
24+
thresholds::ThresholdParameters,
25+
},
26+
state::{ProtocolContractState, running::RunningContractState},
27+
storage_keys::StorageKey,
28+
tee::{tee_state::TeeState, verifier_votes::TeeVerifierVotes},
29+
update::ProposedUpdates,
30+
};
31+
32+
/// The `Config` layout written by the `3.13.0` contract, before
33+
/// `fail_attestation_submission_tera_gas` and the verifier gas knobs were added.
34+
#[derive(Debug, BorshSerialize, BorshDeserialize)]
35+
pub struct OldConfig {
36+
key_event_timeout_blocks: u64,
37+
tee_upgrade_deadline_duration_seconds: u64,
38+
contract_upgrade_deposit_tera_gas: u64,
39+
sign_call_gas_attachment_requirement_tera_gas: u64,
40+
ckd_call_gas_attachment_requirement_tera_gas: u64,
41+
return_signature_and_clean_state_on_success_call_tera_gas: u64,
42+
return_ck_and_clean_state_on_success_call_tera_gas: u64,
43+
fail_on_timeout_tera_gas: u64,
44+
clean_tee_status_tera_gas: u64,
45+
clean_invalid_attestations_tera_gas: u64,
46+
cleanup_orphaned_node_migrations_tera_gas: u64,
47+
remove_non_participant_update_votes_tera_gas: u64,
48+
clean_foreign_chain_data_tera_gas: u64,
49+
remove_non_participant_tee_verifier_votes_tera_gas: u64,
50+
}
51+
52+
impl From<OldConfig> for crate::Config {
53+
fn from(old: OldConfig) -> Self {
54+
crate::Config {
55+
key_event_timeout_blocks: old.key_event_timeout_blocks,
56+
tee_upgrade_deadline_duration_seconds: old.tee_upgrade_deadline_duration_seconds,
57+
contract_upgrade_deposit_tera_gas: old.contract_upgrade_deposit_tera_gas,
58+
sign_call_gas_attachment_requirement_tera_gas: old
59+
.sign_call_gas_attachment_requirement_tera_gas,
60+
ckd_call_gas_attachment_requirement_tera_gas: old
61+
.ckd_call_gas_attachment_requirement_tera_gas,
62+
return_signature_and_clean_state_on_success_call_tera_gas: old
63+
.return_signature_and_clean_state_on_success_call_tera_gas,
64+
return_ck_and_clean_state_on_success_call_tera_gas: old
65+
.return_ck_and_clean_state_on_success_call_tera_gas,
66+
fail_on_timeout_tera_gas: old.fail_on_timeout_tera_gas,
67+
clean_tee_status_tera_gas: old.clean_tee_status_tera_gas,
68+
clean_invalid_attestations_tera_gas: old.clean_invalid_attestations_tera_gas,
69+
cleanup_orphaned_node_migrations_tera_gas: old
70+
.cleanup_orphaned_node_migrations_tera_gas,
71+
remove_non_participant_update_votes_tera_gas: old
72+
.remove_non_participant_update_votes_tera_gas,
73+
clean_foreign_chain_data_tera_gas: old.clean_foreign_chain_data_tera_gas,
74+
remove_non_participant_tee_verifier_votes_tera_gas: old
75+
.remove_non_participant_tee_verifier_votes_tera_gas,
76+
// New in this version: the attestation fail-call and verifier-call gas
77+
// knobs, added alongside the async attestation flow.
78+
..crate::Config::default()
79+
}
80+
}
81+
}
82+
83+
/// Keep this module in sync with [`crate::MpcContract`]: it is the `3.13.0` layout,
84+
/// which differs only by the appended `pending_attestations` map.
85+
#[derive(Debug, BorshSerialize, BorshDeserialize)]
86+
pub struct MpcContract {
87+
protocol_state: ProtocolContractState,
88+
pending_signature_requests: LookupMap<SignatureRequest, Vec<YieldIndex>>,
89+
pending_ckd_requests: LookupMap<CKDRequest, Vec<YieldIndex>>,
90+
pending_verify_foreign_tx_requests: LookupMap<VerifyForeignTransactionRequest, Vec<YieldIndex>>,
91+
proposed_updates: ProposedUpdates,
92+
node_foreign_chain_support: SupportedForeignChainsByNode,
93+
config: OldConfig,
94+
tee_state: TeeState,
95+
accept_requests: bool,
96+
node_migrations: NodeMigrations,
97+
metrics: Metrics,
98+
foreign_chains: Lazy<ForeignChainsMetadata>,
99+
tee_verifier_account_id: Option<AccountId>,
100+
tee_verifier_votes: TeeVerifierVotes,
101+
}
102+
103+
impl From<MpcContract> for crate::MpcContract {
104+
fn from(old: MpcContract) -> Self {
105+
if let ProtocolContractState::Running(running) = &old.protocol_state {
106+
validate_threshold_relation_on_migration(running);
107+
}
108+
109+
crate::MpcContract {
110+
protocol_state: old.protocol_state,
111+
pending_signature_requests: old.pending_signature_requests,
112+
pending_ckd_requests: old.pending_ckd_requests,
113+
pending_verify_foreign_tx_requests: old.pending_verify_foreign_tx_requests,
114+
proposed_updates: old.proposed_updates,
115+
node_foreign_chain_support: old.node_foreign_chain_support,
116+
config: old.config.into(),
117+
tee_state: old.tee_state,
118+
accept_requests: old.accept_requests,
119+
node_migrations: old.node_migrations,
120+
metrics: old.metrics,
121+
foreign_chains: old.foreign_chains,
122+
tee_verifier_account_id: old.tee_verifier_account_id,
123+
tee_verifier_votes: old.tee_verifier_votes,
124+
pending_attestations: LookupMap::new(StorageKey::PendingAttestations),
125+
}
126+
}
127+
}
128+
129+
fn validate_threshold_relation_on_migration(running: &RunningContractState) {
130+
let num_participants = running.parameters.participants().len() as u64;
131+
let max_reconstruction_threshold = max_reconstruction_threshold(running.domains.domains());
132+
if let Err(err) = ThresholdParameters::validate_governance_against_reconstruction(
133+
num_participants,
134+
running.parameters.threshold(),
135+
max_reconstruction_threshold,
136+
) {
137+
env::panic_str(&format!(
138+
"Migration aborted: existing state violates the GovernanceThreshold/ReconstructionThreshold relation ({err:?}). num_participants={}, governance_threshold={}, max_reconstruction_threshold={:?}. Correct it via vote_new_parameters before upgrading.",
139+
num_participants,
140+
running.parameters.threshold().value(),
141+
max_reconstruction_threshold.map(|t| t.inner()),
142+
));
143+
}
144+
}

0 commit comments

Comments
 (0)