442 lines
13 KiB
Rust
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());
|
|
}
|
|
}
|