From acfcd1c8bb6b6e43c99d0f9a4852105ad918f3e4 Mon Sep 17 00:00:00 2001 From: avalonche Date: Fri, 7 Nov 2025 17:25:34 +1100 Subject: [PATCH 1/6] Add workload id as metric to builder --- .../src/flashtestations/attestation.rs | 105 ++++++++++++++++++ .../src/flashtestations/service.rs | 10 +- crates/op-rbuilder/src/metrics.rs | 9 ++ 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/crates/op-rbuilder/src/flashtestations/attestation.rs b/crates/op-rbuilder/src/flashtestations/attestation.rs index b38dcf5a..ec8dc28c 100644 --- a/crates/op-rbuilder/src/flashtestations/attestation.rs +++ b/crates/op-rbuilder/src/flashtestations/attestation.rs @@ -1,8 +1,21 @@ 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; + /// Configuration for attestation #[derive(Default)] pub struct AttestationConfig { @@ -63,3 +76,95 @@ pub fn get_attestation_provider(config: AttestationConfig) -> RemoteAttestationP ) } } + +/// ComputeWorkloadID computes the workload ID from Automata's serialized verifier output +/// This corresponds to QuoteParser.parseV4VerifierOutput in Solidity implementation +/// https://github.com/flashbots/flashtestations/tree/7cc7f68492fe672a823dd2dead649793aac1f216 +/// The workload ID uniquely identifies a TEE workload based on its measurement registers +pub fn compute_workload_id(raw_quote: &[u8]) -> eyre::Result<[u8; 32]> { + // 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 hardcoded offsets to match Solidity implementation exactly + let mr_td = &report_body[136..136 + 48]; + let rt_mr0 = &report_body[328..328 + 48]; + let rt_mr1 = &report_body[376..376 + 48]; + let rt_mr2 = &report_body[424..424 + 48]; + let rt_mr3 = &report_body[472..472 + 48]; + let mr_config_id = &report_body[184..184 + 48]; + + // 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[128..128 + 8].try_into().unwrap()); + let td_attributes = u64::from_be_bytes(report_body[120..120 + 8].try_into().unwrap()); + + // 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 = xfam ^ expected_xfam_bits; + + // Transform tdAttributes: tdAttributes & ~ignoredTdAttributesBitmask + let transformed_td_attributes = 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(mr_td); + concatenated.extend_from_slice(rt_mr0); + concatenated.extend_from_slice(rt_mr1); + concatenated.extend_from_slice(rt_mr2); + concatenated.extend_from_slice(rt_mr3); + concatenated.extend_from_slice(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); + + Ok(workload_id) +} + +#[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..5f67e709 100644 --- a/crates/op-rbuilder/src/flashtestations/service.rs +++ b/crates/op-rbuilder/src/flashtestations/service.rs @@ -13,7 +13,11 @@ use super::{ tx_manager::TxManager, }; use crate::{ - flashtestations::builder_tx::{FlashtestationsBuilderTx, FlashtestationsBuilderTxArgs}, + flashtestations::{ + attestation::compute_workload_id, + builder_tx::{FlashtestationsBuilderTx, FlashtestationsBuilderTxArgs}, + }, + metrics::record_workload_id_metrics, tx_signer::{Signer, generate_key_from_seed, generate_signer}, }; use std::fmt::Debug; @@ -74,6 +78,10 @@ where info!(target: "flashtestations", "requesting TDX attestation"); let attestation = attestation_provider.get_attestation(report_data).await?; + // Compute workload id and record metrics + let workload_id = compute_workload_id(&attestation)?; + record_workload_id_metrics(workload_id); + // 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..ec1f9593 100644 --- a/crates/op-rbuilder/src/metrics.rs +++ b/crates/op-rbuilder/src/metrics.rs @@ -202,6 +202,15 @@ pub fn record_flag_gauge_metrics(builder_args: &OpRbuilderArgs) { .set(builder_args.enable_revert_protection as i32); } +/// Set the workload id metrics +pub fn record_workload_id_metrics(workload_id: [u8; 32]) { + let encoded = hex::encode(workload_id); + let encoded_static: &'static str = Box::leak(encoded.into_boxed_str()); + let labels: [(&str, &str); 1] = [("workload_id", encoded_static)]; + let gauge = gauge!("op_rbuilder_workload_id", &labels); + gauge.set(1); +} + /// Contains version information for the application. #[derive(Debug, Clone)] pub struct VersionInfo { From dea0d1bb55d8136c61d29f8f0a911b4f2e405e9a Mon Sep 17 00:00:00 2001 From: avalonche Date: Sat, 8 Nov 2025 06:29:27 +1100 Subject: [PATCH 2/6] more tdx measurements --- .../src/flashtestations/attestation.rs | 107 ++++++++++++++---- .../src/flashtestations/service.rs | 12 +- crates/op-rbuilder/src/metrics.rs | 40 +++++-- 3 files changed, 122 insertions(+), 37 deletions(-) diff --git a/crates/op-rbuilder/src/flashtestations/attestation.rs b/crates/op-rbuilder/src/flashtestations/attestation.rs index ec8dc28c..f8aa7238 100644 --- a/crates/op-rbuilder/src/flashtestations/attestation.rs +++ b/crates/op-rbuilder/src/flashtestations/attestation.rs @@ -16,6 +16,21 @@ const TD_TDATTRS_VE_DISABLED: u64 = 0x0000000010000000; const TD_TDATTRS_PKS: u64 = 0x0000000040000000; const TD_TDATTRS_KL: u64 = 0x0000000080000000; +/// 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 { @@ -77,11 +92,10 @@ pub fn get_attestation_provider(config: AttestationConfig) -> RemoteAttestationP } } -/// ComputeWorkloadID computes the workload ID from Automata's serialized verifier output -/// This corresponds to QuoteParser.parseV4VerifierOutput in Solidity implementation +/// 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 -/// The workload ID uniquely identifies a TEE workload based on its measurement registers -pub fn compute_workload_id(raw_quote: &[u8]) -> eyre::Result<[u8; 32]> { +pub fn parse_report_body(raw_quote: &[u8]) -> eyre::Result { // Validate quote length if raw_quote.len() < HEADER_LENGTH + TD_REPORT10_LENGTH { eyre::bail!( @@ -96,18 +110,62 @@ pub fn compute_workload_id(raw_quote: &[u8]) -> eyre::Result<[u8; 32]> { // Extract fields exactly as parseRawReportBody does in Solidity // Using hardcoded offsets to match Solidity implementation exactly - let mr_td = &report_body[136..136 + 48]; - let rt_mr0 = &report_body[328..328 + 48]; - let rt_mr1 = &report_body[376..376 + 48]; - let rt_mr2 = &report_body[424..424 + 48]; - let rt_mr3 = &report_body[472..472 + 48]; - let mr_config_id = &report_body[184..184 + 48]; + let mr_td: [u8; 48] = report_body[136..136 + 48] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract mr_td"))?; + let rt_mr0: [u8; 48] = report_body[328..328 + 48] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract rt_mr0"))?; + let rt_mr1: [u8; 48] = report_body[376..376 + 48] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract rt_mr1"))?; + let rt_mr2: [u8; 48] = report_body[424..424 + 48] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract rt_mr2"))?; + let rt_mr3: [u8; 48] = report_body[472..472 + 48] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract rt_mr3"))?; + let mr_config_id: [u8; 48] = report_body[184..184 + 48] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract mr_config_id"))?; + let mr_owner: [u8; 48] = report_body[232..232 + 48] + .try_into() + .map_err(|_| eyre::eyre!("failed to extract mr_owner"))?; + let mr_owner_config: [u8; 48] = report_body[280..280 + 48] + .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[128..128 + 8].try_into().unwrap()); - let td_attributes = u64::from_be_bytes(report_body[120..120 + 8].try_into().unwrap()); + let xfam = u64::from_be_bytes( + report_body[128..128 + 8] + .try_into() + .map_err(|e| eyre::eyre!("failed to parse xfam: {}", e))?, + ); + let td_attributes = u64::from_be_bytes( + report_body[120..120 + 8] + .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; @@ -116,10 +174,10 @@ pub fn compute_workload_id(raw_quote: &[u8]) -> eyre::Result<[u8; 32]> { let ignored_td_attributes_bitmask = TD_TDATTRS_VE_DISABLED | TD_TDATTRS_PKS | TD_TDATTRS_KL; // Transform xFAM: xFAM ^ expectedXfamBits - let transformed_xfam = xfam ^ expected_xfam_bits; + let transformed_xfam = parsed.xfam ^ expected_xfam_bits; // Transform tdAttributes: tdAttributes & ~ignoredTdAttributesBitmask - let transformed_td_attributes = td_attributes & !ignored_td_attributes_bitmask; + 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(); @@ -127,12 +185,12 @@ pub fn compute_workload_id(raw_quote: &[u8]) -> eyre::Result<[u8; 32]> { // Concatenate all fields let mut concatenated = Vec::new(); - concatenated.extend_from_slice(mr_td); - concatenated.extend_from_slice(rt_mr0); - concatenated.extend_from_slice(rt_mr1); - concatenated.extend_from_slice(rt_mr2); - concatenated.extend_from_slice(rt_mr3); - concatenated.extend_from_slice(mr_config_id); + 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); @@ -144,7 +202,14 @@ pub fn compute_workload_id(raw_quote: &[u8]) -> eyre::Result<[u8; 32]> { let mut workload_id = [0u8; 32]; workload_id.copy_from_slice(&result); - Ok(workload_id) + 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)] diff --git a/crates/op-rbuilder/src/flashtestations/service.rs b/crates/op-rbuilder/src/flashtestations/service.rs index 5f67e709..f1e97b3a 100644 --- a/crates/op-rbuilder/src/flashtestations/service.rs +++ b/crates/op-rbuilder/src/flashtestations/service.rs @@ -13,11 +13,8 @@ use super::{ tx_manager::TxManager, }; use crate::{ - flashtestations::{ - attestation::compute_workload_id, - builder_tx::{FlashtestationsBuilderTx, FlashtestationsBuilderTxArgs}, - }, - metrics::record_workload_id_metrics, + flashtestations::builder_tx::{FlashtestationsBuilderTx, FlashtestationsBuilderTxArgs}, + metrics::record_tee_metrics, tx_signer::{Signer, generate_key_from_seed, generate_signer}, }; use std::fmt::Debug; @@ -78,9 +75,8 @@ where info!(target: "flashtestations", "requesting TDX attestation"); let attestation = attestation_provider.get_attestation(report_data).await?; - // Compute workload id and record metrics - let workload_id = compute_workload_id(&attestation)?; - record_workload_id_metrics(workload_id); + // Record TEE metrics (workload ID, MRTD, RTMR0) + record_tee_metrics(&attestation)?; // 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 { diff --git a/crates/op-rbuilder/src/metrics.rs b/crates/op-rbuilder/src/metrics.rs index ec1f9593..5e333e19 100644 --- a/crates/op-rbuilder/src/metrics.rs +++ b/crates/op-rbuilder/src/metrics.rs @@ -4,7 +4,10 @@ use reth_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,13 +205,34 @@ pub fn record_flag_gauge_metrics(builder_args: &OpRbuilderArgs) { .set(builder_args.enable_revert_protection as i32); } -/// Set the workload id metrics -pub fn record_workload_id_metrics(workload_id: [u8; 32]) { - let encoded = hex::encode(workload_id); - let encoded_static: &'static str = Box::leak(encoded.into_boxed_str()); - let labels: [(&str, &str); 1] = [("workload_id", encoded_static)]; - let gauge = gauge!("op_rbuilder_workload_id", &labels); - gauge.set(1); +/// 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]) -> 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 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 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. From e919f7e5528d7434e489eb077266222783c4d2bd Mon Sep 17 00:00:00 2001 From: avalonche Date: Sat, 8 Nov 2025 07:51:03 +1100 Subject: [PATCH 3/6] add address --- crates/op-rbuilder/src/flashtestations/service.rs | 2 +- crates/op-rbuilder/src/metrics.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/op-rbuilder/src/flashtestations/service.rs b/crates/op-rbuilder/src/flashtestations/service.rs index f1e97b3a..00372411 100644 --- a/crates/op-rbuilder/src/flashtestations/service.rs +++ b/crates/op-rbuilder/src/flashtestations/service.rs @@ -76,7 +76,7 @@ where let attestation = attestation_provider.get_attestation(report_data).await?; // Record TEE metrics (workload ID, MRTD, RTMR0) - record_tee_metrics(&attestation)?; + 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 { diff --git a/crates/op-rbuilder/src/metrics.rs b/crates/op-rbuilder/src/metrics.rs index 5e333e19..ee86bee5 100644 --- a/crates/op-rbuilder/src/metrics.rs +++ b/crates/op-rbuilder/src/metrics.rs @@ -208,7 +208,7 @@ pub fn record_flag_gauge_metrics(builder_args: &OpRbuilderArgs) { /// 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]) -> eyre::Result<()> { +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); @@ -220,6 +220,10 @@ pub fn record_tee_metrics(raw_quote: &[u8]) -> eyre::Result<()> { 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.to_string().as_str())]; + 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); From bb575b1cf6ccd0580dde1fa3184040175542fa71 Mon Sep 17 00:00:00 2001 From: avalonche Date: Sat, 8 Nov 2025 08:47:46 +1100 Subject: [PATCH 4/6] add address to metric --- crates/op-rbuilder/src/flashtestations/rpc.rs | 90 +++++++++++++++++++ .../src/flashtestations/service.rs | 2 +- crates/op-rbuilder/src/metrics.rs | 4 +- 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 crates/op-rbuilder/src/flashtestations/rpc.rs diff --git a/crates/op-rbuilder/src/flashtestations/rpc.rs b/crates/op-rbuilder/src/flashtestations/rpc.rs new file mode 100644 index 00000000..14fe2910 --- /dev/null +++ b/crates/op-rbuilder/src/flashtestations/rpc.rs @@ -0,0 +1,90 @@ +use alloy_primitives::{hex, keccak256, Bytes}; +use jsonrpsee::proc_macros::rpc; +use jsonrpsee_core::RpcResult; +use std::sync::Arc; +use tokio::sync::RwLock; + +use super::{ + args::FlashtestationsArgs, + attestation::{AttestationConfig, get_attestation_provider}, + service::load_or_generate_tee_key, +}; + +/// Admin API for op-rbuilder flashtestations +#[rpc(server, namespace = "admin")] +pub trait AdminApi { + /// Get the raw attestation quote (cached after first request) + #[method(name = "getAttestationQuote")] + async fn get_attestation_quote(&self) -> RpcResult>; +} + +/// Admin RPC server implementation +pub struct AdminRpcServer { + flashtestations_args: FlashtestationsArgs, + cached_quote: Arc>>>, +} + +impl AdminRpcServer { + pub fn new(flashtestations_args: FlashtestationsArgs) -> Self { + Self { + flashtestations_args, + cached_quote: Arc::new(RwLock::new(None)), + } + } +} + +#[async_trait::async_trait] +impl AdminApiServer for AdminRpcServer { + async fn get_attestation_quote(&self) -> RpcResult> { + // Check if quote is already cached + { + let cache = self.cached_quote.read().await; + if let Some(quote) = cache.as_ref() { + return Ok(Some(hex::encode(quote))); + } + } + + // Load TEE key using same logic as bootstrap + let tee_service_signer = match load_or_generate_tee_key( + &self.flashtestations_args.flashtestations_key_path, + self.flashtestations_args.debug, + &self.flashtestations_args.debug_tee_key_seed, + ) { + Ok(signer) => signer, + Err(e) => { + tracing::error!(error = %e, "Failed to load TEE key"); + return Ok(None); + } + }; + + // Quote not cached, fetch it + let attestation_provider = get_attestation_provider(AttestationConfig { + debug: self.flashtestations_args.debug, + quote_provider: self.flashtestations_args.quote_provider.clone(), + }); + + // Prepare report data same as in bootstrap + let mut report_data = [0u8; 64]; + let tee_address_bytes: [u8; 20] = tee_service_signer.address.into(); + report_data[0..20].copy_from_slice(&tee_address_bytes); + + // Use empty ext_data same as bootstrap + let ext_data = Bytes::from(b""); + let ext_data_hash = keccak256(ext_data.as_ref()); + report_data[20..52].copy_from_slice(ext_data_hash.as_ref()); + + // Request attestation + match attestation_provider.get_attestation(report_data).await { + Ok(quote) => { + // Cache the quote for future requests + let mut cache = self.cached_quote.write().await; + *cache = Some(quote.clone()); + Ok(Some(hex::encode(quote))) + } + Err(e) => { + tracing::error!(error = %e, "Failed to get attestation quote"); + Ok(None) + } + } + } +} diff --git a/crates/op-rbuilder/src/flashtestations/service.rs b/crates/op-rbuilder/src/flashtestations/service.rs index 00372411..73896119 100644 --- a/crates/op-rbuilder/src/flashtestations/service.rs +++ b/crates/op-rbuilder/src/flashtestations/service.rs @@ -76,7 +76,7 @@ where let attestation = attestation_provider.get_attestation(report_data).await?; // Record TEE metrics (workload ID, MRTD, RTMR0) - record_tee_metrics(&attestation, tee_service_signer.address)?; + 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 { diff --git a/crates/op-rbuilder/src/metrics.rs b/crates/op-rbuilder/src/metrics.rs index ee86bee5..c381fed8 100644 --- a/crates/op-rbuilder/src/metrics.rs +++ b/crates/op-rbuilder/src/metrics.rs @@ -1,3 +1,4 @@ +use alloy_primitives::{Address, hex}; use metrics::IntoF64; use reth_metrics::{ Metrics, @@ -216,12 +217,13 @@ pub fn record_tee_metrics(raw_quote: &[u8], tee_address: &Address) -> eyre::Resu 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.to_string().as_str())]; + 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 From 99687b03ddf7482ff56e5874389ebeb9c245e339 Mon Sep 17 00:00:00 2001 From: avalonche Date: Tue, 11 Nov 2025 05:05:30 +1100 Subject: [PATCH 5/6] remove rpc --- crates/op-rbuilder/src/flashtestations/rpc.rs | 90 ------------------- 1 file changed, 90 deletions(-) delete mode 100644 crates/op-rbuilder/src/flashtestations/rpc.rs diff --git a/crates/op-rbuilder/src/flashtestations/rpc.rs b/crates/op-rbuilder/src/flashtestations/rpc.rs deleted file mode 100644 index 14fe2910..00000000 --- a/crates/op-rbuilder/src/flashtestations/rpc.rs +++ /dev/null @@ -1,90 +0,0 @@ -use alloy_primitives::{hex, keccak256, Bytes}; -use jsonrpsee::proc_macros::rpc; -use jsonrpsee_core::RpcResult; -use std::sync::Arc; -use tokio::sync::RwLock; - -use super::{ - args::FlashtestationsArgs, - attestation::{AttestationConfig, get_attestation_provider}, - service::load_or_generate_tee_key, -}; - -/// Admin API for op-rbuilder flashtestations -#[rpc(server, namespace = "admin")] -pub trait AdminApi { - /// Get the raw attestation quote (cached after first request) - #[method(name = "getAttestationQuote")] - async fn get_attestation_quote(&self) -> RpcResult>; -} - -/// Admin RPC server implementation -pub struct AdminRpcServer { - flashtestations_args: FlashtestationsArgs, - cached_quote: Arc>>>, -} - -impl AdminRpcServer { - pub fn new(flashtestations_args: FlashtestationsArgs) -> Self { - Self { - flashtestations_args, - cached_quote: Arc::new(RwLock::new(None)), - } - } -} - -#[async_trait::async_trait] -impl AdminApiServer for AdminRpcServer { - async fn get_attestation_quote(&self) -> RpcResult> { - // Check if quote is already cached - { - let cache = self.cached_quote.read().await; - if let Some(quote) = cache.as_ref() { - return Ok(Some(hex::encode(quote))); - } - } - - // Load TEE key using same logic as bootstrap - let tee_service_signer = match load_or_generate_tee_key( - &self.flashtestations_args.flashtestations_key_path, - self.flashtestations_args.debug, - &self.flashtestations_args.debug_tee_key_seed, - ) { - Ok(signer) => signer, - Err(e) => { - tracing::error!(error = %e, "Failed to load TEE key"); - return Ok(None); - } - }; - - // Quote not cached, fetch it - let attestation_provider = get_attestation_provider(AttestationConfig { - debug: self.flashtestations_args.debug, - quote_provider: self.flashtestations_args.quote_provider.clone(), - }); - - // Prepare report data same as in bootstrap - let mut report_data = [0u8; 64]; - let tee_address_bytes: [u8; 20] = tee_service_signer.address.into(); - report_data[0..20].copy_from_slice(&tee_address_bytes); - - // Use empty ext_data same as bootstrap - let ext_data = Bytes::from(b""); - let ext_data_hash = keccak256(ext_data.as_ref()); - report_data[20..52].copy_from_slice(ext_data_hash.as_ref()); - - // Request attestation - match attestation_provider.get_attestation(report_data).await { - Ok(quote) => { - // Cache the quote for future requests - let mut cache = self.cached_quote.write().await; - *cache = Some(quote.clone()); - Ok(Some(hex::encode(quote))) - } - Err(e) => { - tracing::error!(error = %e, "Failed to get attestation quote"); - Ok(None) - } - } - } -} From c4bd1be2cc762897570826cccd3a62e55b224a96 Mon Sep 17 00:00:00 2001 From: avalonche Date: Tue, 11 Nov 2025 05:11:52 +1100 Subject: [PATCH 6/6] extract constants --- .../src/flashtestations/attestation.rs | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/crates/op-rbuilder/src/flashtestations/attestation.rs b/crates/op-rbuilder/src/flashtestations/attestation.rs index f8aa7238..6af5cc24 100644 --- a/crates/op-rbuilder/src/flashtestations/attestation.rs +++ b/crates/op-rbuilder/src/flashtestations/attestation.rs @@ -16,6 +16,23 @@ 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 { @@ -109,41 +126,44 @@ pub fn parse_report_body(raw_quote: &[u8]) -> eyre::Result { let report_body = &raw_quote[HEADER_LENGTH..]; // Extract fields exactly as parseRawReportBody does in Solidity - // Using hardcoded offsets to match Solidity implementation exactly - let mr_td: [u8; 48] = report_body[136..136 + 48] + // 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[328..328 + 48] + 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[376..376 + 48] + 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[424..424 + 48] + 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[472..472 + 48] + 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[184..184 + 48] + 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[232..232 + 48] + 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[280..280 + 48] + 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[128..128 + 8] + 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[120..120 + 8] + report_body[OFFSET_TD_ATTRIBUTES..OFFSET_TD_ATTRIBUTES + ATTRIBUTE_LENGTH] .try_into() .map_err(|e| eyre::eyre!("failed to parse td_attributes: {}", e))?, );