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
467 lines
12 KiB
Rust
467 lines
12 KiB
Rust
//! Execution context for contract calls.
|
|
//!
|
|
//! The context provides contracts with information about the current
|
|
//! execution environment (caller, value, block info, etc.).
|
|
|
|
use synor_types::{Address, Hash256, Network};
|
|
|
|
use crate::gas::GasMeter;
|
|
use crate::storage::{ContractStorage, MemoryStorage};
|
|
use crate::ContractId;
|
|
|
|
/// Block information available to contracts.
|
|
#[derive(Clone, Debug)]
|
|
pub struct BlockInfo {
|
|
/// Block height.
|
|
pub height: u64,
|
|
/// Block timestamp (Unix milliseconds).
|
|
pub timestamp: u64,
|
|
/// Block hash (of selected parent).
|
|
pub hash: Hash256,
|
|
/// Blue score.
|
|
pub blue_score: u64,
|
|
/// DAA score (for difficulty).
|
|
pub daa_score: u64,
|
|
/// Coinbase address.
|
|
pub coinbase: Address,
|
|
}
|
|
|
|
impl Default for BlockInfo {
|
|
fn default() -> Self {
|
|
BlockInfo {
|
|
height: 0,
|
|
timestamp: 0,
|
|
hash: Hash256::default(),
|
|
blue_score: 0,
|
|
daa_score: 0,
|
|
coinbase: Address::from_ed25519_pubkey(Network::Mainnet, &[0; 32]),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Transaction information.
|
|
#[derive(Clone, Debug)]
|
|
pub struct TransactionInfo {
|
|
/// Transaction hash.
|
|
pub hash: Hash256,
|
|
/// Transaction origin (original sender).
|
|
pub origin: Address,
|
|
/// Gas price.
|
|
pub gas_price: u64,
|
|
/// Gas limit.
|
|
pub gas_limit: u64,
|
|
}
|
|
|
|
impl Default for TransactionInfo {
|
|
fn default() -> Self {
|
|
TransactionInfo {
|
|
hash: Hash256::default(),
|
|
origin: Address::from_ed25519_pubkey(Network::Mainnet, &[0; 32]),
|
|
gas_price: 1,
|
|
gas_limit: 10_000_000,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Call-specific context.
|
|
#[derive(Clone, Debug)]
|
|
pub struct CallContext {
|
|
/// The contract being called.
|
|
pub contract: ContractId,
|
|
/// The caller (previous contract or EOA).
|
|
pub caller: Address,
|
|
/// Value sent with the call.
|
|
pub value: u64,
|
|
/// Input data (method + args).
|
|
pub input: Vec<u8>,
|
|
/// Whether this is a delegate call.
|
|
pub is_delegate: bool,
|
|
/// Whether this is a static call (read-only).
|
|
pub is_static: bool,
|
|
/// Call depth.
|
|
pub depth: u32,
|
|
}
|
|
|
|
impl CallContext {
|
|
/// Creates a new call context.
|
|
pub fn new(contract: ContractId, caller: Address, value: u64, input: Vec<u8>) -> Self {
|
|
CallContext {
|
|
contract,
|
|
caller,
|
|
value,
|
|
input,
|
|
is_delegate: false,
|
|
is_static: false,
|
|
depth: 0,
|
|
}
|
|
}
|
|
|
|
/// Creates a static (read-only) call context.
|
|
pub fn static_call(contract: ContractId, caller: Address, input: Vec<u8>) -> Self {
|
|
CallContext {
|
|
contract,
|
|
caller,
|
|
value: 0,
|
|
input,
|
|
is_delegate: false,
|
|
is_static: true,
|
|
depth: 0,
|
|
}
|
|
}
|
|
|
|
/// Creates a delegate call context.
|
|
pub fn delegate_call(
|
|
contract: ContractId,
|
|
caller: Address,
|
|
value: u64,
|
|
input: Vec<u8>,
|
|
) -> Self {
|
|
CallContext {
|
|
contract,
|
|
caller,
|
|
value,
|
|
input,
|
|
is_delegate: true,
|
|
is_static: false,
|
|
depth: 0,
|
|
}
|
|
}
|
|
|
|
/// Increments call depth for nested calls.
|
|
pub fn nested(&self, contract: ContractId, value: u64, input: Vec<u8>) -> Self {
|
|
CallContext {
|
|
contract,
|
|
// Current contract becomes caller - use contract hash as "address"
|
|
caller: Address::from_ed25519_pubkey(Network::Mainnet, self.contract.as_bytes()),
|
|
value,
|
|
input,
|
|
is_delegate: false,
|
|
is_static: self.is_static, // Inherit static flag
|
|
depth: self.depth + 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Full execution context.
|
|
pub struct ExecutionContext<S: ContractStorage = MemoryStorage> {
|
|
/// Block information.
|
|
pub block: BlockInfo,
|
|
/// Transaction information.
|
|
pub tx: TransactionInfo,
|
|
/// Current call context.
|
|
pub call: CallContext,
|
|
/// Gas meter.
|
|
pub gas: GasMeter,
|
|
/// Contract storage.
|
|
pub storage: S,
|
|
/// Return data from last call.
|
|
pub return_data: Vec<u8>,
|
|
/// Logs accumulated.
|
|
pub logs: Vec<crate::ContractLog>,
|
|
/// Chain ID.
|
|
pub chain_id: u64,
|
|
}
|
|
|
|
impl<S: ContractStorage> ExecutionContext<S> {
|
|
/// Creates a new execution context.
|
|
pub fn new(
|
|
block: BlockInfo,
|
|
tx: TransactionInfo,
|
|
call: CallContext,
|
|
gas_limit: u64,
|
|
storage: S,
|
|
chain_id: u64,
|
|
) -> Self {
|
|
ExecutionContext {
|
|
block,
|
|
tx,
|
|
call,
|
|
gas: GasMeter::new(gas_limit),
|
|
storage,
|
|
return_data: Vec::new(),
|
|
logs: Vec::new(),
|
|
chain_id,
|
|
}
|
|
}
|
|
|
|
/// Creates a context for testing.
|
|
pub fn testing(call: CallContext) -> ExecutionContext<MemoryStorage> {
|
|
ExecutionContext {
|
|
block: BlockInfo::default(),
|
|
tx: TransactionInfo::default(),
|
|
call,
|
|
gas: GasMeter::new(10_000_000),
|
|
storage: MemoryStorage::new(),
|
|
return_data: Vec::new(),
|
|
logs: Vec::new(),
|
|
chain_id: 1,
|
|
}
|
|
}
|
|
|
|
/// Gets the current contract address as Address.
|
|
pub fn address(&self) -> Address {
|
|
// Contract address derived from contract ID hash
|
|
Address::from_ed25519_pubkey(Network::Mainnet, self.call.contract.as_bytes())
|
|
}
|
|
|
|
/// Gets the caller address.
|
|
pub fn caller(&self) -> Address {
|
|
self.call.caller.clone()
|
|
}
|
|
|
|
/// Gets the transaction origin.
|
|
pub fn origin(&self) -> Address {
|
|
self.tx.origin.clone()
|
|
}
|
|
|
|
/// Gets the call value.
|
|
pub fn value(&self) -> u64 {
|
|
self.call.value
|
|
}
|
|
|
|
/// Gets the call input data.
|
|
pub fn calldata(&self) -> &[u8] {
|
|
&self.call.input
|
|
}
|
|
|
|
/// Gets the call data size.
|
|
pub fn calldatasize(&self) -> usize {
|
|
self.call.input.len()
|
|
}
|
|
|
|
/// Gets remaining gas.
|
|
pub fn gas_remaining(&self) -> u64 {
|
|
self.gas.remaining()
|
|
}
|
|
|
|
/// Gets the block timestamp.
|
|
pub fn timestamp(&self) -> u64 {
|
|
self.block.timestamp
|
|
}
|
|
|
|
/// Gets the block height.
|
|
pub fn block_height(&self) -> u64 {
|
|
self.block.height
|
|
}
|
|
|
|
/// Gets the block hash.
|
|
pub fn block_hash(&self) -> Hash256 {
|
|
self.block.hash
|
|
}
|
|
|
|
/// Checks if this is a static call.
|
|
pub fn is_static(&self) -> bool {
|
|
self.call.is_static
|
|
}
|
|
|
|
/// Checks if state modifications are allowed.
|
|
pub fn can_modify_state(&self) -> bool {
|
|
!self.call.is_static
|
|
}
|
|
|
|
/// Sets the return data.
|
|
pub fn set_return_data(&mut self, data: Vec<u8>) {
|
|
self.return_data = data;
|
|
}
|
|
|
|
/// Gets the last return data.
|
|
pub fn get_return_data(&self) -> &[u8] {
|
|
&self.return_data
|
|
}
|
|
|
|
/// Emits a log.
|
|
pub fn emit_log(&mut self, topics: Vec<Hash256>, data: Vec<u8>) -> Result<(), crate::VmError> {
|
|
if self.is_static() {
|
|
return Err(crate::VmError::ExecutionError(
|
|
"Cannot emit logs in static call".into(),
|
|
));
|
|
}
|
|
|
|
// Charge gas
|
|
self.gas
|
|
.charge_log(data.len(), topics.len())
|
|
.map_err(|_e| crate::VmError::OutOfGas {
|
|
used: self.gas.consumed(),
|
|
limit: self.gas.limit(),
|
|
})?;
|
|
|
|
self.logs.push(crate::ContractLog {
|
|
contract: self.call.contract,
|
|
topics,
|
|
data,
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Creates a nested context for a sub-call.
|
|
pub fn nested_call(
|
|
&self,
|
|
target: ContractId,
|
|
value: u64,
|
|
input: Vec<u8>,
|
|
gas_limit: u64,
|
|
) -> Result<ExecutionContext<MemoryStorage>, crate::VmError> {
|
|
if self.call.depth >= crate::MAX_CALL_DEPTH {
|
|
return Err(crate::VmError::CallDepthExceeded(self.call.depth + 1));
|
|
}
|
|
|
|
let nested_call = self.call.nested(target, value, input);
|
|
|
|
Ok(ExecutionContext {
|
|
block: self.block.clone(),
|
|
tx: self.tx.clone(),
|
|
call: nested_call,
|
|
gas: GasMeter::new(gas_limit),
|
|
storage: MemoryStorage::new(), // Nested calls get fresh storage view
|
|
return_data: Vec::new(),
|
|
logs: Vec::new(),
|
|
chain_id: self.chain_id,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Builder for execution contexts.
|
|
pub struct ContextBuilder<S: ContractStorage = MemoryStorage> {
|
|
block: BlockInfo,
|
|
tx: TransactionInfo,
|
|
call: Option<CallContext>,
|
|
gas_limit: u64,
|
|
storage: Option<S>,
|
|
chain_id: u64,
|
|
}
|
|
|
|
impl<S: ContractStorage> ContextBuilder<S> {
|
|
/// Creates a new context builder.
|
|
pub fn new() -> Self {
|
|
ContextBuilder {
|
|
block: BlockInfo::default(),
|
|
tx: TransactionInfo::default(),
|
|
call: None,
|
|
gas_limit: 10_000_000,
|
|
storage: None,
|
|
chain_id: 1,
|
|
}
|
|
}
|
|
|
|
/// Sets block info.
|
|
pub fn block(mut self, block: BlockInfo) -> Self {
|
|
self.block = block;
|
|
self
|
|
}
|
|
|
|
/// Sets transaction info.
|
|
pub fn transaction(mut self, tx: TransactionInfo) -> Self {
|
|
self.tx = tx;
|
|
self
|
|
}
|
|
|
|
/// Sets call context.
|
|
pub fn call(mut self, call: CallContext) -> Self {
|
|
self.call = Some(call);
|
|
self
|
|
}
|
|
|
|
/// Sets gas limit.
|
|
pub fn gas_limit(mut self, limit: u64) -> Self {
|
|
self.gas_limit = limit;
|
|
self
|
|
}
|
|
|
|
/// Sets storage.
|
|
pub fn storage(mut self, storage: S) -> Self {
|
|
self.storage = Some(storage);
|
|
self
|
|
}
|
|
|
|
/// Sets chain ID.
|
|
pub fn chain_id(mut self, chain_id: u64) -> Self {
|
|
self.chain_id = chain_id;
|
|
self
|
|
}
|
|
|
|
/// Builds the context.
|
|
pub fn build(self) -> Result<ExecutionContext<S>, &'static str> {
|
|
let call = self.call.ok_or("Call context is required")?;
|
|
let storage = self.storage.ok_or("Storage is required")?;
|
|
|
|
Ok(ExecutionContext::new(
|
|
self.block,
|
|
self.tx,
|
|
call,
|
|
self.gas_limit,
|
|
storage,
|
|
self.chain_id,
|
|
))
|
|
}
|
|
}
|
|
|
|
impl Default for ContextBuilder<MemoryStorage> {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::storage::MemoryStorage;
|
|
use synor_types::Network;
|
|
|
|
fn test_contract_id() -> ContractId {
|
|
ContractId::from_bytes([0x42; 32])
|
|
}
|
|
|
|
fn test_address() -> Address {
|
|
Address::from_ed25519_pubkey(Network::Mainnet, &[0x11; 32])
|
|
}
|
|
|
|
#[test]
|
|
fn test_call_context() {
|
|
let contract = test_contract_id();
|
|
let caller = test_address();
|
|
|
|
let ctx = CallContext::new(contract, caller, 1000, vec![1, 2, 3]);
|
|
|
|
assert_eq!(ctx.value, 1000);
|
|
assert_eq!(ctx.depth, 0);
|
|
assert!(!ctx.is_static);
|
|
}
|
|
|
|
#[test]
|
|
fn test_nested_call() {
|
|
let contract = test_contract_id();
|
|
let caller = test_address();
|
|
|
|
let ctx = CallContext::new(contract, caller, 1000, vec![]);
|
|
let nested = ctx.nested(ContractId::from_bytes([0x99; 32]), 500, vec![4, 5, 6]);
|
|
|
|
assert_eq!(nested.depth, 1);
|
|
assert_eq!(nested.value, 500);
|
|
}
|
|
|
|
#[test]
|
|
fn test_execution_context() {
|
|
let contract = test_contract_id();
|
|
let caller = test_address();
|
|
let call = CallContext::new(contract, caller, 1000, vec![1, 2, 3]);
|
|
|
|
let ctx = ExecutionContext::<MemoryStorage>::testing(call);
|
|
|
|
assert_eq!(ctx.value(), 1000);
|
|
assert_eq!(ctx.calldatasize(), 3);
|
|
assert!(ctx.gas_remaining() > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_static_call() {
|
|
let contract = test_contract_id();
|
|
let caller = test_address();
|
|
let call = CallContext::static_call(contract, caller, vec![]);
|
|
|
|
let ctx = ExecutionContext::<MemoryStorage>::testing(call);
|
|
|
|
assert!(ctx.is_static());
|
|
assert!(!ctx.can_modify_state());
|
|
}
|
|
}
|