Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions crates/op-rbuilder/src/flashtestations/attestation.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<ParsedQuote> {
// 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"
);
}
}
4 changes: 4 additions & 0 deletions crates/op-rbuilder/src/flashtestations/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
41 changes: 40 additions & 1 deletion crates/op-rbuilder/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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 {
Expand Down
Loading