Skip to main content
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

Performance

  • 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:
  1. Create Cargo.toml if it doesn’t exist
  2. Write contract source to src/lib.rs
  3. Compile to target/wasm32-unknown-unknown/release/contract.wasm
  4. Precompile and cache the result

Build docs developers (and LLMs) love