Skip to content

Commit 7ab2904

Browse files
committed
fix(node): recognize expiry-stamped mock attestations across upgrade
With the contract now stamping an expiry on accepted mocks, a submitted Mock::Valid is stored as WithConstraints{expiry}. The node confirmed mock submissions by identity (stored == submitted), which would never match the re-stamped form, so a node on the new contract would treat every mock submit as not-landed and retry until it errored. Assuming node-is-upgraded-before-contract, the upgraded node must work with both contract versions: - submitted_attestation_landed: confirm an expiry-carrying mock via the same expiry-changed heuristic used for Dstack; fall back to identity for the bare Valid form stored by older contracts. - read_stored_attestation_expiry (renamed from _dstack): also return the mock expiry so the pre-submit baseline is accurate. - add VerifiedAttestation/MockAttestation::expiry_timestamp_seconds helpers.
1 parent 7257900 commit 7ab2904

5 files changed

Lines changed: 88 additions & 22 deletions

File tree

crates/near-mpc-contract-interface/src/types/attestation.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ pub enum VerifiedAttestation {
7575
Mock(MockAttestation),
7676
}
7777

78+
impl VerifiedAttestation {
79+
/// The stored expiry timestamp, if the attestation carries one. `Dstack`
80+
/// entries always do; a `Mock` entry does only when it was stamped with an
81+
/// expiry — contracts predating #3293 store `Mock::Valid` without one.
82+
pub fn expiry_timestamp_seconds(&self) -> Option<u64> {
83+
match self {
84+
VerifiedAttestation::Dstack(attestation) => Some(attestation.expiry_timestamp_seconds),
85+
VerifiedAttestation::Mock(attestation) => attestation.expiry_timestamp_seconds(),
86+
}
87+
}
88+
}
89+
7890
#[derive(
7991
Clone,
8092
Debug,
@@ -184,6 +196,20 @@ pub enum MockAttestation {
184196
},
185197
}
186198

199+
impl MockAttestation {
200+
/// The configured expiry timestamp, if any. `Valid` and `Invalid` never
201+
/// carry one.
202+
pub fn expiry_timestamp_seconds(&self) -> Option<u64> {
203+
match self {
204+
MockAttestation::WithConstraints {
205+
expiry_timestamp_seconds,
206+
..
207+
} => *expiry_timestamp_seconds,
208+
MockAttestation::Valid | MockAttestation::Invalid => None,
209+
}
210+
}
211+
}
212+
187213
// TODO(#3494): superseded by `tee_verifier_interface::Collateral`; remove
188214
// this serde-carrying copy once `mpc-contract` consumes the Borsh mirrors.
189215
#[derive(

crates/node/src/indexer.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -427,9 +427,9 @@ impl ReadSupportedForeignChain for RealForeignChainPolicyReader {
427427
}
428428

429429
pub(crate) trait ReadAttestationExpiry: Send + Sync {
430-
/// The Dstack attestation expiry currently stored for `tls_public_key`, or `None` if none is
431-
/// stored.
432-
fn read_stored_dstack_expiry<'a>(
430+
/// The attestation expiry currently stored for `tls_public_key`, or `None` if none is stored or
431+
/// the stored attestation carries no expiry (a mock stored by a contract predating #3293).
432+
fn read_stored_attestation_expiry<'a>(
433433
&'a self,
434434
tls_public_key: &'a dtos::Ed25519PublicKey,
435435
) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<Option<u64>>> + Send + 'a>>;
@@ -446,7 +446,7 @@ impl RealAttestationExpiryReader {
446446
}
447447

