synor/crates/synor-vm/src/host.rs
2026-01-08 05:22:24 +05:30

442 lines
13 KiB
Rust

//! Host functions exposed to WASM contracts.
//!
//! These functions provide contracts with access to blockchain state,
//! cryptographic operations, and other capabilities.
use synor_types::{Address, Hash256};
use crate::context::ExecutionContext;
use crate::storage::{ContractStorage, StorageKey, StorageValue};
use crate::{ContractId, VmError};
/// Host functions available to contracts.
pub struct HostFunctions;
impl HostFunctions {
// ==================== Storage Operations ====================
/// Reads a value from contract storage.
pub fn storage_read<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
key: &[u8; 32],
) -> Result<Option<Vec<u8>>, VmError> {
let storage_key = StorageKey::new(*key);
// Charge gas
ctx.gas
.charge_storage_read(32)
.map_err(|_| VmError::OutOfGas {
used: ctx.gas.consumed(),
limit: ctx.gas.limit(),
})?;
Ok(ctx
.storage
.get(&ctx.call.contract, &storage_key)
.map(|v| v.0))
}
/// Writes a value to contract storage.
pub fn storage_write<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
key: &[u8; 32],
value: &[u8],
) -> Result<(), VmError> {
if ctx.is_static() {
return Err(VmError::ExecutionError(
"Cannot write storage in static call".into(),
));
}
let storage_key = StorageKey::new(*key);
// Charge gas based on value size
ctx.gas
.charge_storage_write(value.len())
.map_err(|_| VmError::OutOfGas {
used: ctx.gas.consumed(),
limit: ctx.gas.limit(),
})?;
ctx.storage.set(
&ctx.call.contract,
storage_key,
StorageValue::new(value.to_vec()),
);
Ok(())
}
/// Deletes a value from contract storage.
pub fn storage_delete<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
key: &[u8; 32],
) -> Result<bool, VmError> {
if ctx.is_static() {
return Err(VmError::ExecutionError(
"Cannot delete storage in static call".into(),
));
}
let storage_key = StorageKey::new(*key);
// Add refund for deletion
ctx.gas.add_refund(ctx.gas.config().storage_delete_refund);
let existed = ctx
.storage
.delete(&ctx.call.contract, &storage_key)
.is_some();
Ok(existed)
}
// ==================== Context Information ====================
/// Returns the caller address.
pub fn get_caller<S: ContractStorage>(ctx: &ExecutionContext<S>) -> Address {
ctx.caller()
}
/// Returns the contract's own address.
pub fn get_address<S: ContractStorage>(ctx: &ExecutionContext<S>) -> Address {
ctx.address()
}
/// Returns the transaction origin.
pub fn get_origin<S: ContractStorage>(ctx: &ExecutionContext<S>) -> Address {
ctx.origin()
}
/// Returns the value sent with the call.
pub fn get_value<S: ContractStorage>(ctx: &ExecutionContext<S>) -> u64 {
ctx.value()
}
/// Returns the call data.
pub fn get_calldata<S: ContractStorage>(ctx: &ExecutionContext<S>) -> &[u8] {
ctx.calldata()
}
/// Returns the block timestamp.
pub fn get_timestamp<S: ContractStorage>(ctx: &ExecutionContext<S>) -> u64 {
ctx.timestamp()
}
/// Returns the block height.
pub fn get_block_height<S: ContractStorage>(ctx: &ExecutionContext<S>) -> u64 {
ctx.block_height()
}
/// Returns the block hash.
pub fn get_block_hash<S: ContractStorage>(ctx: &ExecutionContext<S>) -> Hash256 {
ctx.block_hash()
}
/// Returns remaining gas.
pub fn get_gas_remaining<S: ContractStorage>(ctx: &ExecutionContext<S>) -> u64 {
ctx.gas_remaining()
}
/// Returns the chain ID.
pub fn get_chain_id<S: ContractStorage>(ctx: &ExecutionContext<S>) -> u64 {
ctx.chain_id
}
// ==================== Cryptographic Operations ====================
/// Computes SHA3-256 hash.
pub fn sha3<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
data: &[u8],
) -> Result<[u8; 32], VmError> {
ctx.gas
.charge_hash(data.len())
.map_err(|_| VmError::OutOfGas {
used: ctx.gas.consumed(),
limit: ctx.gas.limit(),
})?;
use sha3::{Digest, Sha3_256};
let result = Sha3_256::digest(data);
Ok(result.into())
}
/// Computes Blake3 hash.
pub fn blake3<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
data: &[u8],
) -> Result<[u8; 32], VmError> {
ctx.gas
.charge_hash(data.len())
.map_err(|_| VmError::OutOfGas {
used: ctx.gas.consumed(),
limit: ctx.gas.limit(),
})?;
Ok(blake3::hash(data).into())
}
/// Verifies an Ed25519 signature.
pub fn verify_signature<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
_message: &[u8],
_signature: &[u8; 64],
_public_key: &[u8; 32],
) -> Result<bool, VmError> {
ctx.gas.charge_signature().map_err(|_| VmError::OutOfGas {
used: ctx.gas.consumed(),
limit: ctx.gas.limit(),
})?;
// Use synor_crypto for verification
// Simplified for now
Ok(true) // Placeholder
}
// ==================== Contract Calls ====================
/// Calls another contract.
pub fn call_contract<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
_target: ContractId,
value: u64,
input: &[u8],
_gas_limit: u64,
) -> Result<Vec<u8>, VmError> {
if ctx.is_static() && value > 0 {
return Err(VmError::ExecutionError(
"Cannot send value in static call".into(),
));
}
// Charge gas for call
ctx.gas
.charge_call(input.len())
.map_err(|_| VmError::OutOfGas {
used: ctx.gas.consumed(),
limit: ctx.gas.limit(),
})?;
// This would trigger actual contract execution
// For now, return empty success
Ok(Vec::new())
}
/// Makes a static (read-only) call to another contract.
pub fn static_call<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
_target: ContractId,
input: &[u8],
_gas_limit: u64,
) -> Result<Vec<u8>, VmError> {
ctx.gas
.charge_call(input.len())
.map_err(|_| VmError::OutOfGas {
used: ctx.gas.consumed(),
limit: ctx.gas.limit(),
})?;
Ok(Vec::new())
}
/// Makes a delegate call (code runs in caller's context).
pub fn delegate_call<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
_target: ContractId,
input: &[u8],
_gas_limit: u64,
) -> Result<Vec<u8>, VmError> {
if ctx.is_static() {
return Err(VmError::ExecutionError(
"Cannot delegate call in static call".into(),
));
}
ctx.gas
.charge_call(input.len())
.map_err(|_| VmError::OutOfGas {
used: ctx.gas.consumed(),
limit: ctx.gas.limit(),
})?;
Ok(Vec::new())
}
// ==================== Events ====================
/// Emits a log event.
pub fn emit_log<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
topics: Vec<[u8; 32]>,
data: Vec<u8>,
) -> Result<(), VmError> {
let topics: Vec<Hash256> = topics.into_iter().map(Hash256::from_bytes).collect();
ctx.emit_log(topics, data)
}
// ==================== Control Flow ====================
/// Reverts execution with a message.
pub fn revert(message: &str) -> VmError {
VmError::Revert(message.to_string())
}
/// Returns successfully with data.
pub fn return_data<S: ContractStorage>(ctx: &mut ExecutionContext<S>, data: Vec<u8>) {
ctx.set_return_data(data);
}
// ==================== Balance Operations ====================
/// Gets the balance of an address.
pub fn get_balance<S: ContractStorage>(
_ctx: &ExecutionContext<S>,
_address: &Address,
) -> Result<u64, VmError> {
// Would query UTXO set
Ok(0)
}
/// Transfers value to an address.
pub fn transfer<S: ContractStorage>(
ctx: &mut ExecutionContext<S>,
_to: &Address,
_amount: u64,
) -> Result<(), VmError> {
if ctx.is_static() {
return Err(VmError::ExecutionError(
"Cannot transfer in static call".into(),
));
}
// Would create transfer output
Ok(())
}
}
/// WASM import definitions for host functions.
/// These map to the actual wasmtime host function implementations.
pub mod wasm_imports {
/// Storage read signature: (key_ptr: i32, value_ptr: i32) -> i32 (length or -1)
pub const STORAGE_READ: &str = "synor_storage_read";
/// Storage write signature: (key_ptr: i32, value_ptr: i32, value_len: i32) -> i32
pub const STORAGE_WRITE: &str = "synor_storage_write";
/// Storage delete signature: (key_ptr: i32) -> i32 (existed: 0/1)
pub const STORAGE_DELETE: &str = "synor_storage_delete";
/// Get caller: (out_ptr: i32) -> ()
pub const GET_CALLER: &str = "synor_get_caller";
/// Get address: (out_ptr: i32) -> ()
pub const GET_ADDRESS: &str = "synor_get_address";
/// Get value: () -> i64
pub const GET_VALUE: &str = "synor_get_value";
/// Get calldata size: () -> i32
pub const GET_CALLDATA_SIZE: &str = "synor_get_calldata_size";
/// Get calldata: (out_ptr: i32, offset: i32, length: i32) -> ()
pub const GET_CALLDATA: &str = "synor_get_calldata";
/// Get timestamp: () -> i64
pub const GET_TIMESTAMP: &str = "synor_get_timestamp";
/// Get block height: () -> i64
pub const GET_BLOCK_HEIGHT: &str = "synor_get_block_height";
/// SHA3 hash: (data_ptr: i32, data_len: i32, out_ptr: i32) -> ()
pub const SHA3: &str = "synor_sha3";
/// Blake3 hash: (data_ptr: i32, data_len: i32, out_ptr: i32) -> ()
pub const BLAKE3: &str = "synor_blake3";
/// Emit log: (topics_ptr: i32, topics_count: i32, data_ptr: i32, data_len: i32) -> i32
pub const EMIT_LOG: &str = "synor_emit_log";
/// Call contract: (addr_ptr: i32, value: i64, data_ptr: i32, data_len: i32, gas: i64) -> i32
pub const CALL: &str = "synor_call";
/// Revert: (msg_ptr: i32, msg_len: i32) -> ()
pub const REVERT: &str = "synor_revert";
/// Return: (data_ptr: i32, data_len: i32) -> ()
pub const RETURN: &str = "synor_return";
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::CallContext;
use crate::storage::MemoryStorage;
use synor_types::Network;
fn test_context() -> ExecutionContext<MemoryStorage> {
let contract = ContractId::from_bytes([0x42; 32]);
let caller = Address::from_ed25519_pubkey(Network::Mainnet, &[0x11; 32]);
let call = CallContext::new(contract, caller, 1000, vec![1, 2, 3]);
ExecutionContext::<MemoryStorage>::testing(call)
}
#[test]
fn test_storage_operations() {
let mut ctx = test_context();
let key = [0x01; 32];
let value = vec![1, 2, 3, 4];
// Write
HostFunctions::storage_write(&mut ctx, &key, &value).unwrap();
ctx.storage.commit();
// Read
let read = HostFunctions::storage_read(&mut ctx, &key).unwrap();
assert_eq!(read, Some(value.clone()));
// Delete
let existed = HostFunctions::storage_delete(&mut ctx, &key).unwrap();
assert!(existed);
ctx.storage.commit();
let read = HostFunctions::storage_read(&mut ctx, &key).unwrap();
assert!(read.is_none());
}
#[test]
fn test_context_getters() {
let ctx = test_context();
assert_eq!(HostFunctions::get_value(&ctx), 1000);
assert_eq!(HostFunctions::get_calldata(&ctx), &[1, 2, 3]);
assert!(HostFunctions::get_gas_remaining(&ctx) > 0);
}
#[test]
fn test_hash_functions() {
let mut ctx = test_context();
let data = b"hello world";
let sha3_result = HostFunctions::sha3(&mut ctx, data).unwrap();
let blake3_result = HostFunctions::blake3(&mut ctx, data).unwrap();
assert_ne!(sha3_result, blake3_result);
assert_ne!(sha3_result, [0u8; 32]);
}
#[test]
fn test_static_call_restrictions() {
let contract = ContractId::from_bytes([0x42; 32]);
let caller = Address::from_ed25519_pubkey(Network::Mainnet, &[0x11; 32]);
let call = CallContext::static_call(contract, caller, vec![]);
let mut ctx = ExecutionContext::<MemoryStorage>::testing(call);
// Storage write should fail
let result = HostFunctions::storage_write(&mut ctx, &[0x01; 32], &[1, 2, 3]);
assert!(result.is_err());
// Emit log should fail
let result = ctx.emit_log(vec![], vec![]);
assert!(result.is_err());
}
}