Support for remote account - #2187
Conversation
Sorry for the delay in this response. Similarly to the other PR, not a strong opinion but we can reuse the original log. |
|
Hey @marijamijailovic / @Keinberger, just a heads up, if we want this on the |
|
This PR is blocked for now.
After the fix is introduce, I will:
So this PR stays in draft until then. |
|
Hey @marijamijailovic. Do we want to continue this work? AFAIK the compiler should already be on the VM version that has the fix, right? |
Hi, yes we want! I will rebase this one. |
df256e9 to
142162a
Compare
|
Hi @igamigo, I’ve rebased this and it’s now ready for review. |
|
|
||
| If the target account is not in the local store, the client reads its state from the network and runs the call from one of your own accounts — the default account if one is set, otherwise the first usable one. That account only runs the call; nothing about it changes. | ||
|
|
||
| This requires the target account's state to be public, so the node can serve it, and it requires at least one of your own accounts to run the call from (accounts whose local state is out of sync with the node are skipped). Such calls can only read the account: the account cannot be modified by a call made this way, so only the return values are printed and no state delta is shown. The account has to be named by its full hex ID or its bech32 address — a partial ID is resolved against the local store, which by definition does not have this account. |
There was a problem hiding this comment.
We could still potentially return the state delta after executing a function even if it were on a remote account, right? So would we not want that?
| /// Runs a remote call via FPI. FPI cannot mutate the foreign account, so there is no state delta | ||
| /// to compute — only the read phase runs. | ||
| async fn run_remote_call<AUTH: Keystore + Sync + 'static>( | ||
| client: &mut Client<AUTH>, |
There was a problem hiding this comment.
(not directly related to this PR) I think this &mut requirement comes from execute_transaction() and the RNG requiring mutations, but I think this may be vestigial and we should look into taking the mutable reference out.
| async fn run_remote_call<AUTH: Keystore + Sync + 'static>( | ||
| client: &mut Client<AUTH>, | ||
| call_target: &CallTarget, | ||
| target_id: AccountId, |
There was a problem hiding this comment.
Isn't this redundant with CallTarget::executor?
| // 16 elements; anything deeper lives in the overflow table and cannot be reached | ||
| // by `movup`. So we can't drop args from under more than 15 results. | ||
| // See miden-vm/docs/src/user_docs/assembly/instruction_reference.md (movup row) | ||
| // and miden-vm/docs/src/design/stack/stack_ops.md (MOVUP/MOVDN sections). | ||
| if let Some(n) = result_count | ||
| && n > 15 | ||
| { | ||
| return Err(CliError::InvalidArgument(format!( | ||
| "Procedure returns {n} values; only up to 15 are supported." | ||
| ))); | ||
| } |
There was a problem hiding this comment.
Why was this removed? Seems like now the result would be silently truncated
| print_manifest_signature(&package, procedure); | ||
|
|
||
| let target_id = parse_account_id(&client, account_str).await?; | ||
| let call_target = resolve_call_target(&client, target_id).await?; |
There was a problem hiding this comment.
I think we should try validating other inputs before running this to avoid going to the network if possible, etc.
| /// Resolved call target. Local accounts run themselves; remote accounts are read via FPI | ||
| /// using a local account as executor. | ||
| struct CallTarget { | ||
| executor: AccountId, | ||
| foreign_accounts: BTreeMap<AccountId, ForeignAccount>, | ||
| } |
There was a problem hiding this comment.
Would it make sense to make this an enum (with Local and Remote variants?)
| let local_accounts = client.get_account_headers().await?; | ||
|
|
||
| if local_accounts.iter().any(|(header, _)| header.id() == target_id) { | ||
| return Ok(CallTarget { | ||
| executor: target_id, | ||
| foreign_accounts: BTreeMap::new(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
Can we do Store::get_account_header(target_id) here?
There was a problem hiding this comment.
Second this, fetching all accounts when only one is needed should be avoided.
There was a problem hiding this comment.
Also, get_account_headers returns locked accounts as well, which should be retrieved from the node as per the doc comments:
rust-sdk/crates/rust-client/src/store/account.rs
Lines 144 to 145 in cf66cdb
So the change it's not only a matter of optimization, but also correctness.
| /// Builds a script that invokes `proc_digest` on `foreign_id` via FPI. Args are pushed so | ||
| /// args[0] ends up on top, matching the direct-call convention. `truncate_stack` enforces the | ||
| /// 16-element exit invariant required by FPI component exports. | ||
| fn generate_fpi_tx_script( |
There was a problem hiding this comment.
We have very similar execute_foreign_procedure MASM templates in the repo. They all manually encode the same stack layout.
Could we move this into a build_fpi_script(foreign_id, proc_root, args) helper in rust-client? That would also make FPI available to library/web-sdk users instead of keeping the script construction here. Very soon we might need to ship a Package that solves this.
Also, FPI_INPUT_SLOTS looks like MIN_STACK_DEPTH under a different name.
| /// Path to the package (.masp) file containing the procedure. If omitted, `<PROCEDURE>` must | ||
| /// be a hex digest and the output stack is shown as raw felts. | ||
| #[arg(long, short)] | ||
| package: PathBuf, | ||
| package: Option<PathBuf>, |
There was a problem hiding this comment.
- Should we not resolve packages that are under the
packagesdirectory here as well? Like theinspectcommand is now doing. We should look into making these types of details as consistent as possible - Does this resolve packages with extensions in the path or without? Should we try to do both?
There was a problem hiding this comment.
We need to update the docs stating package is no longer mandatory (rust-client/cli/index.md:446)
| /// Path to the package (.masp) file containing the procedure. If omitted, `<PROCEDURE>` must | ||
| /// be a hex digest and the output stack is shown as raw felts. | ||
| #[arg(long, short)] | ||
| package: PathBuf, | ||
| package: Option<PathBuf>, |
There was a problem hiding this comment.
We need to update the docs stating package is no longer mandatory (rust-client/cli/index.md:446)
| /// Tests calling a procedure on a public account that is not in the caller's local store. The | ||
| /// call is routed through FPI using the caller's local wallet as the executor. | ||
| #[test] | ||
| fn call_remote_account_via_fpi() { |
There was a problem hiding this comment.
let's also cover the failure scenarios
| let local_accounts = client.get_account_headers().await?; | ||
|
|
||
| if local_accounts.iter().any(|(header, _)| header.id() == target_id) { | ||
| return Ok(CallTarget { | ||
| executor: target_id, | ||
| foreign_accounts: BTreeMap::new(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
Second this, fetching all accounts when only one is needed should be avoided.
| let local_accounts = client.get_account_headers().await?; | ||
|
|
||
| if local_accounts.iter().any(|(header, _)| header.id() == target_id) { | ||
| return Ok(CallTarget { | ||
| executor: target_id, | ||
| foreign_accounts: BTreeMap::new(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
Also, get_account_headers returns locked accounts as well, which should be retrieved from the node as per the doc comments:
rust-sdk/crates/rust-client/src/store/account.rs
Lines 144 to 145 in cf66cdb
So the change it's not only a matter of optimization, but also correctness.
| * [FEATURE][cli] Added `account --inspect <ID>[:<PROCEDURE>]` to list the procedures an account exposes, grouped into resolved procedures (with their names and signatures) and unresolved ones (listed by MAST root). Names and signatures are resolved from the `.masp` packages in the configured packages directory plus any passed via `--package` (`-p`). `--verbose` prints each procedure's MASM disassembly. ([#2312](https://github.com/0xMiden/rust-sdk/issues/2312)). | ||
| * Improved the output of the `miden-client init` command when a configuration already exists ([#2357](https://github.com/0xMiden/rust-sdk/pull/2357)). | ||
| * [FEATURE][cli] Added DAP-based transaction debugging with offline record/replay. `miden-client exec` and `consume-notes` accept `--start-debug-adapter <ADDR>` to run a transaction — script, kernel, note scripts, and account code — under a DAP client (e.g. the `miden-debug` TUI) instead of proving and submitting it (`consume-notes` is backed by a new `Client::execute_transaction_with_dap`). During the session the advice mutations produced by the transaction host's event handlers are recorded — readable via the handle from `DapConfig::record_event_mutations()`, and reported by the CLI — and `--record <FILE>` writes a self-contained replay snapshot (program, inputs, resolved code, and event log) that can be replayed offline with `miden-debug --replay <FILE>`, with no node, client, or account state. This uses the `miden-debug` 0.9.2 release ([#2306](https://github.com/0xMiden/rust-sdk/pull/2306)). | ||
| * [FEATURE][cli] `call` now works on public accounts that aren't tracked locally: the account is read from the network via a foreign procedure invocation, run from one of the client's own accounts (the default account when set). Such calls are read-only, so no state delta is shown. `--package` (`-p`) also became optional — without it, `<PROCEDURE>` must be a hex digest and the output stack is printed as raw felts ([#2187](https://github.com/0xMiden/rust-sdk/pull/2187)). |
There was a problem hiding this comment.
| * [FEATURE][cli] `call` now works on public accounts that aren't tracked locally: the account is read from the network via a foreign procedure invocation, run from one of the client's own accounts (the default account when set). Such calls are read-only, so no state delta is shown. `--package` (`-p`) also became optional — without it, `<PROCEDURE>` must be a hex digest and the output stack is printed as raw felts ([#2187](https://github.com/0xMiden/rust-sdk/pull/2187)). | |
| * [FEATURE][cli] `call` now works on public accounts that aren't tracked locally: the account is read from the network via a foreign procedure invocation, run from one of the client's own accounts (the default account when set). Such calls are read-only, so no state delta is shown. `--package` (`-p`) is now optional, if not set, `<PROCEDURE>` must be a hex digest and the output stack is printed as raw felts ([#2187](https://github.com/0xMiden/rust-sdk/pull/2187)). |
Adds remote account support to the
callcommand: accounts not in the local store are resolved as public foreign accounts and invoked via FPI.This is Draft because I am suggesting that we first merge #2179, and then this one , which will close #2097.
cc @Keinberger, @igamigo