Skip to content
Draft
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
4 changes: 4 additions & 0 deletions blacklight-node/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ async fn process_htx_assignment(
info!(htx_id = ?htx_id, "Detected Phala HTX");
verifier.verify_phala_htx(&htx).await
}
Htx::Erc8004(htx) => {
info!(htx_id = ?htx_id, "Detected ERC8004 HTX");
verifier.verify_erc8004_htx(&htx).await
}
},
Err(e) => {
error!(htx_id = ?htx_id, error = %e, "Failed to parse HTX data");
Expand Down
42 changes: 40 additions & 2 deletions blacklight-node/src/verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use attestation_verification::{
};
use attestation_verification::{VerificationError as ExtVerificationError, VmType};
use blacklight_contract_clients::heartbeat_manager::Verdict;
use blacklight_contract_clients::htx::{NillionHtx, PhalaHtx};
use blacklight_contract_clients::htx::{Erc8004Htx, NillionHtx, PhalaHtx};
use dcap_qvl::collateral::get_collateral_and_verify;
use reqwest::Client;
use sha2::{Digest, Sha256};
Expand All @@ -31,6 +31,7 @@ pub enum VerificationError {
PhalaEventLogParse(String),
FetchCerts(String),
DetectProcessor(String),
Erc8004EndpointUnreachable(String),

// Malicious errors - cryptographic verification failures
VerifyReport(String),
Expand Down Expand Up @@ -58,7 +59,8 @@ impl VerificationError {
| PhalaEventLogParse(_)
| FetchCerts(_)
| InvalidCertificate(_)
| DetectProcessor(_) => Verdict::Inconclusive,
| DetectProcessor(_)
| Erc8004EndpointUnreachable(_) => Verdict::Inconclusive,

// Failure - cryptographic verification failures (indicates potential tampering)
VerifyReport(_)
Expand Down Expand Up @@ -92,6 +94,9 @@ impl VerificationError {
FetchCerts(e) => format!("could not fetch AMD certificates: {e}"),
DetectProcessor(e) => format!("could not detect processor type: {e}"),
InvalidCertificate(e) => format!("invalid certificate obtained from AMD: {e}"),
Erc8004EndpointUnreachable(e) => {
format!("ERC-8004 endpoint unreachable or unhealthy: {e}")
}

// Malicious errors
VerifyReport(e) => format!("attestation report verification failed: {e}"),
Expand Down Expand Up @@ -305,6 +310,35 @@ impl HtxVerifier {

Ok(())
}

/// Verify an ERC8004 HTX by performing a health check on the endpoint.
///
/// Validation step: HTTP GET the endpoint URL. Success if response is 2xx.
///
/// Returns Ok(()) if the endpoint is alive and healthy, Err(VerificationError) otherwise.
pub async fn verify_erc8004_htx(&self, htx: &Erc8004Htx) -> Result<(), VerificationError> {
let Erc8004Htx::V1(htx) = htx;

let client = Client::builder()
.timeout(std::time::Duration::from_secs(10))
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Failed to build HTTP client");

let resp = client.get(&htx.endpoint).send().await.map_err(|e| {
VerificationError::Erc8004EndpointUnreachable(format!("request failed: {e}"))
})?;

let status = resp.status();
if !status.is_success() {
return Err(VerificationError::Erc8004EndpointUnreachable(format!(
"endpoint returned {}",
status.as_u16()
)));
}

Ok(())
}
}

#[derive(Default)]
Expand Down Expand Up @@ -344,6 +378,9 @@ mod tests {

let err = VerificationError::PhalaQuoteVerify("quote error".to_string());
assert!(err.message().contains("quote verification failed"));

let err = VerificationError::Erc8004EndpointUnreachable("timeout".to_string());
assert!(err.message().contains("ERC-8004 endpoint unreachable"));
}

#[test]
Expand All @@ -356,6 +393,7 @@ mod tests {
VerificationError::PhalaEventLogParse("missing field".to_string()),
VerificationError::FetchCerts("AMD server unreachable".to_string()),
VerificationError::DetectProcessor("unknown CPU".to_string()),
VerificationError::Erc8004EndpointUnreachable("connection refused".to_string()),
];

for err in inconclusive_errors {
Expand Down
37 changes: 36 additions & 1 deletion crates/blacklight-contract-clients/src/htx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,30 @@ pub enum PhalaHtx {
V1(PhalaHtxV1),
}

// Unified HTX type that can represent both nilCC and Phala HTXs
// ERC8004 HTX types (endpoint health-check validation)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Erc8004HtxV1 {
pub endpoint: String,
/// nilCC URL if needed for validation (defaults to null)
#[serde(rename = "nilcc_url")]
pub nilcc_url: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "version", rename_all = "camelCase")]
pub enum Erc8004Htx {
/// The first ERC8004 HTX format version.
V1(Erc8004HtxV1),
}

// Unified HTX type that can represent nilCC, Phala, and ERC8004 HTXs
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "provider", rename_all = "camelCase")]
pub enum Htx {
Nillion(NillionHtx),
Phala(PhalaHtx),
#[serde(rename = "ERC-8004")]
Erc8004(Erc8004Htx),
}

impl From<NillionHtx> for Htx {
Expand Down Expand Up @@ -243,4 +261,21 @@ mod tests {
let htx: Htx = serde_json::from_str(nilcc_json).unwrap();
assert!(matches!(htx, Htx::Nillion(_)), "not a nillion HTX");
}

#[test]
fn test_deserialize_erc8004() {
let erc8004_json = r#"{
"provider": "ERC-8004",
"version": "v1",
"endpoint": "https://api.nilai.nillion.network/v1/health",
"nilcc_url": null
}"#;

let htx: Htx = serde_json::from_str(erc8004_json).unwrap();
let Htx::Erc8004(Erc8004Htx::V1(htx)) = htx else {
panic!("not an ERC-8004 HTX");
};
assert_eq!(htx.endpoint, "https://api.nilai.nillion.network/v1/health");
assert_eq!(htx.nilcc_url, None);
}
}
6 changes: 6 additions & 0 deletions data/erc8004_htx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"provider": "ERC-8004",
"version": "v1",
"endpoint": "https://api.nilai.nillion.network/v1/health",
"nilcc_url": null
}
4 changes: 4 additions & 0 deletions nilcc-simulator/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ async fn submit_next_htx(
Htx::Phala(PhalaHtx::V1(htx)) => {
htx.app_compose = format!("{}-{:x}", htx.app_compose, nonce);
}
Htx::Erc8004(_) => {
// ERC8004: endpoint is fixed (health URL), no per-submission uniquification
// TODO: add a nonce to the agent_id
}
}
htx
};
Expand Down