The initial core VM implementation is complete and we’re now moving into the fun part: the blockchain integration. Athena can run RISC-V programs compiled to rv32im
and rv32em
but those programs can’t yet do anything blockchain-related. While there are many possible host functions that we may want to explore, I think it makes sense to start with a few basic ones for the purpose of the end-to-end prototype. Now is the time to finalize their design, as well as the design of a few related items (e.g., account state).
@talm previously proposed an initial set of host functions; this seems like a good place to start, although I might leave out templates for now, for simplicity:
The desired outcome here is both the actual Athena semantics for performing these actions, as well as what the athena<>host interface looks like (sample).
Read state object
Keep it simple and use a key-value store. For now, as Tal initially proposed, assume that a contract can only read/write its own state. Use a 32-bit word as the key and a 32-bit word as the value. Possible errors include permission denied (probably not relevant in the simplistic case of a contract reading its own state), key not found, and not enough gas. Don’t do any fancy serialization/deserialization in the runtime; the contract is responsible for doing this work, for now.
fn main() {
// calculate the key as the first 4 bytes of the hash of the key name
let keyname = "my_number";
let mut hasher = Sha256::new();
hasher.update(keyname);
let result = hasher.finalize();
let mut key = [0u8; 4];
key.copy_from_slice(&result[0..4]);
let value: u32 = 42; // Example integer value
// needs error handling
athena_vm::chain::store(&key, value.to_le_bytes())?;
}
Obviously this is a lot of work to calculate the key, but we do this work automatically using helper functions.
Write state object
As above. 32-bit word key, 32-bit word value. Writing to an existing key just overwrites whatever value was already there. Possible errors include permission denied (same caveat), and not enough gas.
fn main() {
// calculate the key as the first 4 bytes of the hash of the key name
let keyname = "my_number";
let mut hasher = Sha256::new();
hasher.update(keyname);
let result = hasher.finalize();
let mut key = [0u8; 4];
key.copy_from_slice(&result[0..4]);
// needs error handling
let value: u32 = athena_vm::chain::read(&key)?;
}
Deploy a new contract (creating a new account)
Host function takes contract code, initialization parameters (immutable state), and an endowment (number of coins). If successful, it returns the newly-deployed contract address, calculated deterministically on the basis of the code, the initialization params, the sender address and nonce. Errors include not enough coins (for endowment), not enough gas, and deployment failed (e.g., code malformed, or init method failed with provided init params).
fn main() {
let init_params = InitParams {
param1: 42,
param2: "Hello, world!".to_string(),
param3: true,
};
let serialized_params = bincode::serialize(&init_params).expect("Serialization failed");
let endowment: u64 = 42; // Using u64 to represent 42
let mut endowment_bytes = [0u8; 32];
let endowment_array = endowment.to_le_bytes();
endowment_bytes[..endowment_array.len()].copy_from_slice(&endowment_array);
// Call the create function; needs error handling
let contract_address = athena_vm::chain::create(&bytecode, &serialized_params, value_bytes);
println!("Newly deployed contract address: {:?}", contract_address);
}
Call account method
This needs some thought and design. I’m not sure whether we want to allow multiple callable methods per contract, in which case the runtime needs to handle calls, or whether we instead want to allow a single callable “entrypoint” per program (as Solana does) where each entrypoint method has the same generic signature, e.g.:
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
In the latter case, it’s the responsibility of the contract program to handle dispatch on the basis of the input
.