Now that we have a basic VM and we’re beginning work to integrate it into go-spacemesh, it’s time to finalize the account design and answer a few remaining questions. This is a continuation of Athena blockchain design and integration. I’m copying over the design and questions from Remaining questions and proposed design · Issue #53 · athenavm/athena · GitHub and a subsequent Slack thread on this topic so that we can continue the conversation here.
Proposed design:
VM-side
- How does one account send native coins to another account? Just using
call
? Is there a “receivable” function on the other end? Is there a way to transfer a balance without a call, i.e., a “simple send” tx type? (probably not, it doesn’t make sense in the context of AA) - How do we pass input into a called function? Do we only allow calling a single entrypoint function? Do we use argc/argv or stick with using IO syscalls like now.
- How do we return a value back from a call? Do we want to support this? Add an opcode or two?
- Do we consume all remaining gas on failure?
Host-side
Encompasses everything the node (i.e., go-spacemesh) needs to implement.
Transactions
This design is maximally “abstracted,” i.e., it puts as little logic as possible in the host and as much as possible in the VM.
There are only two transaction types: self spawn and wallet call.
Type 1: Self spawn
Self spawn allows a funded but unspawned wallet account to be spawned, same as in the existing VM. The transaction specifies the principal address, template address, and immutable state (init params). The wallet template has a self_spawn() method. The host passes the entire tx into this method. The method can either fail, or else internally spawn the template. The spawned wallet program address must be the hash of the template address and the immutable state.
tx := (principal, nonce=0, gas_price, template_address, immutable_state, signature)
spawned_program_address := HASH(template_address, immutable_state)
Type 2: Wallet call
Everything else is a call into a method in a spawned wallet program. We use the term “wallet” loosely, and we don’t cleanly differentiate between wallet programs and non-wallet programs. Effectively a wallet program is any program that implements the “spend” and “proxy” methods described here (you can think of “wallet” like a Rust trait; a template may implement many traits, and a particular wallet may implement a subset of these methods, or additional convenience methods). Here are some examples of functionality wallet apps will likely provide:
Send
To send native coins from their wallet, the user generates a tx with their wallet program as principal. The tx is passed to the “spend” method on the wallet program, which either effectuates a send or fails.
tx := (principal, nonce, gas_price, ENCODE("spend", recipient, amount), signature)
Call/proxy
To use a wallet’s funds to pay gas for a call to another program, the user again generates a tx with their wallet program as principal. The tx is passed to the “proxy” method on the wallet program, which either effectuates the call to another program, or fails.
tx := (principal, nonce, gas_price, ENCODE("proxy", recipient, amount, input), signature)
Deploy
To deploy a new template, the user passes the template code to the wallet program, which uses a VM opcode to deploy a new template. The deploy opcode returns the newly-deployed template address, which is calculated as the hash of the template code. The deploy opcode fails if there’s already a template deployed to this address. There’s no verification of the template code at deploy time. The deploy opcode charges gas based on the size of the code.
tx := (principal, nonce, gas_price, ENCODE("deploy", template_code), signature)
template_address := HASH(template_code)
Spawn
To spawn a program from an existing template, the user passes the template address and immutable state (init vars) to the wallet program, which uses a VM opcode to spawn the program. The spawn opcode returns the newly-spawned program address, which is calculated as the hash of the template address, the principal address, and the nonce. The spawn opcode fails if there’s already a program spawned to this address, or if the template init code fails to run on the input immutable state. The spawn opcode charges gas based on the template init code execution, i.e., same as a cross-contract call.
tx := (principal, nonce, gas_price, ENCODE("spawn", template_address, immutable_state), signature)
program_address := HASH(template_address, immutable_state, principal, nonce)
Accounts
There are three kinds of accounts.
Stubs
A stub is simply an account that has received funds but hasn’t been spawned yet.
Templates
A template contains only code. Funds sent to a template account are effectively burned and cannot be moved. (Note that we cannot prevent funds from being sent to a template address before it’s deployed, but once we know an account is a template we can prevent additional funds from being sent there.)
Programs
A program account contains a balance, a nonce, a state tree, and a link to an associated template address.
Questions
- We need to finalize how wallets handle nonces. Note that the nonce is not fully abstracted into the VM since Spacemesh miners need to be able to read it for tx ordering.
Discussion thread to date
- By “native coin” do you mean L1 funds? I think we should make the native coin work like any other resource, just have a “special” predefined resource address (I posted some ideas about how resources could work to the forum: General Abstract Resources ). In fact, we could make both Athena and L1 coins resources, and use the general mechanisms to transfer between them.
- Can you explain a bit more what you mean by this question?
- I think we have to support return values from cross-contract calls. The reason is that we want to keep all contract data local to the contract (i.e., only the contracts own methods can access it directly) – so the only way to read data from a different contract is a cross-contract call.
I’m not sure spacemesh miners actually need the nonce for tx ordering. We actually have two separate ordering phases:
- Shuffle transactions, considering all transactions with the same principal to be identical
- Internally order transactions with the same principal by nonce.
I think we can have spacemesh miners just perform phase 1, while leaving phase 2 to the executors.What this means is that the UCB contains the full list of transactions and the ordering of tx principals, but doesn’t explicitly commit to the internal ordering.This would allow us a lot more flexibility in defining the nonce (eg, potentially allowing the nonce scheme to be fully defined by the contract).
- By “native coin” do you mean L1 funds?
yes - I’m assuming the “native coin” of Athena is the same as on the L1. keep in mind that we’re starting by launching Athena on a testnet at L1. I have no issue with making the native coin a “special” predefined resource, but this might make things like charging gas a little more complex. we won’t have resources in the first athena testnet, anyway, because I want to get a testnet up soon and it will take time to finish the R&D for the resources stuff. the only other implication I can think of is that the CALL
host function, which allows cross-contract calls, also includes a value
parameter that sends coins along with the call (same as in EVM). we need to consider if we want to change this behavior. as I laid out in my original question list, we also have to decide whether want to implement some sort of “payable” or “receivable” syntax on contracts or functions that can receive coins (also an EVM idea).
- How do we pass input into a called function? Do we only allow calling a single entrypoint function? Do we use argc/argv or stick with using IO syscalls like now.
- Can you explain a bit more what you mean by this question?
different chains handle this differently. EVM allows a call to one of many methods defined on the receiving smart contract, and uses a “selector” (basically a hash of the signature of the function to call) as part of the tx calldata. solana, by contrast, only allows a single entrypoint, and dispatch has to happen inside the smart contract. so, question one is which method do we choose and why? (I’m leaning towards the solana method because it’s simpler and more “abstracted”.)the second question is, how do we pass “calldata” into a called method? one option is to use the standard C argv/argc syntax (Command Line Arguments in C - GeeksforGeeks). in this case, the VM/runtime stores these values in memory and the running program can access them immediately. the alternative is to require the running program to make an IO host call to load these values into memory (it would pass in a pointer). right now we’re doing the latter and it seems fine to me.note that there’s a related question here about how multiple arguments should be encoded/decoded. solana only allows a single bytearray, which the receiving program is responsible for decoding. again, i’m leaning in this direction since it’s simpler.
- I think we have to support return values from cross-contract calls. The reason is that we want to keep all contract data local to the contract (i.e., only the contracts own methods can access it directly) – so the only way to read data from a different contract is a cross-contract call.
one alternative here is to allow the return data to be a resource that’s owned by the caller: the callee creates the resource, saves it, and somehow changes its ownership to the caller.I see three potential models for how to implement this – note that the first two options here are similar to the distinction discussed above about input:
- first-class support for return values: the callee simply calls “return” with a value, the runtime receives the value and saves it to memory and then maybe the CALL host function returns a pointer to where it’s stored.
- implement as independent IO host functions: callee calls a special host::return_value() host function, caller then calls a parallel host::get_return_value() host function after the CALL host function.
- using resources and storage, as described above
allowing the nonce scheme to be fully defined by the contract
I sense some deja vu here would have to go back to old notes on this topic, but - is the idea that the template would expose some sort of get_nonce() method that the executors could call on each tx to read the nonce? I think this is probably fine, but it would definitely be less efficient than requiring the nonce to be explicitly visible in the tx without requiring interpretation inside the VM. it also sort of means we either need a very tight gas limit on this get_nonce() method, or else we need to gas meter it.