use alloy::primitives::B256;
use async_trait::async_trait;
use eigensdk::{crypto_bls::BlsG1Point, types::operator::OperatorId};
use newton_prover_core::state_commit_registry::IStateRootCommittable::StateCommit;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OperatorProposal {
pub new_state_root: B256,
pub da_cert_hash: B256,
pub pcr0_commitment: B256,
}
#[derive(Debug, Error)]
pub enum OperatorClientError {
#[error("operator {operator_id} disagrees on state commit digest at sequence {sequence_no}")]
DigestDisagreement {
operator_id: String,
sequence_no: u64,
},
#[error("operator {operator_id} RPC transport failure: {source}")]
Transport {
operator_id: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("operator {operator_id} returned malformed response: {reason}")]
Malformed {
operator_id: String,
reason: String,
},
#[error("operator {operator_id} timed out after {timeout_ms}ms")]
Timeout {
operator_id: String,
timeout_ms: u64,
},
}
#[async_trait]
pub trait StateCommitOperatorClient: Send + Sync + 'static {
async fn get_state_commit_proposal(
&self,
operator_id: &OperatorId,
sequence_no: u64,
) -> Result<OperatorProposal, OperatorClientError>;
async fn sign_state_commit(
&self,
operator_id: &OperatorId,
digest: B256,
commit: &StateCommit,
reference_timestamp: u32,
) -> Result<BlsG1Point, OperatorClientError>;
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use ark_bn254::G1Affine;
use ark_ec::AffineRepr;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
pub(crate) fn dummy_state_commit() -> StateCommit {
StateCommit {
version: 1,
sequenceNo: 0,
prevStateRoot: B256::ZERO,
newStateRoot: B256::ZERO,
timestamp: 0,
daCertHash: B256::ZERO,
pcr0Commitment: B256::ZERO,
}
}
pub(crate) fn default_proposal() -> OperatorProposal {
OperatorProposal {
new_state_root: B256::repeat_byte(0xaa),
da_cert_hash: B256::repeat_byte(0xbb),
pcr0_commitment: B256::repeat_byte(0xcc),
}
}
pub(crate) struct FakeStateCommitOperatorClient {
state: Arc<Mutex<FakeClientState>>,
}
struct FakeClientState {
default_proposal: OperatorProposal,
proposals: HashMap<String, OperatorProposal>,
errors: HashMap<String, OperatorClientError>,
proposal_call_count: usize,
sign_call_count: usize,
}
impl FakeStateCommitOperatorClient {
pub(crate) fn new() -> Self {
Self {
state: Arc::new(Mutex::new(FakeClientState {
default_proposal: default_proposal(),
proposals: HashMap::new(),
errors: HashMap::new(),
proposal_call_count: 0,
sign_call_count: 0,
})),
}
}
pub(crate) fn set_proposal_for(&self, operator_id: &OperatorId, proposal: OperatorProposal) {
let key = hex::encode(operator_id);
self.state.lock().unwrap().proposals.insert(key, proposal);
}
pub(crate) fn inject_error(&self, operator_id: &OperatorId, error: OperatorClientError) {
let key = hex::encode(operator_id);
self.state.lock().unwrap().errors.insert(key, error);
}
pub(crate) fn proposal_call_count(&self) -> usize {
self.state.lock().unwrap().proposal_call_count
}
pub(crate) fn sign_call_count(&self) -> usize {
self.state.lock().unwrap().sign_call_count
}
}
#[async_trait]
impl StateCommitOperatorClient for FakeStateCommitOperatorClient {
async fn get_state_commit_proposal(
&self,
operator_id: &OperatorId,
_sequence_no: u64,
) -> Result<OperatorProposal, OperatorClientError> {
let key = hex::encode(operator_id);
let mut state = self.state.lock().unwrap();
state.proposal_call_count += 1;
if let Some(err) = state.errors.remove(&key) {
return Err(err);
}
let proposal = state.proposals.get(&key).copied().unwrap_or(state.default_proposal);
Ok(proposal)
}
async fn sign_state_commit(
&self,
operator_id: &OperatorId,
_digest: B256,
_commit: &StateCommit,
_reference_timestamp: u32,
) -> Result<BlsG1Point, OperatorClientError> {
let key = hex::encode(operator_id);
let mut state = self.state.lock().unwrap();
state.sign_call_count += 1;
if let Some(err) = state.errors.remove(&key) {
return Err(err);
}
Ok(BlsG1Point::new(G1Affine::generator()))
}
}
#[tokio::test]
async fn fake_returns_default_proposal_for_unknown_operator() {
let client = FakeStateCommitOperatorClient::new();
let op_id = OperatorId::default();
let proposal = client
.get_state_commit_proposal(&op_id, 1)
.await
.expect("default proposal succeeds");
assert_eq!(proposal, default_proposal());
assert_eq!(client.proposal_call_count(), 1);
}
#[tokio::test]
async fn fake_returns_overridden_proposal_for_configured_operator() {
let client = FakeStateCommitOperatorClient::new();
let op_id = OperatorId::default();
let custom = OperatorProposal {
new_state_root: B256::repeat_byte(0x11),
da_cert_hash: B256::repeat_byte(0x22),
pcr0_commitment: B256::repeat_byte(0x33),
};
client.set_proposal_for(&op_id, custom);
let returned = client
.get_state_commit_proposal(&op_id, 2)
.await
.expect("overridden proposal succeeds");
assert_eq!(returned, custom);
}
#[tokio::test]
async fn fake_one_shot_error_on_proposal_then_recovers() {
let client = FakeStateCommitOperatorClient::new();
let op_id = OperatorId::default();
client.inject_error(
&op_id,
OperatorClientError::Timeout {
operator_id: "test".into(),
timeout_ms: 5_000,
},
);
let err = client
.get_state_commit_proposal(&op_id, 1)
.await
.expect_err("error injected");
assert!(matches!(err, OperatorClientError::Timeout { .. }));
let proposal = client
.get_state_commit_proposal(&op_id, 1)
.await
.expect("recovers after one-shot error");
assert_eq!(proposal, default_proposal());
}
#[tokio::test]
async fn fake_sign_state_commit_returns_zero_g2() {
let client = FakeStateCommitOperatorClient::new();
let op_id = OperatorId::default();
let commit = dummy_state_commit();
let sig = client
.sign_state_commit(&op_id, B256::ZERO, &commit, 0u32)
.await
.expect("sign succeeds");
let _ = sig;
assert_eq!(client.sign_call_count(), 1);
}
#[tokio::test]
async fn fake_one_shot_error_on_sign_then_recovers() {
let client = FakeStateCommitOperatorClient::new();
let op_id = OperatorId::default();
let commit = dummy_state_commit();
client.inject_error(
&op_id,
OperatorClientError::DigestDisagreement {
operator_id: "test".into(),
sequence_no: 7,
},
);
let err = client
.sign_state_commit(&op_id, B256::ZERO, &commit, 0u32)
.await
.expect_err("injected error");
assert!(matches!(err, OperatorClientError::DigestDisagreement { .. }));
client
.sign_state_commit(&op_id, B256::ZERO, &commit, 0u32)
.await
.expect("recovers after one-shot error");
}
}