synor/apps/synord/src/services/contract.rs
Gulshan Yadav 48949ebb3f Initial commit: Synor blockchain monorepo
A complete blockchain implementation featuring:
- synord: Full node with GHOSTDAG consensus
- explorer-web: Modern React blockchain explorer with 3D DAG visualization
- CLI wallet and tools
- Smart contract SDK and example contracts (DEX, NFT, token)
- WASM crypto library for browser/mobile
2026-01-08 05:22:17 +05:30

484 lines
15 KiB
Rust

//! Contract execution service.
//!
//! Provides smart contract deployment and execution using the Synor VM.
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
use synor_storage::{ContractStateStore, ContractStore, Database, StoredContract};
use synor_types::{Address, Hash256};
use synor_vm::{
storage::MemoryStorage, CallContext, ContractId, ContractModule, ContractStorage,
ExecutionContext, StorageKey, StorageValue, VmEngine,
};
/// Contract deployment result.
#[derive(Clone, Debug)]
pub struct DeployResult {
/// Contract ID (code hash).
pub contract_id: [u8; 32],
/// Gas used.
pub gas_used: u64,
/// Deployment address (for reference).
pub address: Vec<u8>,
}
/// Contract call result.
#[derive(Clone, Debug)]
pub struct CallResult {
/// Return data.
pub data: Vec<u8>,
/// Gas used.
pub gas_used: u64,
/// Success status.
pub success: bool,
/// Logs emitted.
pub logs: Vec<LogEntry>,
}
/// Log entry from contract execution.
#[derive(Clone, Debug)]
pub struct LogEntry {
/// Contract that emitted the log.
pub contract_id: [u8; 32],
/// Indexed topics.
pub topics: Vec<[u8; 32]>,
/// Log data.
pub data: Vec<u8>,
}
/// Contract service manages smart contract execution.
pub struct ContractService {
/// VM engine for WASM execution.
engine: RwLock<Option<VmEngine>>,
/// Contract bytecode store.
contract_store: RwLock<Option<ContractStore>>,
/// Contract state store.
state_store: RwLock<Option<ContractStateStore>>,
/// Is running.
running: RwLock<bool>,
/// Default gas limit for calls.
default_gas_limit: u64,
/// Chain ID.
chain_id: u64,
}
impl ContractService {
/// Creates a new contract service.
pub fn new(chain_id: u64) -> Self {
ContractService {
engine: RwLock::new(None),
contract_store: RwLock::new(None),
state_store: RwLock::new(None),
running: RwLock::new(false),
default_gas_limit: 10_000_000,
chain_id,
}
}
/// Starts the contract service.
pub async fn start(&self, db: Arc<Database>) -> anyhow::Result<()> {
info!("Starting contract service");
// Initialize VM engine
let engine =
VmEngine::new().map_err(|e| anyhow::anyhow!("Failed to create VM engine: {}", e))?;
*self.engine.write().await = Some(engine);
// Initialize stores
*self.contract_store.write().await = Some(ContractStore::new(Arc::clone(&db)));
*self.state_store.write().await = Some(ContractStateStore::new(db));
*self.running.write().await = true;
info!("Contract service started");
Ok(())
}
/// Stops the contract service.
pub async fn stop(&self) -> anyhow::Result<()> {
info!("Stopping contract service");
*self.engine.write().await = None;
*self.contract_store.write().await = None;
*self.state_store.write().await = None;
*self.running.write().await = false;
info!("Contract service stopped");
Ok(())
}
/// Checks if service is running.
pub async fn is_running(&self) -> bool {
*self.running.read().await
}
/// Deploys a new contract.
pub async fn deploy(
&self,
bytecode: Vec<u8>,
init_args: Vec<u8>,
deployer: &Address,
gas_limit: Option<u64>,
block_height: u64,
timestamp: u64,
) -> anyhow::Result<DeployResult> {
let engine = self.engine.read().await;
let engine = engine
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Contract service not started"))?;
let gas_limit = gas_limit.unwrap_or(self.default_gas_limit);
// Compile the contract
debug!(size = bytecode.len(), "Compiling contract");
let module = engine
.compile(bytecode.clone())
.map_err(|e| anyhow::anyhow!("Compilation failed: {}", e))?;
let contract_id = *module.id.as_bytes();
// Check if contract already exists
{
let store = self.contract_store.read().await;
if let Some(store) = store.as_ref() {
if store.exists(&contract_id)? {
return Err(anyhow::anyhow!("Contract already deployed"));
}
}
}
// Create execution context for initialization
let call_context = CallContext::new(module.id, deployer.clone(), 0, init_args.clone());
let storage = MemoryStorage::new();
// Load existing state into memory (none for new contract)
let context = ExecutionContext::new(
synor_vm::context::BlockInfo {
height: block_height,
timestamp,
hash: Hash256::default(),
blue_score: block_height,
daa_score: block_height,
coinbase: deployer.clone(),
},
synor_vm::context::TransactionInfo::default(),
call_context,
gas_limit,
storage,
self.chain_id,
);
// Execute initialization
debug!(contract = %module.id, "Executing contract init");
let result = engine
.execute(&module, "__synor_init", &init_args, context, gas_limit)
.map_err(|e| anyhow::anyhow!("Initialization failed: {}", e))?;
// Store the contract
{
let store = self.contract_store.read().await;
let store = store
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Contract store not initialized"))?;
let stored = StoredContract {
code: bytecode,
code_hash: contract_id,
deployer: borsh::to_vec(deployer).unwrap_or_default(),
deployed_at: timestamp,
deployed_height: block_height,
};
store.put(&stored)?;
}
// Cache the compiled module
engine.cache_module(module);
info!(
contract_id = hex::encode(&contract_id[..8]),
gas_used = result.gas_used,
"Contract deployed"
);
Ok(DeployResult {
contract_id,
gas_used: result.gas_used,
address: contract_id.to_vec(),
})
}
/// Calls a contract method.
pub async fn call(
&self,
contract_id: &[u8; 32],
method: &str,
args: Vec<u8>,
caller: &Address,
value: u64,
gas_limit: Option<u64>,
block_height: u64,
timestamp: u64,
) -> anyhow::Result<CallResult> {
let engine = self.engine.read().await;
let engine = engine
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Contract service not started"))?;
let gas_limit = gas_limit.unwrap_or(self.default_gas_limit);
let vm_contract_id = ContractId::from_bytes(*contract_id);
// Get or compile the contract
let module = self.get_or_compile_module(engine, contract_id).await?;
// Load contract state into memory
let mut storage = MemoryStorage::new();
self.load_contract_state(&vm_contract_id, &mut storage)
.await?;
// Build call data (method selector + args)
let method_selector = synor_vm_method_selector(method);
let mut call_data = Vec::with_capacity(4 + args.len());
call_data.extend_from_slice(&method_selector);
call_data.extend_from_slice(&args);
// Create execution context
let call_context = CallContext::new(vm_contract_id, caller.clone(), value, call_data.clone());
let context = ExecutionContext::new(
synor_vm::context::BlockInfo {
height: block_height,
timestamp,
hash: Hash256::default(),
blue_score: block_height,
daa_score: block_height,
coinbase: caller.clone(),
},
synor_vm::context::TransactionInfo::default(),
call_context,
gas_limit,
storage,
self.chain_id,
);
// Execute the call
debug!(
contract = hex::encode(&contract_id[..8]),
method = method,
"Executing contract call"
);
let result = engine.execute(&module, "__synor_call", &call_data, context, gas_limit);
match result {
Ok(exec_result) => {
// Persist storage changes
// Note: In a real implementation, we'd track changes from execution
// For now, we don't persist changes from view calls
let logs = exec_result
.logs
.iter()
.map(|log| LogEntry {
contract_id: *log.contract.as_bytes(),
topics: log.topics.iter().map(|t| *t.as_bytes()).collect(),
data: log.data.clone(),
})
.collect();
Ok(CallResult {
data: exec_result.return_data,
gas_used: exec_result.gas_used,
success: true,
logs,
})
}
Err(e) => {
warn!(error = %e, "Contract call failed");
Ok(CallResult {
data: Vec::new(),
gas_used: gas_limit, // Charge full gas on failure
success: false,
logs: Vec::new(),
})
}
}
}
/// Estimates gas for a contract call.
pub async fn estimate_gas(
&self,
contract_id: &[u8; 32],
method: &str,
args: Vec<u8>,
caller: &Address,
value: u64,
block_height: u64,
timestamp: u64,
) -> anyhow::Result<u64> {
// Run with high gas limit and return actual usage
let result = self
.call(
contract_id,
method,
args,
caller,
value,
Some(100_000_000), // High limit for estimation
block_height,
timestamp,
)
.await?;
if result.success {
// Add 20% buffer for safety
Ok((result.gas_used as f64 * 1.2) as u64)
} else {
Err(anyhow::anyhow!("Call would fail"))
}
}
/// Gets contract bytecode.
pub async fn get_code(&self, contract_id: &[u8; 32]) -> anyhow::Result<Option<Vec<u8>>> {
let store = self.contract_store.read().await;
let store = store
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Contract store not initialized"))?;
Ok(store.get_code(contract_id)?)
}
/// Gets contract metadata.
pub async fn get_contract(&self, contract_id: &[u8; 32]) -> anyhow::Result<Option<StoredContract>> {
let store = self.contract_store.read().await;
let store = store
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Contract store not initialized"))?;
Ok(store.get(contract_id)?)
}
/// Gets a value from contract storage.
pub async fn get_storage_at(
&self,
contract_id: &[u8; 32],
key: &[u8; 32],
) -> anyhow::Result<Option<Vec<u8>>> {
let store = self.state_store.read().await;
let store = store
.as_ref()
.ok_or_else(|| anyhow::anyhow!("State store not initialized"))?;
Ok(store.get(contract_id, key)?)
}
/// Checks if a contract exists.
pub async fn contract_exists(&self, contract_id: &[u8; 32]) -> anyhow::Result<bool> {
let store = self.contract_store.read().await;
let store = store
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Contract store not initialized"))?;
Ok(store.exists(contract_id)?)
}
/// Gets or compiles a contract module.
async fn get_or_compile_module(
&self,
engine: &VmEngine,
contract_id: &[u8; 32],
) -> anyhow::Result<ContractModule> {
let vm_contract_id = ContractId::from_bytes(*contract_id);
// Check cache first
if let Some(module) = engine.get_module(&vm_contract_id) {
return Ok((*module).clone());
}
// Load bytecode and compile
let code = self
.get_code(contract_id)
.await?
.ok_or_else(|| anyhow::anyhow!("Contract not found"))?;
let module = engine
.compile(code)
.map_err(|e| anyhow::anyhow!("Compilation failed: {}", e))?;
// Cache for future use
engine.cache_module(module.clone());
Ok(module)
}
/// Loads contract state into memory storage.
async fn load_contract_state(
&self,
contract_id: &ContractId,
storage: &mut MemoryStorage,
) -> anyhow::Result<()> {
let store = self.state_store.read().await;
let store = store
.as_ref()
.ok_or_else(|| anyhow::anyhow!("State store not initialized"))?;
// Load all state for this contract
let entries = store.get_all(contract_id.as_bytes())?;
for (key, value) in entries {
let storage_key = StorageKey::new(key);
let storage_value = StorageValue::new(value);
storage.set(contract_id, storage_key, storage_value);
}
storage.commit();
Ok(())
}
/// Persists storage changes to the database.
pub async fn persist_storage_changes(
&self,
contract_id: &[u8; 32],
changes: Vec<([u8; 32], Option<Vec<u8>>)>,
) -> anyhow::Result<()> {
let store = self.state_store.read().await;
let store = store
.as_ref()
.ok_or_else(|| anyhow::anyhow!("State store not initialized"))?;
for (key, value) in changes {
match value {
Some(data) => store.set(contract_id, &key, &data)?,
None => store.delete(contract_id, &key)?,
}
}
Ok(())
}
}
/// Computes method selector (first 4 bytes of blake3 hash).
fn synor_vm_method_selector(name: &str) -> [u8; 4] {
let hash = blake3::hash(name.as_bytes());
let bytes = hash.as_bytes();
[bytes[0], bytes[1], bytes[2], bytes[3]]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_method_selector() {
let sel1 = synor_vm_method_selector("transfer");
let sel2 = synor_vm_method_selector("transfer");
let sel3 = synor_vm_method_selector("mint");
assert_eq!(sel1, sel2);
assert_ne!(sel1, sel3);
}
}