0xmiden/compiler/rust-sdk-pitfalls
Claude Code and agent skill by 0xmiden/compiler
- community
Checking that the SKILL.md file is still in the repository.
Critical pitfalls and safety rules for Miden Rust SDK development. Covers felt arithmetic security, comparison operators, argument limits, storage naming, no-std setup, asset layout, P2ID roots, NoteType construction, note-to-component call boundaries, and note input immutability. Use when reviewing, debugging, or writing Miden contract code.
Install
npx vskill@latest install 0xmiden/compiler/rust-sdk-pitfallsSKILL.md
Miden SDK Pitfalls
The project-template uses guest SDK miden and miden-sdk-build-script-support 0.15.0 on nightly-2026-09-01, compiler / cargo-miden 0.11.0 in the current checkout, protocol/standards/testing 0.18.0, and VM/package crates in the 0.36 series. The client crates use the released 0.18.0 versions from crates.io; Cargo.lock records the resolved versions.
P1: Felt Arithmetic is Modular (SECURITY CRITICAL)
Severity: Critical — can cause loss of funds
Felt subtraction wraps around the prime field modulus (p = 2^64 - 2^32 + 1) instead of panicking. Subtracting more than available silently produces a huge positive number.
Rule: ALWAYS check .as_canonical_u64() values before any Felt subtraction.
Max Felt value: The maximum valid Felt is p - 1 = 18446744069414584320, not u64::MAX (18446744073709551615). Using u64::MAX as a sentinel or boundary value causes silent wraparound.
Prefer `AssetAmount` for token quantities. miden::AssetAmount is a validated newtype over Felt (AssetAmount::MAX_U64 = (1 << 63) - (1 << 31)) whose Add / Sub impls panic on overflow / underflow rather than wrapping, and whose ordering is canonical-integer ordering. Constructors and accessors: AssetAmount::new(u64) -> Result<AssetAmount, AssetAmountError>, AssetAmount::max(), AssetAmount::ZERO, as_u64(), as_felt(). It is a valid WordKey and WordValue, so it can be stored directly in StorageValue / StorageMap and used in exported signatures.
P2: Felt Comparison Operators Are Misleading for Quantity Logic
Severity: High — silently produces incorrect results
<, >, <=, >= on Felt values compare field elements, which differs from natural number ordering. In protocol-level code working with field elements, these comparisons may be intentional. For business logic (balances, amounts, counts), the results are misleading.
Rule: For quantity/business logic, ALWAYS convert to .as_canonical_u64() before using comparison operators.
Exception — typed scalars already compare as integers. BlockNumber, Nonce and AssetAmount derive integer ordering, so no conversion is needed. The p2ide-note example relies on this directly:
P3: The 16-Felt Cross-Context Boundary — Two Limits, and Exports Differ From FPI Imports
Severity: High — the wrong mental model makes you refactor a signature that would have compiled, and miss the one that will not
A cross-context call passes its parameters on the MASM operand stack, whose addressable window is 16 felts (4 Words, counting the canonical-ABI result pointer when present). Two different limits govern that boundary, and conflating them is the actual pitfall:
- `MAX_FLAT_PARAMS = 16` — a count of canonical-ABI flat values.
- `MAX_DIRECT_STACK_FELTS = 16` — a felt budget, measured after
u64values expand to two
felts each and any result pointer is added.
Both live in compiler:sdk/v0.14.0:frontend/wasm/src/component/types/mod.rs:44-62, whose own doc comment spells out the distinction: the felt budget "is a Miden VM constraint, distinct from the spec's count-based MAX_FLAT_PARAMS: a signature can stay within 16 flat values while 64-bit values expand it past 16 stack felts."
Exceeding either limit makes canonical-ABI flattening replace the whole parameter list with a single pointer to a tuple in linear memory — flat_params_need_tuple is an OR (compiler:sdk/v0.14.0:frontend/wasm/src/component/flat.rs:212-217,263-270):
What happens to that pointer is where the two sides of the boundary part ways.
FPI imports: the count decides the path, the felt budget can still reject it
The critical subtlety: plan_fpi_call does not reuse the OR above. It re-derives the call shape from the flat-value count alone (compiler:sdk/v0.14.0:frontend/wasm/src/component/lower_imports.rs:330-351):
So the two disagree in exactly one band — count ≤ 16 but felts > 16. Flattening has tupled such a signature, but plan_fpi_call still believes it is a direct call, so it takes the !has_arg_ptr path, sums the operand felts, and rejects:
The source says this check is deliberately ordered first, because "over-budget direct shapes are tupled by canonical ABI flattening, which would otherwise surface as a confusing shape mismatch."
That is the compiler's own negative test, compiler:sdk/v0.14.0:tests/integration-network/src/mockchain/fpi/note/six_u64_struct.rs:5-13 (#[should_panic(expected = "direct FPI calls support at most 16")]).
Above 16 flat values the indirect path is supported — has_arg_ptr is true, the felt-budget check is skipped entirely, and the wrapper reloads the tuple so the backend still sees a direct, felt-only call (compiler:sdk/v0.14.0:frontend/wasm/src/component/lower_imports.rs:439-508). A 22-flat-parameter FPI import is a passing test (compiler:sdk/v0.14.0:tests/integration-network/src/mockchain/fpi/note/sixteen_flattened_params_struct.rs).
Indirect is not unbounded, though. The FPI executor imposes its own caps, checked after the shape is settled (compiler:sdk/v0.14.0:frontend/wasm/src/component/lower_imports.rs:381-399), with values from ExecFpi (compiler:sdk/v0.14.0:dialects/hir/src/ops/invoke.rs:160-169):
| bound | value | diagnostic | |
|---|---|---|---|
PREFIX_FELTS — account id + procedure root, subtracted before the input check | 6 | must pass account id and procedure root | |
MAX_INPUT_FELTS — flattened procedure input felts, after the prefix | 16 | `passes {n} flattened procedure input felts, but execute_foreign_procedure supports at most 16` | |
EXECUTOR_RESULT_FELTS — result felts | 16 | `returns {n} result felts, but execute_foreign_procedure supports at most 16` |
So the tuple pointer buys you past the stack window, not past the protocol: the payload the foreign procedure actually receives is still capped at 16 felts.
Do not "fix" the felt-budget rejection by padding the signature until the count exceeds 16 just to trigger the indirect path. It does flip has_arg_ptr and skip the stack-window check, but the executor's 16-felt input cap then rejects the same payload — you have only moved which diagnostic fires. Reduce what crosses the boundary instead: split the call, or hand over an identifier (a storage key, a note index, a commitment) and let the callee load the rest itself.
Component exports: indirect parameters are not implemented yet
On the export side the tuple pointer is produced the same way but then refused, so either an over-16 flat-value count or an over-16 felt budget fails (compiler:sdk/v0.14.0:frontend/wasm/src/component/lift_exports.rs:68-74):
Export return values are capped separately, at 16 loaded values (a count, with no felt-budget check at all — a record of nine u64 fields is 9 values but 18 felts and is not caught): compiler:sdk/v0.14.0:frontend/wasm/src/component/lift_exports.rs:281-286.
Unrelated, but adjacent
&T parameters are refused before any of this, by the #[component] macro rather than the compiler frontend: references are not supported in component interfaces or exported types (compiler:sdk/v0.14.0:sdk/base-macros/src/types.rs:102-106). It applies to exported method parameters, return types, and exported struct/enum fields alike — &self receivers are fine.
P4: Storage API Is Typed, and a Component Is Three Parts
Severity: Medium — the wrong component shape does not compile
Account storage uses typed slots:
StorageValue<T>for a single typed slotStorageMap<K, V>for typed mapsget()/set()methods instead of.read()/.write()K: WordKey,T: WordValue,V: WordValue
WordValue is implemented for Word, Felt, AssetAmount, Digest, AccountId, Recipient, Tag, NoteIdx, NoteType. WordKey is implemented for Word, Felt, AssetAmount, AccountId, Tag, NoteIdx, NoteType.
An account component is written in three parts: annotate the storage struct with #[component_storage], the API trait with #[component], and the impl Trait for Storage block with #[component]. Every trait method that must be callable from outside the component also needs #[account_procedure] (see P13):
#[component_storage] accepts only unit structs and structs with named fields — a tuple struct fails with ` #[component_storage] only supports unit structs or structs with named fields. ` Generic storage structs are rejected.
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingextra/templates/project/.claude/skills/rust-sdk-pitfalls/SKILL.md
Popularity
- Repository stars
- 116
- Repository forks
- 83
- 7-day trend score
- 29
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/0xmiden/compiler/rust-sdk-pitfalls)