Replies: 5 comments 23 replies
-
It might be possible to use sealed traits for this. In particular, using signature-sealed default methods with public functions to call them might achieve what you want here. (Credit for that playground to the linked article.) |
Beta Was this translation helpful? Give feedback.
-
A few more thoughts on how calling account methods could work under different use case. In the original post I described how we can call account methods which are a part of some standard interface (e.g., First, if we know the full implementation of a given account, we could just pass the whole account struct into a note's clause method. impl MyNote {
#[clause]
pub fn foo(&self, account: &mut MyAccount) {
// here we can call any method of `MyAccount` (even the ones which are uniquely
// defined for this account).
}
} If we don't know the full implementation, we might have to define something similar to an ABI. For example, if we know a signature and MAST root for a method exposed by an account we could define ABI for it like so: #[abi]
trait SomeAccount {
#[mast(0x1234567)]
fn bar(&mut self, x: u32, y: Felt, z: RpoDigest);
} We could even use this approach to attach well-known interfaces to an account like so: use miden::account::std::BasicWallet;
#[abi]
trait SomeAccount: BasicWallet {
#[mast(0x1234567)]
fn bar(&mut self, x: u32, y: Felt, z: RpoDigest);
} For the note, we now can pass impl MyNote {
#[clause]
pub fn foo<A: SomeAccount>(&self, account: &mut A) {
<some code here>
account.foo(x, y, z);
<some code here>
}
} Note that ABI definition does not have to be exhaustive: we could have it cover only the methods we are interested in. The next question would be is how to build an ABI for an account. If we have full source code, this should be pretty straight-forward. We could also have source code only for some of the account's methods - and that could be enough. Without the source code, it might be difficult - but I wonder, if we could put enough information into binary MASM files to help with that. |
Beta Was this translation helpful? Give feedback.
-
Some thoughts on the structure of the MASM output. Let's say we have a very simple (and not very useful) account: #[derive(Account])
pub struct MyAccount {
count: u32
}
impl MyAccount {
pub fn incr_count() {
self.count += 1;
}
} The MASM output file for this (ideal named
(there is a more efficient way to do the above using nondeterminism, but let's keep it simple for now) There is a lot happening in how we translated the above Rust code to MASM code. But the original Rust code introduces several abstractions. If we remove these, we can get Rust code which is much closer to the above MASM code. It could look something like this: use miden::account::Account;
pub mod my_account {
pub fn incr_count(account: &mut Account) {
let mut item: [u32; 4] = account.get_item(0);
item[0] += 1;
account.set_item(0, item);
}
} I would imagine that compiling the above Rust function to the desired MASM would be much simpler than compiling the original Rust struct. But the original Rust struct is much more clear and descriptive. So, I'm wondering how could we bridge the gap. One potential option (and I'm not sure how feasible) - is to use procedural macros to transform the original Rust code to the Rust code above, and then compile the latter. NotesWe can do something similar with notes. Consider the following note: #[derive(Note)]
pub struct MyNote {
increment_count: bool
}
impl MyNote {
pub fn execute(&self, account: &mut MyAccount) {
if self.icrement_count {
account.incr_count()
}
}
} An equivalent code which would mimic the desired MASM output more closely could look something like this: use miden::{accounts::Account, notes::Note};
pub mod my_account {
pub fn incr_count(account: &mut Account) {
let mut item: [u32; 4] = account.get_item(0);
item[0] += 1;
account.set_item(0, item);
}
}
pub mod my_note {
use super::my_account;
#[entrypoint]
pub fn execute(note: &Note, account: &mut Account) {
if note.get_input(0) {
my_account::incr_count(account);
}
}
} |
Beta Was this translation helpful? Give feedback.
-
Yes, and a simple inlining pass can eliminate "pass through" calls. In OmniZK I introduced in 0xPolygonMiden/miden-vm#957 I'm using the wrappers to expose such functions in the API: pub fn pub_input() -> u64 {
#[cfg(feature = "std")]
#[cfg(not(target_arch = "wasm32"))]
return io_native::pub_input();
#[cfg(target_arch = "wasm32")]
return io_wasm::pub_input();
}
pub fn pub_output(x: u64) {
#[cfg(feature = "std")]
#[cfg(not(target_arch = "wasm32"))]
return io_native::pub_output(x);
#[cfg(target_arch = "wasm32")]
return io_wasm::pub_output(x);
}
pub fn secret_input() -> u64 {
#[cfg(feature = "std")]
#[cfg(not(target_arch = "wasm32"))]
return io_native::secret_input();
#[cfg(target_arch = "wasm32")]
return io_wasm::secret_input();
} So the code that uses them: use c2zk_stdlib::*;
#[no_mangle]
pub fn fib_seq() {
let n = pub_input() as u32;
let mut a: u32 = 0;
let mut b: u32 = 1;
for _ in 0..n {
let c = a + b;
a = b;
b = c;
}
pub_output(a as u64);
} via https://github.com/greenhat/omnizk/blob/main/crates/rust-wasm-tests/fib/src/fib.rs with the entry point on for Wasm target - https://github.com/greenhat/omnizk/blob/main/crates/rust-wasm-tests/fib-bin/src/bin/fib.rs On non-wasm targets, it will use mocked IO, so you can write regular tests in Rust when you can check and simulate the IO. Here is a test for the above code that calls it from your regular Rust tests and compares the results against the compiled code that runs in Triton VM - https://github.com/greenhat/omnizk/blob/main/crates/codegen-tritonvm/src/codegen/sem_tests/fib.rs |
Beta Was this translation helpful? Give feedback.
-
One of the things I mentioned as needing to be described in more detail in #12 (reply in thread) is how note scripts make calls into accounts. This is probably the only place where the compiler will need to emit First, let's go over how things will work on the MASM side. Let's say we have an account which exposes two interface methods. This account would be represented by a MASM file (e.g.,
Let's also assume that when compiled into MAST, these procedures have the following MAST roots:
Now, let's say we have a note which needs to invoke procedure
Using a
The above means that we can pass at most 16 values from the caller to the callee directly, and we cannot pass memory references. So, if we need to pass more than 16 values, a way to do this would be to rely on the advice provider. Specifically, we could use the following convention:
Using the above process, we can theoretically pass unlimited amount of data between the caller and the callee, however, this is much more expensive than just passing memory references. So, practically, we probably won't want to do this for more than a few hundred (or maybe few thousand) elements. One note: the above is an example of how things could work given the current VM design. There are other ways to make it work too (e.g., by passing roots of Merkle trees), and we may also be able to update the VM in the future to introduce extra capabilities (e.g., instructions which would allow the callee to read values from the caller's memory directly). Overall, we should define a calling convention for this specific use case to specify what the caller and callee are expected to do (i.e., how they pad and interpret the stack). Representation in WASM/RustRepresenting the above in WASM, shouldn't be too complicated, I think. For example, something like this could work :
(I'm assuming that function names can start with Then, when we see a call to one of these functions we'd know to emit In general, on the Rust side, we'd need to define a sort of an ABI for an account and then note scripts could make calls against this ABI. Here, I wonder if we could follow an approach similar to what NEAR is doing as well. For example: #[abi(my_account)]
trait MyAccount {
#[mast(0x1234567)]
fn foo();
#[mast(0x7654321)]
fn bar();
} And then to invoke it from a note, we'd do something like: pub mod my_note {
use super::my_account;
#[entrypoint]
pub fn execute() {
my_account::foo();
}
} The key question is whether we can translate something similar to the above Rust to something similar to the WASM I outlined before this. Or maybe there is a better way to do this. |
Beta Was this translation helpful? Give feedback.
-
Below is a sketch of how we could potentially express Miden smart contracts in Rust. This is very preliminary, and I'm not sure yet if all of this is possible (or what is the best route for making it work).
Miden smart contracts can be thought of consisting of two components: Accounts and Notes. Both are described below.
First, let's start with accounts:
Now that we have an account defined, we can define a note which could be executed against this account:
Things not covered in the above:
Beta Was this translation helpful? Give feedback.
All reactions