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
484 lines
15 KiB
Rust
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);
|
|
}
|
|
}
|