#![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};
#[cfg(feature = "rpc")]
type SignedTransaction = (Vec<u8>, Vec<u8>, Vec<u8>);
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::SuiConfig;
use crate::error::{SuiError, SuiResult};
use crate::proofs::{CommitmentEventBuilder, EventProofVerifier, StateProofVerifier};
#[cfg(feature = "rpc")]
use crate::rpc::SuiObject;
use crate::rpc::SuiRpc;
use crate::seal::SealRegistry;
use crate::types::{SuiAnchorRef, SuiFinalityProof, SuiInclusionProof, SuiSealRef};
pub struct SuiAnchorLayer {
config: SuiConfig,
seal_registry: Mutex<SealRegistry>,
domain_separator: [u8; 32],
rpc: Box<dyn SuiRpc>,
checkpoint_verifier: CheckpointVerifier,
event_builder: CommitmentEventBuilder,
#[cfg(feature = "rpc")]
signing_key: Option<ed25519_dalek::SigningKey>,
}
fn format_object_id(object_id: [u8; 32]) -> String {
format!("0x{}", hex::encode(object_id))
}
fn parse_object_id(s: &str) -> Result<[u8; 32], String> {
let hex_str = s.trim_start_matches("0x");
let bytes = hex::decode(hex_str).map_err(|e| format!("Invalid hex: {}", e))?;
if bytes.len() != 32 {
return Err(format!("Object ID must be 32 bytes, got {}", bytes.len()));
}
let mut id = [0u8; 32];
id.copy_from_slice(&bytes);
Ok(id)
}
#[cfg(feature = "rpc")]
#[allow(clippy::too_many_arguments)]
fn build_sui_transaction_data(
package_id: [u8; 32],
module_name: &str,
function_name: &str,
seal_object_id: [u8; 32],
commitment: [u8; 32],
sender: [u8; 32],
gas_objects: &[SuiObject],
gas_price: u64,
gas_budget: u64,
) -> Vec<u8> {
fn uleb128_encode(mut n: u64) -> Vec<u8> {
let mut buf = Vec::new();
loop {
let byte = (n & 0x7F) as u8;
n >>= 7;
if n == 0 {
buf.push(byte);
break;
} else {
buf.push(byte | 0x80);
}
}
buf
}
fn bcs_vec_u8(v: &[u8]) -> Vec<u8> {
let mut out = uleb128_encode(v.len() as u64);
out.extend_from_slice(v);
out
}
fn bcs_string(s: &str) -> Vec<u8> {
bcs_vec_u8(s.as_bytes())
}
fn bcs_vec_vec_u8(vv: &[Vec<u8>]) -> Vec<u8> {
let mut out = uleb128_encode(vv.len() as u64);
for v in vv {
out.extend_from_slice(v);
}
out
}
let mut tx = Vec::new();
tx.push(0);
tx.extend_from_slice(&uleb128_encode(2));
tx.push(1);
tx.extend_from_slice(&seal_object_id);
tx.extend_from_slice(&1u64.to_le_bytes()); tx.extend_from_slice(&[0u8; 32]);
tx.push(0);
tx.extend_from_slice(&bcs_vec_u8(&commitment));
tx.extend_from_slice(&uleb128_encode(1));
tx.push(0);
tx.extend_from_slice(&package_id);
tx.extend_from_slice(&bcs_string(module_name));
tx.extend_from_slice(&bcs_string(function_name));
tx.push(0); tx.extend_from_slice(&uleb128_encode(2)); tx.push(1);
tx.extend_from_slice(&0u16.to_le_bytes()); tx.push(1);
tx.extend_from_slice(&1u16.to_le_bytes());
tx.extend_from_slice(&sender);
tx.extend_from_slice(&uleb128_encode(gas_objects.len() as u64));
for obj in gas_objects {
tx.extend_from_slice(&obj.object_id);
tx.extend_from_slice(&obj.version.to_le_bytes());
tx.extend_from_slice(&[0u8; 32]); }
tx.extend_from_slice(&sender);
tx.extend_from_slice(&gas_price.to_le_bytes());
tx.extend_from_slice(&gas_budget.to_le_bytes());
tx.push(0);
tx
}
impl SuiAnchorLayer {
pub fn from_config(config: SuiConfig, rpc: Box<dyn SuiRpc>) -> SuiResult<Self> {
config
.validate()
.map_err(|e| SuiError::SerializationError(format!("Invalid configuration: {}", e)))?;
let mut domain = [0u8; 32];
let chain_id_bytes = config.chain_id().as_bytes();
let copy_len = chain_id_bytes.len().min(24);
domain[..8].copy_from_slice(b"CSV-SUI-");
domain[8..8 + copy_len].copy_from_slice(&chain_id_bytes[..copy_len]);
let package_id_str = config.seal_contract.package_id.as_deref().ok_or_else(|| {
SuiError::SerializationError(
"seal_contract.package_id is not set — deploy the contract first".to_string(),
)
})?;
let package_id = parse_object_id(package_id_str).map_err(SuiError::SerializationError)?;
let event_type = format!(
"{}::{}::AnchorEvent",
package_id_str, config.seal_contract.module_name
);
let event_builder = CommitmentEventBuilder::new(package_id, event_type);
let checkpoint_verifier = CheckpointVerifier::with_config(config.checkpoint.clone());
log::info!(
"Initialized Sui 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() -> SuiResult<Self> {
let mut config = SuiConfig::default();
config.seal_contract.package_id =
Some("0x0000000000000000000000000000000000000000000000000000000000000002".to_string());
let rpc = Box::new(crate::rpc::MockSuiRpc::new(1000));
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: SuiConfig,
_csv_seal_package_id: [u8; 32],
signing_key: ed25519_dalek::SigningKey,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
use crate::real_rpc::SuiRpcClient;
let rpc: Box<dyn SuiRpc> = Box::new(SuiRpcClient::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);
Ok(adapter)
}
#[cfg(not(feature = "rpc"))]
pub fn with_real_rpc(
_config: SuiConfig,
_csv_seal_package_id: [u8; 32],
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
Err("rpc feature not enabled".into())
}
fn verify_seal_available(&self, seal: &SuiSealRef) -> SuiResult<()> {
let registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
if registry.is_seal_used(seal) {
return Err(SuiError::ObjectUsed(format!(
"Object {} with version {} is already consumed",
format_object_id(seal.object_id),
seal.version
)));
}
let obj = StateProofVerifier::verify_object_exists(seal.object_id, self.rpc.as_ref())?;
if obj.is_none() {
return Err(SuiError::StateProofFailed(format!(
"Seal object {} does not exist on-chain",
format_object_id(seal.object_id)
)));
}
Ok(())
}
#[cfg(feature = "rpc")]
fn build_and_sign_move_call(
&self,
seal: &SuiSealRef,
commitment: [u8; 32],
) -> Result<SignedTransaction, 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 package_id = self
.config
.seal_contract
.package_id
.as_deref()
.ok_or("seal_contract.package_id is not set — deploy the contract first")?;
let package_id =
parse_object_id(package_id).map_err(|e| format!("Invalid package ID: {}", e))?;
let module_name = self.config.seal_contract.module_name.clone();
let function_name = "consume_seal".to_string();
log::debug!(
"Building Sui MoveCall: {}::{}::{}(seal={}, commitment={})",
self.config
.seal_contract
.package_id
.as_deref()
.unwrap_or("unknown"),
module_name,
function_name,
format_object_id(seal.object_id),
hex::encode(commitment),
);
let sender = self
.rpc
.sender_address()
.map_err(|e| format!("Failed to get sender address: {}", e))?;
let gas_objects = self
.rpc
.get_gas_objects(sender)
.map_err(|e| format!("Failed to get gas objects: {}", e))?;
if gas_objects.is_empty() {
return Err("No gas objects available for transaction".into());
}
let tx_bytes = build_sui_transaction_data(
package_id,
&module_name,
&function_name,
seal.object_id,
commitment,
sender,
&gas_objects,
self.config.transaction.max_gas_price,
self.config.transaction.max_gas_budget,
);
let signature = signing_key.sign(&tx_bytes);
let public_key = signing_key.verifying_key().to_bytes().to_vec();
let mut sui_signature = Vec::with_capacity(97);
sui_signature.push(0x00); sui_signature.extend_from_slice(signature.to_bytes().as_ref());
sui_signature.extend_from_slice(&public_key);
Ok((tx_bytes, sui_signature, public_key))
}
fn verify_anchor_event(
&self,
anchor: &SuiAnchorRef,
expected_seal: &SuiSealRef,
expected_commitment: Hash,
) -> CoreResult<()> {
let expected_event_data = self
.event_builder
.build(*expected_commitment.as_bytes(), expected_seal.object_id);
let valid = EventProofVerifier::verify_event_in_tx(
anchor.tx_digest,
&expected_event_data,
self.rpc.as_ref(),
)
.map_err(|e: SuiError| AdapterError::InclusionProofFailed(e.to_string()))?;
if !valid {
return Err(AdapterError::InclusionProofFailed(
"Event verification failed: commitment mismatch".to_string(),
));
}
Ok(())
}
}
impl AnchorLayer for SuiAnchorLayer {
type SealRef = SuiSealRef;
type AnchorRef = SuiAnchorRef;
type InclusionProof = SuiInclusionProof;
type FinalityProof = SuiFinalityProof;
fn publish(&self, commitment: Hash, seal: Self::SealRef) -> CoreResult<Self::AnchorRef> {
log::debug!(
"Publishing commitment via seal object {}",
format_object_id(seal.object_id)
);
self.verify_seal_available(&seal)
.map_err(AdapterError::from)?;
#[cfg(feature = "rpc")]
{
let event_data = self
.event_builder
.build(*commitment.as_bytes(), seal.object_id);
let (tx_bytes, signature, public_key) = self
.build_and_sign_move_call(&seal, *commitment.as_bytes())
.map_err(|e| {
AdapterError::PublishFailed(format!(
"Failed to build and sign transaction: {}",
e
))
})?;
let tx_digest = self
.rpc
.execute_signed_transaction(tx_bytes, signature, public_key)
.map_err(|e| {
AdapterError::PublishFailed(format!("Failed to execute transaction: {}", e))
})?;
let block = self
.rpc
.wait_for_transaction(tx_digest, 30_000)
.map_err(|e| AdapterError::NetworkError(e.to_string()))?
.ok_or_else(|| {
AdapterError::PublishFailed(
"Transaction not found after submission".to_string(),
)
})?;
let valid =
EventProofVerifier::verify_event_in_tx(tx_digest, &event_data, self.rpc.as_ref())
.map_err(|e: SuiError| AdapterError::InclusionProofFailed(e.to_string()))?;
if !valid {
return Err(AdapterError::PublishFailed(
"Event verification failed: commitment mismatch".to_string(),
));
}
let checkpoint = block.checkpoint.unwrap_or(0);
let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
registry
.mark_seal_used(&seal, checkpoint)
.map_err(AdapterError::from)?;
Ok(SuiAnchorRef::new(seal.object_id, tx_digest, checkpoint))
}
#[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(AdapterError::from)?;
let _event_data = self
.event_builder
.build(*commitment.as_bytes(), seal.object_id);
Ok(SuiAnchorRef::new(seal.object_id, [0u8; 32], 0))
}
}
fn verify_inclusion(&self, anchor: Self::AnchorRef) -> CoreResult<Self::InclusionProof> {
log::debug!(
"Verifying inclusion for anchor at checkpoint {}",
anchor.checkpoint
);
let checkpoint_info = match self.rpc.get_checkpoint(anchor.checkpoint) {
Ok(Some(info)) => info,
Ok(None) => {
return Err(AdapterError::InclusionProofFailed(format!(
"Checkpoint {} not found",
anchor.checkpoint
)));
}
Err(e) => {
return Err(AdapterError::InclusionProofFailed(format!(
"Failed to fetch checkpoint {}: {}",
anchor.checkpoint, e
)));
}
};
if !checkpoint_info.certified {
let is_certified = match self
.checkpoint_verifier
.is_checkpoint_certified(anchor.checkpoint, self.rpc.as_ref())
{
Ok(info) => info.is_certified,
Err(e) => {
log::warn!("Checkpoint certification check failed: {}", e);
false
}
};
if !is_certified {
return Err(AdapterError::InclusionProofFailed(format!(
"Checkpoint {} is not yet certified",
anchor.checkpoint
)));
}
}
let checkpoint_hash = checkpoint_info.digest;
Ok(SuiInclusionProof::new(
vec![0u8; 32], checkpoint_hash,
anchor.checkpoint,
))
}
fn verify_finality(&self, anchor: Self::AnchorRef) -> CoreResult<Self::FinalityProof> {
log::debug!(
"Verifying finality for anchor at checkpoint {}",
anchor.checkpoint
);
let is_certified = match self
.checkpoint_verifier
.is_checkpoint_certified(anchor.checkpoint, self.rpc.as_ref())
{
Ok(info) => info.is_certified,
Err(e) => {
log::warn!("Finality check failed: {}", e);
false
}
};
Ok(SuiFinalityProof::new(anchor.checkpoint, 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!(
"Object {} already consumed",
format_object_id(seal.object_id)
)));
}
registry
.mark_seal_used(&seal, 0)
.map_err(AdapterError::from)
}
fn create_seal(&self, _value: Option<u64>) -> CoreResult<Self::SealRef> {
use sha2::{Digest, Sha256};
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let mut hasher = Sha256::new();
hasher.update(b"sui-seal");
hasher.update(nonce.to_le_bytes());
let result = hasher.finalize();
let mut object_id = [0u8; 32];
object_id.copy_from_slice(&result);
Ok(SuiSealRef::new(object_id, 1, 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.object_id.to_vec(), Some(anchor.checkpoint))
.map_err(|e| AdapterError::Generic(e.to_string()))?;
let anchor_ref = CoreAnchorRef::new(anchor.object_id.to_vec(), anchor.checkpoint, vec![])
.map_err(|e| AdapterError::Generic(e.to_string()))?;
let inclusion_proof = csv_adapter_core::InclusionProof::new(
inclusion.object_proof,
Hash::new(inclusion.checkpoint_hash),
inclusion.checkpoint_number,
)
.map_err(|e| AdapterError::Generic(e.to_string()))?;
let finality_proof = FinalityProof::new(vec![], finality.checkpoint, 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 checkpoint {}",
anchor.checkpoint
);
let current_checkpoint = self
.rpc
.get_latest_checkpoint_sequence_number()
.map_err(|e| AdapterError::NetworkError(e.to_string()))?;
if anchor.checkpoint > current_checkpoint {
return Err(AdapterError::ReorgInvalid(format!(
"Anchor checkpoint {} beyond current tip {}",
anchor.checkpoint, current_checkpoint
)));
}
if anchor.checkpoint < current_checkpoint {
let mut registry = self.seal_registry.lock().unwrap_or_else(|e| e.into_inner());
let dummy_seal = SuiSealRef::new(anchor.object_id, 0, 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() -> SuiAnchorLayer {
SuiAnchorLayer::with_mock().unwrap()
}
#[test]
fn test_create_seal() {
let adapter = test_adapter();
let seal = adapter.create_seal(None).unwrap();
assert_eq!(seal.version, 1);
}
#[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();
let domain = adapter.domain_separator();
assert_eq!(&domain[..8], b"CSV-SUI-");
}
#[test]
fn test_verify_finality() {
let adapter = test_adapter();
let anchor = SuiAnchorRef::new([1u8; 32], [2u8; 32], 500);
let result = adapter.verify_finality(anchor);
assert!(result.is_ok());
}
#[test]
fn test_parse_object_id() {
let id =
parse_object_id("0x0000000000000000000000000000000000000000000000000000000000000001")
.unwrap();
assert_eq!(id[31], 1);
for i in 0..31 {
assert_eq!(id[i], 0);
}
}
#[test]
fn test_format_object_id() {
let id = [1u8; 32];
let formatted = format_object_id(id);
assert!(formatted.starts_with("0x"));
assert_eq!(formatted.len(), 66); }
#[test]
fn test_seal_registry_replay() {
let adapter = test_adapter();
let seal = adapter.create_seal(None).unwrap();
adapter
.seal_registry
.lock()
.unwrap_or_else(|e| e.into_inner())
.mark_seal_used(&seal, 0)
.unwrap();
assert!(adapter.enforce_seal(seal).is_err());
}
}