Skip to content

Support for remote account - #2187

Open
marijamijailovic wants to merge 4 commits into
0xMiden:nextfrom
walnuthq:pr/call-command-remote-account
Open

Support for remote account#2187
marijamijailovic wants to merge 4 commits into
0xMiden:nextfrom
walnuthq:pr/call-command-remote-account

Conversation

@marijamijailovic

Copy link
Copy Markdown
Contributor

Adds remote account support to the call command: 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

@marijamijailovic

Copy link
Copy Markdown
Contributor Author

Hi @igamigo same question(as on #2179 ) about the changelog for this PR: there is already an entry saying “Added miden-cli call command for invoking account procedures directly from the CLI”. Should we reuse that existing entry for this PR or to add a new changelog entry?

@igamigo

igamigo commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Hi @igamigo same question(as on #2179 ) about the changelog for this PR: there is already an entry saying “Added miden-cli call command for invoking account procedures directly from the CLI”. Should we reuse that existing entry for this PR or to add a new changelog entry?

Sorry for the delay in this response. Similarly to the other PR, not a strong opinion but we can reuse the original log.

@igamigo

igamigo commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Hey @marijamijailovic / @Keinberger, just a heads up, if we want this on the 0.15.x we'll need to rebase this to main.

@marijamijailovic

Copy link
Copy Markdown
Contributor Author

This PR is blocked for now.

miden-client exec and call do not work with the latest compiler on the next branch. We must wait for the fix first.
See: 0xMiden/compiler#1192

After the fix is introduce, I will:

  1. Run full testing locally.
  2. Mark this PR as ready for review.

So this PR stays in draft until then.

@igamigo

igamigo commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Hey @marijamijailovic. Do we want to continue this work? AFAIK the compiler should already be on the VM version that has the fix, right?

@marijamijailovic

Copy link
Copy Markdown
Contributor Author

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.

@marijamijailovic
marijamijailovic force-pushed the pr/call-command-remote-account branch from df256e9 to 142162a Compare August 15, 2026 18:03
@marijamijailovic
marijamijailovic marked this pull request as ready for review August 17, 2026 05:08
@marijamijailovic

marijamijailovic commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this redundant with CallTarget::executor?

Comment on lines -281 to -291
// 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."
)));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should try validating other inputs before running this to avoid going to the network if possible, etc.

Comment on lines +242 to +247
/// 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>,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to make this an enum (with Local and Remote variants?)

Comment on lines +259 to +266
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(),
});
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do Store::get_account_header(target_id) here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second this, fetching all accounts when only one is needed should be avoided.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, get_account_headers returns locked accounts as well, which should be retrieved from the node as per the doc comments:

/// The local account state doesn't match the node's state, rendering it unusable.
/// Only used for private accounts.

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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +54 to +57
/// 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>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Should we not resolve packages that are under the packages directory here as well? Like the inspect command 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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to update the docs stating package is no longer mandatory (rust-client/cli/index.md:446)

Comment on lines +54 to +57
/// 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>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's also cover the failure scenarios

Comment on lines +259 to +266
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(),
});
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second this, fetching all accounts when only one is needed should be avoided.

Comment on lines +259 to +266
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(),
});
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, get_account_headers returns locked accounts as well, which should be retrieved from the node as per the doc comments:

/// The local account state doesn't match the node's state, rendering it unusable.
/// Only used for private accounts.

So the change it's not only a matter of optimization, but also correctness.

Comment thread CHANGELOG.md
* [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)).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* [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)).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

miden-client call: support remote account reads

3 participants