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
676 lines
20 KiB
Rust
676 lines
20 KiB
Rust
//! Synor Contract Compiler
|
|
//!
|
|
//! A comprehensive toolchain for compiling, optimizing, and validating
|
|
//! Synor smart contracts written in Rust and compiled to WebAssembly.
|
|
//!
|
|
//! # Features
|
|
//!
|
|
//! - Compile Rust contracts to optimized WASM
|
|
//! - Strip unnecessary sections (debug info, custom sections)
|
|
//! - Apply wasm-opt optimizations
|
|
//! - Validate output against Synor VM requirements
|
|
//! - Generate contract ABI from exports
|
|
//! - Report code size and estimated gas costs
|
|
//!
|
|
//! # Usage
|
|
//!
|
|
//! ```rust,ignore
|
|
//! use synor_compiler::{Compiler, CompilerConfig};
|
|
//!
|
|
//! let config = CompilerConfig::default();
|
|
//! let compiler = Compiler::new(config)?;
|
|
//!
|
|
//! // Compile from WASM bytecode
|
|
//! let result = compiler.compile(wasm_bytes)?;
|
|
//!
|
|
//! println!("Optimized size: {} bytes", result.code.len());
|
|
//! println!("Estimated gas: {}", result.estimated_gas);
|
|
//! ```
|
|
//!
|
|
//! # Architecture
|
|
//!
|
|
//! ```text
|
|
//! ┌─────────────────────────────────────────────────────────────────┐
|
|
//! │ SYNOR COMPILER │
|
|
//! ├─────────────────────────────────────────────────────────────────┤
|
|
//! │ │
|
|
//! │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
|
|
//! │ │ Parser │ │ Optimizer │ │ Validator │ │
|
|
//! │ │ (wasmparser)│──▶│ (wasm-opt) │──▶│ (VM Compat) │ │
|
|
//! │ └──────────────┘ └──────────────┘ └──────────────────────┘ │
|
|
//! │ │ │ │
|
|
//! │ ▼ ▼ │
|
|
//! │ ┌──────────────┐ ┌──────────────────┐ │
|
|
//! │ │ Metadata │ │ Gas │ │
|
|
//! │ │ Extractor │ │ Estimator │ │
|
|
//! │ └──────────────┘ └──────────────────┘ │
|
|
//! │ │
|
|
//! └─────────────────────────────────────────────────────────────────┘
|
|
//! ```
|
|
|
|
#![allow(dead_code)]
|
|
|
|
pub mod metadata;
|
|
pub mod optimizer;
|
|
pub mod validator;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use thiserror::Error;
|
|
use tracing::{debug, info, warn};
|
|
|
|
use synor_types::Hash256;
|
|
use synor_vm::{ContractId, GasConfig, MAX_CONTRACT_SIZE, MAX_MEMORY_PAGES};
|
|
|
|
pub use metadata::{ContractAbi, ContractMetadata, FunctionAbi, TypeInfo};
|
|
pub use optimizer::{OptimizationLevel, Optimizer, StripOptions};
|
|
pub use validator::{ValidationError, ValidationResult, Validator};
|
|
|
|
/// Compiler errors.
|
|
#[derive(Debug, Error)]
|
|
pub enum CompilerError {
|
|
/// WASM parsing error.
|
|
#[error("WASM parsing error: {0}")]
|
|
ParseError(String),
|
|
|
|
/// Validation error.
|
|
#[error("Validation error: {0}")]
|
|
ValidationError(#[from] validator::ValidationError),
|
|
|
|
/// Optimization error.
|
|
#[error("Optimization error: {0}")]
|
|
OptimizationError(String),
|
|
|
|
/// Contract too large.
|
|
#[error("Contract too large: {size} bytes (max: {max} bytes)")]
|
|
ContractTooLarge { size: usize, max: usize },
|
|
|
|
/// Missing required export.
|
|
#[error("Missing required export: {0}")]
|
|
MissingExport(String),
|
|
|
|
/// Invalid export signature.
|
|
#[error("Invalid export signature for '{name}': expected {expected}, got {actual}")]
|
|
InvalidExportSignature {
|
|
name: String,
|
|
expected: String,
|
|
actual: String,
|
|
},
|
|
|
|
/// IO error.
|
|
#[error("IO error: {0}")]
|
|
IoError(#[from] std::io::Error),
|
|
|
|
/// Encoding error.
|
|
#[error("Encoding error: {0}")]
|
|
EncodingError(String),
|
|
|
|
/// External tool error.
|
|
#[error("External tool error: {tool}: {message}")]
|
|
ExternalToolError { tool: String, message: String },
|
|
}
|
|
|
|
/// Result type for compiler operations.
|
|
pub type Result<T> = std::result::Result<T, CompilerError>;
|
|
|
|
/// Compiler configuration.
|
|
#[derive(Clone, Debug)]
|
|
pub struct CompilerConfig {
|
|
/// Optimization level.
|
|
pub optimization_level: OptimizationLevel,
|
|
|
|
/// Strip options.
|
|
pub strip_options: StripOptions,
|
|
|
|
/// Maximum contract size (bytes).
|
|
pub max_contract_size: usize,
|
|
|
|
/// Maximum memory pages.
|
|
pub max_memory_pages: u32,
|
|
|
|
/// Whether to run wasm-opt external tool.
|
|
pub use_wasm_opt: bool,
|
|
|
|
/// Path to wasm-opt binary.
|
|
pub wasm_opt_path: Option<PathBuf>,
|
|
|
|
/// Whether to validate against VM requirements.
|
|
pub validate: bool,
|
|
|
|
/// Whether to extract and include metadata.
|
|
pub extract_metadata: bool,
|
|
|
|
/// Gas configuration for estimation.
|
|
pub gas_config: GasConfig,
|
|
|
|
/// Whether to generate ABI.
|
|
pub generate_abi: bool,
|
|
}
|
|
|
|
impl Default for CompilerConfig {
|
|
fn default() -> Self {
|
|
CompilerConfig {
|
|
optimization_level: OptimizationLevel::Size,
|
|
strip_options: StripOptions::default(),
|
|
max_contract_size: MAX_CONTRACT_SIZE,
|
|
max_memory_pages: MAX_MEMORY_PAGES,
|
|
use_wasm_opt: true,
|
|
wasm_opt_path: None,
|
|
validate: true,
|
|
extract_metadata: true,
|
|
gas_config: GasConfig::default(),
|
|
generate_abi: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CompilerConfig {
|
|
/// Creates a configuration for maximum optimization.
|
|
pub fn optimized() -> Self {
|
|
CompilerConfig {
|
|
optimization_level: OptimizationLevel::Aggressive,
|
|
strip_options: StripOptions::all(),
|
|
use_wasm_opt: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Creates a configuration for development (faster compilation).
|
|
pub fn development() -> Self {
|
|
CompilerConfig {
|
|
optimization_level: OptimizationLevel::None,
|
|
strip_options: StripOptions::minimal(),
|
|
use_wasm_opt: false,
|
|
validate: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Creates a configuration for testing.
|
|
pub fn testing() -> Self {
|
|
CompilerConfig {
|
|
optimization_level: OptimizationLevel::Basic,
|
|
strip_options: StripOptions::default(),
|
|
use_wasm_opt: false,
|
|
validate: true,
|
|
gas_config: GasConfig::testing(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Compilation result.
|
|
#[derive(Clone, Debug)]
|
|
pub struct CompilationResult {
|
|
/// Contract ID (hash of optimized code).
|
|
pub contract_id: ContractId,
|
|
|
|
/// Optimized WASM bytecode.
|
|
pub code: Vec<u8>,
|
|
|
|
/// Original code size (before optimization).
|
|
pub original_size: usize,
|
|
|
|
/// Optimized code size.
|
|
pub optimized_size: usize,
|
|
|
|
/// Size reduction percentage.
|
|
pub size_reduction: f64,
|
|
|
|
/// Contract metadata.
|
|
pub metadata: Option<ContractMetadata>,
|
|
|
|
/// Contract ABI.
|
|
pub abi: Option<ContractAbi>,
|
|
|
|
/// Estimated deployment gas.
|
|
pub estimated_deploy_gas: u64,
|
|
|
|
/// Validation result.
|
|
pub validation: ValidationResult,
|
|
|
|
/// Compilation warnings.
|
|
pub warnings: Vec<String>,
|
|
}
|
|
|
|
impl CompilationResult {
|
|
/// Returns the code hash.
|
|
pub fn code_hash(&self) -> Hash256 {
|
|
let hash: [u8; 32] = blake3::hash(&self.code).into();
|
|
Hash256::from_bytes(hash)
|
|
}
|
|
|
|
/// Returns size statistics as a formatted string.
|
|
pub fn size_stats(&self) -> String {
|
|
format!(
|
|
"Original: {} bytes, Optimized: {} bytes, Reduction: {:.1}%",
|
|
self.original_size, self.optimized_size, self.size_reduction
|
|
)
|
|
}
|
|
}
|
|
|
|
/// The main contract compiler.
|
|
pub struct Compiler {
|
|
/// Configuration.
|
|
config: CompilerConfig,
|
|
|
|
/// Optimizer instance.
|
|
optimizer: Optimizer,
|
|
|
|
/// Validator instance.
|
|
validator: Validator,
|
|
}
|
|
|
|
impl Compiler {
|
|
/// Creates a new compiler with the given configuration.
|
|
pub fn new(config: CompilerConfig) -> Result<Self> {
|
|
let optimizer = Optimizer::new(
|
|
config.optimization_level,
|
|
config.strip_options.clone(),
|
|
config.wasm_opt_path.clone(),
|
|
);
|
|
|
|
let validator = Validator::new(config.max_contract_size, config.max_memory_pages);
|
|
|
|
Ok(Compiler {
|
|
config,
|
|
optimizer,
|
|
validator,
|
|
})
|
|
}
|
|
|
|
/// Creates a compiler with default configuration.
|
|
pub fn default_compiler() -> Result<Self> {
|
|
Self::new(CompilerConfig::default())
|
|
}
|
|
|
|
/// Compiles WASM bytecode.
|
|
pub fn compile(&self, wasm: Vec<u8>) -> Result<CompilationResult> {
|
|
let original_size = wasm.len();
|
|
info!("Compiling contract ({} bytes)", original_size);
|
|
|
|
// Check initial size
|
|
if original_size > self.config.max_contract_size {
|
|
return Err(CompilerError::ContractTooLarge {
|
|
size: original_size,
|
|
max: self.config.max_contract_size,
|
|
});
|
|
}
|
|
|
|
// Parse and validate the WASM
|
|
debug!("Parsing WASM module");
|
|
self.parse_wasm(&wasm)?;
|
|
|
|
// Extract metadata before optimization (preserves debug info)
|
|
let metadata = if self.config.extract_metadata {
|
|
debug!("Extracting metadata");
|
|
Some(metadata::extract_metadata(&wasm)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Extract ABI
|
|
let abi = if self.config.generate_abi {
|
|
debug!("Generating ABI");
|
|
Some(metadata::extract_abi(&wasm)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Optimize the WASM
|
|
debug!("Optimizing WASM");
|
|
let optimized = self.optimizer.optimize(wasm)?;
|
|
|
|
// Run external wasm-opt if available and configured
|
|
let optimized = if self.config.use_wasm_opt {
|
|
match self.optimizer.run_wasm_opt(&optimized) {
|
|
Ok(further_optimized) => {
|
|
debug!(
|
|
"wasm-opt reduced size from {} to {} bytes",
|
|
optimized.len(),
|
|
further_optimized.len()
|
|
);
|
|
further_optimized
|
|
}
|
|
Err(e) => {
|
|
warn!("wasm-opt not available: {}", e);
|
|
optimized
|
|
}
|
|
}
|
|
} else {
|
|
optimized
|
|
};
|
|
|
|
let optimized_size = optimized.len();
|
|
let size_reduction = if original_size > 0 {
|
|
((original_size - optimized_size) as f64 / original_size as f64) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
info!(
|
|
"Optimization complete: {} -> {} bytes ({:.1}% reduction)",
|
|
original_size, optimized_size, size_reduction
|
|
);
|
|
|
|
// Validate the optimized WASM
|
|
let validation = if self.config.validate {
|
|
debug!("Validating optimized WASM");
|
|
self.validator.validate(&optimized)?
|
|
} else {
|
|
ValidationResult::success()
|
|
};
|
|
|
|
// Calculate contract ID
|
|
let hash: [u8; 32] = blake3::hash(&optimized).into();
|
|
let contract_id = ContractId::from_bytes(hash);
|
|
|
|
// Estimate deployment gas
|
|
let estimated_deploy_gas = self.estimate_deploy_gas(&optimized);
|
|
|
|
// Collect warnings
|
|
let mut warnings = validation.warnings.clone();
|
|
if optimized_size > self.config.max_contract_size / 2 {
|
|
warnings.push(format!(
|
|
"Contract is large ({} bytes), consider optimization",
|
|
optimized_size
|
|
));
|
|
}
|
|
|
|
Ok(CompilationResult {
|
|
contract_id,
|
|
code: optimized,
|
|
original_size,
|
|
optimized_size,
|
|
size_reduction,
|
|
metadata,
|
|
abi,
|
|
estimated_deploy_gas,
|
|
validation,
|
|
warnings,
|
|
})
|
|
}
|
|
|
|
/// Compiles a contract from a file path.
|
|
pub fn compile_file(&self, path: &std::path::Path) -> Result<CompilationResult> {
|
|
let wasm = std::fs::read(path)?;
|
|
self.compile(wasm)
|
|
}
|
|
|
|
/// Parses WASM and performs initial validation.
|
|
fn parse_wasm(&self, wasm: &[u8]) -> Result<()> {
|
|
use wasmparser::{Parser, Payload};
|
|
|
|
let parser = Parser::new(0);
|
|
|
|
for payload in parser.parse_all(wasm) {
|
|
let payload = payload.map_err(|e| CompilerError::ParseError(e.to_string()))?;
|
|
|
|
match payload {
|
|
Payload::Version { encoding, .. } => {
|
|
if encoding != wasmparser::Encoding::Module {
|
|
return Err(CompilerError::ParseError(
|
|
"Only WASM modules are supported (not components)".into(),
|
|
));
|
|
}
|
|
}
|
|
Payload::End(_) => break,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Estimates gas for deploying the contract.
|
|
fn estimate_deploy_gas(&self, wasm: &[u8]) -> u64 {
|
|
let config = &self.config.gas_config;
|
|
|
|
// Base deployment cost
|
|
let mut gas = config.create_base;
|
|
|
|
// Cost per byte of code
|
|
gas += config.create_byte * wasm.len() as u64;
|
|
|
|
// Estimate memory initialization cost
|
|
gas += self.estimate_memory_init_gas(wasm);
|
|
|
|
gas
|
|
}
|
|
|
|
/// Estimates gas for memory initialization.
|
|
fn estimate_memory_init_gas(&self, wasm: &[u8]) -> u64 {
|
|
use wasmparser::{Parser, Payload};
|
|
|
|
let parser = Parser::new(0);
|
|
let mut data_size = 0usize;
|
|
|
|
for payload in parser.parse_all(wasm) {
|
|
if let Ok(Payload::DataSection(reader)) = payload {
|
|
for data in reader {
|
|
if let Ok(data) = data {
|
|
data_size += data.data.len();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.config.gas_config.memory_byte * data_size as u64
|
|
}
|
|
|
|
/// Returns the compiler configuration.
|
|
pub fn config(&self) -> &CompilerConfig {
|
|
&self.config
|
|
}
|
|
|
|
/// Checks if wasm-opt is available.
|
|
pub fn wasm_opt_available(&self) -> bool {
|
|
self.optimizer.wasm_opt_available()
|
|
}
|
|
}
|
|
|
|
/// Builder for creating a compiler with custom configuration.
|
|
pub struct CompilerBuilder {
|
|
config: CompilerConfig,
|
|
}
|
|
|
|
impl CompilerBuilder {
|
|
/// Creates a new builder with default configuration.
|
|
pub fn new() -> Self {
|
|
CompilerBuilder {
|
|
config: CompilerConfig::default(),
|
|
}
|
|
}
|
|
|
|
/// Sets the optimization level.
|
|
pub fn optimization_level(mut self, level: OptimizationLevel) -> Self {
|
|
self.config.optimization_level = level;
|
|
self
|
|
}
|
|
|
|
/// Sets strip options.
|
|
pub fn strip_options(mut self, options: StripOptions) -> Self {
|
|
self.config.strip_options = options;
|
|
self
|
|
}
|
|
|
|
/// Sets the maximum contract size.
|
|
pub fn max_contract_size(mut self, size: usize) -> Self {
|
|
self.config.max_contract_size = size;
|
|
self
|
|
}
|
|
|
|
/// Sets whether to use wasm-opt.
|
|
pub fn use_wasm_opt(mut self, use_it: bool) -> Self {
|
|
self.config.use_wasm_opt = use_it;
|
|
self
|
|
}
|
|
|
|
/// Sets the path to wasm-opt binary.
|
|
pub fn wasm_opt_path(mut self, path: PathBuf) -> Self {
|
|
self.config.wasm_opt_path = Some(path);
|
|
self
|
|
}
|
|
|
|
/// Sets whether to validate.
|
|
pub fn validate(mut self, validate: bool) -> Self {
|
|
self.config.validate = validate;
|
|
self
|
|
}
|
|
|
|
/// Sets whether to extract metadata.
|
|
pub fn extract_metadata(mut self, extract: bool) -> Self {
|
|
self.config.extract_metadata = extract;
|
|
self
|
|
}
|
|
|
|
/// Sets whether to generate ABI.
|
|
pub fn generate_abi(mut self, generate: bool) -> Self {
|
|
self.config.generate_abi = generate;
|
|
self
|
|
}
|
|
|
|
/// Sets the gas configuration.
|
|
pub fn gas_config(mut self, config: GasConfig) -> Self {
|
|
self.config.gas_config = config;
|
|
self
|
|
}
|
|
|
|
/// Builds the compiler.
|
|
pub fn build(self) -> Result<Compiler> {
|
|
Compiler::new(self.config)
|
|
}
|
|
}
|
|
|
|
impl Default for CompilerBuilder {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Convenience function to compile WASM with default settings.
|
|
pub fn compile(wasm: Vec<u8>) -> Result<CompilationResult> {
|
|
let compiler = Compiler::default_compiler()?;
|
|
compiler.compile(wasm)
|
|
}
|
|
|
|
/// Convenience function to compile WASM with size optimization.
|
|
pub fn compile_optimized(wasm: Vec<u8>) -> Result<CompilationResult> {
|
|
let compiler = Compiler::new(CompilerConfig::optimized())?;
|
|
compiler.compile(wasm)
|
|
}
|
|
|
|
/// Convenience function to compile for development.
|
|
pub fn compile_dev(wasm: Vec<u8>) -> Result<CompilationResult> {
|
|
let compiler = Compiler::new(CompilerConfig::development())?;
|
|
compiler.compile(wasm)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// Minimal valid WASM module
|
|
fn minimal_wasm() -> Vec<u8> {
|
|
// Minimal WASM module with memory export
|
|
vec![
|
|
0x00, 0x61, 0x73, 0x6d, // WASM magic
|
|
0x01, 0x00, 0x00, 0x00, // Version 1
|
|
// Type section
|
|
0x01, 0x04, 0x01, 0x60, 0x00, 0x00, // Type: () -> ()
|
|
// Function section
|
|
0x03, 0x02, 0x01, 0x00, // Function 0 uses type 0
|
|
// Memory section
|
|
0x05, 0x03, 0x01, 0x00, 0x01, // Memory: min 0, max 1 page
|
|
// Export section
|
|
0x07, 0x08, 0x01, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02,
|
|
0x00, // Export "memory" as memory 0
|
|
// Code section
|
|
0x0a, 0x04, 0x01, 0x02, 0x00, 0x0b, // Code: nop, end
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn test_compiler_creation() {
|
|
let compiler = Compiler::default_compiler();
|
|
assert!(compiler.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_compiler_config() {
|
|
let config = CompilerConfig::default();
|
|
assert_eq!(config.max_contract_size, MAX_CONTRACT_SIZE);
|
|
assert!(config.validate);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compiler_config_optimized() {
|
|
let config = CompilerConfig::optimized();
|
|
assert_eq!(config.optimization_level, OptimizationLevel::Aggressive);
|
|
assert!(config.use_wasm_opt);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compiler_config_development() {
|
|
let config = CompilerConfig::development();
|
|
assert_eq!(config.optimization_level, OptimizationLevel::None);
|
|
assert!(!config.use_wasm_opt);
|
|
}
|
|
|
|
#[test]
|
|
fn test_compiler_builder() {
|
|
let compiler = CompilerBuilder::new()
|
|
.optimization_level(OptimizationLevel::Size)
|
|
.use_wasm_opt(false)
|
|
.validate(true)
|
|
.build();
|
|
|
|
assert!(compiler.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_contract_too_large() {
|
|
let config = CompilerConfig {
|
|
max_contract_size: 10,
|
|
..CompilerConfig::development()
|
|
};
|
|
let compiler = Compiler::new(config).unwrap();
|
|
|
|
let large_wasm = vec![0u8; 100];
|
|
let result = compiler.compile(large_wasm);
|
|
|
|
assert!(matches!(
|
|
result,
|
|
Err(CompilerError::ContractTooLarge { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_invalid_wasm() {
|
|
let compiler = Compiler::default_compiler().unwrap();
|
|
let invalid = vec![0x00, 0x01, 0x02, 0x03];
|
|
let result = compiler.compile(invalid);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_compilation_result_stats() {
|
|
let result = CompilationResult {
|
|
contract_id: ContractId::from_bytes([0; 32]),
|
|
code: vec![1, 2, 3],
|
|
original_size: 100,
|
|
optimized_size: 50,
|
|
size_reduction: 50.0,
|
|
metadata: None,
|
|
abi: None,
|
|
estimated_deploy_gas: 1000,
|
|
validation: ValidationResult::success(),
|
|
warnings: vec![],
|
|
};
|
|
|
|
let stats = result.size_stats();
|
|
assert!(stats.contains("100"));
|
|
assert!(stats.contains("50"));
|
|
assert!(stats.contains("50.0%"));
|
|
}
|
|
}
|