Skip to content

Commit 592ddad

Browse files
authored
Add workload id as metric to builder (#315)
* Add workload id as metric to builder * more tdx measurements * add address * add address to metric * remove rpc * extract constants
1 parent 04c4d78 commit 592ddad

File tree

3 files changed

+234
-1
lines changed

3 files changed

+234
-1
lines changed

crates/op-rbuilder/src/flashtestations/attestation.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,53 @@
11
use reqwest::Client;
2+
use sha3::{Digest, Keccak256};
23
use tracing::info;
34

45
const DEBUG_QUOTE_SERVICE_URL: &str = "http://ns31695324.ip-141-94-163.eu:10080/attest";
56

7+
// Raw TDX v4 quote structure constants
8+
// Raw quote has a 48-byte header before the TD10ReportBody
9+
const HEADER_LENGTH: usize = 48;
10+
const TD_REPORT10_LENGTH: usize = 584;
11+
12+
// TDX workload constants
13+
const TD_XFAM_FPU: u64 = 0x0000000000000001;
14+
const TD_XFAM_SSE: u64 = 0x0000000000000002;
15+
const TD_TDATTRS_VE_DISABLED: u64 = 0x0000000010000000;
16+
const TD_TDATTRS_PKS: u64 = 0x0000000040000000;
17+
const TD_TDATTRS_KL: u64 = 0x0000000080000000;
18+
19+
// TD10ReportBody field offsets
20+
// These offsets correspond to the Solidity parseRawReportBody implementation
21+
const OFFSET_TD_ATTRIBUTES: usize = 120;
22+
const OFFSET_XFAM: usize = 128;
23+
const OFFSET_MR_TD: usize = 136;
24+
const OFFSET_MR_CONFIG_ID: usize = 184;
25+
const OFFSET_MR_OWNER: usize = 232;
26+
const OFFSET_MR_OWNER_CONFIG: usize = 280;
27+
const OFFSET_RT_MR0: usize = 328;
28+
const OFFSET_RT_MR1: usize = 376;
29+
const OFFSET_RT_MR2: usize = 424;
30+
const OFFSET_RT_MR3: usize = 472;
31+
32+
// Field lengths
33+
const MEASUREMENT_REGISTER_LENGTH: usize = 48;
34+
const ATTRIBUTE_LENGTH: usize = 8;
35+
36+
/// Parsed TDX quote report body containing measurement registers and attributes
37+
#[derive(Debug, Clone)]
38+
pub struct ParsedQuote {
39+
pub mr_td: [u8; 48],
40+
pub rt_mr0: [u8; 48],
41+
pub rt_mr1: [u8; 48],
42+
pub rt_mr2: [u8; 48],
43+
pub rt_mr3: [u8; 48],
44+
pub mr_config_id: [u8; 48],
45+
pub mr_owner: [u8; 48],
46+
pub mr_owner_config: [u8; 48],
47+
pub xfam: u64,
48+
pub td_attributes: u64,
49+
}
50+
651
/// Configuration for attestation
752
#[derive(Default)]
853
pub struct AttestationConfig {
@@ -63,3 +108,148 @@ pub fn get_attestation_provider(config: AttestationConfig) -> RemoteAttestationP
63108
)
64109
}
65110
}
111+
112+
/// Parse the TDX report body from a raw quote
113+
/// Extracts measurement registers and attributes according to TD10ReportBody specification
114+
/// https://github.com/flashbots/flashtestations/tree/7cc7f68492fe672a823dd2dead649793aac1f216
115+
pub fn parse_report_body(raw_quote: &[u8]) -> eyre::Result<ParsedQuote> {
116+
// Validate quote length
117+
if raw_quote.len() < HEADER_LENGTH + TD_REPORT10_LENGTH {
118+
eyre::bail!(
119+
"invalid quote length: {}, expected at least {}",
120+
raw_quote.len(),
121+
HEADER_LENGTH + TD_REPORT10_LENGTH
122+
);
123+
}
124+
125+
// Skip the 48-byte header to get to the TD10ReportBody
126+
let report_body = &raw_quote[HEADER_LENGTH..];
127+
128+
// Extract fields exactly as parseRawReportBody does in Solidity
129+
// Using named offset constants to match Solidity implementation exactly
130+
let mr_td: [u8; 48] = report_body[OFFSET_MR_TD..OFFSET_MR_TD + MEASUREMENT_REGISTER_LENGTH]
131+
.try_into()
132+
.map_err(|_| eyre::eyre!("failed to extract mr_td"))?;
133+
let rt_mr0: [u8; 48] = report_body[OFFSET_RT_MR0..OFFSET_RT_MR0 + MEASUREMENT_REGISTER_LENGTH]
134+
.try_into()
135+
.map_err(|_| eyre::eyre!("failed to extract rt_mr0"))?;
136+
let rt_mr1: [u8; 48] = report_body[OFFSET_RT_MR1..OFFSET_RT_MR1 + MEASUREMENT_REGISTER_LENGTH]
137+
.try_into()
138+
.map_err(|_| eyre::eyre!("failed to extract rt_mr1"))?;
139+
let rt_mr2: [u8; 48] = report_body[OFFSET_RT_MR2..OFFSET_RT_MR2 + MEASUREMENT_REGISTER_LENGTH]
140+
.try_into()
141+
.map_err(|_| eyre::eyre!("failed to extract rt_mr2"))?;
142+
let rt_mr3: [u8; 48] = report_body[OFFSET_RT_MR3..OFFSET_RT_MR3 + MEASUREMENT_REGISTER_LENGTH]
143+
.try_into()
144+
.map_err(|_| eyre::eyre!("failed to extract rt_mr3"))?;
145+
let mr_config_id: [u8; 48] = report_body
146+
[OFFSET_MR_CONFIG_ID..OFFSET_MR_CONFIG_ID + MEASUREMENT_REGISTER_LENGTH]
147+
.try_into()
148+
.map_err(|_| eyre::eyre!("failed to extract mr_config_id"))?;
149+
let mr_owner: [u8; 48] = report_body
150+
[OFFSET_MR_OWNER..OFFSET_MR_OWNER + MEASUREMENT_REGISTER_LENGTH]
151+
.try_into()
152+
.map_err(|_| eyre::eyre!("failed to extract mr_owner"))?;
153+
let mr_owner_config: [u8; 48] = report_body
154+
[OFFSET_MR_OWNER_CONFIG..OFFSET_MR_OWNER_CONFIG + MEASUREMENT_REGISTER_LENGTH]
155+
.try_into()
156+
.map_err(|_| eyre::eyre!("failed to extract mr_owner_config"))?;
157+
158+
// Extract xFAM and tdAttributes (8 bytes each)
159+
// In Solidity, bytes8 is treated as big-endian for bitwise operations
160+
let xfam = u64::from_be_bytes(
161+
report_body[OFFSET_XFAM..OFFSET_XFAM + ATTRIBUTE_LENGTH]
162+
.try_into()
163+
.map_err(|e| eyre::eyre!("failed to parse xfam: {}", e))?,
164+
);
165+
let td_attributes = u64::from_be_bytes(
166+
report_body[OFFSET_TD_ATTRIBUTES..OFFSET_TD_ATTRIBUTES + ATTRIBUTE_LENGTH]
167+
.try_into()
168+
.map_err(|e| eyre::eyre!("failed to parse td_attributes: {}", e))?,
169+
);
170+
171+
Ok(ParsedQuote {
172+
mr_td,
173+
rt_mr0,
174+
rt_mr1,
175+
rt_mr2,
176+
rt_mr3,
177+
mr_config_id,
178+
mr_owner,
179+
mr_owner_config,
180+
xfam,
181+
td_attributes,
182+
})
183+
}
184+
185+
/// Compute workload ID from parsed quote data
186+
/// This corresponds to QuoteParser.parseV4VerifierOutput in Solidity implementation
187+
/// The workload ID uniquely identifies a TEE workload based on its measurement registers
188+
pub fn compute_workload_id_from_parsed(parsed: &ParsedQuote) -> [u8; 32] {
189+
// Apply transformations as per the Solidity implementation
190+
// expectedXfamBits = TD_XFAM_FPU | TD_XFAM_SSE
191+
let expected_xfam_bits = TD_XFAM_FPU | TD_XFAM_SSE;
192+
193+
// ignoredTdAttributesBitmask = TD_TDATTRS_VE_DISABLED | TD_TDATTRS_PKS | TD_TDATTRS_KL
194+
let ignored_td_attributes_bitmask = TD_TDATTRS_VE_DISABLED | TD_TDATTRS_PKS | TD_TDATTRS_KL;
195+
196+
// Transform xFAM: xFAM ^ expectedXfamBits
197+
let transformed_xfam = parsed.xfam ^ expected_xfam_bits;
198+
199+
// Transform tdAttributes: tdAttributes & ~ignoredTdAttributesBitmask
200+
let transformed_td_attributes = parsed.td_attributes & !ignored_td_attributes_bitmask;
201+
202+
// Convert transformed values to bytes (big-endian, to match Solidity bytes8)
203+
let xfam_bytes = transformed_xfam.to_be_bytes();
204+
let td_attributes_bytes = transformed_td_attributes.to_be_bytes();
205+
206+
// Concatenate all fields
207+
let mut concatenated = Vec::new();
208+
concatenated.extend_from_slice(&parsed.mr_td);
209+
concatenated.extend_from_slice(&parsed.rt_mr0);
210+
concatenated.extend_from_slice(&parsed.rt_mr1);
211+
concatenated.extend_from_slice(&parsed.rt_mr2);
212+
concatenated.extend_from_slice(&parsed.rt_mr3);
213+
concatenated.extend_from_slice(&parsed.mr_config_id);
214+
concatenated.extend_from_slice(&xfam_bytes);
215+
concatenated.extend_from_slice(&td_attributes_bytes);
216+
217+
// Compute keccak256 hash
218+
let mut hasher = Keccak256::new();
219+
hasher.update(&concatenated);
220+
let result = hasher.finalize();
221+
222+
let mut workload_id = [0u8; 32];
223+
workload_id.copy_from_slice(&result);
224+
225+
workload_id
226+
}
227+
228+
/// Compute workload ID from raw quote bytes
229+
/// This is a convenience function that combines parsing and computation
230+
pub fn compute_workload_id(raw_quote: &[u8]) -> eyre::Result<[u8; 32]> {
231+
let parsed = parse_report_body(raw_quote)?;
232+
Ok(compute_workload_id_from_parsed(&parsed))
233+
}
234+
235+
#[cfg(test)]
236+
mod tests {
237+
use crate::tests::WORKLOAD_ID;
238+
239+
use super::*;
240+
241+
#[test]
242+
fn test_compute_workload_id_from_test_quote() {
243+
// Load the test quote output used in integration tests
244+
let quote_output = include_bytes!("../tests/framework/artifacts/test-quote.bin");
245+
246+
// Compute the workload ID
247+
let workload_id = compute_workload_id(quote_output)
248+
.expect("failed to compute workload ID from test quote");
249+
250+
assert_eq!(
251+
workload_id, WORKLOAD_ID,
252+
"workload ID mismatch for test quote"
253+
);
254+
}
255+
}