448448
impl ReadAttestationExpiry for RealAttestationExpiryReader {
449-
fn read_stored_dstack_expiry<'a>(
449+
fn read_stored_attestation_expiry<'a>(
450450
&'a self,
451451
tls_public_key: &'a dtos::Ed25519PublicKey,
452452
) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<Option<u64>>> + Send + 'a>>
@@ -457,10 +457,7 @@ impl ReadAttestationExpiry for RealAttestationExpiryReader {
457457
.view_client
458458
.get_participant_attestation(&self.indexer_state.mpc_contract_id, tls_public_key)
459459
.await?;
460-
Ok(match stored {
461-
Some(dtos::VerifiedAttestation::Dstack(a)) => Some(a.expiry_timestamp_seconds),
462-
_ => None,
463-
})
460+
Ok(stored.and_then(|attestation| attestation.expiry_timestamp_seconds()))
464461
})
465462
}
466463
}

crates/node/src/indexer/fake.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub struct FakeReadSupportedForeignChain {
6363
struct FakeAttestationExpiryReader;
6464

6565
impl ReadAttestationExpiry for FakeAttestationExpiryReader {
66-
fn read_stored_dstack_expiry<'a>(
66+
fn read_stored_attestation_expiry<'a>(
6767
&'a self,
6868
_tls_public_key: &'a near_mpc_contract_interface::types::Ed25519PublicKey,
6969
) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<Option<u64>>> + Send + 'a>>

crates/node/src/indexer/tx_sender.rs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,13 @@ fn attestation_expiry_changed(pre_submit_expiry: Option<u64>, stored_expiry: u64
190190

191191
/// Whether the attestation we submitted is now the one stored on chain.
192192
///
193-
/// Mock is matched by identity. A Dstack entry keeps no stored per-submission identity, so it is
194-
/// confirmed via [`attestation_expiry_changed`]: an accepted submit re-stamps the entry's expiry
195-
/// (to the submit block time plus
193+
/// An entry that carries an expiry keeps no stored per-submission identity, so it is confirmed via
194+
/// [`attestation_expiry_changed`]: an accepted submit re-stamps the entry's expiry (to the submit
195+
/// block time plus
196196
/// [`DEFAULT_EXPIRATION_DURATION_SECONDS`](mpc_attestation::attestation::DEFAULT_EXPIRATION_DURATION_SECONDS)),
197-
/// and only the owning account may rewrite it, so a changed expiry means our submit landed.
197+
/// and only the owning account may rewrite it, so a changed expiry means our submit landed. This
198+
/// covers every Dstack entry, and mock entries stored by contracts that stamp expiries (#3293).
199+
/// A mock stored by an older contract carries no expiry and is matched by identity instead.
198200
// TODO(#1639): match a certificate-derived identity instead of this expiry heuristic.
199201
fn submitted_attestation_landed(
200202
pre_submit_expiry: Option<u64>,
@@ -205,7 +207,12 @@ fn submitted_attestation_landed(
205207
(VerifiedAttestation::Dstack(stored), Attestation::Dstack(_)) => {
206208
attestation_expiry_changed(pre_submit_expiry, stored.expiry_timestamp_seconds)
207209
}
208-
(VerifiedAttestation::Mock(stored), Attestation::Mock(submitted)) => stored == submitted,
210+
(VerifiedAttestation::Mock(stored), Attestation::Mock(submitted)) => {
211+
match stored.expiry_timestamp_seconds() {
212+
Some(expiry) => attestation_expiry_changed(pre_submit_expiry, expiry),
213+
None => stored == submitted,
214+
}
215+
}
209216
_ => false,
210217
}
211218
}
@@ -283,10 +290,7 @@ async fn observe_tx_result(
283290
return Ok(TransactionStatus::NotExecuted);
284291
};
285292

286-
let stored_expiry = match &stored_attestation {
287-
VerifiedAttestation::Dstack(stored) => Some(stored.expiry_timestamp_seconds),
288-
VerifiedAttestation::Mock(_) => None,
289-
};
293+
let stored_expiry = stored_attestation.expiry_timestamp_seconds();
290294
let attestation_landed = submitted_attestation_landed(
291295
*pre_submit_expiry,
292296
&stored_attestation,
@@ -476,4 +480,43 @@ mod tests {
476480
// Then
477481
assert!(!landed);
478482
}
483+
484+
fn mock_with_expiry(expiry_timestamp_seconds: u64) -> MockAttestation {
485+
MockAttestation::WithConstraints {
486+
mpc_docker_image_hash: None,
487+
launcher_docker_compose_hash: None,
488+
expiry_timestamp_seconds: Some(expiry_timestamp_seconds),
489+
expected_measurements: None,
490+
}
491+
}
492+
493+
#[test]
494+
#[expect(non_snake_case)]
495+
fn submitted_attestation_landed__should_confirm_mock_with_changed_expiry() {
496+
// Given: a contract that stamps expiries on mocks (#3293) re-stamped our
497+
// submitted `Mock::Valid` as an expiring `WithConstraints`, changing the expiry.
498+
let stored = VerifiedAttestation::Mock(mock_with_expiry(200));
499+
let submitted = Attestation::Mock(MockAttestation::Valid);
500+
501+
// When
502+
let landed = submitted_attestation_landed(Some(100), &stored, &submitted);
503+
504+
// Then: the changed expiry confirms our submit landed.
505+
assert!(landed);
506+
}
507+
508+
#[test]
509+
#[expect(non_snake_case)]
510+
fn submitted_attestation_landed__should_reject_mock_with_unchanged_expiry() {
511+
// Given: an expiry-carrying mock whose stored expiry is unchanged since before
512+
// our submit (our resubmit did not land).
513+
let stored = VerifiedAttestation::Mock(mock_with_expiry(200));
514+
let submitted = Attestation::Mock(MockAttestation::Valid);
515+
516+
// When
517+
let landed = submitted_attestation_landed(Some(200), &stored, &submitted);
518+
519+
// Then: no change means the submit is treated as not executed.
520+
assert!(!landed);
521+
}
479522
}

crates/node/src/tee/remote_attestation.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ pub async fn periodic_attestation_submission<T: TransactionSender + Clone, I: Ti
208208
let allowed_launcher_compose_hashes_in_contract =
209209
allowed_launcher_compose_hashes_in_contract.borrow().clone();
210210
let pre_submit_expiry = match attestation_reader
211-
.read_stored_dstack_expiry(&tls_public_key)
211+
.read_stored_attestation_expiry(&tls_public_key)
212212
.await
213213
{
214214
Ok(baseline) => baseline, // None just means nothing stored yet (e.g. first submit)
@@ -333,7 +333,7 @@ pub async fn monitor_attestation_removal<T: TransactionSender + Clone>(
333333
let allowed_launcher_compose_hashes_in_contract =
334334
allowed_launcher_compose_hashes_in_contract.borrow().clone();
335335
let pre_submit_expiry = match attestation_reader
336-
.read_stored_dstack_expiry(&tls_public_key)
336+
.read_stored_attestation_expiry(&tls_public_key)
337337
.await
338338
{
339339
Ok(baseline) => baseline, // None just means nothing stored yet (e.g. first submit)
@@ -409,7 +409,7 @@ mod tests {
409409
struct StubAttestationExpiryReader;
410410

411411
impl crate::indexer::ReadAttestationExpiry for StubAttestationExpiryReader {
412-
fn read_stored_dstack_expiry<'a>(
412+
fn read_stored_attestation_expiry<'a>(
413413
&'a self,
414414
_tls_public_key: &'a Ed25519PublicKey,
415415
) -> std::pin::Pin<
@@ -422,7 +422,7 @@ mod tests {
422422
struct FailingAttestationExpiryReader;
423423

424424
impl crate::indexer::ReadAttestationExpiry for FailingAttestationExpiryReader {
425-
fn read_stored_dstack_expiry<'a>(
425+
fn read_stored_attestation_expiry<'a>(
426426
&'a self,
427427
_tls_public_key: &'a Ed25519PublicKey,
428428
) -> std::pin::Pin<

0 commit comments

Comments
 (0)