|
| 1 | +//! Attestation-freshness gauges. |
| 2 | +//! |
| 3 | +//! Both are absolute timestamps rather than remaining durations: a node that stops re-attesting |
| 4 | +//! also stops updating them, and only the absolute form keeps decaying towards now, so a staleness |
| 5 | +//! alert still fires on a frozen gauge. At rest both advance hourly — the expiry is re-read on |
| 6 | +//! every submission observation, the landing timestamp only when one is confirmed. |
| 7 | +
|
| 8 | +use near_mpc_contract_interface::types::VerifiedAttestation; |
| 9 | +use near_time::Clock; |
| 10 | + |
| 11 | +use crate::metrics::{ |
| 12 | + MPC_ATTESTATION_EXPIRY_TIMESTAMP_SECONDS, MPC_ATTESTATION_LAST_LANDED_TIMESTAMP_SECONDS, |
| 13 | +}; |
| 14 | + |
| 15 | +const NO_ATTESTATION_STORED: i64 = 0; |
| 16 | +const NO_EXPIRY: i64 = -1; |
| 17 | + |
| 18 | +pub(crate) fn record_stored_attestation_expiry(stored: Option<&VerifiedAttestation>) { |
| 19 | + MPC_ATTESTATION_EXPIRY_TIMESTAMP_SECONDS.set(expiry_gauge_value(stored)); |
| 20 | +} |
| 21 | + |
| 22 | +pub(crate) fn record_attestation_landed(clock: &Clock) { |
| 23 | + MPC_ATTESTATION_LAST_LANDED_TIMESTAMP_SECONDS.set(clock.now_utc().unix_timestamp()); |
| 24 | +} |
| 25 | + |
| 26 | +fn expiry_gauge_value(stored: Option<&VerifiedAttestation>) -> i64 { |
| 27 | + match stored.map(VerifiedAttestation::expiry_timestamp_seconds) { |
| 28 | + None => NO_ATTESTATION_STORED, |
| 29 | + Some(None) => NO_EXPIRY, |
| 30 | + Some(Some(expiry)) => i64::try_from(expiry).unwrap_or(i64::MAX), |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +#[cfg(test)] |
| 35 | +#[expect(non_snake_case)] |
| 36 | +mod tests { |
| 37 | + use super::*; |
| 38 | + use near_mpc_contract_interface::types::MockAttestation; |
| 39 | + use near_time::{FakeClock, Utc}; |
| 40 | + use rstest::rstest; |
| 41 | + |
| 42 | + const EXPIRES_AT: u64 = 1_754_000_000; |
| 43 | + |
| 44 | + fn mock_with_expiry(expiry_timestamp_seconds: Option<u64>) -> VerifiedAttestation { |
| 45 | + VerifiedAttestation::Mock(MockAttestation::WithConstraints { |
| 46 | + mpc_docker_image_hash: None, |
| 47 | + launcher_docker_compose_hash: None, |
| 48 | + expiry_timestamp_seconds, |
| 49 | + expected_measurements: None, |
| 50 | + }) |
| 51 | + } |
| 52 | + |
| 53 | + #[test] |
| 54 | + fn record_attestation_landed__should_set_the_gauge_to_the_landing_time() { |
| 55 | + // Given |
| 56 | + let landed_at = Utc::from_unix_timestamp(EXPIRES_AT as i64).unwrap(); |
| 57 | + |
| 58 | + // When |
| 59 | + record_attestation_landed(&FakeClock::new(landed_at).clock()); |
| 60 | + |
| 61 | + // Then |
| 62 | + assert_eq!( |
| 63 | + MPC_ATTESTATION_LAST_LANDED_TIMESTAMP_SECONDS.get(), |
| 64 | + EXPIRES_AT as i64 |
| 65 | + ); |
| 66 | + } |
| 67 | + |
| 68 | + #[rstest] |
| 69 | + #[case::nothing_stored(None, NO_ATTESTATION_STORED)] |
| 70 | + #[case::stored_with_expiry(Some(mock_with_expiry(Some(EXPIRES_AT))), EXPIRES_AT as i64)] |
| 71 | + #[case::stored_without_expiry(Some(mock_with_expiry(None)), NO_EXPIRY)] |
| 72 | + #[case::unstamped_mock(Some(VerifiedAttestation::Mock(MockAttestation::Valid)), NO_EXPIRY)] |
| 73 | + #[case::expiry_beyond_i64(Some(mock_with_expiry(Some(u64::MAX))), i64::MAX)] |
| 74 | + fn expiry_gauge_value__should_report_stored_expiry_or_a_sentinel( |
| 75 | + #[case] stored: Option<VerifiedAttestation>, |
| 76 | + #[case] expected: i64, |
| 77 | + ) { |
| 78 | + // When |
| 79 | + let value = expiry_gauge_value(stored.as_ref()); |
| 80 | + |
| 81 | + // Then |
| 82 | + assert_eq!(value, expected); |
| 83 | + } |
| 84 | +} |
0 commit comments