#![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::checkpoint::CheckpointVerifier;
use crate::config::AptosConfig;
use crate::error::{AptosError, AptosResult};
use crate::proofs::{CommitmentEventBuilder, EventProofVerifier, StateProofVerifier};
use crate::rpc::AptosRpc;
use crate::seal::SealRegistry;
use crate::types::{AptosAnchorRef, AptosFinalityProof, AptosInclusionProof, AptosSealRef};
pub struct AptosAnchorLayer {
config: AptosConfig,
seal_registry: Mutex<SealRegistry>,
domain_separator: [u8; 32],
rpc: Box<dyn AptosRpc>,
checkpoint_verifier: CheckpointVerifier,
event_builder: CommitmentEventBuilder,
#[cfg(feature = "rpc")]
signing_key: Option<ed25519_dalek::SigningKey>,
}
impl AptosAnchorLayer {
pub fn from_config(config: AptosConfig, rpc: Box<dyn AptosRpc>) -> AptosResult<Self> {
config
.validate()
.map_err(|e| AptosError::SerializationError(format!("Invalid configuration: {}", e)))?;
let mut domain = [0u8; 32];
domain[..10].copy_from_slice(b"CSV-APTOS-");
domain[10] = config.chain_id();
let module_address = parse_aptos_address(&config.seal_contract.module_address)
.map_err(|e| AptosError::SerializationError(e))?;
let event_type = format!(
"{}::csv_seal::AnchorEvent",
config.seal_contract.module_address
);
let event_builder = CommitmentEventBuilder::new(module_address, event_type);
let checkpoint_verifier = CheckpointVerifier::with_config(config.checkpoint.clone());
log::info!(
"Initialized Aptos adapter for network {:?} (chain_id={})",
config.network,
config.chain_id()
);
Ok(Self {
config,
seal_registry: Mutex::new(SealRegistry::new()),
domain_separator: domain,
rpc,
checkpoint_verifier,
event_builder,
#[cfg(feature = "rpc")]
signing_key: None,
})
}
#[cfg(debug_assertions)]
pub fn with_mock() -> AptosResult<Self> {
let config = AptosConfig::default();
let rpc = Box::new(crate::rpc::MockAptosRpc::new(5000));
Self::from_config(config, rpc)
}
#[cfg(feature = "rpc")]
pub fn with_signing_key(mut self, signing_key: ed25519_dalek::SigningKey) -> Self {
self.signing_key = Some(signing_key);
self
}
#[cfg(feature = "rpc")]
pub fn with_real_rpc(
config: AptosConfig,
csv_seal_address: [u8; 32],
signing_key: ed25519_dalek::SigningKey,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
use crate::real_rpc::AptosRpcClient;
let rpc: Box<dyn AptosRpc> = Box::new(AptosRpcClient::new(&config.rpc_url));
let mut adapter = Self::from_config(config, rpc)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
adapter.signing_key = Some(signing_key);
adapter.config.seal_contract.module_address = format_address(csv_seal_address);
Ok(adapter)
}
#[cfg(not(feature = "rpc"))]
pub fn with_real_rpc(
_config: AptosConfig,
_csv_seal_address: [u8; 32],
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
Err("rpc feature not enabled".into())
}
fn verify_seal_available(&self, seal: &AptosSealRef) -> AptosResult<()> {
let registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
if registry.is_seal_used(seal) {
return Err(AptosError::ResourceUsed(format!(
"Seal at address {} is already consumed",
format_address(seal.account_address)
)));
}
let resource_type = format!(
"{}::csv_seal::{}",
self.config.seal_contract.module_address, self.config.seal_contract.seal_resource
);
let exists = StateProofVerifier::verify_resource_exists(
seal.account_address,
&resource_type,
self.rpc.as_ref(),
)?;
if !exists {
return Err(AptosError::StateProofFailed(format!(
"Seal resource at {} does not exist on-chain",
format_address(seal.account_address)
)));
}
Ok(())
}
#[cfg(feature = "rpc")]
fn build_and_sign_entry_function(
&self,
seal: &AptosSealRef,
commitment: [u8; 32],
) -> Result<(serde_json::Value, Vec<u8>), Box<dyn std::error::Error + Send + Sync>> {
use ed25519_dalek::Signer;
let signing_key = self
.signing_key
.as_ref()
.ok_or("No signing key configured")?;
let sender = self
.rpc
.sender_address()
.map_err(|e| format!("Failed to get sender address: {}", e))?;
let sender_hex = format_address(sender);
let sequence_number = self
.rpc
.get_account_sequence_number(sender)
.map_err(|e| format!("Failed to get sequence number: {}", e))?;
let ledger = self
.rpc
.get_ledger_info()
.map_err(|e| format!("Failed to get ledger info: {}", e))?;
let event_data = self.event_builder.build(commitment, seal.account_address);
let module_address = &self.config.seal_contract.module_address;
let function_name = &self.config.seal_contract.seal_resource;
let function = format!("{}::csv_seal::delete_{}", module_address, function_name);
log::debug!(
"Building Aptos Entry Function: {}(seal={}, commitment={})",
function,
format_address(seal.account_address),
hex::encode(commitment),
);
let expiration_secs = (ledger.ledger_timestamp / 1_000_000) + 600;
let tx_payload = serde_json::json!({
"sender": sender_hex,
"sequence_number": sequence_number.to_string(),
"max_gas_amount": self.config.transaction.max_gas.to_string(),
"gas_unit_price": "100",
"expiration_timestamp_secs": expiration_secs.to_string(),
"payload": {
"type": "entry_function_payload",
"function": function,
"type_arguments": [],
"arguments": [
format!("0x{}", hex::encode(seal.account_address)),
format!("0x{}", hex::encode(commitment))
]
}
});
let tx_json_str = serde_json::to_string(&tx_payload).unwrap_or_default();
let mut hasher = sha2::Sha256::new();
sha2::Digest::update(&mut hasher, tx_json_str.as_bytes());
let message = sha2::Digest::finalize(hasher);
let signature = signing_key.sign(&message);
let public_key = signing_key.verifying_key().to_bytes();
let signed_tx = serde_json::json!({
"sender": sender_hex,
"sequence_number": sequence_number.to_string(),
"max_gas_amount": self.config.transaction.max_gas.to_string(),
"gas_unit_price": "100",
"expiration_timestamp_secs": expiration_secs.to_string(),
"payload": tx_payload["payload"],
"signature": {
"type": "ed25519_signature",
"public_key": format!("0x{}", hex::encode(public_key)),
"signature": format!("0x{}", hex::encode(signature.to_bytes()))
}
});
Ok((signed_tx, event_data))
}
fn verify_anchor_event(
&self,
anchor: &AptosAnchorRef,
expected_seal: &AptosSealRef,
expected_commitment: Hash,
) -> CoreResult<()> {
let expected_event_data = self.event_builder.build(
*expected_commitment.as_bytes(),
expected_seal.account_address,
);
let valid: bool = EventProofVerifier::verify_event_in_tx(
anchor.version,
&expected_event_data,
self.rpc.as_ref(),
)
.map_err(|e: AptosError| AdapterError::InclusionProofFailed(e.to_string()))?;
if !valid {
return Err(AdapterError::InclusionProofFailed(
"Event verification failed: commitment mismatch".to_string(),
));
}
Ok(())
}
}
fn format_address(addr: [u8; 32]) -> String {
format!("0x{}", hex::encode(addr))
}
fn parse_aptos_address(s: &str) -> Result<[u8; 32], String> {
let hex_str = s.trim_start_matches("0x");
let mut padded = String::new();
for _ in 0..(64 - hex_str.len()) {
padded.push('0');
}
padded.push_str(hex_str);
let bytes = hex::decode(&padded).map_err(|e| format!("Invalid hex: {}", e))?;
if bytes.len() != 32 {
return Err(format!("Address must be 32 bytes, got {}", bytes.len()));
}
let mut addr = [0u8; 32];
addr.copy_from_slice(&bytes);
Ok(addr)
}
impl AnchorLayer for AptosAnchorLayer {
type SealRef = AptosSealRef;
type AnchorRef = AptosAnchorRef;
type InclusionProof = AptosInclusionProof;
type FinalityProof = AptosFinalityProof;
fn publish(&self, commitment: Hash, seal: Self::SealRef) -> CoreResult<Self::AnchorRef> {
log::debug!(
"Publishing commitment via seal {}",
format_address(seal.account_address)
);
self.verify_seal_available(&seal)
.map_err(|e| AdapterError::from(e))?;
#[cfg(feature = "rpc")]
{
let (tx_json, expected_event_data) = self
.build_and_sign_entry_function(&seal, *commitment.as_bytes())
.map_err(|e| {
AdapterError::PublishFailed(format!(
"Failed to build and sign transaction: {}",
e
))
})?;
let submit_result = self.rpc.submit_signed_transaction(tx_json).map_err(|e| {
AdapterError::PublishFailed(format!("Failed to submit transaction: {}", e))
})?;
let tx = self
.rpc
.wait_for_transaction(submit_result)
.map_err(|e| AdapterError::NetworkError(e.to_string()))?;
let valid = EventProofVerifier::verify_event_in_tx(
tx.version,
&expected_event_data,
self.rpc.as_ref(),
)
.map_err(|e: AptosError| AdapterError::InclusionProofFailed(e.to_string()))?;
if !valid {
return Err(AdapterError::PublishFailed(
"Event verification failed: commitment mismatch".to_string(),
));
}
let version = tx.version;
let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
registry
.mark_seal_used(&seal, version)
.map_err(|e| AdapterError::from(e))?;
Ok(AptosAnchorRef::new(version, seal.account_address, version))
}
#[cfg(not(feature = "rpc"))]
{
let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
registry
.mark_seal_used(&seal, 0)
.map_err(|e| AdapterError::from(e))?;
let _event_data = self
.event_builder
.build(*commitment.as_bytes(), seal.account_address);
Ok(AptosAnchorRef::new(0, seal.account_address, 0))
}
}
fn verify_inclusion(&self, anchor: Self::AnchorRef) -> CoreResult<Self::InclusionProof> {
log::debug!(
"Verifying inclusion for anchor at version {}",
anchor.version
);
let tx = match self.rpc.get_transaction(anchor.version) {
Ok(Some(tx)) => tx,
Ok(None) => {
return Err(AdapterError::InclusionProofFailed(format!(
"Transaction at version {} not found",
anchor.version
)));
}
Err(e) => {
return Err(AdapterError::InclusionProofFailed(format!(
"Failed to fetch transaction at version {}: {}",
anchor.version, e
)));
}
};
if !tx.success {
return Err(AdapterError::InclusionProofFailed(format!(
"Transaction at version {} failed: {}",
anchor.version, tx.vm_status
)));
}
let ledger_info = match self.rpc.get_ledger_info() {
Ok(info) => info,
Err(e) => {
return Err(AdapterError::InclusionProofFailed(format!(
"Failed to fetch ledger info: {}",
e
)));
}
};
if tx.version > ledger_info.ledger_version {
return Err(AdapterError::InclusionProofFailed(format!(
"Transaction version {} exceeds latest ledger version {}",
tx.version, ledger_info.ledger_version
)));
}
let transaction_proof = tx.hash.to_vec();
let ledger_proof = ledger_info.ledger_version.to_le_bytes().to_vec();
Ok(AptosInclusionProof::new(
transaction_proof,
ledger_proof,
anchor.version,
))
}
fn verify_finality(&self, anchor: Self::AnchorRef) -> CoreResult<Self::FinalityProof> {
log::debug!(
"Verifying finality for anchor at version {}",
anchor.version
);
let f_plus_one = self.config.f_plus_one();
let is_certified = match self.checkpoint_verifier.is_version_finalized(
anchor.version,
self.rpc.as_ref(),
f_plus_one,
) {
Ok(info) => info.is_certified,
Err(e) => {
log::warn!("Finality check failed: {}", e);
false
}
};
Ok(AptosFinalityProof::new(anchor.version, is_certified))
}
fn enforce_seal(&self, seal: Self::SealRef) -> CoreResult<()> {
let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
if registry.is_seal_used(&seal) {
return Err(AdapterError::SealReplay(format!(
"Resource already consumed at {}",
format_address(seal.account_address)
)));
}
registry
.mark_seal_used(&seal, 0)
.map_err(|e| AdapterError::from(e))
}
fn create_seal(&self, _value: Option<u64>) -> CoreResult<Self::SealRef> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(b"aptos-seal");
let result = hasher.finalize();
let mut addr = [0u8; 32];
addr.copy_from_slice(&result);
Ok(AptosSealRef::new(addr, "CSV::Seal".to_string(), 0))
}
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.event_handle.to_vec(), Some(anchor.version))
.map_err(|e| AdapterError::Generic(e.to_string()))?;
let anchor_ref = CoreAnchorRef::new(anchor.event_handle.to_vec(), anchor.version, vec![])
.map_err(|e| AdapterError::Generic(e.to_string()))?;
let inclusion_proof = csv_adapter_core::InclusionProof::new(
inclusion.transaction_proof,
Hash::zero(),
inclusion.version,
)
.map_err(|e| AdapterError::Generic(e.to_string()))?;
let finality_proof = FinalityProof::new(vec![], finality.version, finality.is_certified)
.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<()> {
log::warn!(
"Rollback requested for anchor at version {}",
anchor.version
);
let current_version = self
.rpc
.get_latest_version()
.map_err(|e| AdapterError::NetworkError(e.to_string()))?;
if anchor.version > current_version {
return Err(AdapterError::ReorgInvalid(format!(
"Anchor version {} beyond current tip {}",
anchor.version, current_version
)));
}
if anchor.version < current_version {
let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
let dummy_seal = AptosSealRef::new(anchor.event_handle, "CSV::Seal".to_string(), 0);
if let Err(e) = registry.clear_seal(&dummy_seal) {
log::debug!("Rollback: seal not found in registry (this is OK): {}", e);
}
}
Ok(())
}
fn domain_separator(&self) -> [u8; 32] {
self.domain_separator
}
fn signature_scheme(&self) -> csv_adapter_core::SignatureScheme {
csv_adapter_core::SignatureScheme::Ed25519
}
}
#[cfg(all(test, debug_assertions))]
mod tests {
use super::*;
fn test_adapter() -> AptosAnchorLayer {
AptosAnchorLayer::with_mock().unwrap()
}
#[test]
fn test_create_seal() {
let adapter = test_adapter();
let seal = adapter.create_seal(None).unwrap();
assert_eq!(seal.nonce, 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_domain_separator() {
let adapter = test_adapter();
assert_eq!(&adapter.domain_separator()[..10], b"CSV-APTOS-");
assert_eq!(adapter.domain_separator()[10], 4); }
#[test]
fn test_verify_finality() {
let adapter = test_adapter();
let anchor = AptosAnchorRef::new(1500, [1u8; 32], 0);
let result = adapter.verify_finality(anchor);
assert!(result.is_ok());
}
#[test]
fn test_publish_seal_available() {
let config = AptosConfig::default();
let mock = crate::rpc::MockAptosRpc::new(5000);
let resource_type = format!(
"{}::csv_seal::{}",
config.seal_contract.module_address, config.seal_contract.seal_resource
);
mock.set_resource(
[1u8; 32],
resource_type.as_str(),
crate::rpc::AptosResource { data: vec![0u8; 8] },
);
let rpc = Box::new(mock);
let adapter = AptosAnchorLayer::from_config(config.clone(), rpc).unwrap();
let seal = AptosSealRef::new([1u8; 32], resource_type.clone(), 0);
let commitment = Hash::new([1u8; 32]);
let result = adapter.publish(commitment, seal);
assert!(result.is_ok());
}
#[test]
fn test_publish_seal_replay() {
let config = AptosConfig::default();
let mock = crate::rpc::MockAptosRpc::new(5000);
let resource_type = format!(
"{}::csv_seal::{}",
config.seal_contract.module_address, config.seal_contract.seal_resource
);
mock.set_resource(
[1u8; 32],
resource_type.as_str(),
crate::rpc::AptosResource { data: vec![0u8; 8] },
);
let rpc = Box::new(mock);
let adapter = AptosAnchorLayer::from_config(config.clone(), rpc).unwrap();
let seal = AptosSealRef::new([1u8; 32], resource_type.clone(), 0);
let commitment = Hash::new([1u8; 32]);
adapter.publish(commitment, seal.clone()).unwrap();
let commitment2 = Hash::new([2u8; 32]);
let result = adapter.publish(commitment2, seal);
assert!(result.is_err());
}
#[test]
fn test_parse_aptos_address() {
let addr = parse_aptos_address("0x1").unwrap();
assert_eq!(addr[31], 1);
for i in 0..31 {
assert_eq!(addr[i], 0);
}
}
#[test]
fn test_parse_aptos_address_full() {
let full = "0xdeadbeef00000000000000000000000000000000000000000000000000000001";
let addr = parse_aptos_address(full).unwrap();
assert_eq!(addr[0], 0xDE);
assert_eq!(addr[1], 0xAD);
assert_eq!(addr[31], 0x01);
}
}