diff --git a/crates/op-rbuilder/src/flashtestations/attestation.rs b/crates/op-rbuilder/src/flashtestations/attestation.rs index b38dcf5a..6af5cc24 100644 --- a/crates/op-rbuilder/src/flashtestations/attestation.rs +++ b/crates/op-rbuilder/src/flashtestations/attestation.rs @@ -1,8 +1,53 @@ use reqwest::Client; +use sha3::{Digest, Keccak256}; use tracing::info; const DEBUG_QUOTE_SERVICE_URL: &str = "http://ns31695324.ip-141-94-163.eu:10080/attest"; +// Raw TDX v4 quote structure constants +// Raw quote has a 48-byte header before the TD10ReportBody +const HEADER_LENGTH: usize = 48; +const TD_REPORT10_LENGTH: usize = 584; + +// TDX workload constants +const TD_XFAM_FPU: u64 = 0x0000000000000001; +const TD_XFAM_SSE: u64 = 0x0000000000000002; +const TD_TDATTRS_VE_DISABLED: u64 = 0x0000000010000000; +const TD_TDATTRS_PKS: u64 = 0x0000000040000000; +const TD_TDATTRS_KL: u64 = 0x0000000080000000; + +// TD10ReportBody field offsets +// These offsets correspond to the Solidity parseRawReportBody implementation +const OFFSET_TD_ATTRIBUTES: usize = 120; +const OFFSET_XFAM: usize = 128; +const OFFSET_MR_TD: usize = 136; +const OFFSET_MR_CONFIG_ID: usize = 184; +const OFFSET_MR_OWNER: usize = 232; +const OFFSET_MR_OWNER_CONFIG: usize = 280; +const OFFSET_RT_MR0: usize = 328; +const OFFSET_RT_MR1: usize = 376; +const OFFSET_RT_MR2: usize = 424; +const OFFSET_RT_MR3: usize = 472; + +// Field lengths +const MEASUREMENT_REGISTER_LENGTH: usize = 48; +const ATTRIBUTE_LENGTH: usize = 8; + +/// Parsed TDX quote report body containing measurement registers and attributes +#[derive(Debug, Clone)] +pub struct ParsedQuote { + pub mr_td: [u8; 48], + pub rt_mr0: [u8; 48], + pub rt_mr1: [u8; 48], + pub rt_mr2: [u8; 48], + pub rt_mr3: [u8; 48], + pub mr_config_id: [u8; 48], + pub mr_owner: [u8; 48], + pub mr_owner_config: [u8; 48], + pub xfam: u64, + pub td_attributes: u64, +} + /// Configuration for attestation #[derive(Default)] pub struct AttestationConfig { @@ -63,3 +108,148 @@ pub fn get_attestation_provider(config: AttestationConfig) -> RemoteAttestationP ) } } + +/// Parse the TDX report body from a raw quote +/// Extracts measurement registers and attributes according to TD10ReportBody specification +/// https://github.com/flashbots/flashtestations/tree/7cc7f68492fe672a823dd2dead649793aac1f216 +pub fn parse_report_body(raw_quote: &[u8]) -> eyre::Result { + // Validate quote length + if raw_quote.len() < HEADER_LENGTH + TD_REPORT10_LENGTH { + eyre::bail!( + "invalid quote length: {}, expected at least {}", + raw_quote.len(), + HEADER_LENGTH + TD_REPORT10_LENGTH + ); + } + + // Skip the 48-byte header to get to the TD10ReportBody + let report_body = &raw_quote[HEADER_LENGTH..]; + + // Extract fields exactly as parseRawReportBody does in Solidity + // Using named offset constants to match Solidity implementation exactly + let mr_td: [u8; 48] = report_body[OFFSET_MR_TD..OFFSET_MR_TD + MEASUREMENT_REGISTER_LENGTH] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract mr_td"))?; + let rt_mr0: [u8; 48] = report_body[OFFSET_RT_MR0..OFFSET_RT_MR0 + MEASUREMENT_REGISTER_LENGTH] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract rt_mr0"))?; + let rt_mr1: [u8; 48] = report_body[OFFSET_RT_MR1..OFFSET_RT_MR1 + MEASUREMENT_REGISTER_LENGTH] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract rt_mr1"))?; + let rt_mr2: [u8; 48] = report_body[OFFSET_RT_MR2..OFFSET_RT_MR2 + MEASUREMENT_REGISTER_LENGTH] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract rt_mr2"))?; + let rt_mr3: [u8; 48] = report_body[OFFSET_RT_MR3..OFFSET_RT_MR3 + MEASUREMENT_REGISTER_LENGTH] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract rt_mr3"))?; + let mr_config_id: [u8; 48] = report_body + [OFFSET_MR_CONFIG_ID..OFFSET_MR_CONFIG_ID + MEASUREMENT_REGISTER_LENGTH] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract mr_config_id"))?; + let mr_owner: [u8; 48] = report_body + [OFFSET_MR_OWNER..OFFSET_MR_OWNER + MEASUREMENT_REGISTER_LENGTH] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract mr_owner"))?; + let mr_owner_config: [u8; 48] = report_body + [OFFSET_MR_OWNER_CONFIG..OFFSET_MR_OWNER_CONFIG + MEASUREMENT_REGISTER_LENGTH] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract mr_owner_config"))?; + + // Extract xFAM and tdAttributes (8 bytes each) + // In Solidity, bytes8 is treated as big-endian for bitwise operations + let xfam = u64::from_be_bytes( + report_body[OFFSET_XFAM..OFFSET_XFAM + ATTRIBUTE_LENGTH] + .try_into() + .map_err(|e| eyre::eyre!("failed to parse xfam: {}", e))?, + ); + let td_attributes = u64::from_be_bytes( + report_body[OFFSET_TD_ATTRIBUTES..OFFSET_TD_ATTRIBUTES + ATTRIBUTE_LENGTH] + .try_into() + .map_err(|e| eyre::eyre!("failed to parse td_attributes: {}", e))?, + ); + + Ok(ParsedQuote { + mr_td, + rt_mr0, + rt_mr1, + rt_mr2, + rt_mr3, + mr_config_id, + mr_owner, + mr_owner_config, + xfam, + td_attributes, + }) +} + +/// Compute workload ID from parsed quote data +/// This corresponds to QuoteParser.parseV4VerifierOutput in Solidity implementation +/// The workload ID uniquely identifies a TEE workload based on its measurement registers +pub fn compute_workload_id_from_parsed(parsed: &ParsedQuote) -> [u8; 32] { + // Apply transformations as per the Solidity implementation + // expectedXfamBits = TD_XFAM_FPU | TD_XFAM_SSE + let expected_xfam_bits = TD_XFAM_FPU | TD_XFAM_SSE; + + // ignoredTdAttributesBitmask = TD_TDATTRS_VE_DISABLED | TD_TDATTRS_PKS | TD_TDATTRS_KL + let ignored_td_attributes_bitmask = TD_TDATTRS_VE_DISABLED | TD_TDATTRS_PKS | TD_TDATTRS_KL; + + // Transform xFAM: xFAM ^ expectedXfamBits + let transformed_xfam = parsed.xfam ^ expected_xfam_bits; + + // Transform tdAttributes: tdAttributes & ~ignoredTdAttributesBitmask + let transformed_td_attributes = parsed.td_attributes & !ignored_td_attributes_bitmask; + + // Convert transformed values to bytes (big-endian, to match Solidity bytes8) + let xfam_bytes = transformed_xfam.to_be_bytes(); + let td_attributes_bytes = transformed_td_attributes.to_be_bytes(); + + // Concatenate all fields + let mut concatenated = Vec::new(); + concatenated.extend_from_slice(&parsed.mr_td); + concatenated.extend_from_slice(&parsed.rt_mr0); + concatenated.extend_from_slice(&parsed.rt_mr1); + concatenated.extend_from_slice(&parsed.rt_mr2); + concatenated.extend_from_slice(&parsed.rt_mr3); + concatenated.extend_from_slice(&parsed.mr_config_id); + concatenated.extend_from_slice(&xfam_bytes); + concatenated.extend_from_slice(&td_attributes_bytes); + + // Compute keccak256 hash + let mut hasher = Keccak256::new(); + hasher.update(&concatenated); + let result = hasher.finalize(); + + let mut workload_id = [0u8; 32]; + workload_id.copy_from_slice(&result); + + workload_id +} + +/// Compute workload ID from raw quote bytes +/// This is a convenience function that combines parsing and computation +pub fn compute_workload_id(raw_quote: &[u8]) -> eyre::Result<[u8; 32]> { + let parsed = parse_report_body(raw_quote)?; + Ok(compute_workload_id_from_parsed(&parsed)) +} + +#[cfg(test)] +mod tests { + use crate::tests::WORKLOAD_ID; + + use super::*; + + #[test] + fn test_compute_workload_id_from_test_quote() { + // Load the test quote output used in integration tests + let quote_output = include_bytes!("../tests/framework/artifacts/test-quote.bin"); + + // Compute the workload ID + let workload_id = compute_workload_id(quote_output) + .expect("failed to compute workload ID from test quote"); + + assert_eq!( + workload_id, WORKLOAD_ID, + "workload ID mismatch for test quote" + ); + } +} diff --git a/crates/op-rbuilder/src/flashtestations/service.rs b/crates/op-rbuilder/src/flashtestations/service.rs index 3bd5b854..73896119 100644 --- a/crates/op-rbuilder/src/flashtestations/service.rs +++ b/crates/op-rbuilder/src/flashtestations/service.rs @@ -14,6 +14,7 @@ use super::{ }; use crate::{ flashtestations::builder_tx::{FlashtestationsBuilderTx, FlashtestationsBuilderTxArgs}, + metrics::record_tee_metrics, tx_signer::{Signer, generate_key_from_seed, generate_signer}, }; use std::fmt::Debug; @@ -74,6 +75,9 @@ where info!(target: "flashtestations", "requesting TDX attestation"); let attestation = attestation_provider.get_attestation(report_data).await?; + // Record TEE metrics (workload ID, MRTD, RTMR0) + record_tee_metrics(&attestation, &tee_service_signer.address)?; + // Use an external rpc when the builder is not the same as the builder actively building blocks onchain let registered = if let Some(rpc_url) = args.rpc_url { let tx_manager = TxManager::new( diff --git a/crates/op-rbuilder/src/metrics.rs b/crates/op-rbuilder/src/metrics.rs index c268c43c..c381fed8 100644 --- a/crates/op-rbuilder/src/metrics.rs +++ b/crates/op-rbuilder/src/metrics.rs @@ -1,10 +1,14 @@ +use alloy_primitives::{Address, hex}; use metrics::IntoF64; use reth_metrics::{ Metrics, metrics::{Counter, Gauge, Histogram, gauge}, }; -use crate::args::OpRbuilderArgs; +use crate::{ + args::OpRbuilderArgs, + flashtestations::attestation::{compute_workload_id_from_parsed, parse_report_body}, +}; /// The latest version from Cargo.toml. pub const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -202,6 +206,41 @@ pub fn record_flag_gauge_metrics(builder_args: &OpRbuilderArgs) { .set(builder_args.enable_revert_protection as i32); } +/// Record TEE workload ID and measurement metrics +/// Parses the quote, computes workload ID, and records workload_id, mr_td (TEE measurement), and rt_mr0 (runtime measurement register 0) +/// These identify the trusted execution environment configuration provided by GCP +pub fn record_tee_metrics(raw_quote: &[u8], tee_address: &Address) -> eyre::Result<()> { + let parsed_quote = parse_report_body(raw_quote)?; + let workload_id = compute_workload_id_from_parsed(&parsed_quote); + + let workload_id_hex = hex::encode(workload_id); + let mr_td_hex = hex::encode(parsed_quote.mr_td); + let rt_mr0_hex = hex::encode(parsed_quote.rt_mr0); + + let tee_address_static: &'static str = Box::leak(tee_address.to_string().into_boxed_str()); + let workload_id_static: &'static str = Box::leak(workload_id_hex.into_boxed_str()); + let mr_td_static: &'static str = Box::leak(mr_td_hex.into_boxed_str()); + let rt_mr0_static: &'static str = Box::leak(rt_mr0_hex.into_boxed_str()); + + // Record TEE address + let tee_address_labels: [(&str, &str); 1] = [("tee_address", tee_address_static)]; + gauge!("op_rbuilder_tee_address", &tee_address_labels).set(1); + + // Record workload ID + let workload_labels: [(&str, &str); 1] = [("workload_id", workload_id_static)]; + gauge!("op_rbuilder_tee_workload_id", &workload_labels).set(1); + + // Record MRTD (TEE measurement) + let mr_td_labels: [(&str, &str); 1] = [("mr_td", mr_td_static)]; + gauge!("op_rbuilder_tee_mr_td", &mr_td_labels).set(1); + + // Record RTMR0 (runtime measurement register 0) + let rt_mr0_labels: [(&str, &str); 1] = [("rt_mr0", rt_mr0_static)]; + gauge!("op_rbuilder_tee_rt_mr0", &rt_mr0_labels).set(1); + + Ok(()) +} + /// Contains version information for the application. #[derive(Debug, Clone)] pub struct VersionInfo {