diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1e4ee345..109675ccd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,11 @@ * [FEATURE][cli] Added `account --inspect [:]` 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 ` 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 ` writes a self-contained replay snapshot (program, inputs, resolved code, and event log) that can be replayed offline with `miden-debug --replay `, 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` reads the procedure's signature from the package manifest: it prints the signature with type names (`add-points(point, point) -> point`), takes each argument as one token of its own type (an `account-id` as `0x..`, an `asset` as `::`) and renders the result the same way. A procedure exported without a WIT signature keeps the raw-felt path, with its arguments written in decimal and its results printed as a stack dump. Arguments that don't fit the stack the called procedure can see are now rejected instead of arriving as zeros, and a procedure that only reads reports that the transaction was rejected for having no effects ([#2179](https://github.com/0xMiden/rust-sdk/pull/2179)). + +### Changes + +* [rust] Re-exported the typed view over a package's exported signatures as `miden_client::vm::typed`, `TransactionExecutorError` from the crate root, and `ExecutionError`, `OperationError` and `error_code_from_msg` as `miden_client::vm`, so callers can match a failed transaction against a specific kernel assertion ([#2179](https://github.com/0xMiden/rust-sdk/pull/2179)). ### Fixes diff --git a/Cargo.lock b/Cargo.lock index e906815eef..45fcc7d19c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1378,7 +1378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2042,15 +2042,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.15.0" @@ -2323,9 +2314,8 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "miden-ace-codegen" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66d3f9f8abd86405b76e5fb33425ca663a6c7db5bc3b93d1386f6aa817633f6" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-constraint-compiler", "miden-core", @@ -2358,9 +2348,8 @@ dependencies = [ [[package]] name = "miden-air" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06523a8e7130f70e5974c40861e808085a34eec27df1d397fb767850425a44e1" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-ace-codegen", "miden-core", @@ -2374,9 +2363,8 @@ dependencies = [ [[package]] name = "miden-assembly" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c580e0f6b4a1fe06aa1ebe5dfa8924ed58ab5a0d0afeb845fe0cbafd481917f" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "env_logger", "log", @@ -2392,9 +2380,8 @@ dependencies = [ [[package]] name = "miden-assembly-syntax" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2acfd54fda86d88c08e2d4b23538c4f6011327e21a9576de88be90dc95fa77b" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "env_logger", "log", @@ -2415,9 +2402,8 @@ dependencies = [ [[package]] name = "miden-assembly-syntax-cst" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c983ea80cf3c747f39f61a9cffbf048f9107b7a5175b873d37527398019c6760" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-debug-types", "miden-rowan", @@ -2447,6 +2433,7 @@ dependencies = [ "gloo-timers", "hex", "miden-agglayer", + "miden-assembly-syntax", "miden-debug", "miden-node-proto-build", "miden-note-transport-proto-build", @@ -2578,9 +2565,8 @@ dependencies = [ [[package]] name = "miden-constraint-compiler" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc2de03e3ca664006f6a523810c1a5a3b4254bbda8bf78e5aa009e2502c4f5d" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-core", "miden-crypto", @@ -2588,9 +2574,8 @@ dependencies = [ [[package]] name = "miden-core" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8394188e9b1fb828ad304d9984247b78660e53595f34880d8a403216a0e804e" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "derive_more", "log", @@ -2607,9 +2592,8 @@ dependencies = [ [[package]] name = "miden-core-lib" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef7ef6c4f708261e948f0b59c85c055c4a1169f1066c04ca56a87d3c7e0ef119" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "env_logger", "fs-err", @@ -2628,9 +2612,8 @@ dependencies = [ [[package]] name = "miden-core-lib-codegen" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823c735b6d34b34ae6bdc390cefafc6d1e5c81fe08cc14efe6c78834c59a59cc" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-core", "miden-precompiles", @@ -2638,9 +2621,8 @@ dependencies = [ [[package]] name = "miden-crypto" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8bc266f7dc1f7069eb4f0786500241ad9b499d34cb5b8aa5b1a42fcb55e2b" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "blake3", "cc", @@ -2680,9 +2662,8 @@ dependencies = [ [[package]] name = "miden-crypto-derive" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "832fa19dfec7601a3e7ca007e55e16b38e333e553f233249e00417432409674d" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "quote", "syn 2.0.118", @@ -2756,9 +2737,8 @@ dependencies = [ [[package]] name = "miden-debug-types" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4556cf4e43be205707d04d9532dd302cd313162ee986160a37923b4a613a71c1" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "memchr", "miden-crypto", @@ -2776,9 +2756,8 @@ dependencies = [ [[package]] name = "miden-field" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86dc6aae6776bfdd2c3d148d2c7c29c56510887056dc48aa41ffc87c2e6eae4" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-serde-utils", "num-bigint 0.5.1", @@ -2804,9 +2783,8 @@ dependencies = [ [[package]] name = "miden-lifted-air" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb6a87c95e6bb384ec290d29daf0d7d3fc5104d0ed9b9ff0c4a311fab65b73" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "p3-air", "p3-challenger", @@ -2818,9 +2796,8 @@ dependencies = [ [[package]] name = "miden-lifted-stark" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17c540c72c6a0adfa7e473f98bbf30b31ebd726026a76266eb77a5d00961931" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-lifted-air", "miden-stark-transcript", @@ -2841,9 +2818,8 @@ dependencies = [ [[package]] name = "miden-mast-package" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffa6fd0d84e932cbd8c51fd2e71c4f48dc4edea1dbef9259ff440e4212341e7d" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "hashbrown 0.17.1", "log", @@ -2896,7 +2872,7 @@ dependencies = [ [[package]] name = "miden-node-proto-build" version = "0.16.0-rc.1" -source = "git+https://github.com/0xMiden/node.git?branch=next#92dd097a2835143d6e70de11cbfebc42e5f0b6bd" +source = "git+https://github.com/0xMiden/node.git?branch=next#83e573d644dfcd1dd51bd4e9518f9f8716a45569" dependencies = [ "build-rs", "codegen", @@ -2920,9 +2896,8 @@ dependencies = [ [[package]] name = "miden-package-registry" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe36161cad2d5945b81c21c55a71bcb5d36445183556908eb7196a2465e1852" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-assembly-syntax", "miden-core", @@ -2936,9 +2911,8 @@ dependencies = [ [[package]] name = "miden-precompiles" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee334f0a36c1a24d041323898b087da3b9bf2aa3b520d0d44ebc2ed07f5f617" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-core", "miden-crypto", @@ -2946,9 +2920,8 @@ dependencies = [ [[package]] name = "miden-precompiles-prover" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf74c0b721b4a04852561c4bad282644e5173089732132749dccc0cbca37088" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-ace-codegen", "miden-air", @@ -2968,9 +2941,8 @@ dependencies = [ [[package]] name = "miden-processor" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b1429a0c3ac67db377fa5656937888ad89c6f7d8e6cb51afa549066d432d60" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "hashbrown 0.17.1", "itertools 0.15.0", @@ -2989,9 +2961,8 @@ dependencies = [ [[package]] name = "miden-project" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aff0af79739a77d0128a307811cf6d1d134d14637fd2295d44eb7c4af28378d" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-assembly-syntax", "miden-core", @@ -3053,9 +3024,8 @@ dependencies = [ [[package]] name = "miden-prover" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86609ed83aa69f23cd8dfe7391dfe5d16cd9a160f2922c3eb230e44c7c39aed7" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-air", "miden-core", @@ -3079,9 +3049,8 @@ dependencies = [ [[package]] name = "miden-serde-utils" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8bc2e1ffd6d9c0a445259f3e32f02b79af2776d34d7dc5dbd6e39e19bbebdce" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "p3-field", "p3-goldilocks", @@ -3107,9 +3076,8 @@ dependencies = [ [[package]] name = "miden-stark-transcript" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc72d9321645757904aebc53e2ee78de46f992a961a04cd15a996137fedd0ce" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "p3-challenger", "p3-field", @@ -3119,9 +3087,8 @@ dependencies = [ [[package]] name = "miden-stateful-hasher" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99c1612fbad6502deb79a34bd852a53b30cbcae92fbf20a2f2c220a708e1aec7" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "p3-field", "p3-symmetric", @@ -3178,9 +3145,8 @@ dependencies = [ [[package]] name = "miden-utils-core-derive" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21bd1f2a35b3e8e48b391c51bd5d4d3d080066d9f0cd8a1567be477fd6acbed" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "proc-macro2", "quote", @@ -3189,9 +3155,8 @@ dependencies = [ [[package]] name = "miden-utils-diagnostics" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9927aebed020f53666d1daf01b834e134ce80ced02cef6f6b82c3a2f855fb85c" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-debug-types", "miden-miette", @@ -3200,9 +3165,8 @@ dependencies = [ [[package]] name = "miden-utils-indexing" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e1ce02f96261808a50f86088c83bc9eef809205bbc51f76f10432bb9a5e1337" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-serde-utils", "proptest", @@ -3212,9 +3176,8 @@ dependencies = [ [[package]] name = "miden-utils-sync" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579fdffa79eb4e9d90f53ee6cc29d7122b50b095c388a0f4ca1e1f6e04df7faf" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "lock_api", "loom", @@ -3224,9 +3187,8 @@ dependencies = [ [[package]] name = "miden-verifier" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a6cf11175c58139dd15330caf77c8462f742bfc73d7e0216c8ece45e53f90f2" +version = "0.29.1" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-air", "miden-core", @@ -3242,8 +3204,7 @@ dependencies = [ [[package]] name = "midenc-hir-type" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e03aa1e30a8eec3e08eba9a1fd17c7c4462f0d49dfbd64cb65193b68ee14cdcc" +source = "git+https://github.com/walnuthq/miden-vm?branch=pr%2Fdebug_info_encoder_decoder#766a8d6b439e7f535915bd2489f0be335be52114" dependencies = [ "miden-formatting", "miden-serde-utils", @@ -4051,7 +4012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ "heck", - "itertools 0.14.0", + "itertools 0.13.0", "log", "multimap", "petgraph", @@ -4072,7 +4033,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.118", @@ -4552,7 +4513,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5104,7 +5065,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5916,7 +5877,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5927,9 +5888,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wincode" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5d39d1a984eb7ae37afa348f058216d62a6d5f71640f4113a8114386c2a812a" +checksum = "bfc6339f1ba427bf7ad7c42403b28e524832ba2ddb5eef1bb2cc3b85db6b7b75" dependencies = [ "pastey", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index d73f5f1c34..b6cfc9c8f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,8 +52,10 @@ miden-node-proto-build = { branch = "next", default-features = false, miden-note-transport-proto-build = { default-features = false, version = "0.5.0-alpha.1" } # Miden debug dependency -miden-debug = { default-features = false, features = ["dap", "std"], version = "0.10" } -miden-processor = { default-features = false, version = "0.29" } +miden-assembly-syntax = { default-features = false, version = "0.29" } +miden-debug = { default-features = false, features = ["dap", "std"], version = "0.10" } +miden-mast-package = { default-features = false, version = "0.29" } +miden-processor = { default-features = false, version = "0.29" } # External dependencies anyhow = { default-features = false, version = "1.0" } @@ -80,6 +82,44 @@ tracing = { version = "0.1" } tracing-subscriber = { default-features = false, version = "0.3" } uuid = { version = "1.10" } +# TEMPORARY (do not merge): `miden call` reads argument and result types through +# `miden_assembly_syntax::ast::types::signatures`, which is not in a published miden-vm release yet. +# Point the whole miden-vm crate set at the branch carrying it; patching a single crate pulls a +# second `miden-core` and breaks type unification. Delete this entire block once the module lands in +# a release — the caret `"0.29"` requirement then picks it up from crates.io. +[patch.crates-io] +miden-ace-codegen = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-air = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-assembly = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-assembly-syntax = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-assembly-syntax-cst = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-constraint-compiler = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-core = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-core-lib = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-core-lib-codegen = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-crypto = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-crypto-derive = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-debug-types = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-field = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-lifted-air = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-lifted-stark = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-mast-package = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-package-registry = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-precompiles = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-precompiles-prover = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-processor = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-project = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-prover = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-serde-utils = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-stark-transcript = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-stateful-hasher = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-utils-core-derive = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-utils-diagnostics = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-utils-indexing = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-utils-sync = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +miden-verifier = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } +midenc-hir-type = { branch = "pr/debug_info_encoder_decoder", git = "https://github.com/walnuthq/miden-vm" } + # Lints are set to warn for development, which are promoted to errors in CI. [workspace.lints.clippy] # Pedantic lints are set to a lower priority which allows lints in the group to be selectively enabled. diff --git a/bin/miden-cli/Cargo.toml b/bin/miden-cli/Cargo.toml index 69b68ab6ac..a76c84be5d 100644 --- a/bin/miden-cli/Cargo.toml +++ b/bin/miden-cli/Cargo.toml @@ -48,7 +48,7 @@ miden-client = { features = ["std"], workspace = true } anyhow = { workspace = true } assert_cmd = { workspace = true } miden-client = { features = ["testing"], workspace = true } -miden-mast-package = { version = "0.29" } +miden-mast-package = { workspace = true } midenc-hir-type = { version = "0.10" } predicates = { workspace = true } regex = { workspace = true } diff --git a/bin/miden-cli/src/codecs/account_id.rs b/bin/miden-cli/src/codecs/account_id.rs new file mode 100644 index 0000000000..93d191ae05 --- /dev/null +++ b/bin/miden-cli/src/codecs/account_id.rs @@ -0,0 +1,85 @@ +//! The `account-id` codec for typed `call` rendering. +//! +//! `account-id` felts are validated with protocol-level rules, so the CLI registers this codec +//! (via [`TypedProcInfo::with_scalar_codec`]) to encode one hex token into the two stack felts the +//! procedure expects and render the returned felts back as `account-id(0x..)`. +//! +//! [`TypedProcInfo::with_scalar_codec`]: miden_client::vm::typed::TypedProcInfo::with_scalar_codec + +use miden_client::Felt; +use miden_client::account::AccountId; +use miden_client::vm::typed::{MIDEN_CORE_TYPES, TypedError, WitScalarCodec}; + +use crate::codecs::invalid_scalar; + +/// Bare WIT type name the typed encoder matches this codec against, regardless of the package and +/// version in the full type name (e.g. `miden:base/core-types@1.0.0/account-id`). +const ACCOUNT_ID_WIT_NAME: &str = "account-id"; + +/// Encodes and renders the WIT `account-id` type: one hex token, two stack felts. +pub struct AccountIdCodec; + +impl WitScalarCodec for AccountIdCodec { + fn wit_name(&self) -> &str { + ACCOUNT_ID_WIT_NAME + } + + fn wit_interface(&self) -> Option<&str> { + Some(MIDEN_CORE_TYPES) + } + + fn encode(&self, token: &str) -> Result, TypedError> { + let id = AccountId::from_hex(token) + .map_err(|err| invalid_scalar(ACCOUNT_ID_WIT_NAME, token, &err))?; + Ok(vec![id.prefix().into(), id.suffix()]) + } + + fn decode(&self, felts: &[Felt]) -> Result { + // The caller passes as many felts as the type occupies, so any other count means the + // signature and this codec disagree about the value's width. + let [prefix, suffix] = felts else { + return Err(TypedError::MalformedResult { + ty: ACCOUNT_ID_WIT_NAME.to_string(), + reason: "an account id occupies exactly two felts", + }); + }; + let id = AccountId::try_from_elements(*suffix, *prefix).map_err(|_| { + TypedError::MalformedResult { + ty: ACCOUNT_ID_WIT_NAME.to_string(), + reason: "the felts are not a valid account id", + } + })?; + Ok(format!("account-id({})", id.to_hex())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn account_id_one_hex_token_roundtrips() { + let codec = AccountIdCodec; + let hex = "0xaa0000000000bb110000cc000000dd"; + + // Compared against the felts the account id itself carries: a round-trip alone would also + // pass if `encode` and `decode` had the two fields the same way around. + let id = AccountId::from_hex(hex).unwrap(); + let expected = [Felt::from(id.prefix()), id.suffix()]; + assert_eq!(codec.encode(hex).unwrap(), expected); + + assert_eq!(codec.decode(&expected).unwrap(), format!("account-id({hex})")); + } + + #[test] + fn felts_that_are_not_an_account_id_are_rejected() { + let err = AccountIdCodec.decode(&[Felt::from(1u32), Felt::from(2u32)]).unwrap_err(); + assert!(matches!(err, TypedError::MalformedResult { .. })); + } + + #[test] + fn invalid_account_id_token_is_rejected() { + let err = AccountIdCodec.encode("not-hex").unwrap_err(); + assert!(matches!(err, TypedError::InvalidScalar { .. })); + } +} diff --git a/bin/miden-cli/src/codecs/asset.rs b/bin/miden-cli/src/codecs/asset.rs new file mode 100644 index 0000000000..b79c7137e7 --- /dev/null +++ b/bin/miden-cli/src/codecs/asset.rs @@ -0,0 +1,124 @@ +//! The `asset` codec for typed `call` rendering. +//! +//! On the stack a WIT `asset` is its eight felts — the id word followed by the value word, i.e. +//! [`Asset::as_elements`]. The CLI registers this codec so an asset argument can be given as a +//! single `::` token instead of two raw word hexes, and so a returned asset +//! renders back the same way. The token form matches the one the rest of the CLI takes for +//! fungible assets, minus the token symbol and address spellings: resolving those needs the +//! client, and a codec only sees the text. + +use miden_client::account::AccountId; +use miden_client::asset::{Asset, FungibleAsset}; +use miden_client::vm::typed::{MIDEN_CORE_TYPES, TypedError, WitScalarCodec}; +use miden_client::{Felt, Word}; + +use crate::codecs::invalid_scalar; + +/// Bare WIT type name the typed encoder matches this codec against (e.g. the leaf of +/// `miden:base/core-types@1.0.0/asset`). +const ASSET_WIT_NAME: &str = "asset"; + +/// Encodes and renders the WIT `asset` type: one `::` token, eight stack felts. +/// Only fungible assets are supported by this token form. +pub struct AssetCodec; + +impl WitScalarCodec for AssetCodec { + fn wit_name(&self) -> &str { + ASSET_WIT_NAME + } + + fn wit_interface(&self) -> Option<&str> { + Some(MIDEN_CORE_TYPES) + } + + fn encode(&self, token: &str) -> Result, TypedError> { + let (amount, faucet) = token.split_once("::").ok_or_else(|| { + invalid_scalar(ASSET_WIT_NAME, token, "expected `::`") + })?; + let amount: u64 = amount.parse().map_err(|e: core::num::ParseIntError| { + invalid_scalar(ASSET_WIT_NAME, token, &format!("invalid amount: {e}")) + })?; + let faucet_id = + AccountId::from_hex(faucet).map_err(|e| invalid_scalar(ASSET_WIT_NAME, token, &e))?; + let asset: Asset = FungibleAsset::new(faucet_id, amount) + .map_err(|e| invalid_scalar(ASSET_WIT_NAME, token, &e))? + .into(); + Ok(asset.as_elements().to_vec()) + } + + fn decode(&self, felts: &[Felt]) -> Result { + // As in `AccountIdCodec`, any count other than the type's width means the signature and + // this codec disagree. + let [k0, k1, k2, k3, v0, v1, v2, v3] = felts else { + return Err(malformed_asset("an asset occupies exactly eight felts")); + }; + let id = Word::from([*k0, *k1, *k2, *k3]); + let value = Word::from([*v0, *v1, *v2, *v3]); + let asset = Asset::from_id_and_value_words(id, value) + .map_err(|_| malformed_asset("the felts are not a valid asset"))?; + Ok(match asset { + Asset::Fungible(f) => format!("asset({}::{})", f.amount(), f.faucet_id().to_hex()), + Asset::NonFungible(_) => "asset(non-fungible)".to_string(), + }) + } +} + +/// Builds the error for result felts that are not an asset this codec can render. +fn malformed_asset(reason: &'static str) -> TypedError { + TypedError::MalformedResult { ty: ASSET_WIT_NAME.to_string(), reason } +} + +#[cfg(test)] +mod tests { + use miden_client::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET; + + use super::*; + + fn hex(id: u128) -> String { + AccountId::try_from(id).unwrap().to_hex() + } + + fn faucet_token(amount: u64) -> String { + format!("{amount}::{}", hex(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)) + } + + #[test] + fn a_fungible_asset_token_roundtrips() { + let token = faucet_token(100); + + let felts = AssetCodec.encode(&token).unwrap(); + assert_eq!(felts.len(), 8); + + assert_eq!(AssetCodec.decode(&felts).unwrap(), format!("asset({token})")); + } + + #[test] + fn a_token_with_a_single_colon_is_rejected() { + let err = AssetCodec.encode(&faucet_token(100).replace("::", ":")).unwrap_err(); + assert!(matches!(err, TypedError::InvalidScalar { .. })); + } + + #[test] + fn a_token_in_the_reverse_order_is_rejected() { + let token = format!("{}::100", hex(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)); + let err = AssetCodec.encode(&token).unwrap_err(); + assert!(matches!(err, TypedError::InvalidScalar { .. })); + } + + #[test] + fn felts_that_are_not_an_asset_are_rejected() { + let felts: Vec = (1..=8u32).map(Felt::from).collect(); + let err = AssetCodec.decode(&felts).unwrap_err(); + assert!(matches!(err, TypedError::MalformedResult { .. })); + } + + #[test] + fn a_felt_count_other_than_eight_is_rejected() { + let felts = AssetCodec.encode(&faucet_token(100)).unwrap(); + + for len in [0, 4, 7] { + let err = AssetCodec.decode(&felts[..len]).unwrap_err(); + assert!(matches!(err, TypedError::MalformedResult { .. }), "len {len} was accepted"); + } + } +} diff --git a/bin/miden-cli/src/codecs/mod.rs b/bin/miden-cli/src/codecs/mod.rs new file mode 100644 index 0000000000..59ad5ec81c --- /dev/null +++ b/bin/miden-cli/src/codecs/mod.rs @@ -0,0 +1,45 @@ +//! Protocol-aware WIT scalar codecs. +//! +//! The encode/decode engine and the [`WitScalarCodec`] trait live in `miden-assembly-syntax`, which +//! does not depend on `miden-protocol` and should not: the VM does not depend on the protocol. So +//! it ships the two codecs it can write itself, `word` and `felt`, and leaves the trait for the +//! rest. +//! +//! `account-id` and `asset` are the rest. `AccountId::from_hex` says what a valid id is, and +//! `Asset` says what a valid asset is, so both codecs live on this side. +//! +//! [`with_cli_codecs`] registers them in one place, so the commands that render typed signatures +//! do not know the individual types. +//! +//! [`WitScalarCodec`]: miden_client::vm::typed::WitScalarCodec +//! [`TypedProcInfo`]: miden_client::vm::typed::TypedProcInfo + +use miden_client::vm::typed::{TypedError, TypedProcInfo}; + +mod account_id; +mod asset; + +pub use account_id::AccountIdCodec; +pub use asset::AssetCodec; + +/// Builds the `InvalidScalar` error a codec returns when it can't parse `token`. Shared so every +/// codec reports the same error shape from one place. +pub(crate) fn invalid_scalar( + wit_name: &str, + token: &str, + reason: &(impl ToString + ?Sized), +) -> TypedError { + TypedError::InvalidScalar { + wit_name: wit_name.to_string(), + token: token.to_string(), + reason: reason.to_string(), + } +} + +/// Registers every CLI scalar codec onto `typed`. New codecs are added here so the commands that +/// render typed signatures stay agnostic of the individual WIT types. +pub fn with_cli_codecs(typed: TypedProcInfo) -> TypedProcInfo { + typed + .with_scalar_codec(Box::new(AccountIdCodec)) + .with_scalar_codec(Box::new(AssetCodec)) +} diff --git a/bin/miden-cli/src/commands/call.rs b/bin/miden-cli/src/commands/call.rs index 7fc478ce32..45f6614b94 100644 --- a/bin/miden-cli/src/commands/call.rs +++ b/bin/miden-cli/src/commands/call.rs @@ -6,21 +6,35 @@ use clap::Parser; use miden_client::assembly::CodeBuilder; use miden_client::keystore::Keystore; use miden_client::transaction::{AdviceInputs, TransactionRequestBuilder, TransactionScript}; -use miden_client::vm::{Package, PackageExport}; -use miden_client::{Client, Deserializable, Felt, Word}; +use miden_client::vm::typed::TypedProcInfo; +use miden_client::vm::{ + ExecutionError, + MIN_STACK_DEPTH, + OperationError, + Package, + PackageExport, + PackageManifest, + ProcedureExport, + error_code_from_msg, +}; +use miden_client::{Client, ClientError, Deserializable, Felt, TransactionExecutorError, Word}; use crate::advice_inputs::load_advice_map_from_file; +use crate::codecs::with_cli_codecs; use crate::errors::CliError; -use crate::utils::{ - parse_account_id, - print_executed_program_stack, - print_executed_transaction, - split_procedure_target, -}; +use crate::utils::{parse_account_id, print_executed_program_stack, print_executed_transaction}; // CALL COMMAND // ================================================================================================ +/// The transaction kernel's assertion message for a transaction that changes nothing. +/// +/// The message is hashed into the error code the assertion carries, and compared against it. It +/// only decides whether an explanatory line is printed: if the kernel ever rewords it, the line +/// stops appearing and nothing else changes. +const EMPTY_TRANSACTION_ASSERTION: &str = + "executed transaction neither changed the account state, nor consumed any notes"; + #[derive(Debug, Clone, Parser)] #[command(about = "Call a procedure on a local account and display the result and state delta")] pub struct CallCmd { @@ -53,13 +67,10 @@ impl CallCmd { mut client: Client, ) -> Result<(), CliError> { if client.get_sync_height().await? == 0.into() { - return Err(CliError::InvalidArgument( - "Client has not been synced yet. Run `miden-client sync` first.".to_string(), - )); + return Err(CliError::NotSynced); } - let (account_str, procedure) = split_procedure_target(&self.target); - let procedure = procedure.ok_or_else(|| { + let (account_str, procedure) = self.target.split_once(':').ok_or_else(|| { CliError::InvalidArgument(format!( "Expected `:`, got '{}'.", self.target @@ -71,47 +82,55 @@ impl CallCmd { let package = load_package(&self.package)?; - let digest = resolve_procedure_digest(&package, procedure)?; - let ProcedureSignature { param_felts, result_felts } = - print_manifest_signature(&package, procedure); + let export = resolve_procedure_export(&package.manifest, procedure)?; + let digest = export.digest; + // The signature prints under the name the package carries, not the one the user typed, so + // `call increment_by` shows `increment-by(felt) -> felt`. + let name = export.path.last().ok_or_else(|| { + CliError::InvalidArgument(format!( + "The export matching '{procedure}' has an empty path, so it names no procedure." + )) + })?; + + // Only a Component Model signature describes the values the user passes and reads; a + // `C`-ABI or `Fast` one describes the lowering instead, which would encode the wrong thing. + let typed = match export.signature.clone() { + Some(signature) if signature.abi.is_wasm_canonical_abi() => { + Some(with_cli_codecs(TypedProcInfo::new(name, signature)?)) + }, + _ => None, + }; - let args = parse_args(&self.args)?; + let args = if let Some(typed) = &typed { + println!("Signature: {typed}\n"); + // Checks the argument count as well, and names the procedure and both counts when it + // is wrong, so there is nothing to check here first. + typed.encode_args(&self.args)? + } else { + println!("Signature: {name}(...) [no type info]\n"); + println!( + "Warning: the package does not describe the types of '{procedure}', so each \ + argument is passed as one field element, the argument count is not checked, and \ + the result is printed as a stack dump." + ); + encode_raw_args(&self.args)? + }; let advice_entries = match &self.inputs_path { Some(path) => load_advice_map_from_file(path)?, None => vec![], }; - match param_felts { - Some(expected) if args.len() != expected => { - return Err(CliError::InvalidArgument(format!( - "Procedure '{procedure}' expects {expected} value(s), got {}. Types wider \ - than one field element are passed as one value per element, as shown in the \ - signature above.", - args.len() - ))); - }, - None => { - println!( - "Warning: no type info for procedure '{procedure}'. Skipping argument \ - count check. Passing a wrong number of arguments may cause errors or \ - wrong results." - ); - }, - _ => {}, - } - - // The account's code is loaded into the from the client's store in th VM runtime, so we - // don't need the library into the compiled script. But the assembler still needs - // it at compile time to resolve `call.` to a known procedure — otherwise it - // emits a "phantom target" warning. Dynamic linking provides that resolution without - // embedding the library bytes in the script. + // The account's code is loaded from the client's store at runtime, so the library does not + // need to be embedded in the compiled script. The assembler still needs it at compile time + // to resolve `call.` to a known procedure — otherwise it emits a "phantom target" + // warning. Dynamic linking provides that resolution without embedding the library bytes in + // the script. let linked_builder = client.code_builder().with_dynamically_linked_package(&package)?; - // 1) Read-only execution to get return values. If `result_felts` is unknown we skip - // the drop sequence and let `print_output_stack` auto-detect results from the stack. + // 1) Read-only execution to get return values. let read_tx_script = - generate_tx_script(linked_builder.clone(), &digest, &args, result_felts)?; + generate_tx_script(linked_builder.clone(), &digest, &args, Cleanup::KeepResults)?; let advice_inputs = AdviceInputs::default().with_map(advice_entries.clone()); @@ -119,10 +138,21 @@ impl CallCmd { .execute_program(account_id, read_tx_script, advice_inputs, BTreeMap::new()) .await?; - print_executed_program_stack(&output_stack, result_felts); + match &typed { + // A procedure that returns nothing has no result to show; anything else that cannot be + // rendered is an error, since a raw stack dump would hide that the result is not a + // valid value of its type. + Some(typed) => { + if let Some(rendered) = typed.decode_result(output_stack.as_slice())? { + println!("Result: {rendered}"); + } + }, + // Nothing says where the results end, so the dump runs to the last non-zero value. + None => print_executed_program_stack(&output_stack, None), + } - // 2) Transaction execution to get state delta. - let delta_tx_script = generate_tx_script(linked_builder, &digest, &args, Some(0))?; + // 2) Transaction execution to get the state delta. + let delta_tx_script = generate_tx_script(linked_builder, &digest, &args, Cleanup::DropAll)?; let tx_request = TransactionRequestBuilder::new() .custom_script(delta_tx_script) @@ -136,9 +166,7 @@ impl CallCmd { Ok(tx_result) => { print_executed_transaction(&mut client, tx_result.executed_transaction()).await?; }, - Err(e) => { - println!("\n(Could not compute state delta: {e})"); - }, + Err(e) => report_failed_delta(&e), } Ok(()) @@ -161,132 +189,311 @@ fn load_package(path: &Path) -> Result { }) } -fn resolve_procedure_digest(package: &Package, procedure_name: &str) -> Result { +/// Finds the export `procedure_name` names, which carries both the digest to call and the +/// signature the arguments are encoded against. +/// +/// The compiler writes two exports for the same Component Model procedure: one with its WIT +/// signature, `add-points(point, point) -> point`, and one lowered to the C ABI, +/// `fn(felt, felt, felt, felt) -> i32`. Arguments are encoded and results are rendered from the +/// signature this picks, so it has to be the WIT one. The lowered signature describes the ABI +/// plumbing instead: its parameters are the flattened felts, and its `i32` result is a pointer to +/// the value rather than the value. +fn resolve_procedure_export<'a>( + manifest: &'a PackageManifest, + procedure_name: &str, +) -> Result<&'a ProcedureExport, CliError> { // The user passes a bare name (e.g. `get_count`); match it // against each export's name without the module path. Export names may be kebab (Rust/WIT) or // snake (hand-written MASM bare identifiers), so compare with `_` and `-` treated as equal. let target = procedure_name.replace('_', "-"); let mut available = Vec::new(); - for export in package.manifest.exports() { + let mut untyped = None; + + for export in manifest.exports() { let PackageExport::Procedure(proc) = export else { continue; }; + // Every procedure goes on the list, so a "not found" error shows the whole surface. + available.push(format!(" {}", proc.path)); + if export.name().replace('_', "-") != target { - // Not the requested procedure; keep it for the "not found" error list. - available.push(format!(" {}", proc.path)); continue; } // The same leaf name is exported both as a `C`-ABI lowering (for `exec`) and as the // `ComponentModel` export (the cross-context `call` target); pick the latter. if proc.signature.as_ref().is_some_and(|sig| sig.abi.is_wasm_canonical_abi()) { - return Ok(proc.digest); + return Ok(proc); } + // Hand-written MASM carries no signature the caller can encode against, but it is still + // callable with raw field elements. Keep it and go on looking: the manifest is free to + // write the Component Model export after this one, and that one is worth more. + untyped.get_or_insert(proc); + } + + untyped.ok_or_else(|| { + CliError::InvalidArgument(format!( + "Procedure '{procedure_name}' not found. Available:\n{}", + available.join("\n") + )) + }) +} + +/// Reports a transaction that did not execute, in place of the state delta it would have shown. +/// +/// The read-only execution has already printed the result by this point, so this never fails the +/// command: the call itself succeeded, only its effects could not be reported. +fn report_failed_delta(error: &ClientError) { + let is_empty_transaction = matches!( + error, + ClientError::TransactionExecutorError( + TransactionExecutorError::TransactionProgramExecutionFailed( + ExecutionError::OperationError { + err: OperationError::FailedAssertion { err_code, .. }, + .. + }, + ), + ) if *err_code == error_code_from_msg(EMPTY_TRANSACTION_ASSERTION) + ); + + if is_empty_transaction { + // A procedure that only reads, on an account whose components write nothing, leaves the + // transaction with no effects at all, and the kernel refuses those. For a read-only call + // that is the expected outcome rather than a fault, so it is reported instead of dumping + // the assertion chain. The kernel rejects only when the account was left unchanged and + // nothing was consumed, so that is all this can report; it says nothing about created + // notes. + println!(); + println!("The transaction was rejected because it had no effects:\n"); + println!("No notes were consumed."); + println!(); + println!("Account Storage was not changed."); + println!("Account Vault was not changed."); + println!("Account nonce was not changed."); + return; + } + + let mut report = String::new(); + let mut cause = std::error::Error::source(error); + while let Some(err) = cause { + writeln!(report, " caused by: {err}").unwrap(); + cause = err.source(); } - Err(CliError::InvalidArgument(format!( - "Procedure '{procedure_name}' not found. Available:\n{}", - available.join("\n") - ))) + println!("\n(Could not compute state delta: {error})"); + print!("{report}"); } -fn parse_args(args: &[String]) -> Result, CliError> { +/// Parses `args` as one field element each, for a procedure whose types the package does not +/// describe. +/// +/// A value is written the way a `felt` is written on the typed path: in decimal. The untyped path +/// is the fallback, so accepting more than the typed one would teach a syntax that stops working +/// as soon as the procedure gains a signature. +fn encode_raw_args(args: &[String]) -> Result, CliError> { args.iter() .map(|arg| { - let n = arg.parse::().map_err(|_| { - CliError::InvalidArgument(format!("Invalid argument '{arg}'. Expected u64.")) + let value: u64 = arg.parse().map_err(|_| { + CliError::InvalidArgument(format!("Invalid argument '{arg}'. Expected a felt.")) })?; - Felt::try_from(n) - .map_err(|_| CliError::InvalidArgument(format!("Argument '{arg}' is too large."))) + Felt::try_from(value).map_err(|_| { + CliError::InvalidArgument(format!("Argument '{arg}' is too large for a felt.")) + }) }) .collect() } -/// How many field elements a procedure's arguments and results occupy on the stack. A multi-felt -/// type such as `Word` counts as its flattened width, not as one item. `None` means the -/// information is unavailable (procedure missing from manifest or export lacks type info). -struct ProcedureSignature { - param_felts: Option, - result_felts: Option, -} +// TESTS +// ================================================================================================ -/// Prints the signature of `procedure_name` from the package manifest and returns the stack width -/// of its arguments and results. If the procedure is missing, prints the list of available exports. -fn print_manifest_signature(package: &Package, procedure_name: &str) -> ProcedureSignature { - const UNKNOWN: ProcedureSignature = - ProcedureSignature { param_felts: None, result_felts: None }; +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use miden_mast_package::PathBuf; + use midenc_hir_type::{CallConv, FunctionType, Type}; + + use super::*; + + /// A manifest exporting every `(path, signature)` pair. Resolution matches on the path and + /// reads the signature, so the digest is left zero. + fn manifest_with_exports(exports: &[(&str, Option)]) -> PackageManifest { + let exports = exports.iter().map(|(path, signature)| { + let path: Arc<_> = path.parse::().expect("path should parse").into(); + PackageExport::Procedure(ProcedureExport::new( + path, + None, + Word::default(), + signature.clone(), + )) + }); - let kebab_name = procedure_name.replace('_', "-"); - let quoted_kebab = format!("\"{kebab_name}\""); - let quoted_name = format!("\"{procedure_name}\""); + PackageManifest::new(exports).expect("manifest should be valid") + } - for export in package.manifest.exports() { - let PackageExport::Procedure(proc_export) = export else { - continue; - }; + /// The interface form of a Component Model export. It keeps the WIT types. + fn interface_form() -> (&'static str, Option) { + ( + "::\"miden:counter/counter@0.1.0\"::\"increment-by\"", + Some(FunctionType::new(CallConv::ComponentModel, [Type::Felt], [Type::Felt])), + ) + } - let path_str = proc_export.path.to_string(); - if !path_str.ends_with(&kebab_name) - && !path_str.ends_with(procedure_name) - && !path_str.ends_with("ed_kebab) - && !path_str.ends_with("ed_name) - { - continue; - } + /// The lowered form of the same export. The C ABI flattens the types and returns the big value + /// by reference: an `i32` pointer, not the value. + fn lowered_form() -> (&'static str, Option) { + ( + "::\"miden:counter/counter@0.1.0\"::cc::\"miden:counter/counter@0.1.0#increment-by\"", + Some(FunctionType::new(CallConv::C, [Type::Felt], [Type::I32])), + ) + } + + #[test] + fn the_interface_form_wins_over_the_lowered_one() { + // The compiler is free to write the two exports in either order, so neither may decide it. + for exports in [[interface_form(), lowered_form()], [lowered_form(), interface_form()]] { + let manifest = manifest_with_exports(&exports); - if let Some(sig) = &proc_export.signature { - let mut param_felts = Vec::with_capacity(sig.params.len()); - for ty in &sig.params { - param_felts.push(ty.size_in_felts()); - } - let mut result_felts = Vec::with_capacity(sig.results.len()); - for ty in &sig.results { - result_felts.push(ty.size_in_felts()); - } - - println!("Raw Signature: {sig}\n"); - - // The stack is flat, so the counts that matter are the flattened widths: a `Word` - // parameter takes four stack slots, not one. - return ProcedureSignature { - param_felts: Some(param_felts.iter().sum()), - result_felts: Some(result_felts.iter().sum()), - }; + let export = resolve_procedure_export(&manifest, "increment-by").unwrap(); + assert_eq!(export.signature, interface_form().1); } - println!("Raw Signature: {procedure_name}(...) [no type info]\n"); - return UNKNOWN; } - println!("(procedure '{procedure_name}' not found in manifest exports)"); - println!("Available exports:"); - for export in package.manifest.exports() { - if let PackageExport::Procedure(p) = export { - println!(" {}", p.path); + #[test] + fn a_lowered_name_is_not_reachable_by_the_bare_procedure_name() { + // The last part of the lowered path holds the whole interface, so it never equals the + // plain name. Were it found, its `i32` return would be printed as a value. + let manifest = manifest_with_exports(&[lowered_form()]); + + let err = resolve_procedure_export(&manifest, "increment-by").unwrap_err(); + assert_eq!( + err.to_string(), + "invalid argument: Procedure 'increment-by' not found. Available:\n \ + ::\"miden:counter/counter@0.1.0\"::cc::\"miden:counter/counter@0.1.0#increment-by\"" + ); + } + + #[test] + fn an_underscore_query_finds_a_kebab_export() { + let manifest = manifest_with_exports(&[interface_form(), lowered_form()]); + + let export = resolve_procedure_export(&manifest, "increment_by").unwrap(); + assert_eq!(export.signature, interface_form().1); + } + + #[test] + fn a_hand_written_masm_export_does_not_shadow_the_component_model_one() { + // A MASM `increment_by` matches the query by name, but `call` needs the Component Model + // signature: only that one describes the values the user passes and reads. + let masm = + || ("::mix::increment_by", Some(FunctionType::new(CallConv::Fast, [], [Type::U32]))); + for exports in [[interface_form(), masm()], [masm(), interface_form()]] { + let manifest = manifest_with_exports(&exports); + + let export = resolve_procedure_export(&manifest, "increment_by").unwrap(); + assert_eq!(export.signature, interface_form().1); } } - println!(); - UNKNOWN + + #[test] + fn an_unknown_procedure_lists_the_whole_export_surface() { + let manifest = manifest_with_exports(&[interface_form(), lowered_form()]); + + let err = resolve_procedure_export(&manifest, "no-such-proc").unwrap_err(); + assert_eq!( + err.to_string(), + "invalid argument: Procedure 'no-such-proc' not found. Available:\n \ + ::\"miden:counter/counter@0.1.0\"::\"increment-by\"\n \ + ::\"miden:counter/counter@0.1.0\"::cc::\"miden:counter/counter@0.1.0#increment-by\"" + ); + } + + #[test] + fn an_export_without_a_signature_is_still_resolved() { + // MASM written by hand: the export has the name we ask for, but no type info. It is still + // callable with raw field elements, so it has to resolve rather than be rejected. + let manifest = manifest_with_exports(&[("::mix::\"increment-by\"", None)]); + + let export = resolve_procedure_export(&manifest, "increment-by").unwrap(); + assert_eq!(export.signature, None); + } + + /// The Goldilocks field modulus, `2^64 - 2^32 + 1`. The first value with no felt of its own. + const FIELD_MODULUS: u64 = 18_446_744_069_414_584_321; + + #[test] + fn raw_arguments_are_read_as_decimal_felts() { + let args = ["0", "10", (FIELD_MODULUS - 1).to_string().as_str()].map(String::from); + + let encoded = encode_raw_args(&args).unwrap(); + + let expected = [0, 10, FIELD_MODULUS - 1].map(|v| Felt::new(v).unwrap()); + assert_eq!(encoded, expected); + } + + #[test] + fn a_raw_argument_at_the_field_modulus_is_rejected() { + // The modulus is what an unchecked `u64` argument would silently wrap around to. + let err = encode_raw_args(&[FIELD_MODULUS.to_string()]).unwrap_err(); + + assert_eq!( + err.to_string(), + format!("invalid argument: Argument '{FIELD_MODULUS}' is too large for a felt.") + ); + } + + #[test] + fn a_raw_hex_argument_is_rejected() { + // The typed path writes a `felt` in decimal and reserves `0x` for wider values, so the + // untyped path cannot take hex either: it would work only until the procedure is given a + // signature. + let err = encode_raw_args(&["0xff".to_string()]).unwrap_err(); + + assert_eq!(err.to_string(), "invalid argument: Invalid argument '0xff'. Expected a felt."); + } + + #[test] + fn the_component_model_export_wins_over_an_untyped_one_written_before_it() { + // The untyped export is seen first, but it is only the fallback: resolution has to go on + // and take the Component Model one. + let manifest = + manifest_with_exports(&[("::mix::\"increment-by\"", None), interface_form()]); + + let export = resolve_procedure_export(&manifest, "increment-by").unwrap(); + assert_eq!(export.signature, interface_form().1); + } +} + +/// What the script leaves on the stack for the caller to read. +#[derive(Clone, Copy)] +enum Cleanup { + /// Leave the results on top for the caller to read. + KeepResults, + /// Drop the results. Dropping past the point where the script started is harmless, since the + /// stack never goes below its minimum depth. + DropAll, } -/// Builds a transaction script that pushes `args`, calls the procedure at `digest`, and optionally -/// drops the pushed args from under the results. `Some(n)` keeps the top `n` values; `None` skips -/// drops. +/// Builds a transaction script that pushes `args`, calls the procedure at `digest`, and leaves the +/// stack at the depth the kernel requires the script to return with. +/// +/// `cleanup` decides whether the results are kept for the caller to read or dropped. fn generate_tx_script( code_builder: CodeBuilder, digest: &Word, args: &[Felt], - result_count: Option, + cleanup: Cleanup, ) -> Result { - // MASM `movup.n` only works for n in 2..=15. The VM stack exposes only the top - // 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 - { + // Every argument is pushed and then paid for with one of the `MIN_STACK_DEPTH` padding values + // the script starts with, reached with `movup`, which addresses at most position 15. One more + // argument than that leaves no padding to reach: position 15 would hold an argument. + // See miden-vm/docs/src/user_docs/assembly/instruction_reference.md (movup row). + let max_args = MIN_STACK_DEPTH - 1; + if args.len() > max_args { return Err(CliError::InvalidArgument(format!( - "Procedure returns {n} values; only up to 15 are supported." + "The arguments occupy {} stack values, and a called procedure can only be passed {}.", + args.len(), + max_args ))); } @@ -297,27 +504,22 @@ fn generate_tx_script( writeln!(script, " push.{arg}").unwrap(); } + // The kernel invokes this script with `dyncall`, which requires it to return at + // `MIN_STACK_DEPTH`, so the pushes above have to be paid for. Dropping the deepest reachable + // value once per argument does that without touching the arguments themselves: what it + // discards is the script's own initial padding, and the callee is still passed the arguments + // on top with padding underneath. + for _ in 0..args.len() { + writeln!(script, " movup.{max_args} drop").unwrap(); + } + writeln!(script, " call.{}", digest.to_hex()).unwrap(); - let to_drop = args.len(); - if to_drop > 0 { - match result_count { - Some(0) => { - for _ in 0..to_drop { - script.push_str(" drop\n"); - } - }, - Some(1) => { - for _ in 0..to_drop { - script.push_str(" swap drop\n"); - } - }, - Some(n) => { - for _ in 0..to_drop { - writeln!(script, " movup.{n} drop").unwrap(); - } - }, - None => {}, + // A `call` returns at `MIN_STACK_DEPTH` as well, so the results need no cleanup to keep the + // script's own depth right; dropping them is only about what the caller reads back. + if matches!(cleanup, Cleanup::DropAll) { + for _ in 0..MIN_STACK_DEPTH { + script.push_str(" drop\n"); } } diff --git a/bin/miden-cli/src/errors.rs b/bin/miden-cli/src/errors.rs index fe2b3e5a9a..ccdae4075a 100644 --- a/bin/miden-cli/src/errors.rs +++ b/bin/miden-cli/src/errors.rs @@ -5,6 +5,7 @@ use std::error::Error; use miden_client::account::{AccountId, AddressError}; use miden_client::keystore::KeyStoreError; +use miden_client::vm::typed::TypedError; use miden_client::{ AccountError, AccountIdError, @@ -104,8 +105,20 @@ pub enum CliError { MissingFlag(String), #[error("network id error")] NetworkIdError(#[from] NetworkIdError), + #[error("client has not been synced yet")] + #[diagnostic( + code(cli::not_synced), + help("Run `{} sync` first.", client_binary_name().display()) + )] + NotSynced, #[error("invalid argument: {0}")] InvalidArgument(String), + // Covers both directions of the typed path: encoding arguments and decoding results. The + // inner error already states the whole problem, so it is shown in place of a wrapper message + // rather than under one, where it would be printed twice. + #[error(transparent)] + #[diagnostic(code(cli::typed_error))] + Typed(#[from] TypedError), #[error("parse error: {1}")] #[diagnostic(code(cli::parse_error), help("Check the inputs."))] Parse(#[source] SourceError, String), diff --git a/bin/miden-cli/src/lib.rs b/bin/miden-cli/src/lib.rs index 2297456276..7972ad7a3a 100644 --- a/bin/miden-cli/src/lib.rs +++ b/bin/miden-cli/src/lib.rs @@ -276,6 +276,7 @@ impl DerefMut for CliClient { } mod advice_inputs; +mod codecs; pub mod config; // These modules intentionally shadow the miden_client re-exports - CLI has its own errors/utils #[allow(hidden_glob_reexports)] diff --git a/bin/miden-cli/tests/cli.rs b/bin/miden-cli/tests/cli.rs index 87f645bb8a..7dc8dab513 100644 --- a/bin/miden-cli/tests/cli.rs +++ b/bin/miden-cli/tests/cli.rs @@ -1723,9 +1723,19 @@ fn call_nonexistent_procedure() { /// writes the serialized `.masp` to `out_path`. fn call_test_exports(package: &miden_client::vm::Package) -> Vec { use miden_client::vm::{PackageExport, ProcedureExport, QualifiedProcedureName}; - use midenc_hir_type::{CallConv, FunctionType, Type}; - - let signature_overrides: [(&str, FunctionType); 3] = [ + use midenc_hir_type::{CallConv, FunctionType, StructType, Type}; + + // The `account-id` core type as the compiler records it: a named record of two field + // elements. Its name is what the CLI's `account-id` codec matches against. + let account_id = Type::Struct(Arc::new(StructType::named( + Arc::from("miden:base/core-types@1.0.0/account-id"), + [ + (Arc::::from("prefix"), Type::Felt), + (Arc::::from("suffix"), Type::Felt), + ], + ))); + + let signature_overrides: [(&str, FunctionType); 5] = [ ( "add", FunctionType::new(CallConv::ComponentModel, [Type::Felt, Type::Felt], [Type::Felt]), @@ -1739,6 +1749,14 @@ fn call_test_exports(package: &miden_client::vm::Package) -> Vec(stdout: &'a str, prefix: &str) -> &'a str { + let mut matching = stdout.lines().filter(|line| line.starts_with(prefix)); + let line = matching + .next() + .unwrap_or_else(|| panic!("no line starts with `{prefix}`:\n{stdout}")); + assert!( + matching.next().is_none(), + "more than one line starts with `{prefix}`:\n{stdout}" + ); + line +} + +/// Tests the typed encode/decode path: an `account-id` hex token is expanded to two felts on +/// the way in and rendered back as `account-id(0x..)` on the way out. +#[test] +fn call_typed_account_id_roundtrip() { + let (temp_dir, account_id, masp_path) = setup_call_test_account(); + + let acct_hex = "0xaa0000000000bb110000cc000000dd"; + let mut cmd = cargo_bin_cmd!("miden-client"); + cmd.args([ + "call", + &format!("{account_id}:take_account_id"), + acct_hex, + "--package", + masp_path.to_str().unwrap(), + ]); + + let output = cmd.current_dir(&temp_dir).output().unwrap(); + assert!( + output.status.success(), + "Call failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!( + output_line(&stdout, "Signature:"), + "Signature: take_account_id(account-id) -> account-id" + ); + assert_eq!(output_line(&stdout, "Result:"), format!("Result: account-id({acct_hex})")); +} + +/// Tests the untyped fallback: a procedure the package describes no WIT types for is still called, +/// with one field element per argument and the output stack printed as-is. +#[test] +fn call_untyped_procedure_falls_back_to_raw_felts() { + let (temp_dir, account_id, masp_path) = setup_call_test_account(); + + let mut cmd = cargo_bin_cmd!("miden-client"); + cmd.args([ + "call", + &format!("{account_id}:raw_add"), + "3", + "7", + "--package", + masp_path.to_str().unwrap(), + ]); + + let output = cmd.current_dir(&temp_dir).output().unwrap(); + assert!( + output.status.success(), + "Call failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!(output_line(&stdout, "Signature:"), "Signature: raw_add(...) [no type info]"); + // The dump runs to the last non-zero value, and `add` leaves nothing but the sum. + assert_eq!(output_line(&stdout, "Result:"), "Result: 10"); +} + +/// Tests that an untyped procedure takes its arguments the way a `felt` is written on the typed +/// path, so the fallback cannot teach a syntax that stops working once the types arrive. +#[test] +fn call_untyped_procedure_rejects_a_hex_argument() { + let (temp_dir, account_id, masp_path) = setup_call_test_account(); + + let mut cmd = cargo_bin_cmd!("miden-client"); + cmd.args([ + "call", + &format!("{account_id}:raw_add"), + "0xff", + "7", + "--package", + masp_path.to_str().unwrap(), + ]); + + let output = cmd.current_dir(&temp_dir).output().unwrap(); + assert!(!output.status.success(), "Expected failure for a hex argument"); assert!( - stdout.contains("Result: 22"), - "Expected advice-derived result in output:\n{stdout}" + String::from_utf8_lossy(&output.stderr) + .contains("Invalid argument '0xff'. Expected a felt."), + "Unexpected stderr:\n{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +/// Tests that the two felts of an `account-id` argument reach the procedure in signature order. +/// The identity round-trip above cannot show this: encoding and decoding would agree even if both +/// had the fields the wrong way around. +#[test] +fn call_typed_account_id_field_order() { + let (temp_dir, account_id, masp_path) = setup_call_test_account(); + + let acct_hex = "0xaa0000000000bb110000cc000000dd"; + let suffix = AccountId::from_hex(acct_hex).unwrap().suffix(); + + let mut cmd = cargo_bin_cmd!("miden-client"); + cmd.args([ + "call", + &format!("{account_id}:account_id_suffix"), + acct_hex, + "--package", + masp_path.to_str().unwrap(), + ]); + + let output = cmd.current_dir(&temp_dir).output().unwrap(); + assert!( + output.status.success(), + "Call failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!( + output_line(&stdout, "Signature:"), + "Signature: account_id_suffix(account-id) -> felt" ); + assert_eq!(output_line(&stdout, "Result:"), format!("Result: {suffix}felt")); } /// Tests that calling a `add` with the wrong number of arguments fails @@ -2038,7 +2208,7 @@ fn call_rejects_wrong_arg_count() { assert!(!out.status.success(), "Expected failure for too-few args"); let stderr = String::from_utf8_lossy(&out.stderr); assert!( - stderr.contains("expects 2 value") && stderr.contains("got 1"), + stderr.contains("expects 2 argument") && stderr.contains("got 1"), "Unexpected stderr:\n{stderr}" ); @@ -2057,7 +2227,7 @@ fn call_rejects_wrong_arg_count() { assert!(!out.status.success(), "Expected failure for too-many args"); let stderr = String::from_utf8_lossy(&out.stderr); assert!( - stderr.contains("expects 2 value") && stderr.contains("got 3"), + stderr.contains("expects 2 argument") && stderr.contains("got 3"), "Unexpected stderr:\n{stderr}" ); } diff --git a/crates/rust-client/Cargo.toml b/crates/rust-client/Cargo.toml index 540f2e3e7c..162649d758 100644 --- a/crates/rust-client/Cargo.toml +++ b/crates/rust-client/Cargo.toml @@ -48,13 +48,15 @@ crate-type = ["lib"] # `std`; the duplication in the `std` feature list is intentional so # each consumer can pick the smallest feature set they need. concurrent = ["miden-tx/concurrent"] -dap = ["dep:miden-debug", "dep:miden-processor", "std"] +dap = ["dep:miden-debug", "std"] default = ["std"] std = [ "concurrent", "dep:tempfile", "dep:tokio", "miden-agglayer/std", + "miden-assembly-syntax/std", + "miden-processor/std", "miden-protocol/std", "miden-tx/std", "tonic/tls-native-roots", @@ -73,14 +75,15 @@ tonic = [] [dependencies] # Miden dependencies -miden-agglayer = { workspace = true } -miden-debug = { optional = true, workspace = true } -miden-processor = { optional = true, workspace = true } -miden-protocol = { workspace = true } -miden-standards = { workspace = true } -miden-testing = { optional = true, workspace = true } -miden-tx = { workspace = true } -miden-tx-batch = { workspace = true } +miden-agglayer = { workspace = true } +miden-assembly-syntax = { workspace = true } +miden-debug = { optional = true, workspace = true } +miden-processor = { workspace = true } +miden-protocol = { workspace = true } +miden-standards = { workspace = true } +miden-testing = { optional = true, workspace = true } +miden-tx = { workspace = true } +miden-tx-batch = { workspace = true } # External dependencies anyhow = { workspace = true } diff --git a/crates/rust-client/src/errors.rs b/crates/rust-client/src/errors.rs index a06b0269cc..9e99a1d5f9 100644 --- a/crates/rust-client/src/errors.rs +++ b/crates/rust-client/src/errors.rs @@ -20,15 +20,10 @@ use miden_protocol::note::NoteId; // ================================================================================================ pub use miden_standards::errors::CodeBuilderError; use miden_standards::tx_script::SendNotesTransactionScriptError; -pub use miden_tx::AuthenticationError; use miden_tx::utils::HexParseError; use miden_tx::utils::serde::DeserializationError; -use miden_tx::{ - DataStoreError, - NoteCheckerError, - TransactionExecutorError, - TransactionProverError, -}; +pub use miden_tx::{AuthenticationError, TransactionExecutorError}; +use miden_tx::{DataStoreError, NoteCheckerError, TransactionProverError}; use thiserror::Error; use crate::note::NoteScreenerError; diff --git a/crates/rust-client/src/lib.rs b/crates/rust-client/src/lib.rs index 4926eb2b3f..c32dc46f27 100644 --- a/crates/rust-client/src/lib.rs +++ b/crates/rust-client/src/lib.rs @@ -299,6 +299,10 @@ pub mod address { /// Provides types for working with the virtual machine within the Miden network. pub mod vm { + pub use miden_assembly_syntax::ast::types::signatures as typed; + pub use miden_processor::ExecutionError; + pub use miden_processor::mast::error_code_from_msg; + pub use miden_processor::operation::OperationError; pub use miden_protocol::vm::{ AdviceInputs, AdviceMap, diff --git a/docs/external/src/rust-client/cli/index.md b/docs/external/src/rust-client/cli/index.md index 44f0120e0c..edf7461b2e 100644 --- a/docs/external/src/rust-client/cli/index.md +++ b/docs/external/src/rust-client/cli/index.md @@ -448,7 +448,21 @@ Usage: `miden-client call : [ARGS]... --package The target is a single argument of the form `:`. The account ID may be given as a partial ID. The procedure name is matched against the package's exports with `_` and `-` treated as equivalent, so it can be written in either snake_case or kebab-case (`get_count` matches the export `get-count`). -Arguments are passed positionally after the target. Each one is a `u64` field element, and they are pushed onto the stack so that the first argument ends up on top. Their number is checked against the procedure's signature in the package manifest. If the package does not record a signature, the check is skipped and a warning is printed, in which case passing the wrong number of arguments may fail or produce a wrong result. +Arguments are passed positionally after the target, one token per value in the procedure's signature. The signature comes from the package manifest, and it also decides how each token is read and how the result is printed: + +| Type | Token form | Example | +| ---- | ---------- | ------- | +| `felt` | decimal field element | `42` | +| integers (`u8`…`u128`, `i8`…`i128`) | decimal, range-checked against the type | `-1` | +| `bool` | `true`, `false`, `1` or `0` | `true` | +| `word` | hex | `0x00..` | +| `account-id` | hex account ID | `0x4614b8bf575eab71455e97bd394e90` | +| `asset` | `::`, fungible only | `100::0xabcdef0123456789` | +| records and fixed arrays | one token per field, in order | `3 4` for `point { x, y }` | + +Only procedures exported from a WIT interface carry a signature. A procedure without one is still called, with one raw field element per argument written in decimal (a `0x` hex literal is not accepted); the argument count is not checked and the result is printed as a stack dump. + +The arguments are pushed onto the stack so that the first one ends up on top, and together they may occupy at most 15 stack values. `--inputs-path` takes the same TOML format as [`exec`](#exec). The entries are loaded into the VM's advice map and are visible to the called procedure. @@ -463,7 +477,7 @@ miden-client call 0x4614b8bf575eab71455e97bd394e90:increment-count --package tar The command first prints the procedure's signature and its return values, then the effects the call has on the account: ```sh -Raw Signature: extern "fast" fn() -> felt +Signature: increment-count() -> felt Result: 1 The transaction will have the following effects: @@ -485,6 +499,8 @@ Account Vault will not be changed. Nonce incremented by: 1. ``` +A procedure that only reads leaves the transaction with no effects at all, which the transaction kernel does not allow. The result is still printed, followed by a note that the transaction was rejected for having no effects. + :::note The call is executed locally. No proof is generated, nothing is submitted to the network, and the account's stored state is left unchanged. :::