crates/op-rbuilder/src/flashtestations/service.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use super::{
1414
};
1515
use crate::{
1616
flashtestations::builder_tx::{FlashtestationsBuilderTx, FlashtestationsBuilderTxArgs},
17+
metrics::record_tee_metrics,
1718
tx_signer::{Signer, generate_key_from_seed, generate_signer},
1819
};
1920
use std::fmt::Debug;
@@ -74,6 +75,9 @@ where
7475
info!(target: "flashtestations", "requesting TDX attestation");
7576
let attestation = attestation_provider.get_attestation(report_data).await?;
7677

78+
// Record TEE metrics (workload ID, MRTD, RTMR0)
79+
record_tee_metrics(&attestation, &tee_service_signer.address)?;
80+
7781
// Use an external rpc when the builder is not the same as the builder actively building blocks onchain
7882
let registered = if let Some(rpc_url) = args.rpc_url {
7983
let tx_manager = TxManager::new(

crates/op-rbuilder/src/metrics.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
use alloy_primitives::{Address, hex};
12
use metrics::IntoF64;
23
use reth_metrics::{
34
Metrics,
45
metrics::{Counter, Gauge, Histogram, gauge},
56
};
67

7-
use crate::args::OpRbuilderArgs;
8+
use crate::{
9+
args::OpRbuilderArgs,
10+
flashtestations::attestation::{compute_workload_id_from_parsed, parse_report_body},
11+
};
812

913
/// The latest version from Cargo.toml.
1014
pub const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -202,6 +206,41 @@ pub fn record_flag_gauge_metrics(builder_args: &OpRbuilderArgs) {
202206
.set(builder_args.enable_revert_protection as i32);
203207
}
204208

209+
/// Record TEE workload ID and measurement metrics
210+
/// Parses the quote, computes workload ID, and records workload_id, mr_td (TEE measurement), and rt_mr0 (runtime measurement register 0)
211+
/// These identify the trusted execution environment configuration provided by GCP
212+
pub fn record_tee_metrics(raw_quote: &[u8], tee_address: &Address) -> eyre::Result<()> {
213+
let parsed_quote = parse_report_body(raw_quote)?;
214+
let workload_id = compute_workload_id_from_parsed(&parsed_quote);
215+
216+
let workload_id_hex = hex::encode(workload_id);
217+
let mr_td_hex = hex::encode(parsed_quote.mr_td);
218+
let rt_mr0_hex = hex::encode(parsed_quote.rt_mr0);
219+
220+
let tee_address_static: &'static str = Box::leak(tee_address.to_string().into_boxed_str());
221+
let workload_id_static: &'static str = Box::leak(workload_id_hex.into_boxed_str());
222+
let mr_td_static: &'static str = Box::leak(mr_td_hex.into_boxed_str());
223+
let rt_mr0_static: &'static str = Box::leak(rt_mr0_hex.into_boxed_str());
224+
225+
// Record TEE address
226+
let tee_address_labels: [(&str, &str); 1] = [("tee_address", tee_address_static)];
227+
gauge!("op_rbuilder_tee_address", &tee_address_labels).set(1);
228+
229+
// Record workload ID
230+
let workload_labels: [(&str, &str); 1] = [("workload_id", workload_id_static)];
231+
gauge!("op_rbuilder_tee_workload_id", &workload_labels).set(1);
232+
233+
// Record MRTD (TEE measurement)
234+
let mr_td_labels: [(&str, &str); 1] = [("mr_td", mr_td_static)];
235+
gauge!("op_rbuilder_tee_mr_td", &mr_td_labels).set(1);
236+
237+
// Record RTMR0 (runtime measurement register 0)
238+
let rt_mr0_labels: [(&str, &str); 1] = [("rt_mr0", rt_mr0_static)];
239+
gauge!("op_rbuilder_tee_rt_mr0", &rt_mr0_labels).set(1);
240+
241+
Ok(())
242+
}
243+
205244
/// Contains version information for the application.
206245
#[derive(Debug, Clone)]
207246
pub struct VersionInfo {

0 commit comments

Comments
 (0)