TAPLE Core uses WebAssembly (WASM) smart contracts to evaluate state transitions. This allows for deterministic, sandboxed execution of business logic across distributed nodes.
Contract Execution Architecture
The contract executor uses Wasmtime to run compiled WASM modules in a controlled environment. Each contract receives:
- Current state: The subject’s current state as JSON
- Event data: The incoming event requesting a state change
- Is owner: Boolean indicating if the requester owns the subject
The contract returns:
- Final state: The new state after applying the event
- Success: Whether the execution succeeded
- Approval required: Whether the change needs approval
Contract Result Structure
From core/src/evaluator/runner/executor.rs:24-29:
pub struct ContractResult {
pub final_state: ValueWrapper,
pub approval_required: bool,
pub success: bool,
}
WASM Contract Interface
Contracts must implement the following interface to interact with the TAPLE runtime.
Entry Point
Every contract must export a main_function with this signature:
// Entry point: (state_ptr, event_ptr, is_owner) -> result_ptr
main_function(state_ptr: u32, event_ptr: u32, is_owner: u32) -> u32
SDK Functions
Contracts can import these functions from the env module (from core/src/evaluator/runner/executor.rs:164-210):
// Get length of data at pointer
pointer_len(pointer: i32) -> u32
// Allocate memory of specified length
alloc(len: u32) -> u32
// Write a byte to memory
write_byte(ptr: u32, offset: u32, data: u32)
// Read a byte from memory
read_byte(index: i32) -> u32
// Debug output
cout(ptr: u32)
Contract Compilation
The compiler automatically compiles Rust smart contracts to WASM and validates their imports.
Compilation Process
From core/src/evaluator/compiler/compiler.rs:123-162:
async fn compile(
&self,
contract: String,
governance_id: &str,
schema_id: &str,
sn: u64,
) -> Result<(), CompilerErrorResponses> {
// Write contract source to lib.rs
fs::write(format!("{}/src/lib.rs", self.contracts_path), contract)
.await
.map_err(|_| CompilerErrorResponses::WriteFileError)?;
// Compile with cargo
let status = Command::new("cargo")
.arg("build")
.arg(format!("--manifest-path={}/Cargo.toml", self.contracts_path))
.arg("--target")
.arg("wasm32-unknown-unknown")
.arg("--release")
.output()
.map_err(|_| CompilerErrorResponses::CargoExecError)?;
if !status.status.success() {
return Err(CompilerErrorResponses::CargoExecError);
}
Ok(())
}
AOT Compilation
Contracts are precompiled using Wasmtime’s Ahead-of-Time (AOT) compilation for better performance (from core/src/evaluator/compiler/compiler.rs:164-194):
async fn add_contract(&self) -> Result<Vec<u8>, CompilerErrorResponses> {
// Read compiled WASM
let file = fs::read(format!(
"{}/target/wasm32-unknown-unknown/release/contract.wasm",
self.contracts_path
))
.await
.map_err(|_| CompilerErrorResponses::AddContractFail)?;
// Precompile module
let module_bytes = self
.engine
.precompile_module(&file)
.map_err(|_| CompilerErrorResponses::AddContractFail)?;
// Validate imports
let module = unsafe {
wasmtime::Module::deserialize(&self.engine, &module_bytes).unwrap()
};
let imports = module.imports();
for import in imports {
if !self.available_imports_set.contains(import.name()) {
return Err(CompilerErrorResponses::InvalidImportFound);
}
}
Ok(module_bytes)
}
Governance Contract
TAPLE includes a built-in governance contract for managing governance updates using JSON Patch.
From core/src/evaluator/runner/executor.rs:55-83:
async fn execute_gov_contract(
&self,
state: &ValueWrapper,
event: &ValueWrapper,
) -> Result<ContractResult, ExecutorErrorResponses> {
let Ok(event) = serde_json::from_value::<GovernanceEvent>(event.0.clone()) else {
return Ok(ContractResult::error())
};
match &event {
GovernanceEvent::Patch { data } => {
// Apply JSON patch to state
let Ok(patched_state) = apply_patch(data.0.clone(), state.0.clone()) else {
return Ok(ContractResult::error());
};
// Validate new governance state
if let Ok(_) = check_governance_state(&patched_state) {
Ok(ContractResult {
final_state: ValueWrapper(serde_json::to_value(patched_state).unwrap()),
approval_required: true,
success: true,
})
} else {
Ok(ContractResult {
final_state: state.clone(),
approval_required: false,
success: false,
})
}
}
}
}
Memory Management
The contract executor manages memory through a MemoryManager context that handles serialization and pointer management.
From core/src/evaluator/runner/executor.rs:123-140:
fn generate_context(
&self,
state: &ValueWrapper,
event: &ValueWrapper,
) -> Result<(MemoryManager, u32, u32), ExecutorErrorResponses> {
let mut context = MemoryManager::new();
// Serialize state and get pointer
let state_ptr = context.add_data_raw(
&state
.try_to_vec()
.map_err(|_| ExecutorErrorResponses::BorshSerializationError)?,
);
// Serialize event and get pointer
let event_ptr = context.add_data_raw(
&event
.try_to_vec()
.map_err(|_| ExecutorErrorResponses::BorshSerializationError)?,
);
Ok((context, state_ptr as u32, event_ptr as u32))
}
Contract Execution Flow
The complete execution flow from core/src/evaluator/runner/executor.rs:85-121:
pub async fn execute_contract(
&self,
state: &ValueWrapper,
event: &ValueWrapper,
compiled_contract: Contract,
is_owner: bool,
) -> Result<ContractResult, ExecutorErrorResponses> {
// Handle governance contract specially
let Contract::CompiledContract(contract_bytes) = compiled_contract else {
return self.execute_gov_contract(state, event).await;
};
// Load WASM module
let module = unsafe {
Module::deserialize(&self.engine, contract_bytes).unwrap()
};
// Generate context and serialize inputs
let (context, state_ptr, event_ptr) = self.generate_context(&state, &event)?;
let mut store = Store::new(&self.engine, context);
// Generate linker with SDK functions
let linker = self.generate_linker(&self.engine)?;
// Instantiate contract
let instance = linker
.instantiate(&mut store, &module)
.map_err(|_| ExecutorErrorResponses::ContractNotInstantiated)?;
// Get entry point
let contract_entrypoint = instance
.get_typed_func::<(u32, u32, u32), u32>(&mut store, "main_function")
.map_err(|_| ExecutorErrorResponses::ContractEntryPointNotFound)?;
// Execute contract
let result_ptr = contract_entrypoint
.call(
&mut store,
(state_ptr, event_ptr, if is_owner { 1 } else { 0 }),
)
.map_err(|_| ExecutorErrorResponses::ContractExecutionFailed)?;
// Deserialize result
let contract_result = self.get_result(&store, result_ptr)?;
Ok(contract_result)
}
Contracts must be deterministic - they should produce the same output for the same inputs across all nodes. Avoid using random numbers, current time, or any non-deterministic operations.
Best Practices
Security
- Validate all inputs: Check state and event data before processing
- Use safe arithmetic: Handle overflow and underflow conditions
- Limit resource usage: Avoid unbounded loops or excessive memory allocation
- Minimize allocations: Reuse buffers when possible
- Keep contracts small: Large contracts take longer to compile and execute
- Cache compiled contracts: The database stores precompiled contracts by governance version
Testing
- Unit test contract logic: Test Rust code before compilation
- Integration test: Verify contract behavior in TAPLE environment
- Test edge cases: Empty states, invalid events, boundary conditions
Configuration
Set the smart contracts directory in node settings:
let mut settings = Settings::default();
settings.node.smartcontracts_directory = "./contracts".to_string();
The compiler will:
- Create
Cargo.toml if it doesn’t exist
- Write contract source to
src/lib.rs
- Compile to
target/wasm32-unknown-unknown/release/contract.wasm
- Precompile and cache the result