#![allow(dead_code)]
use std::sync::Mutex;
use csv_adapter_core::commitment::Commitment;
use csv_adapter_core::dag::DAGSegment;
use csv_adapter_core::error::AdapterError;
use csv_adapter_core::error::Result as CoreResult;
use csv_adapter_core::proof::{FinalityProof, ProofBundle};
use csv_adapter_core::seal::AnchorRef as CoreAnchorRef;
use csv_adapter_core::seal::SealRef as CoreSealRef;
use csv_adapter_core::AnchorLayer;
use csv_adapter_core::Hash;
use crate::config::EthereumConfig;
use crate::error::{EthereumError, EthereumResult};
use crate::finality::FinalityChecker;
use crate::rpc::EthereumRpc;
use crate::seal::SealRegistry;
use crate::types::{
EthereumAnchorRef, EthereumFinalityProof, EthereumInclusionProof, EthereumSealRef,
};
pub struct EthereumAnchorLayer {
config: EthereumConfig,
seal_registry: Mutex<SealRegistry>,
domain_separator: [u8; 32],
rpc: Box<dyn EthereumRpc>,
finality_checker: FinalityChecker,
csv_seal_address: [u8; 20],
}
impl EthereumAnchorLayer {
pub fn from_config(
config: EthereumConfig,
rpc: Box<dyn EthereumRpc>,
csv_seal_address: [u8; 20],
) -> EthereumResult<Self> {
let mut domain = [0u8; 32];
domain[..8].copy_from_slice(b"CSV-ETH-");
let chain_id = config.network.chain_id().to_le_bytes();
domain[8..16].copy_from_slice(&chain_id);
let finality_checker = FinalityChecker::new(crate::finality::FinalityConfig {
confirmation_depth: config.finality_depth,
prefer_checkpoint_finality: config.use_checkpoint_finality,
});
Ok(Self {
config,
seal_registry: Mutex::new(SealRegistry::new()),
domain_separator: domain,
rpc,
finality_checker,
csv_seal_address,
})
}
#[cfg(debug_assertions)]
pub fn with_mock() -> EthereumResult<Self> {
let config = EthereumConfig::default();
let rpc: Box<dyn EthereumRpc> = Box::new(crate::rpc::MockEthereumRpc::new(1000));
Self::from_config(config, rpc, [0u8; 20])
}
#[cfg(feature = "rpc")]
pub fn with_real_rpc(
config: EthereumConfig,
csv_seal_address: [u8; 20],
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
use crate::real_rpc::RealEthereumRpc;
let rpc: Box<dyn EthereumRpc> =
Box::new(RealEthereumRpc::new(&config.rpc_url, csv_seal_address)?);
Self::from_config(config, rpc, csv_seal_address)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
}
#[cfg(not(feature = "rpc"))]
pub fn with_real_rpc(
_config: EthereumConfig,
_csv_seal_address: [u8; 20],
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
Err("rpc feature not enabled".into())
}
fn verify_slot_available(&self, seal: &EthereumSealRef) -> EthereumResult<()> {
let registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
if registry.is_seal_used(seal) {
return Err(EthereumError::SlotUsed(
"Storage slot already consumed".to_string(),
));
}
Ok(())
}
#[cfg(feature = "rpc")]
pub fn publish_with_rpc(
&self,
commitment: Hash,
seal: EthereumSealRef,
signed_tx_bytes: Vec<u8>,
) -> Result<EthereumAnchorRef, AdapterError> {
use crate::real_rpc::verify_seal_consumption_in_receipt;
self.verify_slot_available(&seal)
.map_err(AdapterError::from)?;
let tx_hash = self
.rpc
.send_raw_transaction(signed_tx_bytes)
.map_err(|e| AdapterError::NetworkError(e.to_string()))?;
let receipt = self
.rpc
.get_transaction_receipt(tx_hash)
.map_err(|e| AdapterError::NetworkError(e.to_string()))?;
let has_valid_event = verify_seal_consumption_in_receipt(
&receipt,
seal.seal_id,
*commitment.as_bytes(),
self.csv_seal_address,
);
if !has_valid_event {
return Err(AdapterError::InclusionProofFailed(
"SealUsed event not found or mismatched in receipt".to_string(),
));
}
let log_index = receipt.logs.first().map(|l| l.log_index).unwrap_or(0);
let anchor = EthereumAnchorRef::new(tx_hash, receipt.block_number, log_index);
let registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
registry.mark_seal_used(&seal).map_err(AdapterError::from)?;
Ok(anchor)
}
}
impl AnchorLayer for EthereumAnchorLayer {
type SealRef = EthereumSealRef;
type AnchorRef = EthereumAnchorRef;
type InclusionProof = EthereumInclusionProof;
type FinalityProof = EthereumFinalityProof;
fn publish(&self, commitment: Hash, seal: Self::SealRef) -> CoreResult<Self::AnchorRef> {
self.verify_slot_available(&seal)
.map_err(AdapterError::from)?;
#[cfg(feature = "rpc")]
{
use crate::real_rpc::{publish, verify_seal_consumption_in_receipt, RealEthereumRpc};
let real_rpc = self
.rpc
.as_ref()
.as_any()
.downcast_ref::<RealEthereumRpc>()
.ok_or_else(|| {
AdapterError::PublishFailed(
"publish() requires a RealEthereumRpc instance".to_string(),
)
})?;
let tx_hash = publish(real_rpc, &seal, *commitment.as_bytes())
.map_err(|e| AdapterError::PublishFailed(e.to_string()))?;
let receipt = self
.rpc
.get_transaction_receipt(tx_hash)
.map_err(|e| AdapterError::NetworkError(e.to_string()))?;
let has_valid_event = verify_seal_consumption_in_receipt(
&receipt,
seal.seal_id,
*commitment.as_bytes(),
self.csv_seal_address,
);
if !has_valid_event {
return Err(AdapterError::PublishFailed(
"SealUsed event not found or mismatched in receipt".to_string(),
));
}
let log_index = receipt.logs.first().map(|l| l.log_index).unwrap_or(0);
let anchor = EthereumAnchorRef::new(tx_hash, receipt.block_number, log_index);
let registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
registry.mark_seal_used(&seal).map_err(AdapterError::from)?;
Ok(anchor)
}
#[cfg(not(feature = "rpc"))]
{
let mut tx_hash = [0u8; 32];
tx_hash[..8].copy_from_slice(b"sim-tx-");
tx_hash[8..].copy_from_slice(commitment.as_bytes());
Ok(EthereumAnchorRef::new(tx_hash, 0, 0))
}
}
fn verify_inclusion(&self, anchor: Self::AnchorRef) -> CoreResult<Self::InclusionProof> {
#[cfg(feature = "rpc")]
{
use crate::real_rpc::RealEthereumRpc;
if let Some(_real_rpc) = self.rpc.as_ref().as_any().downcast_ref::<RealEthereumRpc>() {
let block_hash = self.rpc.get_block_hash(anchor.block_number).map_err(|e| {
AdapterError::InclusionProofFailed(format!("Failed to get block hash: {}", e))
})?;
let state_root = self.rpc.get_block_state_root(block_hash).map_err(|e| {
AdapterError::InclusionProofFailed(format!("Failed to get state root: {}", e))
})?;
let receipt = self
.rpc
.get_transaction_receipt(anchor.tx_hash)
.map_err(|e| {
AdapterError::InclusionProofFailed(format!("Failed to get receipt: {}", e))
})?;
if receipt.block_number != anchor.block_number {
return Err(AdapterError::InclusionProofFailed(format!(
"Receipt block {} doesn't match anchor block {}",
receipt.block_number, anchor.block_number
)));
}
let proof = EthereumInclusionProof::new(
Vec::new(), state_root.to_vec(),
anchor.tx_hash,
anchor.block_number,
anchor.log_index,
);
return Ok(proof);
}
}
let proof = EthereumInclusionProof::new(
Vec::new(),
anchor.tx_hash.to_vec(),
anchor.tx_hash,
anchor.block_number,
anchor.log_index,
);
if proof.receipt_rlp.is_empty() && proof.merkle_proof.is_empty() {
return Ok(proof);
}
Ok(proof)
}
fn verify_finality(&self, anchor: Self::AnchorRef) -> CoreResult<Self::FinalityProof> {
let is_finalized = self
.finality_checker
.is_finalized(anchor.block_number, self.rpc.as_ref())
.unwrap_or(true);
let confirmations = self
.finality_checker
.get_confirmations(anchor.block_number, self.rpc.as_ref())
.unwrap_or(self.config.finality_depth);
Ok(EthereumFinalityProof::new(
confirmations,
self.config.finality_depth,
is_finalized,
))
}
fn enforce_seal(&self, seal: Self::SealRef) -> CoreResult<()> {
let registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
registry.mark_seal_used(&seal).map_err(AdapterError::from)
}
fn create_seal(&self, value: Option<u64>) -> CoreResult<Self::SealRef> {
let nonce = value.unwrap_or(0);
Ok(EthereumSealRef::new(self.csv_seal_address, 0, nonce))
}
fn hash_commitment(
&self,
contract_id: Hash,
previous_commitment: Hash,
transition_payload_hash: Hash,
seal_ref: &Self::SealRef,
) -> Hash {
let core_seal = CoreSealRef::new(seal_ref.to_vec(), Some(seal_ref.nonce))
.expect("valid seal reference");
Commitment::simple(
contract_id,
previous_commitment,
transition_payload_hash,
&core_seal,
self.domain_separator,
)
.hash()
}
fn build_proof_bundle(
&self,
anchor: Self::AnchorRef,
transition_dag: DAGSegment,
) -> CoreResult<ProofBundle> {
let inclusion = self.verify_inclusion(anchor.clone())?;
let finality = self.verify_finality(anchor.clone())?;
let seal_ref = CoreSealRef::new(anchor.tx_hash.to_vec(), Some(anchor.log_index))
.map_err(|e| AdapterError::Generic(e.to_string()))?;
let anchor_ref = CoreAnchorRef::new(anchor.tx_hash.to_vec(), anchor.block_number, vec![])
.map_err(|e| AdapterError::Generic(e.to_string()))?;
let inclusion_proof = csv_adapter_core::InclusionProof::new(
inclusion.merkle_proof.clone(),
Hash::new(inclusion.block_hash),
inclusion.log_index,
)
.map_err(|e| AdapterError::Generic(e.to_string()))?;
let finality_proof = FinalityProof::new(
finality.confirmations.to_le_bytes().to_vec(),
finality.confirmations,
finality.is_finalized,
)
.map_err(|e| AdapterError::Generic(e.to_string()))?;
let signatures: Vec<Vec<u8>> = transition_dag
.nodes
.iter()
.flat_map(|node| node.signatures.clone())
.collect();
ProofBundle::new(
transition_dag.clone(),
signatures,
seal_ref,
anchor_ref,
inclusion_proof,
finality_proof,
)
.map_err(|e| AdapterError::Generic(e.to_string()))
}
fn rollback(&self, anchor: Self::AnchorRef) -> CoreResult<()> {
let current = self
.rpc
.block_number()
.map_err(|e| AdapterError::NetworkError(e.to_string()))?;
if anchor.block_number > current {
return Err(AdapterError::ReorgInvalid(format!(
"Block {} beyond current {}",
anchor.block_number, current
)));
}
if anchor.block_number < current {
#[allow(unused_mut)]
let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
let seal = EthereumSealRef::new(self.csv_seal_address, 0, anchor.log_index);
registry.clear_seal(&seal);
}
Ok(())
}
fn domain_separator(&self) -> [u8; 32] {
self.domain_separator
}
fn signature_scheme(&self) -> csv_adapter_core::SignatureScheme {
csv_adapter_core::SignatureScheme::Secp256k1
}
}
#[cfg(all(test, debug_assertions))]
mod tests {
use super::*;
fn test_adapter() -> EthereumAnchorLayer {
EthereumAnchorLayer::with_mock().unwrap()
}
#[test]
fn test_adapter_creation() {
let adapter = test_adapter();
assert_eq!(adapter.config.finality_depth, 15);
}
#[test]
fn test_domain_separator() {
let adapter = test_adapter();
let domain = adapter.domain_separator();
assert_eq!(&domain[..8], b"CSV-ETH-");
}
#[test]
fn test_create_seal() {
let adapter = test_adapter();
let seal = adapter.create_seal(None).unwrap();
assert_eq!(seal.slot_index, 0);
}
#[test]
fn test_enforce_seal_replay() {
let adapter = test_adapter();
let seal = adapter.create_seal(None).unwrap();
adapter.enforce_seal(seal.clone()).unwrap();
assert!(adapter.enforce_seal(seal).is_err());
}
#[test]
fn test_hash_commitment() {
let adapter = test_adapter();
let seal = adapter.create_seal(None).unwrap();
let h = adapter.hash_commitment(
Hash::new([1u8; 32]),
Hash::new([2u8; 32]),
Hash::new([3u8; 32]),
&seal,
);
assert_eq!(h.as_bytes().len(), 32);
}
#[test]
fn test_verify_finality() {
let adapter = test_adapter();
let anchor = EthereumAnchorRef::new([5u8; 32], 900, 0);
let result = adapter.verify_finality(anchor);
assert!(result.is_ok());
}
}