use async_trait::async_trait;
use std::time::Duration;
use tokio::sync::RwLock;
use crate::da::{DaBackend, DaBackendId, DaBackendStatus, DaPointer};
use crate::error::{Result, StorageError};
pub struct EigenDaBackend {
client: reqwest::Client,
base_url: String,
last_submission_ms: RwLock<Option<i64>>,
last_fetch_ms: RwLock<Option<i64>>,
}
impl EigenDaBackend {
pub fn connect(base_url: impl Into<String>) -> Result<Self> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(60))
.build()
.map_err(|e| {
StorageError::Generic(format!("EigenDA reqwest client build failed: {e}"))
})?;
let mut base = base_url.into();
while base.ends_with('/') {
base.pop();
}
if base.is_empty() {
return Err(StorageError::InvalidValue(
"EigenDA base URL must not be empty".into(),
));
}
Ok(Self {
client,
base_url: base,
last_submission_ms: RwLock::new(None),
last_fetch_ms: RwLock::new(None),
})
}
fn get_url(&self, commitment_hex: &str) -> String {
format!("{}/get/0x{}", self.base_url, commitment_hex)
}
fn put_url(&self) -> String {
format!("{}/put/", self.base_url)
}
}
fn now_ms() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
fn decode_commitment_hex(body: &str) -> Result<Vec<u8>> {
let trimmed = body.trim();
let stripped = trimmed.strip_prefix("0x").unwrap_or(trimmed);
hex::decode(stripped).map_err(|e| {
StorageError::InvalidValue(format!(
"EigenDA proxy returned non-hex commitment {trimmed:?}: {e}"
))
})
}
fn locator_to_hex(locator: &[u8]) -> Result<String> {
let s = std::str::from_utf8(locator).map_err(|e| {
StorageError::InvalidValue(format!("EigenDA locator not valid UTF-8: {e}"))
})?;
let stripped = s.strip_prefix("0x").unwrap_or(s);
let bytes = hex::decode(stripped).map_err(|e| {
StorageError::InvalidValue(format!("EigenDA locator hex invalid: {e}"))
})?;
Ok(hex::encode(bytes))
}
#[async_trait]
impl DaBackend for EigenDaBackend {
fn id(&self) -> DaBackendId {
DaBackendId::EigenDA
}
fn status(&self) -> DaBackendStatus {
let last_submission_ms = self.last_submission_ms.try_read().ok().and_then(|g| *g);
let last_fetch_ms = self.last_fetch_ms.try_read().ok().and_then(|g| *g);
DaBackendStatus {
backend: DaBackendId::EigenDA,
healthy: true,
last_submission_ms,
last_fetch_ms,
error_rate_bps: 0,
}
}
async fn submit(&self, namespace: &[u8], payload: &[u8]) -> Result<DaPointer> {
let resp = self
.client
.post(self.put_url())
.header(reqwest::header::CONTENT_TYPE, "application/octet-stream")
.body(payload.to_vec())
.send()
.await
.map_err(|e| StorageError::Generic(format!("EigenDA POST /put/ failed: {e}")))?;
let status = resp.status();
let body = resp
.text()
.await
.map_err(|e| StorageError::Generic(format!("EigenDA /put/ body read failed: {e}")))?;
if !status.is_success() {
return Err(StorageError::Generic(format!(
"EigenDA /put/ returned HTTP {status}: {body}"
)));
}
let commitment_bytes = decode_commitment_hex(&body)?;
*self.last_submission_ms.write().await = Some(now_ms());
Ok(DaPointer {
backend: DaBackendId::EigenDA,
namespace: namespace.to_vec(),
locator: hex::encode(&commitment_bytes).into_bytes(),
commitment_kzg: Some(commitment_bytes),
attestation_root: None,
})
}
async fn fetch(&self, pointer: &DaPointer) -> Result<Vec<u8>> {
if pointer.backend != DaBackendId::EigenDA {
return Err(StorageError::InvalidValue(format!(
"EigenDaBackend cannot fetch pointer for backend {:?}",
pointer.backend
)));
}
let hex_str = locator_to_hex(&pointer.locator)?;
let resp = self
.client
.get(self.get_url(&hex_str))
.send()
.await
.map_err(|e| StorageError::Generic(format!("EigenDA GET /get/ failed: {e}")))?;
let status = resp.status();
let bytes = resp
.bytes()
.await
.map_err(|e| StorageError::Generic(format!("EigenDA /get/ body read failed: {e}")))?;
if !status.is_success() {
return Err(StorageError::Generic(format!(
"EigenDA /get/ returned HTTP {status} for commitment 0x{hex_str}"
)));
}
*self.last_fetch_ms.write().await = Some(now_ms());
Ok(bytes.to_vec())
}
async fn verify_availability(&self, pointer: &DaPointer) -> Result<()> {
if pointer.backend != DaBackendId::EigenDA {
return Err(StorageError::InvalidValue(format!(
"EigenDaBackend cannot verify pointer for backend {:?}",
pointer.backend
)));
}
let hex_str = locator_to_hex(&pointer.locator)?;
let resp = self
.client
.head(self.get_url(&hex_str))
.send()
.await
.map_err(|e| StorageError::Generic(format!("EigenDA HEAD /get/ failed: {e}")))?;
if resp.status().is_success() {
Ok(())
} else {
Err(StorageError::Generic(format!(
"EigenDA proxy reports commitment 0x{hex_str} unavailable: HTTP {}",
resp.status()
)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::da::compute_commitment;
#[test]
fn connect_rejects_empty_base_url() {
assert!(EigenDaBackend::connect("").is_err());
assert!(EigenDaBackend::connect("///").is_err());
}
#[test]
fn connect_strips_trailing_slash() {
let b = EigenDaBackend::connect("http://localhost:3100/").unwrap();
assert_eq!(b.base_url, "http://localhost:3100");
assert_eq!(b.put_url(), "http://localhost:3100/put/");
assert_eq!(b.get_url("deadbeef"), "http://localhost:3100/get/0xdeadbeef");
}
#[test]
fn decode_commitment_hex_accepts_prefix_and_whitespace() {
let body = " 0xDEADBEEF\n";
let bytes = decode_commitment_hex(body).unwrap();
assert_eq!(bytes, vec![0xde, 0xad, 0xbe, 0xef]);
}
#[test]
fn decode_commitment_hex_rejects_garbage() {
assert!(decode_commitment_hex("xyz").is_err());
assert!(decode_commitment_hex("0xnothex").is_err());
}
#[test]
fn locator_to_hex_round_trip() {
let commitment = vec![0xab, 0xcd, 0xef, 0x01];
let locator = hex::encode(&commitment).into_bytes();
let hex_str = locator_to_hex(&locator).unwrap();
assert_eq!(hex_str, "abcdef01");
let prefixed = format!("0x{}", hex_str).into_bytes();
assert_eq!(locator_to_hex(&prefixed).unwrap(), hex_str);
}
#[test]
fn locator_to_hex_rejects_non_utf8() {
let bad = vec![0xff, 0xfe, 0xfd];
assert!(locator_to_hex(&bad).is_err());
}
#[tokio::test]
#[ignore = "requires EIGENDA_PROXY_URL env var and a reachable eigenda-proxy"]
async fn live_submit_fetch_verify_round_trip() {
let url = match std::env::var("EIGENDA_PROXY_URL") {
Ok(u) => u,
Err(_) => {
eprintln!("skipping: EIGENDA_PROXY_URL not set");
return;
}
};
let backend = EigenDaBackend::connect(url).expect("connect");
let payload = b"tenzro eigenda DA round-trip smoke test".to_vec();
let sha = compute_commitment(&payload);
let pointer = backend.submit(b"tenzro/inference", &payload).await.expect("submit");
assert_eq!(pointer.backend, DaBackendId::EigenDA);
assert!(pointer.commitment_kzg.is_some());
assert_ne!(
pointer.commitment_kzg.as_ref().unwrap().as_slice(),
sha.as_bytes()
);
let fetched = backend.fetch(&pointer).await.expect("fetch");
assert_eq!(fetched, payload);
assert_eq!(compute_commitment(&fetched), sha);
backend.verify_availability(&pointer).await.expect("verify");
}
}