|
| 1 | +//! Metrics for Optimism-specific EVM validation. |
| 2 | +
|
| 3 | +#[cfg(feature = "std")] |
| 4 | +use metrics::{counter, describe_counter}; |
| 5 | +use op_alloy_consensus::PostExecPayloadValidationError; |
| 6 | + |
| 7 | +/// Counter incremented when an execution-client path rejects `PostExec` block structure. |
| 8 | +pub const POST_EXEC_VALIDATION_FAILURES: &str = "optimism_post_exec.validation_failures"; |
| 9 | + |
| 10 | +/// Stable, bounded reason labels for `PostExec` structural validation failures. |
| 11 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 12 | +pub enum PostExecValidationFailureReason { |
| 13 | + /// The `0x7D` transaction or its schema could not be decoded. |
| 14 | + InvalidEncodingOrSchema, |
| 15 | + /// A block contained `0x7D` before SDM activation. |
| 16 | + SdmInactive, |
| 17 | + /// A block contained more than one `0x7D` transaction. |
| 18 | + MultipleTransactions, |
| 19 | + /// The `0x7D` transaction was not the final transaction. |
| 20 | + NotLast, |
| 21 | + /// The payload block number did not match the containing block. |
| 22 | + BlockNumberMismatch, |
| 23 | +} |
| 24 | + |
| 25 | +impl PostExecValidationFailureReason { |
| 26 | + /// Returns the stable metric label for this failure reason. |
| 27 | + pub const fn as_str(self) -> &'static str { |
| 28 | + match self { |
| 29 | + Self::InvalidEncodingOrSchema => "invalid_encoding_or_schema", |
| 30 | + Self::SdmInactive => "sdm_inactive", |
| 31 | + Self::MultipleTransactions => "multiple_transactions", |
| 32 | + Self::NotLast => "not_last", |
| 33 | + Self::BlockNumberMismatch => "block_number_mismatch", |
| 34 | + } |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +impl From<&PostExecPayloadValidationError> for PostExecValidationFailureReason { |
| 39 | + fn from(error: &PostExecPayloadValidationError) -> Self { |
| 40 | + match error { |
| 41 | + PostExecPayloadValidationError::UnexpectedPostExecTx { .. } => Self::SdmInactive, |
| 42 | + PostExecPayloadValidationError::MultiplePostExecTxs { .. } => { |
| 43 | + Self::MultipleTransactions |
| 44 | + } |
| 45 | + PostExecPayloadValidationError::PostExecTxNotLast { .. } => Self::NotLast, |
| 46 | + PostExecPayloadValidationError::BlockNumberMismatch { .. } => Self::BlockNumberMismatch, |
| 47 | + } |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +/// Records a `PostExec` structural validation failure. |
| 52 | +/// |
| 53 | +/// Callers must classify failures with [`PostExecValidationFailureReason`] so the `reason` label |
| 54 | +/// remains low-cardinality. |
| 55 | +pub fn record_post_exec_validation_failure(reason: PostExecValidationFailureReason) { |
| 56 | + #[cfg(feature = "std")] |
| 57 | + { |
| 58 | + describe_counter!( |
| 59 | + POST_EXEC_VALIDATION_FAILURES, |
| 60 | + "PostExec structural validation failures by reason" |
| 61 | + ); |
| 62 | + counter!(POST_EXEC_VALIDATION_FAILURES, "reason" => reason.as_str()).increment(1); |
| 63 | + } |
| 64 | + #[cfg(not(feature = "std"))] |
| 65 | + let _ = reason; |
| 66 | +} |
| 67 | + |
| 68 | +#[cfg(all(test, feature = "std"))] |
| 69 | +mod tests { |
| 70 | + use super::*; |
| 71 | + use metrics_util::debugging::{DebugValue, DebuggingRecorder, Snapshot}; |
| 72 | + |
| 73 | + fn failure_count(snapshot: Snapshot, reason: PostExecValidationFailureReason) -> u64 { |
| 74 | + snapshot |
| 75 | + .into_vec() |
| 76 | + .into_iter() |
| 77 | + .find_map(|(composite_key, _, _, value)| { |
| 78 | + let key = composite_key.key(); |
| 79 | + let matches = key.name() == POST_EXEC_VALIDATION_FAILURES && |
| 80 | + key.labels().any(|label| { |
| 81 | + label.key() == "reason" && label.value() == reason.as_str() |
| 82 | + }); |
| 83 | + matches.then_some(match value { |
| 84 | + DebugValue::Counter(value) => value, |
| 85 | + _ => 0, |
| 86 | + }) |
| 87 | + }) |
| 88 | + .unwrap_or_default() |
| 89 | + } |
| 90 | + |
| 91 | + #[test] |
| 92 | + fn records_reason_labeled_post_exec_validation_failure() { |
| 93 | + let recorder = DebuggingRecorder::new(); |
| 94 | + let snapshotter = recorder.snapshotter(); |
| 95 | + metrics::with_local_recorder(&recorder, || { |
| 96 | + record_post_exec_validation_failure( |
| 97 | + PostExecValidationFailureReason::BlockNumberMismatch, |
| 98 | + ); |
| 99 | + }); |
| 100 | + |
| 101 | + assert_eq!( |
| 102 | + failure_count( |
| 103 | + snapshotter.snapshot(), |
| 104 | + PostExecValidationFailureReason::BlockNumberMismatch |
| 105 | + ), |
| 106 | + 1 |
| 107 | + ); |
| 108 | + } |
| 109 | + |
| 110 | + #[test] |
| 111 | + fn maps_structural_errors_to_stable_reasons() { |
| 112 | + for (error, expected) in [ |
| 113 | + ( |
| 114 | + PostExecPayloadValidationError::UnexpectedPostExecTx { tx_index: 0 }, |
| 115 | + PostExecValidationFailureReason::SdmInactive, |
| 116 | + ), |
| 117 | + ( |
| 118 | + PostExecPayloadValidationError::MultiplePostExecTxs { |
| 119 | + first_index: 0, |
| 120 | + duplicate_index: 1, |
| 121 | + }, |
| 122 | + PostExecValidationFailureReason::MultipleTransactions, |
| 123 | + ), |
| 124 | + ( |
| 125 | + PostExecPayloadValidationError::PostExecTxNotLast { tx_index: 0, last_index: 1 }, |
| 126 | + PostExecValidationFailureReason::NotLast, |
| 127 | + ), |
| 128 | + ( |
| 129 | + PostExecPayloadValidationError::BlockNumberMismatch { |
| 130 | + payload_block_number: 1, |
| 131 | + block_number: 2, |
| 132 | + }, |
| 133 | + PostExecValidationFailureReason::BlockNumberMismatch, |
| 134 | + ), |
| 135 | + ] { |
| 136 | + assert_eq!(PostExecValidationFailureReason::from(&error), expected); |
| 137 | + } |
| 138 | + } |
| 139 | +} |
0 commit comments