diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f68e55fe10..414b91159b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,8 +147,10 @@ jobs: run: | cargo make check --tests - name: Test + # The p2id end-to-end tests drive full nested Cargo builds, so the + # release-profile job runs them. run: | - cargo make test -E 'not (package(midenc-integration-tests) or package(midenc-integration-network-tests) or package(cargo-miden) or package(midenc-template-tests))' + cargo make test -E 'not (package(midenc-integration-tests) or package(midenc-integration-network-tests) or package(cargo-miden) or package(midenc-template-tests) or test(~p2id_schema_builds_and_decodes_account_id_storage) or test(~generated_p2id_bindings_compile_and_run_in_a_consumer_crate))' - name: Upload test timings and failures if: ${{ always() }} diff --git a/.release/config.toml b/.release/config.toml index 55f7ce7667..7e7e8f6cdb 100644 --- a/.release/config.toml +++ b/.release/config.toml @@ -217,6 +217,34 @@ unit = "sdk" name = "midenc-frontend-wasm-metadata" unit = "sdk" +[[packages]] +name = "miden-note-schema" +unit = "sdk" + +[[packages]] +name = "miden-note-schema-codegen" +unit = "sdk" + +[[packages]] +name = "miden-note-bindings" +unit = "sdk" + +[[packages]] +name = "miden-note-bindings-macros" +unit = "sdk" + +[[packages]] +name = "miden-note-codec" +unit = "sdk" + +[[packages]] +name = "miden-note-codec-macros" +unit = "sdk" + +[[packages]] +name = "miden-note-codec-wit" +unit = "sdk" + # --- private: repository infrastructure, never published ---------------------- [[packages]] diff --git a/Cargo.lock b/Cargo.lock index d48b4a042b..f70bbc9023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,15 @@ dependencies = [ "regex", ] +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli 0.31.1", +] + [[package]] name = "addr2line" version = "0.25.1" @@ -242,6 +251,21 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object 0.39.1", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "ark-ff" version = "0.3.0" @@ -761,6 +785,9 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2 0.2.21", +] [[package]] name = "byte-slice-cast" @@ -806,8 +833,11 @@ dependencies = [ "liquid", "liquid-core", "log", + "miden-mast-package", "midenc-compile", + "midenc-frontend-wasm-metadata", "midenc-hir", + "midenc-integration-test-support", "midenc-log", "midenc-session", "path-absolutize", @@ -816,6 +846,8 @@ dependencies = [ "tempfile", "toml_edit", "walkdir", + "wit-component", + "wit-parser 0.247.0", ] [[package]] @@ -1012,6 +1044,15 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + [[package]] name = "codegen" version = "0.3.0" @@ -1154,6 +1195,43 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce81edaca6167d1f78da026afa92d7ff957a80aa82a79076e11cd34cde20165" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d0d51e12f958551165969c6e8767e1e461729f6c1ccae923b0ba1d5cbcbbbf8" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41294c755094d2c8a514cea903039742474423f2e91601332eab5f4094f76333" +dependencies = [ + "cranelift-entity 0.121.2", +] + +[[package]] +name = "cranelift-bitset" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb6f5d0df5bd0d02c63ec48e8f2e38a176b123f59e084f22caf89a0d0593e7e" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "cranelift-bitset" version = "0.135.1" @@ -1163,16 +1241,116 @@ dependencies = [ "wasmtime-internal-core", ] +[[package]] +name = "cranelift-codegen" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e543cdb278b7c15f739021cf880ee1808c68fa2402febb87edb9307f552c8fec" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset 0.121.2", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity 0.121.2", + "cranelift-isle", + "gimli 0.31.1", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f979c75cfd712dbc754799dfe4a4d0db7a51defc2e36d006b27a8a63e018eece" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f36e74ba4033490587a47952f74390cb7d4f1fc1fa28ace50564e491f1e38f" + +[[package]] +name = "cranelift-control" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6671962c7d65b9a7ad038cd92da6784744d8a9ecf8ded8bb9a1f7046dbe2ccf" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee832f8329fa87c5df6c1d64a8506a58031e6f8a190d9b21b1900272a4dbb47d" +dependencies = [ + "cranelift-bitset 0.121.2", + "serde", + "serde_derive", +] + [[package]] name = "cranelift-entity" version = "0.135.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0ec225d6b79c5d61358cb5ea081a1384ec541dbf8c1a8c618d187875af326ad" dependencies = [ - "cranelift-bitset", + "cranelift-bitset 0.135.1", "wasmtime-internal-core", ] +[[package]] +name = "cranelift-frontend" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7bc17aa3277214eab4b63a03544b1b46962154012b751c9f14c2a5419c6471" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff02dcecae2e7e9c61b713f1fb46eabecdca9f55b49f99859ceb1a3e7f4a9cb" + +[[package]] +name = "cranelift-native" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f76fd681f35bdf17be9c3e516b9acc0c7bd61b81faf95496decd8e0000979c" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3d9071bc5ee5573e723d9d84a45b7025a29e8f2c5ad81b3b9d0293129541d9" + [[package]] name = "crc32fast" version = "1.5.0" @@ -1543,6 +1721,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "ena" version = "0.14.4" @@ -1552,6 +1742,15 @@ dependencies = [ "log", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-ordinalize" version = "4.4.2" @@ -1910,6 +2109,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap 2.14.0", + "stable_deref_trait", +] + [[package]] name = "gimli" version = "0.32.3" @@ -2001,6 +2211,7 @@ dependencies = [ "allocator-api2 0.2.21", "equivalent", "foldhash 0.1.5", + "serde", ] [[package]] @@ -2783,6 +2994,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "macro-string" version = "0.2.0" @@ -2809,6 +3029,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + [[package]] name = "memmap2" version = "0.9.11" @@ -2959,11 +3188,13 @@ dependencies = [ "miden-mast-package", "miden-project", "miden-protocol", + "midenc-expect-test", "midenc-frontend-wasm-metadata", "proc-macro2", "quote", "semver 1.0.28", "syn 2.0.119", + "tempfile", "toml 1.1.4+spec-1.1.0", "wit-bindgen-core", "wit-bindgen-rust", @@ -3378,6 +3609,104 @@ dependencies = [ "tonic-prost-build", ] +[[package]] +name = "miden-note-bindings" +version = "0.14.0" +dependencies = [ + "miden-field", + "miden-field-repr", + "miden-mast-package", + "miden-note-bindings-macros", + "miden-note-schema", + "miden-protocol", + "midenc-frontend-wasm-metadata", + "midenc-integration-test-support", + "tempfile", +] + +[[package]] +name = "miden-note-bindings-macros" +version = "0.14.0" +dependencies = [ + "miden-note-schema", + "miden-note-schema-codegen", + "midenc-expect-test", + "midenc-frontend-wasm-metadata", + "prettyplease", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-note-codec" +version = "0.14.0" +dependencies = [ + "miden-field", + "miden-field-repr", + "miden-note-codec-macros", + "miden-note-codec-wit", + "miden-note-schema", + "miden-protocol", + "midenc-frontend-wasm-metadata", + "tempfile", + "wit-bindgen", + "wit-component", + "wit-parser 0.247.0", +] + +[[package]] +name = "miden-note-codec-macros" +version = "0.14.0" +dependencies = [ + "miden-note-codec-wit", + "miden-note-schema", + "miden-note-schema-codegen", + "midenc-frontend-wasm-metadata", + "prettyplease", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-note-codec-wit" +version = "0.14.0" + +[[package]] +name = "miden-note-schema" +version = "0.14.0" +dependencies = [ + "miden-core", + "miden-field", + "miden-field-repr", + "miden-mast-package", + "miden-note-codec-wit", + "miden-protocol", + "midenc-frontend-wasm-metadata", + "midenc-integration-test-support", + "tempfile", + "toml 1.1.4+spec-1.1.0", + "wasmparser 0.248.0", + "wasmtime", + "wat", + "wit-parser 0.247.0", +] + +[[package]] +name = "miden-note-schema-codegen" +version = "0.14.0" +dependencies = [ + "heck", + "miden-note-schema", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "miden-note-transport-proto-build" version = "0.5.0-alpha.1" @@ -3834,6 +4163,8 @@ dependencies = [ "miden-assembly", "miden-assembly-syntax", "miden-mast-package", + "miden-note-codec-wit", + "miden-note-schema", "miden-package-registry", "miden-thiserror", "midenc-codegen-masm", @@ -3845,8 +4176,12 @@ dependencies = [ "midenc-hir", "midenc-hir-transform", "midenc-session", + "sha2", + "tempfile", "toml_edit", "wat", + "wit-component", + "wit-parser 0.247.0", ] [[package]] @@ -3962,7 +4297,7 @@ version = "0.10.0" dependencies = [ "addr2line 0.26.1", "anyhow", - "cranelift-entity", + "cranelift-entity 0.135.1", "gimli 0.33.0", "indexmap 2.14.0", "log", @@ -3979,7 +4314,7 @@ dependencies = [ "midenc-hir-symbol", "midenc-session", "wasmparser 0.248.0", - "wasmprinter", + "wasmprinter 0.248.0", "wat", ] @@ -4137,12 +4472,14 @@ dependencies = [ "miden-field", "miden-field-repr", "miden-mast-package", + "miden-note-schema", "miden-protocol", "miden-standards", "miden-testing", "miden-tx-script-args", "midenc-expect-test", "midenc-frontend-wasm", + "midenc-frontend-wasm-metadata", "midenc-integration-test-support", "rand 0.10.2", "tokio", @@ -4210,6 +4547,7 @@ dependencies = [ "syn 2.0.119", "wasmi", "wat", + "wit-bindgen-core", ] [[package]] @@ -4474,6 +4812,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "object" version = "0.37.3" @@ -4949,6 +5299,18 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -5210,6 +5572,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "pubgrub" version = "0.3.0" @@ -5244,6 +5616,29 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "pulley-interpreter" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14280b69a9cbb6ada02a7aa5f7b3f1b72d1043b5bc9336990b700525dea6e3" +dependencies = [ + "cranelift-bitset 0.121.2", + "log", + "pulley-macros", + "wasmtime-math", +] + +[[package]] +name = "pulley-macros" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076f1be746801280af4c96c4407b5fd1d09cfa53ab27ba0ac7dd8f207e7bbf83" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pyo3-build-config" version = "0.28.3" @@ -5457,6 +5852,20 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "regalloc2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" +dependencies = [ + "allocator-api2 0.2.21", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + [[package]] name = "regex" version = "1.13.1" @@ -7068,6 +7477,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.233.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9679ae3cf7cfa2ca3a327f7fab97f27f3294d402fd1a76ca8ab514e17973e4d3" +dependencies = [ + "leb128fmt", + "wasmparser 0.233.0", +] + [[package]] name = "wasm-encoder" version = "0.247.0" @@ -7154,6 +7573,19 @@ dependencies = [ "wasmi_core", ] +[[package]] +name = "wasmparser" +version = "0.233.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51cb03afce7964bbfce46602d6cb358726f36430b6ba084ac6020d8ce5bc102" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver 1.0.28", + "serde", +] + [[package]] name = "wasmparser" version = "0.239.0" @@ -7198,6 +7630,17 @@ dependencies = [ "semver 1.0.28", ] +[[package]] +name = "wasmprinter" +version = "0.233.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf8e5b732895c99b21aa615f1b73352e51bbe2b2cb6c87eae7f990d07c1ac18" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.233.0", +] + [[package]] name = "wasmprinter" version = "0.248.0" @@ -7209,6 +7652,149 @@ dependencies = [ "wasmparser 0.248.0", ] +[[package]] +name = "wasmtime" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec10e50038f22ab407fdd8708120b8feed3450a02618efcf26ca47e82122927d" +dependencies = [ + "addr2line 0.24.2", + "anyhow", + "bitflags 2.13.1", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "libc", + "log", + "mach2", + "memfd", + "object 0.36.7", + "once_cell", + "postcard", + "psm", + "pulley-interpreter", + "rustix", + "semver 1.0.28", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser 0.233.0", + "wasmtime-asm-macros", + "wasmtime-component-macro", + "wasmtime-component-util", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-fiber", + "wasmtime-jit-icache-coherence", + "wasmtime-math", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "wasmtime-winch", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d379cda46d6fd18619e282a75fbb09b70b3d0f166b605f45b4059dfaf9dc6ce" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-component-macro" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b08be093e0a876da45f79070c2ada4656f2785eb77c01b86ce60be3153920a5" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser 0.233.0", +] + +[[package]] +name = "wasmtime-component-util" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0451ce0dd94a33d0dbd57934ce666a04c2753a5262ca2bc84cf6a67cf5303dc" + +[[package]] +name = "wasmtime-cranelift" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15aa836683d7398f13f2f26bbe74c404ceaba66b6bbb96700d6b7f91bec90e03" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity 0.121.2", + "cranelift-frontend", + "cranelift-native", + "gimli 0.31.1", + "itertools 0.14.0", + "log", + "object 0.36.7", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser 0.233.0", + "wasmtime-environ", + "wasmtime-math", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317081a0cbbb1f749d348b262575608fc082d47ab11b6247bbe9163eeb955777" +dependencies = [ + "anyhow", + "cranelift-bitset 0.121.2", + "cranelift-entity 0.121.2", + "gimli 0.31.1", + "indexmap 2.14.0", + "log", + "object 0.36.7", + "postcard", + "semver 1.0.28", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.233.0", + "wasmparser 0.233.0", + "wasmprinter 0.233.0", + "wasmtime-component-util", +] + +[[package]] +name = "wasmtime-fiber" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6763b33eceefc443f6477d84dc8751df5f23d280d7e01f28339fa3ec4b00ff13" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-asm-macros", + "wasmtime-versioned-export-macros", + "windows-sys 0.59.0", +] + [[package]] name = "wasmtime-internal-core" version = "48.0.1" @@ -7219,6 +7805,73 @@ dependencies = [ "libm", ] +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea6b740d1a35f2cebfe88e013ac8a4a84ff8dabc3a392df920abf554e871cf2" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-math" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62fa317691aedc64aae3a86b3d786e4b2b0007bc0b56e0b6098b8b5a85ab2134" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-slab" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a06819d24370273021054b50589e3078e7f5cfac15515e58b3fbbebf5e5b39" + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca100ed168ffc9b37aefc07a5be440645eab612a2ff6e2ff884e8cc3740e666" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "wasmtime-winch" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595f51430606a7b5578f34e0d7c73dca52a22ed24756f2ba9d4d0c1bde8631af" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli 0.31.1", + "object 0.36.7", + "target-lexicon", + "wasmparser 0.233.0", + "wasmtime-cranelift", + "wasmtime-environ", + "winch-codegen", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233fdcb96f9097be697319ba647ef42bdbdb40e89f04c8ae3713103813b5b793" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "wit-parser 0.233.0", +] + [[package]] name = "wast" version = "256.0.0" @@ -7282,6 +7935,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "34.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf007d7940f62127ce4f33a8aa92dadedfdc78c3860a057e06c8c24e26e180d" +dependencies = [ + "anyhow", + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli 0.31.1", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser 0.233.0", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-math", +] + [[package]] name = "wincode" version = "0.6.1" @@ -7404,6 +8077,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -7518,7 +8200,7 @@ checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.247.0", ] [[package]] @@ -7568,7 +8250,25 @@ dependencies = [ "wasm-encoder 0.247.0", "wasm-metadata", "wasmparser 0.247.0", - "wit-parser", + "wit-parser 0.247.0", +] + +[[package]] +name = "wit-parser" +version = "0.233.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22f1cd55247a2e616870b619766e9522df36b7abafbb29bbeb34b7a9da7e9f0" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver 1.0.28", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.233.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 48e765c5ec..ee0e568878 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,13 @@ members = [ "sdk/base-macros", "sdk/base-sys", "sdk/build-script-support", + "sdk/note-bindings", + "sdk/note-bindings/macros", + "sdk/note-codec", + "sdk/note-codec/macros", + "sdk/note-codec/wit-crate", + "sdk/note-schema", + "sdk/note-schema/codegen", "sdk/sdk", "sdk/stdlib-sys", "sdk/tx-script-args", @@ -52,7 +59,6 @@ exclude = [ # `name = "{{crate_name}}"`, and a nested workspace, neither of which Cargo # should ever try to interpret as part of this workspace. "extra", - "sdk/.cargo", "tests/", "examples", "tools/fuzza-agent", @@ -139,6 +145,14 @@ serde = { version = "1.0", default-features = false, features = [ ] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } heck = "0.5" +prettyplease = "0.2" +proc-macro-crate = "3.5" +# Deliberate skew: WIT emission (wit-bindgen-core 0.57) and attach/consume validation +# (workspace wit-parser/wit-component) parse with 0.247; 0.233 enters only through wasmtime's +# component bindgen macros on the codec-consumption path. +wit-bindgen-core = "0.57" +wit-bindgen-rust = { version = "0.57", default-features = false } +wit-bindgen = { version = "0.57", default-features = false, features = ["macros"] } smallvec = { version = "1.15", default-features = false, features = [ "union", "const_generics", @@ -160,6 +174,13 @@ wasmparser = { version = "^0.248", default-features = false, features = [ "simd", ] } wit-component = "0.247" +wit-parser = "0.247" +wasmtime = { version = "34.0.0", default-features = false, features = [ + "component-model", + "cranelift", + "runtime", + "std", +] } # Workspace crates midenc-codegen-masm = { version = "0.10.0", path = "codegen/masm" } @@ -186,6 +207,11 @@ midenc-integration-test-support = { path = "tests/support" } midenc-expect-test = { path = "tools/expect-test" } miden-base-sys = { version = "0.14.0", path = "sdk/base-sys" } miden-field-repr = { version = "0.14.0", path = "sdk/field-repr/repr" } +miden-note-bindings-macros = { version = "0.14.0", path = "sdk/note-bindings/macros" } +miden-note-codec-macros = { version = "0.14.0", path = "sdk/note-codec/macros" } +miden-note-codec-wit = { version = "0.14.0", path = "sdk/note-codec/wit-crate" } +miden-note-schema = { version = "0.14.0", path = "sdk/note-schema" } +miden-note-schema-codegen = { version = "0.14.0", path = "sdk/note-schema/codegen" } miden-stdlib-sys = { version = "0.14.0", path = "sdk/stdlib-sys" } miden-tx-script-args = { version = "0.14.0", path = "sdk/tx-script-args" } diff --git a/examples/auth-component-no-auth/Cargo.lock b/examples/auth-component-no-auth/Cargo.lock index f3ef9c100e..7dffb808b1 100644 --- a/examples/auth-component-no-auth/Cargo.lock +++ b/examples/auth-component-no-auth/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/auth-component-rpo-falcon512/Cargo.lock b/examples/auth-component-rpo-falcon512/Cargo.lock index 964a353654..b08b7fbf55 100644 --- a/examples/auth-component-rpo-falcon512/Cargo.lock +++ b/examples/auth-component-rpo-falcon512/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/basic-wallet-tx-script/Cargo.lock b/examples/basic-wallet-tx-script/Cargo.lock index 51651bd2e6..84e6bb00b1 100644 --- a/examples/basic-wallet-tx-script/Cargo.lock +++ b/examples/basic-wallet-tx-script/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/basic-wallet/Cargo.lock b/examples/basic-wallet/Cargo.lock index c9648c396c..35e66bf1fe 100644 --- a/examples/basic-wallet/Cargo.lock +++ b/examples/basic-wallet/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/collatz/Cargo.lock b/examples/collatz/Cargo.lock index 40f3ef060c..76d1a26a1b 100644 --- a/examples/collatz/Cargo.lock +++ b/examples/collatz/Cargo.lock @@ -11,4 +11,4 @@ dependencies = [ [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" diff --git a/examples/counter-contract/Cargo.lock b/examples/counter-contract/Cargo.lock index 34dc5734ed..8b34bdb550 100644 --- a/examples/counter-contract/Cargo.lock +++ b/examples/counter-contract/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/counter-note/Cargo.lock b/examples/counter-note/Cargo.lock index d751b8136e..601e794a03 100644 --- a/examples/counter-note/Cargo.lock +++ b/examples/counter-note/Cargo.lock @@ -951,7 +951,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1072,7 +1072,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1484,11 +1484,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1525,14 +1525,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1604,7 +1604,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/dex-note-codec/.gitignore b/examples/dex-note-codec/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/examples/dex-note-codec/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/dex-note-codec/Cargo.lock b/examples/dex-note-codec/Cargo.lock new file mode 100644 index 0000000000..04ccd62d58 --- /dev/null +++ b/examples/dex-note-codec/Cargo.lock @@ -0,0 +1,4218 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24671b1f62edcf0f9b62994c7bf72cd621a04a4b99f5020ece1a647b40e2f103" +dependencies = [ + "arrayvec", + "bytes", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint 0.4.8", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint 0.4.8", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint 0.4.8", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" +dependencies = [ + "ark-ff-asm 0.6.0", + "ark-ff-macros 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "digest 0.10.7", + "educe", + "num-bigint 0.4.8", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-asm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint 0.4.8", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint 0.4.8", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint 0.4.8", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" +dependencies = [ + "num-bigint 0.4.8", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint 0.4.8", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint 0.4.8", +] + +[[package]] +name = "ark-serialize" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.6.0", + "digest 0.10.7", + "num-bigint 0.4.8", + "serde_with", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "ark-std" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer", + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.11.3", + "fiat-crypto", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.119", +] + +[[package]] +name = "dex-note-codec" +version = "0.1.0" +dependencies = [ + "miden-note-codec", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "dissimilar" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der", + "digest 0.11.3", + "elliptic-curve", + "rfc6979", + "signature", + "spki", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff", + "group", + "hkdf", + "hybrid-array", + "pkcs8", + "rand_core 0.10.1", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.7", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "spin 0.9.9", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8" +dependencies = [ + "cpubits", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", + "wnaf", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miden-ace-codegen" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a217e3f1fec32105bca7dc421d85ab39199b78d2e43081fc0fa92411de9a84" +dependencies = [ + "miden-constraint-compiler", + "miden-core", + "miden-crypto", + "thiserror", +] + +[[package]] +name = "miden-air" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b7b3a23756f2bfdba2b37c8d1551b3b531fce0de42e3a9a7f840bb69018106" +dependencies = [ + "miden-ace-codegen", + "miden-core", + "miden-crypto", + "miden-utils-indexing", + "p3-field", + "proptest", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-assembly" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727de4350d9ba263be17d9f93dc4b8d0deebabab29c3bed34f32c4af7b1cf20c" +dependencies = [ + "log", + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "miden-project", + "proptest", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-assembly-syntax" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3874c53f40656a56f74afe8e957d75667218808894ab8f5dbd91e58a8a0c3393" +dependencies = [ + "log", + "miden-assembly-syntax-cst", + "miden-core", + "miden-debug-types", + "miden-utils-diagnostics", + "midenc-hir-type", + "proptest", + "regex", + "rustc_version 0.4.1", + "semver 1.0.28", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-assembly-syntax-cst" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151b657a50aee75a0591dbd66d57774e2f9ec1f00fa17d773820e736bcca0cb6" +dependencies = [ + "miden-debug-types", + "miden-rowan", + "miden-utils-diagnostics", + "thiserror", +] + +[[package]] +name = "miden-constraint-compiler" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f126188b824423edbbc5eebf9b61c872905c21e3f1e9e9b69d1afbba12288" +dependencies = [ + "miden-core", + "miden-crypto", +] + +[[package]] +name = "miden-core" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55df0c9b5fddcfc2c03c6e476bec0523328fb85090e716d1ab9a4db1d2267c67" +dependencies = [ + "derive_more", + "log", + "miden-crypto", + "miden-debug-types", + "miden-formatting", + "miden-utils-core-derive", + "miden-utils-indexing", + "miden-utils-sync", + "serde", + "thiserror", +] + +[[package]] +name = "miden-core-lib" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f5b4dca8d29c99859e3470ec1fadfa89506e6b349ade7fa475574e3104db85" +dependencies = [ + "env_logger", + "fs-err", + "miden-assembly", + "miden-assembly-syntax", + "miden-core", + "miden-core-lib-codegen", + "miden-crypto", + "miden-mast-package", + "miden-package-registry", + "miden-precompiles", + "miden-processor", + "miden-utils-sync", + "thiserror", +] + +[[package]] +name = "miden-core-lib-codegen" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d926e5e8a288a2a55c42541059d8e52c43c10d373643b309add169a3a0eb914" +dependencies = [ + "miden-core", + "miden-precompiles", +] + +[[package]] +name = "miden-crypto" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de00899e7045ee3bea78d4c4c690d8e2c14fff1f3a1ede1fb2922ed699fdda14" +dependencies = [ + "blake3", + "cc", + "chacha20poly1305", + "curve25519-dalek", + "der", + "ed25519-dalek", + "flume", + "hkdf", + "k256", + "miden-crypto-derive", + "miden-field", + "miden-lifted-stark", + "miden-serde-utils", + "num", + "num-complex", + "once_cell", + "p3-blake3", + "p3-challenger", + "p3-dft", + "p3-goldilocks", + "p3-keccak", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.10.2", + "rand_chacha 0.10.0", + "rayon", + "serde", + "sha2", + "sha3", + "subtle", + "thiserror", + "x25519-dalek", +] + +[[package]] +name = "miden-crypto-derive" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b5a5c8b0fd14982e60ebd010f452b06f693b9efa116054487aff1f2657d2f4" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-debug-types" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793b37eaafbac33a4c8bf8f57d4c1d0ce8b8a7d25698ddfd2e1a2a6552e94f6c" +dependencies = [ + "memchr", + "miden-crypto", + "miden-formatting", + "miden-miette", + "miden-utils-indexing", + "miden-utils-sync", + "paste", + "proptest", + "serde", + "serde_spanned", + "thiserror", + "zerocopy", +] + +[[package]] +name = "miden-field" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41be9f7f5c0ef020bcedf526afe54b3a97244e207a5385e4453ca47799fe2afe" +dependencies = [ + "miden-serde-utils", + "num-bigint 0.5.1", + "p3-challenger", + "p3-field", + "p3-goldilocks", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "subtle", + "thiserror", +] + +[[package]] +name = "miden-field-repr" +version = "0.14.0" +dependencies = [ + "miden-field", + "miden-field-repr-derive", +] + +[[package]] +name = "miden-field-repr-derive" +version = "0.14.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-formatting" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e392e0a8c34b32671012b439de35fa8987bf14f0f8aac279b97f8b8cc6e263b" +dependencies = [ + "unicode-width 0.1.14", +] + +[[package]] +name = "miden-lifted-air" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0ad52f76750f8b9dc8a7c4bed32644628198fab48daf31167c2f750c939378a" +dependencies = [ + "p3-air", + "p3-challenger", + "p3-field", + "p3-matrix", + "p3-util", + "thiserror", +] + +[[package]] +name = "miden-lifted-stark" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212a017744ef2aaca36f89bad2c880949311ec778586b8405a5e47f9e6ca59de" +dependencies = [ + "miden-lifted-air", + "miden-stark-transcript", + "miden-stateful-hasher", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-goldilocks", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.10.2", + "serde", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-mast-package" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26062f2e4cb18e7fa9244f8b18fe2adea2bfe3f016431f75f80b5b98249c34f6" +dependencies = [ + "hashbrown 0.17.1", + "log", + "miden-assembly-syntax", + "miden-core", + "miden-debug-types", + "miden-utils-indexing", + "rustc-hash", + "serde", + "thiserror", + "zerocopy", +] + +[[package]] +name = "miden-miette" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef536978f24a179d94fa2a41e4f92b28e7d8aab14b8d23df28ad2a3d7098b20" +dependencies = [ + "cfg-if", + "futures", + "indenter", + "lazy_static", + "miden-miette-derive", + "owo-colors", + "regex", + "rustc_version 0.2.3", + "rustversion", + "serde_json", + "spin 0.9.9", + "strip-ansi-escapes", + "syn 2.0.119", + "textwrap", + "thiserror", + "trybuild", + "unicode-width 0.1.14", +] + +[[package]] +name = "miden-miette-derive" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a905f3ea65634dd4d1041a4f0fd0a3e77aa4118341d265af1a94339182222f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-note-codec" +version = "0.14.0" +dependencies = [ + "miden-field", + "miden-field-repr", + "miden-note-codec-macros", + "miden-note-codec-wit", + "miden-protocol", + "wit-bindgen", +] + +[[package]] +name = "miden-note-codec-macros" +version = "0.14.0" +dependencies = [ + "miden-note-codec-wit", + "miden-note-schema", + "miden-note-schema-codegen", + "midenc-frontend-wasm-metadata", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-note-codec-wit" +version = "0.14.0" + +[[package]] +name = "miden-note-schema" +version = "0.14.0" +dependencies = [ + "miden-field", + "miden-field-repr", + "miden-mast-package", + "miden-protocol", + "midenc-frontend-wasm-metadata", + "toml", + "wasmparser 0.248.0", + "wit-parser", +] + +[[package]] +name = "miden-note-schema-codegen" +version = "0.14.0" +dependencies = [ + "heck", + "miden-note-schema", + "proc-macro2", + "quote", +] + +[[package]] +name = "miden-package-registry" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8653a8792d81ec1807815e5735de98d9d928f89b7bb5280ffadb848e87eb3ed3" +dependencies = [ + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "proptest", + "pubgrub", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-precompiles" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d01b98147f622a733c9c206db9e78c7e1a353b2309e34e569e2757a435a77e0" +dependencies = [ + "miden-core", + "miden-crypto", +] + +[[package]] +name = "miden-precompiles-prover" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed0d94d331324d6b378f19ee925a6be6e17731e108d43f034c23653048a8ca0" +dependencies = [ + "miden-ace-codegen", + "miden-air", + "miden-core", + "miden-crypto", + "miden-lifted-air", + "miden-lifted-stark", + "miden-precompiles", + "miden-serde-utils", + "ruint", + "serde", + "serde-wincode", + "thiserror", + "tracing", + "wincode", +] + +[[package]] +name = "miden-processor" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b3f6dbcea246715c7c528bbc2fbee46464493c11ac417c12c9562a229b136f" +dependencies = [ + "hashbrown 0.17.1", + "itertools 0.15.0", + "miden-air", + "miden-core", + "miden-debug-types", + "miden-mast-package", + "miden-precompiles", + "miden-utils-diagnostics", + "miden-utils-indexing", + "paste", + "rayon", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-project" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02ac82735cd86ea17fcf8614496ce7e462f3e80dbc1113bf821e8197dcda49d" +dependencies = [ + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "proptest", + "serde", + "serde-untagged", + "thiserror", + "toml", +] + +[[package]] +name = "miden-protocol" +version = "0.16.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e275feebbe9c2458c5877c5f2a03b8e2152831ce956376f015fc4c50acfffb50" +dependencies = [ + "bech32", + "fs-err", + "getrandom 0.4.3", + "miden-assembly", + "miden-assembly-syntax", + "miden-core", + "miden-core-lib", + "miden-crypto", + "miden-crypto-derive", + "miden-mast-package", + "miden-package-registry", + "miden-processor", + "miden-protocol-build-utils", + "miden-utils-sync", + "miden-verifier", + "rand 0.10.2", + "regex", + "semver 1.0.28", + "serde", + "thiserror", + "toml", +] + +[[package]] +name = "miden-protocol-build-utils" +version = "0.16.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1145d0f5cb0efde2484af06587225b2a222fbf4811c4606d0d7f1bd3ff40dc" +dependencies = [ + "fs-err", + "miden-assembly", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "miden-project", + "regex", + "walkdir", +] + +[[package]] +name = "miden-rowan" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13695bf99aabaa21d6572b807c66bb26251aa3d9b75e828b3c99b97a3b1ce7e" +dependencies = [ + "hashbrown 0.17.1", + "rustc-hash", +] + +[[package]] +name = "miden-serde-utils" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa5e67d63441ddaec820a7cf0cabefa8312c15db7a1f2b108ce1b3f589eeeb23" +dependencies = [ + "p3-field", + "p3-goldilocks", + "wincode", +] + +[[package]] +name = "miden-stark-transcript" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ebdd546c7583ba045b20afdde9986e42e7070d1f5fb8841e13f3c921110873" +dependencies = [ + "p3-challenger", + "p3-field", + "serde", + "thiserror", +] + +[[package]] +name = "miden-stateful-hasher" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47d09c4dbfa32918847a4d6426a69d3d527adbf4e01cf1decf714c95b1bf894" +dependencies = [ + "p3-field", + "p3-symmetric", +] + +[[package]] +name = "miden-utils-core-derive" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5764abe966b8c0e7cf377e38de4cbcbbc83a3483f111043c461b7208619bdb96" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "miden-utils-diagnostics" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10561ffd67ee21baca489b96fad11e11579573e1f07f1fc5fb8a111d196fea59" +dependencies = [ + "miden-debug-types", + "miden-miette", + "tracing", +] + +[[package]] +name = "miden-utils-indexing" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c18e4f69d5ebe556a72cc9da474acc53eceb4a75c1247b1f542f20f73145deb" +dependencies = [ + "miden-serde-utils", + "proptest", + "serde", + "thiserror", +] + +[[package]] +name = "miden-utils-sync" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa9c0af7dab7842819c02ef386ed1640884db5c2efa32d30da7d4739548f70e" +dependencies = [ + "lock_api", + "loom", + "once_cell", + "parking_lot", +] + +[[package]] +name = "miden-verifier" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900c0473191ecbe2328e3d6550571f0b1ab42d1417f43c1d98a50cafc3ae4ff1" +dependencies = [ + "miden-air", + "miden-core", + "miden-crypto", + "miden-precompiles", + "miden-precompiles-prover", + "miden-serde-utils", + "serde", + "serde-wincode", + "thiserror", +] + +[[package]] +name = "midenc-frontend-wasm-metadata" +version = "0.14.0" +dependencies = [ + "miden-mast-package", + "serde", + "serde_json", +] + +[[package]] +name = "midenc-hir-type" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e03aa1e30a8eec3e08eba9a1fd17c7c4462f0d49dfbd64cb65193b68ee14cdcc" +dependencies = [ + "miden-formatting", + "miden-serde-utils", + "serde", + "serde_repr", + "smallvec", + "thiserror", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint 0.4.8", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint 0.4.8", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "p3-air" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb1be05c0d6f691afe0c9f468018a9a37cfa904dee78a8081ec96eb3cdd88e8" +dependencies = [ + "p3-field", + "p3-matrix", + "tracing", +] + +[[package]] +name = "p3-blake3" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f202f5fbcceb6f56f783d98efb5de27e5a171470e3364de97b0923b39c87ab5" +dependencies = [ + "blake3", + "p3-symmetric", + "p3-util", +] + +[[package]] +name = "p3-challenger" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5d5e1ecf2c80b09b48ce870e8abd08b643454101c5dc9d0fd71bfbd78224d" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin 0.12.2", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-goldilocks" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b3f31080df31be723b876709246f8f1e532e1c5b82efb5281d705c8304c63" +dependencies = [ + "num-bigint 0.5.1", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "spin 0.12.2", +] + +[[package]] +name = "p3-keccak" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae50c8c37eb847c660298fb275e53c025c49b2623a8cfabf67f5322258b2b4db" +dependencies = [ + "p3-symmetric", + "p3-util", + "tiny-keccak", +] + +[[package]] +name = "p3-matrix" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fddfd435f96394769414cf5590b77058aa506659bf20d6592e9d1989e04440" +dependencies = [ + "rayon", +] + +[[package]] +name = "p3-mds" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ba0ab2cccd89f85a99450224898aff224e323bbf61f777ba6344f0896ef10" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.2", +] + +[[package]] +name = "p3-monty-31" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" +dependencies = [ + "itertools 0.15.0", + "num-bigint 0.5.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "spin 0.12.2", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0d304e9a1f29c0d66534aa84e69528e2118351fdce08dcf5898af4e0fecc32" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.10.2", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb8a73a26d14becaed1c67c3e8a047e4311d7909b402383c82ca9643ba17c6" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.2", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" +dependencies = [ + "itertools 0.15.0", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5466fc40e6df89d3b291a2eff16b33e68e8571207790370137ec18090aadab" +dependencies = [ + "rayon", + "serde", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pest" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "primefield", + "serdect", + "wnaf", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "priority-queue" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" +dependencies = [ + "equivalent", + "indexmap 2.14.0", + "serde", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + +[[package]] +name = "pubgrub" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f5df7e552bc7edd075f5783a87fbfc21d6a546e32c16985679c488c18192d83" +dependencies = [ + "indexmap 2.14.0", + "log", + "priority-queue", + "rustc-hash", + "thiserror", + "version-ranges", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "ruint" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "ark-ff 0.6.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint 0.4.8", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.7", + "rand 0.9.5", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.28", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser 0.7.0", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser 0.10.3", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde-wincode" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa9d3a86c66cf10ce79df36f555a5a4c8d72a82515d9ea8ca420e02c925c30f" +dependencies = [ + "serde", + "thiserror", + "wincode", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "time", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.3", +] + +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest 0.11.3", + "keccak", + "sponge-cursor", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8abadc99fd9c7bbb7d0ca2b31d72a067d0c0dcd7aad25ab8cac71ba91417694b" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-triple" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trybuild" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" +dependencies = [ + "dissimilar", + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e9bd4e9c9ff6a2a9b5969462ba26216af3e010df0377dad8320ab515262ef8" +dependencies = [ + "smallvec", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +dependencies = [ + "leb128fmt", + "wasmparser 0.247.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser 0.247.0", +] + +[[package]] +name = "wasmparser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "semver 1.0.28", +] + +[[package]] +name = "wasmparser" +version = "0.248.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4439c5eee9df71ee0c6efb37f63b1fcb1fec38f85f5142c54e7ed05d33091a" +dependencies = [ + "bitflags 2.13.1", + "semver 1.0.28", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "wincode" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d39d1a984eb7ae37afa348f058216d62a6d5f71640f4113a8114386c2a812a" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.119", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +dependencies = [ + "anyhow", + "bitflags 2.13.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser 0.247.0", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +dependencies = [ + "anyhow", + "hashbrown 0.17.1", + "id-arena", + "indexmap 2.14.0", + "log", + "semver 1.0.28", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.247.0", +] + +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" +dependencies = [ + "curve25519-dalek", + "rand_core 0.10.1", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/dex-note-codec/Cargo.toml b/examples/dex-note-codec/Cargo.toml new file mode 100644 index 0000000000..875476c045 --- /dev/null +++ b/examples/dex-note-codec/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "dex-note-codec" +version = "0.1.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden-note-codec = { path = "../../sdk/note-codec" } diff --git a/examples/dex-note-codec/src/lib.rs b/examples/dex-note-codec/src/lib.rs new file mode 100644 index 0000000000..6deafba9dd --- /dev/null +++ b/examples/dex-note-codec/src/lib.rs @@ -0,0 +1,142 @@ +//! Author-side codecs for the DEX note storage schema. + +use miden_note_codec::AuthorTypeCodec; + +miden_note_codec::from_project!("../dex-note"); + +#[miden_note_codec::note_codec] +impl AuthorTypeCodec for LimitPrice { + fn parse(value: &str) -> Result { + parse_limit_price(value) + } + + fn display(&self) -> String { + display_limit_price(self) + } + + fn validate(&self) -> Result<(), String> { + if self.denominator == 0 { + Err("the limit-price denominator must not be zero".to_owned()) + } else { + Ok(()) + } + } +} + +miden_note_codec::export_codecs!(); + +/// Parses a fraction or finite decimal limit price. +fn parse_limit_price(value: &str) -> Result { + let (numerator, denominator) = if let Some((numerator, denominator)) = value.split_once('/') { + (parse_part(numerator, "numerator")?, parse_part(denominator, "denominator")?) + } else if let Some((whole, fraction)) = value.split_once('.') { + if whole.is_empty() || fraction.is_empty() || fraction.contains('.') { + return Err(format!("invalid decimal limit price `{value}`")); + } + let whole = parse_part(whole, "whole part")?; + let fraction_value = parse_part(fraction, "fractional part")?; + let scale = u32::try_from(fraction.len()) + .map_err(|_| format!("decimal limit price `{value}` has too many digits"))?; + let denominator = 10_u64 + .checked_pow(scale) + .ok_or_else(|| format!("decimal limit price `{value}` has too many digits"))?; + let numerator = whole + .checked_mul(denominator) + .and_then(|whole| whole.checked_add(fraction_value)) + .ok_or_else(|| format!("decimal limit price `{value}` is too large"))?; + (numerator, denominator) + } else { + (parse_part(value, "value")?, 1) + }; + let divisor = greatest_common_divisor(numerator, denominator); + let (numerator, denominator) = if divisor > 1 { + (numerator / divisor, denominator / divisor) + } else { + (numerator, denominator) + }; + Ok(LimitPrice { + numerator, + denominator, + }) +} + +/// Parses one unsigned integer part. +fn parse_part(value: &str, name: &str) -> Result { + if value.is_empty() { + return Err(format!("the limit-price {name} is empty")); + } + value + .parse::() + .map_err(|error| format!("invalid limit-price {name} `{value}`: {error}")) +} + +/// Displays finite fractions as decimals and other fractions as ratios. +fn display_limit_price(value: &LimitPrice) -> String { + if value.denominator == 0 { + // Guard the factor loop against a parsed-but-unvalidated zero denominator: it would + // otherwise never terminate. + return format!("{}/{}", value.numerator, value.denominator); + } + let divisor = greatest_common_divisor(value.numerator, value.denominator); + let numerator = value.numerator / divisor; + let denominator = value.denominator / divisor; + let mut remainder = denominator; + let mut twos = 0_u32; + let mut fives = 0_u32; + while remainder.is_multiple_of(2) { + remainder /= 2; + twos += 1; + } + while remainder.is_multiple_of(5) { + remainder /= 5; + fives += 1; + } + if remainder != 1 { + return format!("{numerator}/{denominator}"); + } + + let scale = twos.max(fives); + let Some(power) = 10_u64.checked_pow(scale) else { + return format!("{numerator}/{denominator}"); + }; + let Some(scaled) = numerator.checked_mul(power / denominator) else { + return format!("{numerator}/{denominator}"); + }; + if scale == 0 { + return scaled.to_string(); + } + let whole = scaled / power; + let mut fraction = format!("{:0width$}", scaled % power, width = scale as usize); + while fraction.ends_with('0') { + fraction.pop(); + } + format!("{whole}.{fraction}") +} + +/// Returns the greatest common divisor of two integers. +fn greatest_common_divisor(mut left: u64, mut right: u64) -> u64 { + while right != 0 { + (left, right) = (right, left % right); + } + left.max(1) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_fraction_and_decimal_forms() { + let fraction = LimitPrice::parse("3/2").unwrap(); + let decimal = LimitPrice::parse("1.5").unwrap(); + assert_eq!(fraction, decimal); + assert_eq!(fraction.display(), "1.5"); + fraction.validate().unwrap(); + } + + #[test] + fn rejects_zero_denominator_during_validation() { + let value = LimitPrice::parse("1/0").unwrap(); + assert!(value.validate().unwrap_err().contains("denominator")); + } +} diff --git a/examples/dex-note/.cargo/config.toml b/examples/dex-note/.cargo/config.toml new file mode 100644 index 0000000000..82112cc9a5 --- /dev/null +++ b/examples/dex-note/.cargo/config.toml @@ -0,0 +1,8 @@ +# This example is intended to be built as Wasm for the Miden VM. + +[build] +target = "wasm32-wasip2" + +[target.wasm32-wasip2] +# Force-enable `cfg(miden)` for Miden-VM-targeted builds (including editor/LSP workflows). +rustflags = ["--cfg", "miden"] diff --git a/examples/dex-note/.gitignore b/examples/dex-note/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/examples/dex-note/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/dex-note/Cargo.lock b/examples/dex-note/Cargo.lock new file mode 100644 index 0000000000..438d3dddfa --- /dev/null +++ b/examples/dex-note/Cargo.lock @@ -0,0 +1,3172 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer", + "crypto-common", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.119", +] + +[[package]] +name = "dex-note" +version = "0.1.0" +dependencies = [ + "miden", + "miden-field-repr", + "miden-sdk-build-script-support", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "ctutils", +] + +[[package]] +name = "dissimilar" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common", + "digest", + "ff", + "group", + "hkdf", + "hybrid-array", + "pkcs8", + "rand_core 0.10.1", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "spin 0.9.9", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fs-err" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", +] + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core 0.10.1", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8" +dependencies = [ + "cpubits", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", + "wnaf", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miden" +version = "0.14.0" +dependencies = [ + "miden-base", + "miden-base-macros", + "miden-base-sys", + "miden-field", + "miden-field-repr", + "miden-sdk-alloc", + "miden-stdlib-sys", + "miden-tx-script-args", + "wit-bindgen", +] + +[[package]] +name = "miden-ace-codegen" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a217e3f1fec32105bca7dc421d85ab39199b78d2e43081fc0fa92411de9a84" +dependencies = [ + "miden-constraint-compiler", + "miden-core", + "miden-crypto", + "thiserror", +] + +[[package]] +name = "miden-air" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b7b3a23756f2bfdba2b37c8d1551b3b531fce0de42e3a9a7f840bb69018106" +dependencies = [ + "miden-ace-codegen", + "miden-core", + "miden-crypto", + "miden-utils-indexing", + "p3-field", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-assembly" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727de4350d9ba263be17d9f93dc4b8d0deebabab29c3bed34f32c4af7b1cf20c" +dependencies = [ + "log", + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "miden-project", + "proptest", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-assembly-syntax" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3874c53f40656a56f74afe8e957d75667218808894ab8f5dbd91e58a8a0c3393" +dependencies = [ + "log", + "miden-assembly-syntax-cst", + "miden-core", + "miden-debug-types", + "miden-utils-diagnostics", + "midenc-hir-type", + "proptest", + "regex", + "rustc_version 0.4.1", + "semver 1.0.28", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-assembly-syntax-cst" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151b657a50aee75a0591dbd66d57774e2f9ec1f00fa17d773820e736bcca0cb6" +dependencies = [ + "miden-debug-types", + "miden-rowan", + "miden-utils-diagnostics", + "thiserror", +] + +[[package]] +name = "miden-base" +version = "0.14.0" +dependencies = [ + "miden-base-sys", + "miden-stdlib-sys", +] + +[[package]] +name = "miden-base-macros" +version = "0.14.0" +dependencies = [ + "heck", + "miden-assembly-syntax", + "miden-debug-types", + "miden-formatting", + "miden-mast-package", + "miden-project", + "miden-protocol", + "midenc-frontend-wasm-metadata", + "proc-macro2", + "quote", + "semver 1.0.28", + "syn 2.0.119", + "toml", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "miden-base-sys" +version = "0.14.0" +dependencies = [ + "miden-field-repr", + "miden-stdlib-sys", +] + +[[package]] +name = "miden-constraint-compiler" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f126188b824423edbbc5eebf9b61c872905c21e3f1e9e9b69d1afbba12288" +dependencies = [ + "miden-core", + "miden-crypto", +] + +[[package]] +name = "miden-core" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55df0c9b5fddcfc2c03c6e476bec0523328fb85090e716d1ab9a4db1d2267c67" +dependencies = [ + "derive_more", + "log", + "miden-crypto", + "miden-debug-types", + "miden-formatting", + "miden-utils-core-derive", + "miden-utils-indexing", + "miden-utils-sync", + "serde", + "thiserror", +] + +[[package]] +name = "miden-core-lib" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f5b4dca8d29c99859e3470ec1fadfa89506e6b349ade7fa475574e3104db85" +dependencies = [ + "env_logger", + "fs-err", + "miden-assembly", + "miden-assembly-syntax", + "miden-core", + "miden-core-lib-codegen", + "miden-crypto", + "miden-mast-package", + "miden-package-registry", + "miden-precompiles", + "miden-processor", + "miden-utils-sync", + "thiserror", +] + +[[package]] +name = "miden-core-lib-codegen" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d926e5e8a288a2a55c42541059d8e52c43c10d373643b309add169a3a0eb914" +dependencies = [ + "miden-core", + "miden-precompiles", +] + +[[package]] +name = "miden-crypto" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de00899e7045ee3bea78d4c4c690d8e2c14fff1f3a1ede1fb2922ed699fdda14" +dependencies = [ + "blake3", + "cc", + "chacha20poly1305", + "curve25519-dalek", + "der", + "ed25519-dalek", + "flume", + "hkdf", + "k256", + "miden-crypto-derive", + "miden-field", + "miden-lifted-stark", + "miden-serde-utils", + "num", + "num-complex", + "once_cell", + "p3-blake3", + "p3-challenger", + "p3-dft", + "p3-goldilocks", + "p3-keccak", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.10.2", + "rand_chacha 0.10.0", + "serde", + "sha2", + "sha3", + "subtle", + "thiserror", + "x25519-dalek", +] + +[[package]] +name = "miden-crypto-derive" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b5a5c8b0fd14982e60ebd010f452b06f693b9efa116054487aff1f2657d2f4" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-debug-types" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793b37eaafbac33a4c8bf8f57d4c1d0ce8b8a7d25698ddfd2e1a2a6552e94f6c" +dependencies = [ + "memchr", + "miden-crypto", + "miden-formatting", + "miden-miette", + "miden-utils-indexing", + "miden-utils-sync", + "paste", + "proptest", + "serde", + "serde_spanned", + "thiserror", + "zerocopy", +] + +[[package]] +name = "miden-field" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41be9f7f5c0ef020bcedf526afe54b3a97244e207a5385e4453ca47799fe2afe" +dependencies = [ + "miden-serde-utils", + "num-bigint 0.5.1", + "p3-challenger", + "p3-field", + "p3-goldilocks", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "subtle", + "thiserror", +] + +[[package]] +name = "miden-field-repr" +version = "0.14.0" +dependencies = [ + "miden-field", + "miden-field-repr-derive", +] + +[[package]] +name = "miden-field-repr-derive" +version = "0.14.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-formatting" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e392e0a8c34b32671012b439de35fa8987bf14f0f8aac279b97f8b8cc6e263b" +dependencies = [ + "unicode-width 0.1.14", +] + +[[package]] +name = "miden-lifted-air" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0ad52f76750f8b9dc8a7c4bed32644628198fab48daf31167c2f750c939378a" +dependencies = [ + "p3-air", + "p3-challenger", + "p3-field", + "p3-matrix", + "p3-util", + "thiserror", +] + +[[package]] +name = "miden-lifted-stark" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212a017744ef2aaca36f89bad2c880949311ec778586b8405a5e47f9e6ca59de" +dependencies = [ + "miden-lifted-air", + "miden-stark-transcript", + "miden-stateful-hasher", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-goldilocks", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.10.2", + "serde", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-mast-package" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26062f2e4cb18e7fa9244f8b18fe2adea2bfe3f016431f75f80b5b98249c34f6" +dependencies = [ + "hashbrown", + "log", + "miden-assembly-syntax", + "miden-core", + "miden-debug-types", + "miden-utils-indexing", + "rustc-hash", + "serde", + "thiserror", + "zerocopy", +] + +[[package]] +name = "miden-miette" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef536978f24a179d94fa2a41e4f92b28e7d8aab14b8d23df28ad2a3d7098b20" +dependencies = [ + "cfg-if", + "futures", + "indenter", + "lazy_static", + "miden-miette-derive", + "owo-colors", + "regex", + "rustc_version 0.2.3", + "rustversion", + "serde_json", + "spin 0.9.9", + "strip-ansi-escapes", + "syn 2.0.119", + "textwrap", + "thiserror", + "trybuild", + "unicode-width 0.1.14", +] + +[[package]] +name = "miden-miette-derive" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86a905f3ea65634dd4d1041a4f0fd0a3e77aa4118341d265af1a94339182222f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miden-package-registry" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8653a8792d81ec1807815e5735de98d9d928f89b7bb5280ffadb848e87eb3ed3" +dependencies = [ + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "proptest", + "pubgrub", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "miden-precompiles" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d01b98147f622a733c9c206db9e78c7e1a353b2309e34e569e2757a435a77e0" +dependencies = [ + "miden-core", + "miden-crypto", +] + +[[package]] +name = "miden-precompiles-prover" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed0d94d331324d6b378f19ee925a6be6e17731e108d43f034c23653048a8ca0" +dependencies = [ + "miden-air", + "miden-core", + "miden-crypto", + "miden-lifted-air", + "miden-lifted-stark", + "miden-precompiles", + "miden-serde-utils", + "ruint", + "serde", + "serde-wincode", + "thiserror", + "tracing", + "wincode", +] + +[[package]] +name = "miden-processor" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b3f6dbcea246715c7c528bbc2fbee46464493c11ac417c12c9562a229b136f" +dependencies = [ + "hashbrown", + "itertools", + "miden-air", + "miden-core", + "miden-debug-types", + "miden-mast-package", + "miden-precompiles", + "miden-utils-diagnostics", + "miden-utils-indexing", + "paste", + "rayon", + "thiserror", + "tracing", +] + +[[package]] +name = "miden-project" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02ac82735cd86ea17fcf8614496ce7e462f3e80dbc1113bf821e8197dcda49d" +dependencies = [ + "miden-assembly-syntax", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "proptest", + "serde", + "serde-untagged", + "thiserror", + "toml", +] + +[[package]] +name = "miden-protocol" +version = "0.16.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e275feebbe9c2458c5877c5f2a03b8e2152831ce956376f015fc4c50acfffb50" +dependencies = [ + "bech32", + "fs-err", + "getrandom 0.4.3", + "miden-assembly", + "miden-assembly-syntax", + "miden-core", + "miden-core-lib", + "miden-crypto", + "miden-crypto-derive", + "miden-mast-package", + "miden-package-registry", + "miden-processor", + "miden-protocol-build-utils", + "miden-utils-sync", + "miden-verifier", + "rand 0.10.2", + "regex", + "semver 1.0.28", + "thiserror", +] + +[[package]] +name = "miden-protocol-build-utils" +version = "0.16.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1145d0f5cb0efde2484af06587225b2a222fbf4811c4606d0d7f1bd3ff40dc" +dependencies = [ + "fs-err", + "miden-assembly", + "miden-core", + "miden-mast-package", + "miden-package-registry", + "miden-project", + "regex", + "walkdir", +] + +[[package]] +name = "miden-rowan" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13695bf99aabaa21d6572b807c66bb26251aa3d9b75e828b3c99b97a3b1ce7e" +dependencies = [ + "hashbrown", + "rustc-hash", +] + +[[package]] +name = "miden-sdk-alloc" +version = "0.14.0" + +[[package]] +name = "miden-sdk-build-script-support" +version = "0.14.0" + +[[package]] +name = "miden-serde-utils" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa5e67d63441ddaec820a7cf0cabefa8312c15db7a1f2b108ce1b3f589eeeb23" +dependencies = [ + "p3-field", + "p3-goldilocks", + "wincode", +] + +[[package]] +name = "miden-stark-transcript" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ebdd546c7583ba045b20afdde9986e42e7070d1f5fb8841e13f3c921110873" +dependencies = [ + "p3-challenger", + "p3-field", + "serde", + "thiserror", +] + +[[package]] +name = "miden-stateful-hasher" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47d09c4dbfa32918847a4d6426a69d3d527adbf4e01cf1decf714c95b1bf894" +dependencies = [ + "p3-field", + "p3-symmetric", +] + +[[package]] +name = "miden-stdlib-sys" +version = "0.14.0" +dependencies = [ + "miden-field", +] + +[[package]] +name = "miden-tx-script-args" +version = "0.14.0" +dependencies = [ + "miden-field", + "miden-field-repr", + "miden-stdlib-sys", +] + +[[package]] +name = "miden-utils-core-derive" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5764abe966b8c0e7cf377e38de4cbcbbc83a3483f111043c461b7208619bdb96" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "miden-utils-diagnostics" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10561ffd67ee21baca489b96fad11e11579573e1f07f1fc5fb8a111d196fea59" +dependencies = [ + "miden-debug-types", + "miden-miette", + "tracing", +] + +[[package]] +name = "miden-utils-indexing" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c18e4f69d5ebe556a72cc9da474acc53eceb4a75c1247b1f542f20f73145deb" +dependencies = [ + "miden-serde-utils", + "proptest", + "serde", + "thiserror", +] + +[[package]] +name = "miden-utils-sync" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa9c0af7dab7842819c02ef386ed1640884db5c2efa32d30da7d4739548f70e" +dependencies = [ + "lock_api", + "loom", + "once_cell", + "parking_lot", +] + +[[package]] +name = "miden-verifier" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900c0473191ecbe2328e3d6550571f0b1ab42d1417f43c1d98a50cafc3ae4ff1" +dependencies = [ + "miden-air", + "miden-core", + "miden-crypto", + "miden-precompiles", + "miden-precompiles-prover", + "miden-serde-utils", + "serde", + "serde-wincode", + "thiserror", +] + +[[package]] +name = "midenc-frontend-wasm-metadata" +version = "0.14.0" +dependencies = [ + "miden-mast-package", + "serde", + "serde_json", +] + +[[package]] +name = "midenc-hir-type" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e03aa1e30a8eec3e08eba9a1fd17c7c4462f0d49dfbd64cb65193b68ee14cdcc" +dependencies = [ + "miden-formatting", + "miden-serde-utils", + "serde", + "serde_repr", + "smallvec", + "thiserror", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint 0.4.8", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint 0.4.8", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "p3-air" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb1be05c0d6f691afe0c9f468018a9a37cfa904dee78a8081ec96eb3cdd88e8" +dependencies = [ + "p3-field", + "p3-matrix", + "tracing", +] + +[[package]] +name = "p3-blake3" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f202f5fbcceb6f56f783d98efb5de27e5a171470e3364de97b0923b39c87ab5" +dependencies = [ + "blake3", + "p3-symmetric", + "p3-util", +] + +[[package]] +name = "p3-challenger" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d5d5e1ecf2c80b09b48ce870e8abd08b643454101c5dc9d0fd71bfbd78224d" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-monty-31", + "p3-symmetric", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-dft" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321a952da2721ecd85ca593ea189798dfb4e439a2cc1378ce1442091880f173" +dependencies = [ + "itertools", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "spin 0.12.2", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53db75d38e04fc255826f388eca9d05976733dc9754aa3db411bc9ea1a37c1a0" +dependencies = [ + "itertools", + "num-bigint 0.5.1", + "p3-maybe-rayon", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-goldilocks" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b3f31080df31be723b876709246f8f1e532e1c5b82efb5281d705c8304c63" +dependencies = [ + "num-bigint 0.5.1", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "spin 0.12.2", +] + +[[package]] +name = "p3-keccak" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae50c8c37eb847c660298fb275e53c025c49b2623a8cfabf67f5322258b2b4db" +dependencies = [ + "p3-symmetric", + "p3-util", + "tiny-keccak", +] + +[[package]] +name = "p3-matrix" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473eb920c446a6f4536e0d3528fbdca2a23c0e24e1d0d7767452e6d385dd335c" +dependencies = [ + "itertools", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.10.2", + "serde", + "tracing", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fddfd435f96394769414cf5590b77058aa506659bf20d6592e9d1989e04440" + +[[package]] +name = "p3-mds" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551ba0ab2cccd89f85a99450224898aff224e323bbf61f777ba6344f0896ef10" +dependencies = [ + "p3-dft", + "p3-field", + "p3-symmetric", + "p3-util", + "rand 0.10.2", +] + +[[package]] +name = "p3-monty-31" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f635f7340cd0868b17e43e0c98fefdafdaed90469d0725caf6d8372a2a47c" +dependencies = [ + "itertools", + "num-bigint 0.5.1", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon1", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "paste", + "rand 0.10.2", + "serde", + "spin 0.12.2", + "tracing", +] + +[[package]] +name = "p3-poseidon1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d0d304e9a1f29c0d66534aa84e69528e2118351fdce08dcf5898af4e0fecc32" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.10.2", +] + +[[package]] +name = "p3-poseidon2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb8a73a26d14becaed1c67c3e8a047e4311d7909b402383c82ca9643ba17c6" +dependencies = [ + "p3-field", + "p3-mds", + "p3-symmetric", + "p3-util", + "rand 0.10.2", +] + +[[package]] +name = "p3-symmetric" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015ea80cad969b6aabf27a04884286fe1354393b166d968ee0d80a95126b2a4" +dependencies = [ + "itertools", + "p3-field", + "p3-util", + "serde", +] + +[[package]] +name = "p3-util" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5466fc40e6df89d3b291a2eff16b33e68e8571207790370137ec18090aadab" +dependencies = [ + "serde", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "primefield", + "serdect", + "wnaf", +] + +[[package]] +name = "priority-queue" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" +dependencies = [ + "equivalent", + "indexmap", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags 2.13.1", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + +[[package]] +name = "pubgrub" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f5df7e552bc7edd075f5783a87fbfc21d6a546e32c16985679c488c18192d83" +dependencies = [ + "indexmap", + "log", + "priority-queue", + "rustc-hash", + "thiserror", + "version-ranges", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac", +] + +[[package]] +name = "ruint" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970" +dependencies = [ + "ruint-macro", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.28", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde-wincode" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa9d3a86c66cf10ce79df36f555a5a4c8d72a82515d9ea8ca420e02c925c30f" +dependencies = [ + "serde", + "thiserror", + "wincode", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest", + "keccak", + "sponge-cursor", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest", + "rand_core 0.10.1", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8abadc99fd9c7bbb7d0ca2b31d72a067d0c0dcd7aad25ab8cac71ba91417694b" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-triple" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trybuild" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" +dependencies = [ + "dissimilar", + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common", + "ctutils", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e9bd4e9c9ff6a2a9b5969462ba26216af3e010df0377dad8320ab515262ef8" +dependencies = [ + "smallvec", +] + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +dependencies = [ + "bitflags 2.13.1", + "hashbrown", + "indexmap", + "semver 1.0.28", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "wincode" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d39d1a984eb7ae37afa348f058216d62a6d5f71640f4113a8114386c2a812a" +dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.119", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" +dependencies = [ + "anyhow", + "bitflags 2.13.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.247.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver 1.0.28", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + +[[package]] +name = "x25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" +dependencies = [ + "curve25519-dalek", + "rand_core 0.10.1", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/dex-note/Cargo.toml b/examples/dex-note/Cargo.toml new file mode 100644 index 0000000000..e3f9cc6ab3 --- /dev/null +++ b/examples/dex-note/Cargo.toml @@ -0,0 +1,22 @@ +cargo-features = ["trim-paths"] + +[package] +name = "dex-note" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden = { path = "../../sdk/sdk" } +miden-field-repr = { path = "../../sdk/field-repr/repr" } + +[build-dependencies] +miden-sdk-build-script-support = { path = "../../sdk/build-script-support" } + +[profile.release] +trim-paths = ["diagnostics", "object"] + +[profile.dev] +trim-paths = ["diagnostics", "object"] diff --git a/examples/dex-note/build.rs b/examples/dex-note/build.rs new file mode 100644 index 0000000000..1db40a34c2 --- /dev/null +++ b/examples/dex-note/build.rs @@ -0,0 +1,3 @@ +fn main() { + miden_sdk_build_script_support::prepare_package_cache(); +} diff --git a/examples/dex-note/cargo-generate.toml b/examples/dex-note/cargo-generate.toml new file mode 100644 index 0000000000..26029f3e76 --- /dev/null +++ b/examples/dex-note/cargo-generate.toml @@ -0,0 +1,2 @@ +[template] +ignore = ["target"] diff --git a/examples/dex-note/miden-project.toml b/examples/dex-note/miden-project.toml new file mode 100644 index 0000000000..c391a6b534 --- /dev/null +++ b/examples/dex-note/miden-project.toml @@ -0,0 +1,17 @@ +[package] +name = "dex-note" +version = "0.1.0" + +[lib] +kind = "note" +namespace = "miden:dex-note/miden-dex-note@0.1.0" +path = "src/lib.rs" + +[dependencies] +miden-core = "*" +miden-protocol = "*" +basic-wallet = { path = "../basic-wallet" } + +# The `midenc` namespace table groups compiler plugin configuration. +[package.metadata.midenc.note-codec] +crate = "../dex-note-codec" diff --git a/examples/dex-note/rust-toolchain.toml b/examples/dex-note/rust-toolchain.toml new file mode 100644 index 0000000000..9148f62f16 --- /dev/null +++ b/examples/dex-note/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "nightly-2026-09-01" +components = ["rustfmt", "rust-src", "clippy"] +targets = ["wasm32-wasip2"] +profile = "minimal" diff --git a/examples/dex-note/src/lib.rs b/examples/dex-note/src/lib.rs new file mode 100644 index 0000000000..8456e726d2 --- /dev/null +++ b/examples/dex-note/src/lib.rs @@ -0,0 +1,45 @@ +//! DEX note with a custom limit-price storage type. + +#![no_std] +#![feature(alloc_error_handler)] + +use miden::{AccountId, Word, account, active_note, export_type, note}; +use miden_field_repr::{FromFeltRepr, ToFeltRepr}; + +/// A rational limit price. +#[export_type] +#[derive(FromFeltRepr, ToFeltRepr)] +pub struct LimitPrice { + /// The price numerator. + pub numerator: u64, + /// The price denominator. + pub denominator: u64, +} + +/// Storage for one DEX note. +#[note] +struct DexNote { + /// The account that can consume this note. + target: AccountId, + /// The exchange limit price. + price: LimitPrice, +} + +/// Native account interface used by the note script. +#[account(basic_wallet::BasicWallet)] +pub struct Wallet; + +#[note] +impl DexNote { + /// Sends every note asset to the target account. + #[note_script] + pub fn script(self, _arg: Word, account: &mut Wallet) { + assert_eq!(account.get_id(), self.target); + let _limit_price = self.price; + + let assets = active_note::get_initial_assets(); + for asset in assets { + account.receive_asset(asset); + } + } +} diff --git a/examples/fibonacci/Cargo.lock b/examples/fibonacci/Cargo.lock index 430fec63c6..75a9b2d1f0 100644 --- a/examples/fibonacci/Cargo.lock +++ b/examples/fibonacci/Cargo.lock @@ -11,4 +11,4 @@ dependencies = [ [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" diff --git a/examples/is-prime/Cargo.lock b/examples/is-prime/Cargo.lock index bcb4d8e62f..67592bf65c 100644 --- a/examples/is-prime/Cargo.lock +++ b/examples/is-prime/Cargo.lock @@ -11,4 +11,4 @@ dependencies = [ [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" diff --git a/examples/p2id-note/Cargo.lock b/examples/p2id-note/Cargo.lock index 0d3c4ef203..b4b60a3a81 100644 --- a/examples/p2id-note/Cargo.lock +++ b/examples/p2id-note/Cargo.lock @@ -943,7 +943,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1064,7 +1064,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1476,11 +1476,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1517,14 +1517,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1596,7 +1596,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/p2id-tx-script/Cargo.lock b/examples/p2id-tx-script/Cargo.lock index b86fffb4bb..2279796154 100644 --- a/examples/p2id-tx-script/Cargo.lock +++ b/examples/p2id-tx-script/Cargo.lock @@ -933,7 +933,7 @@ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1025,7 +1025,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1033,7 +1033,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1054,7 +1054,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1211,7 +1211,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1219,7 +1219,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1466,11 +1466,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1507,14 +1507,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1586,7 +1586,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/p2ide-note/Cargo.lock b/examples/p2ide-note/Cargo.lock index 91c3b9cfcb..4afbc389d5 100644 --- a/examples/p2ide-note/Cargo.lock +++ b/examples/p2ide-note/Cargo.lock @@ -943,7 +943,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1064,7 +1064,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1476,11 +1476,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1517,14 +1517,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1596,7 +1596,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/examples/storage-example/Cargo.lock b/examples/storage-example/Cargo.lock index d8e4262ae2..c6e8f74861 100644 --- a/examples/storage-example/Cargo.lock +++ b/examples/storage-example/Cargo.lock @@ -943,7 +943,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1064,7 +1064,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1476,11 +1476,11 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-sdk-build-script-support" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1517,14 +1517,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1596,7 +1596,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md b/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md index a3efcea92e..ae4797d53f 100644 --- a/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md +++ b/extra/templates/project/.claude/skills/rust-sdk-patterns/SKILL.md @@ -139,7 +139,7 @@ A note script reads from `active_note::*` and forwards work to a public account- The `#[note]` macro generates `TryFrom<&[Felt]>` for the note struct, so the note's serialized storage is deserialized into typed fields before the script runs. The `#[note_script]` method receives the deserialized note as `self` (by value) and never indexes a raw Felt slice manually. Alongside the required `Word` arg, the method may optionally accept a `&Account` or `&mut Account` parameter. See [compiler/sdk/base-macros/src/lib.rs](https://github.com/0xMiden/compiler/blob/main/sdk/base-macros/src/lib.rs) for the macro contract and [compiler/sdk/base-macros/src/note.rs](https://github.com/0xMiden/compiler/blob/main/sdk/base-macros/src/note.rs) for the generated deserialization (each named field is read via `::from_felt_repr(...)` and EOF is asserted at the end). -Supported field types include `Felt`, the unsigned integer scalars (`u64`, `u32`, `u8`), `bool`, `Option`, and `Vec` via the `FromFeltRepr` trait (`compiler/sdk/field-repr/repr/src/lib.rs`), plus any user type that opts in with `#[derive(FromFeltRepr)]` (this is how `AccountId` supports the macro - see `compiler/sdk/base-sys/src/bindings/types.rs`). Do **not** use `Asset` or `Word` directly as note struct fields; those types do not currently derive `FromFeltRepr`. If you need asset-shaped data inside the note, flatten it into supported scalar fields and reconstruct inside the script, or keep it on the side as a separate `active_note::get_assets()` read. +The supported field types are `Felt`, `Word`, the SDK core-type records that implement the felt-representation traits (`AccountId`, `Asset`, `Recipient`, `Tag`, `NoteType` - see `compiler/sdk/base-sys/src/bindings/types.rs`), the unsigned integer scalars (`u64`, `u32`, `u8`), `bool`, `Option` over any of those, and your own records or enums that are marked `#[export_type]` and derive both felt-representation traits with `#[derive(FromFeltRepr, ToFeltRepr)]`. A custom field type must be declared **before** the `#[note]` struct; otherwise the macro reports that the type "needs #[export_type] on its definition before the #[note] struct". See [compiler/examples/dex-note/src/lib.rs](https://github.com/0xMiden/compiler/blob/main/examples/dex-note/src/lib.rs) for a custom `LimitPrice` field type in the required form. `Vec` is not supported: the note storage schema needs a stable, named position for each stored value, so a dynamic vector is a hard error. Two more rules follow from the schema: a `#[note]` struct must have named fields or be a unit struct (tuple structs are rejected), and a crate may contain only one `#[note]` struct (move each extra note struct into its own crate). For Cargo.toml wiring (cross-component dependencies + bindings import), see "Cross-Component Dependencies" above. See [increment-note/src/lib.rs](../../../contracts/increment-note/src/lib.rs) for the project-template's local example of the `#[note] struct + #[note] impl` macro form. diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index 9957433030..99cdaf7498 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -79,3 +79,28 @@ pub(crate) fn supported_features() -> WasmFeatures { pub(crate) fn supported_component_model_features() -> WasmFeatures { supported_features() | WasmFeatures::COMPONENT_MODEL } + +#[cfg(test)] +mod tests { + use midenc_frontend_wasm_metadata::WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME; + + use super::*; + + /// A `#[note]` crate compiled as a raw core module keeps its schema section: the + /// translation wraps the module as a component and propagates the captured sections. + #[test] + fn core_module_propagates_note_storage_schema_section() { + let wat = format!( + r#"(module (@custom "{WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME}" "schema"))"# + ); + let wasm = wat::parse_str(wat).expect("core module WAT must parse"); + let context = Rc::new(Context::default()); + let output = translate(&wasm, &WasmTranslationConfig::default(), context) + .expect("a core module with a schema section must translate"); + assert_eq!( + output.sections.note_storage_schema.as_deref(), + Some(b"schema".as_slice()), + "the schema section must be propagated into the frontend output" + ); + } +} diff --git a/frontend/wasm/src/module/module_env.rs b/frontend/wasm/src/module/module_env.rs index ad9feb054a..496bfeb8bd 100644 --- a/frontend/wasm/src/module/module_env.rs +++ b/frontend/wasm/src/module/module_env.rs @@ -6,7 +6,8 @@ use cranelift_entity::{PrimaryMap, packed_option::ReservedValue}; use midenc_frontend_wasm_metadata::{ FrontendMetadata, PackageSections, WASM_ACCOUNT_COMPONENT_METADATA_CUSTOM_SECTION_NAME, WASM_COMPONENT_WIT_CUSTOM_SECTION_NAME, WASM_FRONTEND_METADATA_CUSTOM_SECTION_NAME, - count_top_level_wit_packages, decode_section, + WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME, count_top_level_wit_packages, decode_section, + trim_trailing_nuls, }; use midenc_hir::{FxHashMap, FxHashSet, Ident, interner::Symbol}; use midenc_session::diagnostics::{DiagnosticsHandler, IntoDiagnostic, Report, Severity}; @@ -96,6 +97,8 @@ pub struct ParsedModule<'data> { pub account_component_metadata_bytes: Option<&'data [u8]>, /// The component's public WIT source emitted by the `#[component]` macro. pub component_wit_bytes: Option<&'data [u8]>, + /// The note storage schema emitted by the `#[note]` macro. + pub note_storage_schema_bytes: Option<&'data [u8]>, /// Frontend-only component metadata entries emitted by SDK macros (empty when none present). pub component_frontend_metadata: Vec, } @@ -149,6 +152,7 @@ pub(crate) fn collect_package_sections<'a, 'data: 'a>( ) -> WasmResult { let mut account_component_metadata = None; let mut component_wit = None; + let mut note_storage_schema = None; for module in modules { merge_section_payload( &mut account_component_metadata, @@ -162,10 +166,17 @@ pub(crate) fn collect_package_sections<'a, 'data: 'a>( WASM_COMPONENT_WIT_CUSTOM_SECTION_NAME, diagnostics, )?; + merge_section_payload( + &mut note_storage_schema, + module.note_storage_schema_bytes, + WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME, + diagnostics, + )?; } Ok(PackageSections { account_component_metadata: account_component_metadata.map(<[u8]>::to_vec), component_wit: component_wit.map(<[u8]>::to_vec), + note_storage_schema: note_storage_schema.map(<[u8]>::to_vec), }) } @@ -451,6 +462,28 @@ impl<'a, 'data> ModuleEnvironment<'a, 'data> { .into_report()); } } + Payload::CustomSection(s) + if s.name() == WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME => + { + core::str::from_utf8(trim_trailing_nuls(s.data())).map_err(|err| { + diagnostics + .diagnostic(Severity::Error) + .with_message(format!( + "failed to parse note storage schema section as UTF-8: {err}" + )) + .into_report() + })?; + if self.result.note_storage_schema_bytes.replace(s.data()).is_some() { + return Err(diagnostics + .diagnostic(Severity::Error) + .with_message(format!( + "wasm error: multiple \ + '{WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME}' custom sections \ + were found; only one is allowed per core Wasm module" + )) + .into_report()); + } + } Payload::CustomSection(s) if s.name() == WASM_FRONTEND_METADATA_CUSTOM_SECTION_NAME => { let metadata = decode_section(s.data()).map_err(|err| { diagnostics diff --git a/hir-macros/src/operation.rs b/hir-macros/src/operation.rs index 6376833969..90e8ec2014 100644 --- a/hir-macros/src/operation.rs +++ b/hir-macros/src/operation.rs @@ -376,7 +376,7 @@ impl OpDefinition { }; let symbol = Symbol { name: field_name.clone(), - ty: SymbolType::Concrete(field_ty), + ty: SymbolType::Concrete(Box::new(field_ty)), }; create_params.push(OpCreateParam { param_ty: OpCreateParamType::Symbol(symbol.clone()), @@ -2955,7 +2955,7 @@ pub enum SymbolType { /// Any `Symbol + CallableOpInterface` implementation can be used Callable, /// Only the specific concrete type can be used, it must implement `Op` and `Symbol` traits - Concrete(syn::Type), + Concrete(Box), /// Any implementation of the provided trait can be used. /// /// The given trait type _must_ have `Symbol` as a supertrait. diff --git a/midenc-compile/Cargo.toml b/midenc-compile/Cargo.toml index 95c03398ff..60f8a6e91c 100644 --- a/midenc-compile/Cargo.toml +++ b/midenc-compile/Cargo.toml @@ -25,8 +25,13 @@ std = [ "midenc-session/std", "dep:cargo_metadata", "dep:clap", + "dep:miden-note-codec-wit", + "dep:miden-note-schema", + "dep:sha2", "dep:toml_edit", "dep:wat", + "dep:wit-component", + "dep:wit-parser", ] [dependencies] @@ -37,6 +42,8 @@ midenc-codegen-masm.workspace = true miden-assembly.workspace = true miden-assembly-syntax.workspace = true miden-mast-package.workspace = true +miden-note-codec-wit = { workspace = true, optional = true } +miden-note-schema = { workspace = true, optional = true } miden-package-registry.workspace = true midenc-frontend-wasm.workspace = true midenc-frontend-wasm-metadata.workspace = true @@ -46,6 +53,12 @@ midenc-dialect-hir.workspace = true midenc-hir.workspace = true midenc-hir-transform.workspace = true midenc-session.workspace = true +sha2 = { workspace = true, optional = true } toml_edit = { workspace = true, optional = true, features = ["parse", "display"] } thiserror.workspace = true wat = { workspace = true, optional = true } +wit-component = { workspace = true, optional = true } +wit-parser = { workspace = true, optional = true } + +[dev-dependencies] +tempfile.workspace = true diff --git a/midenc-compile/src/cargo.rs b/midenc-compile/src/cargo.rs index 0cf4543221..e4f2d06118 100644 --- a/midenc-compile/src/cargo.rs +++ b/midenc-compile/src/cargo.rs @@ -1,19 +1,58 @@ -use core::str::FromStr; +use core::{fmt::Write as _, str::FromStr}; use std::{ boxed::Box, + env, fs, path::{Path, PathBuf}, + process::{Command, Stdio}, rc::Rc, string::{String, ToString}, sync::Arc, + time::Duration, vec::Vec, }; -use miden_assembly::SourceManager; +use miden_assembly::{SourceManager, serde::Serializable}; +use miden_mast_package::Package as MastPackage; +use miden_note_codec_wit::NOTE_CODEC_WIT; +use midenc_frontend_wasm_metadata::{NESTED_CARGO_SCRUB_ENV, package_cache}; use midenc_hir::Report; use midenc_session::{InputFile, LinkLibrary, Session, miden_project}; +use sha2::{Digest, Sha256}; +use wit_component::DecodedWasm; +use wit_parser::{ + Function, FunctionKind, Resolve, Type, TypeDefKind, TypeId, WorldId, WorldItem, WorldKey, +}; use crate::{CodegenOutput, CompilerResult}; +/// Defines the metadata namespace for compiler plugin configuration. +const NOTE_CODEC_NAMESPACE: &str = "midenc"; + +/// Names the metadata table that configures the author-side note codec. +const NOTE_CODEC_TABLE: &str = "note-codec"; + +/// Defines the dotted metadata table name for the author-side note codec. +/// +/// The same name is the work directory the codec build creates under the codec crate, so the +/// build artifacts sit next to the setting that asked for them. +pub(crate) const NOTE_CODEC_TABLE_NAME: &str = "midenc.note-codec"; + +/// Names the metadata key that contains the codec crate directory. +const NOTE_CODEC_CRATE_KEY: &str = "crate"; + +/// Rust target used to build note codec components; rustc links the cdylib as a +/// Wasm component, so no separate encoding step is necessary. +const NOTE_CODEC_TARGET: &str = "wasm32-wasip2"; + +/// Maximum age of an unused staged note package. +const NOTE_PACKAGE_CACHE_MAX_AGE: Duration = Duration::from_secs(7 * 24 * 60 * 60); + +/// One immutable staged package. +struct StagedNotePackage { + /// Content-addressed directory exposed to codec macro expansion as its package cache. + cache_dir: PathBuf, +} + /// Cargo-specific options extracted from the `Compiler` struct. /// /// These options are recognized by `cargo miden build` and forwarded to the underlying @@ -28,6 +67,10 @@ pub struct CargoOptions { pub workspace: bool, /// Packages to build pub packages: Vec, + /// Require Cargo.lock to remain unchanged. + pub locked: bool, + /// Prevent Cargo from accessing the network. + pub offline: bool, } /// Represents a cargo package specifier. @@ -99,6 +142,8 @@ impl CargoOptions { manifest_path: options.manifest_path.clone(), workspace: options.workspace, packages, + locked: options.cargo_locked, + offline: options.cargo_offline, }) } } @@ -215,6 +260,8 @@ pub(crate) fn cargo_build( diagnostics: options.diagnostics, remap_path_prefixes: options.remap_path_prefixes.clone(), rustflags: options.rustflags.clone(), + cargo_locked: options.cargo_locked, + cargo_offline: options.cargo_offline, link_libraries: vec![LinkLibrary::core()], ..midenc_session::Options::new( Some(package_name.clone()), @@ -249,24 +296,6 @@ pub(crate) fn cargo_build( filesystem_cache_dir, context, ) - // We expect dependencies to *always* produce packages (.masp) - /* - let CodegenOutput { - component, - account_component_metadata_bytes, - } = crate::pipeline::frontends::rust::compile_manifest(&manifest_path, None, context.clone())? - else { - panic!( - "expected cargo build of {package_name} to produce component, but got HIR output \ - instead", - ); - }; - - Ok(CodegenOutput { - component, - account_component_metadata_bytes, - }) - */ //component.source_inputs(target, context.session()) @@ -311,6 +340,597 @@ pub fn write_package_atomic( }) } +/// Returns true when project metadata declares an author-side note codec crate. +pub(crate) fn has_project_note_codec(metadata: &miden_project::MetadataSet) -> bool { + metadata + .get(NOTE_CODEC_NAMESPACE) + .is_some_and(|namespace| namespace.get(NOTE_CODEC_TABLE).is_some()) +} + +/// Builds the optional note codec component declared by a project package. +pub(crate) fn build_project_note_codec( + project_package: &miden_project::Package, + project_manifest_path: &Path, + note_project_dir: &Path, + note_package: &MastPackage, +) -> CompilerResult>> { + let Some(codec_crate_dir) = + note_codec_crate_dir(project_package.metadata(), project_manifest_path, note_project_dir)? + else { + return Ok(None); + }; + + build_note_codec_component(&codec_crate_dir, note_package).map(Some) +} + +/// Reads the optional codec crate path from Miden project metadata. +fn note_codec_crate_dir( + metadata: &miden_project::MetadataSet, + project_manifest_path: &Path, + project_dir: &Path, +) -> CompilerResult> { + let Some(namespace) = metadata.get(NOTE_CODEC_NAMESPACE) else { + return Ok(None); + }; + let Some(value) = namespace.get(NOTE_CODEC_TABLE) else { + return Ok(None); + }; + let table = value.inner().as_table().ok_or_else(|| { + Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}]` in '{}' must be a table, but it is {}", + project_manifest_path.display(), + value.inner().type_str() + )) + })?; + for key in table.keys() { + if key != NOTE_CODEC_CRATE_KEY { + return Err(Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}]` in '{}' has unknown key '{key}'; \ + the table accepts only '{NOTE_CODEC_CRATE_KEY}'", + project_manifest_path.display() + ))); + } + } + let path = table.get(NOTE_CODEC_CRATE_KEY).ok_or_else(|| { + Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}]` in '{}' must define a string \ + `{NOTE_CODEC_CRATE_KEY}`", + project_manifest_path.display() + )) + })?; + let path = path.as_str().ok_or_else(|| { + Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}].{NOTE_CODEC_CRATE_KEY}` in '{}' must be \ + a string", + project_manifest_path.display() + )) + })?; + if path.is_empty() { + return Err(Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}].{NOTE_CODEC_CRATE_KEY}` in '{}' must not \ + be empty", + project_manifest_path.display() + ))); + } + + let codec_crate_dir = project_dir.join(path); + let codec_crate_dir = codec_crate_dir.canonicalize().map_err(|error| { + Report::msg(format!( + "note codec crate '{}' does not exist; update \ + `[package.metadata.{NOTE_CODEC_TABLE_NAME}].{NOTE_CODEC_CRATE_KEY}` in '{}': {error}", + codec_crate_dir.display(), + project_manifest_path.display() + )) + })?; + if !codec_crate_dir.is_dir() { + return Err(Report::msg(format!( + "note codec crate path '{}' is not a directory; update \ + `[package.metadata.{NOTE_CODEC_TABLE_NAME}].{NOTE_CODEC_CRATE_KEY}` in '{}'", + codec_crate_dir.display(), + project_manifest_path.display() + ))); + } + Ok(Some(codec_crate_dir)) +} + +/// Builds and componentizes one author-side note codec crate. +/// +/// The codec crate contains the work directory at `/target/midenc.note-codec`. The +/// build does not use the session target directory. It never shares a build lock with the outer +/// build. This layout matches the VM event handler plugin. That plugin uses `/target/midenc.event-handlers`. +fn build_note_codec_component( + codec_crate_dir: &Path, + note_package: &MastPackage, +) -> CompilerResult> { + // Check the manifest before staging, or a mis-pointed `crate` path creates a work directory + // in a directory that is not a crate. + let manifest_path = codec_crate_dir.join("Cargo.toml"); + if !manifest_path.is_file() { + return Err(Report::msg(format!( + "note codec crate has no manifest at '{}'", + manifest_path.display() + ))); + } + let work_dir = codec_crate_dir.join("target").join(NOTE_CODEC_TABLE_NAME); + let staged_package = stage_note_package(&work_dir, codec_crate_dir, note_package)?; + + let cargo_env = env::var_os("CARGO").map(PathBuf::from); + let cargo_path = cargo_env.as_deref().unwrap_or_else(|| Path::new("cargo")); + let toolchain = if cargo_env.is_none() { + crate::rust::rustup_toolchain() + } else { + None + }; + crate::rust::install_wasm32_target("wasip2", toolchain.as_deref(), false)?; + + // Give nested Cargo a separate target directory. Sharing the outer lock can deadlock. + let cargo_target_dir = work_dir.join("cargo-target"); + let mut cargo = note_codec_cargo_command( + cargo_path, + toolchain.as_deref(), + codec_crate_dir, + &manifest_path, + &cargo_target_dir, + &staged_package.cache_dir, + ); + cargo.stdout(Stdio::piped()).stderr(Stdio::inherit()); + + let manifest_path = manifest_path.canonicalize().map_err(|error| { + Report::msg(format!( + "failed to resolve note codec manifest '{}': {error}", + manifest_path.display() + )) + })?; + let artifacts = crate::rust::run_cargo(cargo, cargo_path) + .map_err(|error| note_codec_cargo_error(error, &manifest_path))?; + let mut wasm_paths = artifacts + .into_iter() + .filter(|artifact| { + artifact.manifest_path.as_std_path() == manifest_path + && artifact.target.crate_types.contains(&cargo_metadata::CrateType::CDyLib) + }) + .flat_map(|artifact| artifact.filenames) + .filter(|path| path.extension() == Some("wasm")) + .map(|path| path.into_std_path_buf()) + .collect::>(); + wasm_paths.sort(); + wasm_paths.dedup(); + if wasm_paths.len() != 1 { + return Err(Report::msg(format!( + "note codec build for '{}' must produce exactly one `{NOTE_CODEC_TARGET}` cdylib; set \ + `[lib] crate-type = [\"cdylib\"]` in '{}', found: {wasm_paths:#?}", + codec_crate_dir.display(), + manifest_path.display() + ))); + } + let wasm_path = wasm_paths.pop().expect("one codec artifact was checked above"); + + // The wasm32-wasip2 target links through wasm-component-ld, so the produced + // cdylib artifact is already a Wasm component. + let component = fs::read(&wasm_path).map_err(|error| { + Report::msg(format!( + "note codec build produced an unreadable component '{}': {error}", + wasm_path.display() + )) + })?; + validate_note_codec_component(&component).map_err(|error| { + Report::msg(format!( + "note codec crate '{}' produced an invalid component: {error}", + codec_crate_dir.display() + )) + })?; + gc_staged_note_packages(&staged_package.cache_dir); + Ok(component) +} + +/// Builds the nested Cargo command that compiles one note codec crate. +/// +/// The command pins `RUSTFLAGS`. That environment variable replaces the rustflags in the codec +/// crate's own cargo config, so the crate cannot enable a Wasm feature that consumers reject. +/// Replacing them is safe only because the command always passes `--target`: with a target +/// selected, Cargo applies these flags to the codec crate alone, and host build scripts and +/// proc macros keep the flags of the host they build for. +fn note_codec_cargo_command( + cargo_path: &Path, + toolchain: Option<&str>, + codec_crate_dir: &Path, + manifest_path: &Path, + cargo_target_dir: &Path, + package_cache_dir: &Path, +) -> Command { + let mut cargo = Command::new(cargo_path); + if let Some(toolchain) = toolchain { + cargo.arg(format!("+{toolchain}")); + } + cargo + .current_dir(codec_crate_dir) + .arg("build") + .arg("--manifest-path") + .arg(manifest_path) + .arg("--lib") + // The codec is a host-side wasmtime artifact; a dev-profile cdylib carries debug + // info far past the consumer size limit, so every Miden profile builds it release. + .arg("--release") + .arg("--target") + .arg(NOTE_CODEC_TARGET) + .arg("--target-dir") + .arg(cargo_target_dir) + .arg("--message-format") + .arg("json-render-diagnostics") + // Let `from_project!` find the staged note package during codec macro expansion. + .env(package_cache::PACKAGE_CACHE_ENV, package_cache_dir); + for &variable in NESTED_CARGO_SCRUB_ENV { + cargo.env_remove(variable); + } + // Pin the guest rustflags after the scrub. A codec crate cannot enable a Wasm feature + // that every consumer rejects, whatever its own cargo config says. + cargo.env("RUSTFLAGS", miden_note_schema::NOTE_CODEC_GUEST_RUSTFLAGS); + cargo +} + +/// Stages the current package in a content-addressed package-cache directory. +fn stage_note_package( + work_dir: &Path, + codec_crate_dir: &Path, + note_package: &MastPackage, +) -> CompilerResult { + let package_bytes = note_package.to_bytes(); + let mut hasher = Sha256::new(); + hasher.update(&package_bytes); + // Separate the variable-length package bytes from the codec-crate path in the build key. + hasher.update([0]); + hasher.update(codec_crate_dir.as_os_str().to_string_lossy().as_bytes()); + let mut build_key = String::with_capacity(64); + for byte in hasher.finalize() { + write!(&mut build_key, "{byte:02x}").expect("writing to a string cannot fail"); + } + let cache_dir = work_dir.join("package-cache").join(&build_key); + let package_path = cache_dir.join(package_cache::package_file_name(¬e_package.name)); + + if package_path.is_file() { + let staged_bytes = fs::read(&package_path).map_err(|error| { + Report::msg(format!( + "failed to read staged note package '{}': {error}", + package_path.display() + )) + })?; + if staged_bytes != package_bytes { + return Err(Report::msg(format!( + "content-addressed note package path '{}' contains different bytes", + package_path.display() + ))); + } + // Refresh the package mtime on reuse, or the age-based GC of a concurrent build + // could remove an actively used entry that was staged long ago. + touch_file(&package_path); + } else { + write_package_atomic(note_package, &cache_dir).map_err(|error| { + Report::msg(format!( + "failed to stage note package {}@{} for codec generation: {error}", + note_package.name, note_package.version + )) + })?; + } + + Ok(StagedNotePackage { cache_dir }) +} + +/// Sets a file's modification time to now; failures are ignored. +fn touch_file(file: &Path) { + if let Ok(handle) = fs::File::options().append(true).open(file) { + let _ = handle.set_modified(std::time::SystemTime::now()); + } +} + +/// Returns the newest modification time of a cache entry and its direct child files. +fn newest_cache_entry_mtime(path: &Path, metadata: &fs::Metadata) -> Option { + let mut newest = metadata.modified().ok(); + if !metadata.is_dir() { + return newest; + } + let Ok(entries) = fs::read_dir(path) else { + return newest; + }; + for entry in entries.flatten() { + let Ok(metadata) = entry.metadata() else { + continue; + }; + if !metadata.is_file() { + continue; + } + let Ok(modified) = metadata.modified() else { + continue; + }; + if newest.is_none_or(|current| modified > current) { + newest = Some(modified); + } + } + newest +} + +/// Removes staged note packages that have not been used for more than seven days. +fn gc_staged_note_packages(current_cache_dir: &Path) { + let Some(cache_parent) = current_cache_dir.parent() else { + return; + }; + let Ok(entries) = fs::read_dir(cache_parent) else { + return; + }; + + for entry in entries.flatten() { + let path = entry.path(); + if path == current_cache_dir { + continue; + } + let Ok(metadata) = entry.metadata() else { + continue; + }; + let Some(modified) = newest_cache_entry_mtime(&path, &metadata) else { + continue; + }; + let Ok(age) = modified.elapsed() else { + continue; + }; + if age > NOTE_PACKAGE_CACHE_MAX_AGE { + if metadata.is_dir() { + let _ = fs::remove_dir_all(path); + } else { + // A stray file under the cache parent is not a staged package; collect it too. + let _ = fs::remove_file(path); + } + } + } +} + +/// Returns the outer Cargo resolution policy arguments for a nested command. +pub(crate) fn apply_cargo_policy( + locked: bool, + offline: bool, +) -> impl Iterator { + [locked.then_some("--locked"), offline.then_some("--offline")] + .into_iter() + .flatten() +} + +/// Attributes a nested Cargo failure to the codec manifest. +fn note_codec_cargo_error(error: Report, manifest_path: &Path) -> Report { + Report::msg(format!( + "the nested note codec build for '{}' failed: {error}", + manifest_path.display() + )) +} + +/// Verifies the component sandbox and the versioned codec interface export. +fn validate_note_codec_component(component: &[u8]) -> CompilerResult<()> { + // The byte cap, the Wasm feature set, and the structural caps are fixed policy, so the + // producer applies the consumer rules here through the one shared entry point: a package + // that builds is a package that consumers accept. + miden_note_schema::validate_note_codec_component( + component, + miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES, + ) + .map_err(|error| { + Report::msg(format!( + "note codec component fails the structural limits consumers enforce: {error}" + )) + })?; + let DecodedWasm::Component(resolve, world_id) = + wit_component::decode(component).map_err(|error| { + Report::msg(format!("failed to decode the encoded note codec component: {error}")) + })? + else { + return Err(Report::msg("note codec output is not a component")); + }; + + let mut expected = Resolve::default(); + let package_id = expected.push_str("note-codec.wit", NOTE_CODEC_WIT).map_err(|error| { + Report::msg(format!("failed to resolve the pinned note codec WIT: {error:#}")) + })?; + let expected_package = &expected.packages[package_id]; + if expected_package.worlds.len() != 1 { + return Err(Report::msg(format!( + "pinned note codec WIT must define exactly one world, found: {:#?}", + expected_package.worlds + ))); + } + let expected_world_id = *expected_package + .worlds + .values() + .next() + .expect("one pinned codec world was checked above"); + let (expected_identity, expected_interface_id) = + codec_world_export(&expected, expected_world_id)?; + + let world = &resolve.worlds[world_id]; + // The wasm32-wasip2 standard library wires WASI interfaces into every component. + // Consumers stub them as trapping imports, so only `wasi:*` imports are permitted. + for (key, _) in world.imports.iter() { + let name = resolve.name_world_key(key); + if !name.starts_with("wasi:") { + return Err(Report::msg(format!( + "note codec component may import only `wasi:*` interfaces, found `{name}`" + ))); + } + } + if world.exports.len() != 1 { + return Err(Report::msg(format!( + "note codec component must export only `{expected_identity}`, found: {:#?}", + world.exports, + ))); + } + + let (actual_identity, actual_interface_id) = codec_world_export(&resolve, world_id)?; + if actual_identity != expected_identity { + return Err(Report::msg(format!( + "note codec world exports `{actual_identity}`, expected `{expected_identity}`" + ))); + } + compare_codec_interface_signatures( + &resolve, + actual_interface_id, + &expected, + expected_interface_id, + &expected_identity, + ) +} + +/// Returns the package-qualified identity and interface ID of the first codec world export. +fn codec_world_export( + resolve: &Resolve, + world_id: WorldId, +) -> CompilerResult<(String, wit_parser::InterfaceId)> { + let world = &resolve.worlds[world_id]; + let (key, item) = world + .exports + .iter() + .next() + .ok_or_else(|| Report::msg("note codec world does not export an interface"))?; + let WorldItem::Interface { id, .. } = item else { + return Err(Report::msg(format!( + "note codec world export `{}` is not an interface", + resolve.name_world_key(key) + ))); + }; + let interface = &resolve.interfaces[*id]; + let interface_name = interface + .name + .as_deref() + .ok_or_else(|| Report::msg("note codec world exports an unnamed interface"))?; + let package_id = interface.package.ok_or_else(|| { + Report::msg("note codec world exports an interface without a package identity") + })?; + let package = &resolve.packages[package_id].name; + let interface_identity = package.interface_id(interface_name); + if !matches!(key, WorldKey::Interface(export_id) if export_id == id) { + return Err(Report::msg(format!( + "note codec world export `{}` must use the package-qualified interface identity \ + `{interface_identity}`", + resolve.name_world_key(key), + ))); + } + Ok((interface_identity, *id)) +} + +/// Compares all exported codec function names and structural signatures. +fn compare_codec_interface_signatures( + actual_resolve: &Resolve, + actual_id: wit_parser::InterfaceId, + expected_resolve: &Resolve, + expected_id: wit_parser::InterfaceId, + expected_identity: &str, +) -> CompilerResult<()> { + let actual = &actual_resolve.interfaces[actual_id]; + let expected = &expected_resolve.interfaces[expected_id]; + if actual.functions.len() != expected.functions.len() { + return Err(Report::msg(format!( + "`{expected_identity}` exports {} functions, expected {}", + actual.functions.len(), + expected.functions.len() + ))); + } + for (name, expected_function) in &expected.functions { + let actual_function = actual + .functions + .get(name) + .ok_or_else(|| Report::msg(format!("`{expected_identity}` is missing `{name}`")))?; + let actual_signature = function_signature(actual_resolve, actual_function)?; + let expected_signature = function_signature(expected_resolve, expected_function)?; + if actual_signature != expected_signature { + return Err(Report::msg(format!( + "`{expected_identity}.{name}` has signature `{actual_signature}`, expected \ + `{expected_signature}`" + ))); + } + } + Ok(()) +} + +/// Returns a structural function signature with aliases resolved. +fn function_signature(resolve: &Resolve, function: &Function) -> CompilerResult { + if function.kind != FunctionKind::Freestanding { + return Err(Report::msg(format!( + "note codec function `{}` must be freestanding", + function.name + ))); + } + let params = function + .params + .iter() + .map(|param| { + canonical_wit_type(resolve, param.ty, &mut Vec::new()) + .map(|ty| format!("{}: {ty}", param.name)) + }) + .collect::>>()? + .join(", "); + let result = function + .result + .map(|ty| canonical_wit_type(resolve, ty, &mut Vec::new())) + .transpose()? + .unwrap_or_else(|| "_".to_string()); + Ok(format!("func({params}) -> {result}")) +} + +/// Returns a structural WIT type signature with aliases resolved. +fn canonical_wit_type( + resolve: &Resolve, + ty: Type, + active: &mut Vec, +) -> CompilerResult { + let primitive = match ty { + Type::Bool => Some("bool"), + Type::U8 => Some("u8"), + Type::U16 => Some("u16"), + Type::U32 => Some("u32"), + Type::U64 => Some("u64"), + Type::S8 => Some("s8"), + Type::S16 => Some("s16"), + Type::S32 => Some("s32"), + Type::S64 => Some("s64"), + Type::F32 => Some("f32"), + Type::F64 => Some("f64"), + Type::Char => Some("char"), + Type::String => Some("string"), + Type::ErrorContext => Some("error-context"), + Type::Id(_) => None, + }; + if let Some(primitive) = primitive { + return Ok(primitive.to_string()); + } + let Type::Id(id) = ty else { + unreachable!("all primitive WIT types returned above") + }; + if active.contains(&id) { + return Err(Report::msg("recursive types are not valid in the note codec interface")); + } + active.push(id); + let result = match &resolve.types[id].kind { + TypeDefKind::Type(ty) => canonical_wit_type(resolve, *ty, active), + TypeDefKind::List(ty) => { + canonical_wit_type(resolve, *ty, active).map(|ty| format!("list<{ty}>")) + } + TypeDefKind::Result(result) => { + let ok = result + .ok + .map(|ty| canonical_wit_type(resolve, ty, active)) + .transpose()? + .unwrap_or_else(|| "_".to_string()); + let err = result + .err + .map(|ty| canonical_wit_type(resolve, ty, active)) + .transpose()? + .unwrap_or_else(|| "_".to_string()); + Ok(format!("result<{ok}, {err}>")) + } + kind => Err(Report::msg(format!( + "unsupported `{}` type in the note codec interface signature", + kind.as_str() + ))), + }; + active.pop(); + result +} + /// Parse `cargo -Zscript`-style frontmatter from a given input string, if present. /// /// Returns `Ok(None)` if the input does not define Cargo frontmatter. @@ -390,3 +1010,315 @@ pub fn parse_cargo_frontmatter( Ok(Some(dependencies)) } + +#[cfg(test)] +mod tests { + use std::ffi::OsStr; + + use miden_assembly_syntax::debuginfo::Span; + + use super::*; + + /// Builds metadata with one value in the note codec namespace. + fn note_codec_metadata(value: miden_project::Value) -> miden_project::MetadataSet { + let mut namespace = miden_project::Metadata::default(); + namespace.insert(Span::unknown(Arc::::from(NOTE_CODEC_TABLE)), Span::unknown(value)); + let mut metadata = miden_project::MetadataSet::default(); + metadata.insert(Span::unknown(Arc::::from(NOTE_CODEC_NAMESPACE)), namespace); + metadata + } + + /// Builds a note codec metadata table from the given entries. + fn note_codec_table( + entries: impl IntoIterator, + ) -> miden_project::Value { + miden_project::Value::Table( + entries.into_iter().map(|(key, value)| (key.to_string(), value)).collect(), + ) + } + + #[test] + fn note_codec_crate_resolves_from_namespaced_metadata() { + let root = tempfile::TempDir::new().unwrap(); + let codec_crate = root.path().join("codec"); + fs::create_dir(&codec_crate).unwrap(); + let metadata = note_codec_metadata(note_codec_table([( + NOTE_CODEC_CRATE_KEY, + miden_project::Value::String("codec".to_string()), + )])); + + let resolved = + note_codec_crate_dir(&metadata, &root.path().join("miden-project.toml"), root.path()) + .unwrap(); + + assert_eq!(resolved, Some(codec_crate.canonicalize().unwrap())); + } + + #[test] + fn note_codec_metadata_requires_crate_key() { + let root = tempfile::TempDir::new().unwrap(); + let manifest_path = root.path().join("miden-project.toml"); + let metadata = note_codec_metadata(note_codec_table([])); + + let error = note_codec_crate_dir(&metadata, &manifest_path, root.path()) + .unwrap_err() + .to_string(); + + assert!(error.contains(&manifest_path.display().to_string())); + assert!(error.contains("must define a string `crate`")); + } + + #[test] + fn note_codec_metadata_rejects_unknown_key() { + let root = tempfile::TempDir::new().unwrap(); + let manifest_path = root.path().join("miden-project.toml"); + let metadata = note_codec_metadata(note_codec_table([ + (NOTE_CODEC_CRATE_KEY, miden_project::Value::String("codec".to_string())), + ("module", miden_project::Value::String("codec.wasm".to_string())), + ])); + + let error = note_codec_crate_dir(&metadata, &manifest_path, root.path()) + .unwrap_err() + .to_string(); + + assert!(error.contains(&manifest_path.display().to_string())); + assert!(error.contains("unknown key 'module'")); + } + + #[test] + fn note_codec_metadata_value_must_be_a_table() { + let root = tempfile::TempDir::new().unwrap(); + let manifest_path = root.path().join("miden-project.toml"); + let metadata = note_codec_metadata(miden_project::Value::String("codec".to_string())); + + let error = note_codec_crate_dir(&metadata, &manifest_path, root.path()) + .unwrap_err() + .to_string(); + + assert!(error.contains(&manifest_path.display().to_string())); + assert!(error.contains("must be a table")); + } + + #[test] + fn note_codec_metadata_without_namespace_is_absent() { + let root = tempfile::TempDir::new().unwrap(); + + let resolved = note_codec_crate_dir( + &miden_project::MetadataSet::default(), + &root.path().join("miden-project.toml"), + root.path(), + ) + .unwrap(); + + assert_eq!(resolved, None); + } + + #[test] + fn codec_interface_validation_compares_function_signatures() { + let (expected_resolve, expected_id) = resolve_codec_interface(NOTE_CODEC_WIT); + let expected_identity = codec_interface_identity(&expected_resolve, expected_id); + compare_codec_interface_signatures( + &expected_resolve, + expected_id, + &expected_resolve, + expected_id, + &expected_identity, + ) + .unwrap(); + + let changed = NOTE_CODEC_WIT.replace( + "parse: func(type-fqn: string, value: string)", + "parse: func(type-fqn: string, value: u64)", + ); + let (actual_resolve, actual_id) = resolve_codec_interface(&changed); + let error = compare_codec_interface_signatures( + &actual_resolve, + actual_id, + &expected_resolve, + expected_id, + &expected_identity, + ) + .unwrap_err() + .to_string(); + + assert!(error.contains(".parse` has signature")); + assert!(error.contains("value: u64")); + assert!(error.contains("value: string")); + } + + #[test] + fn staged_package_reuse_refreshes_the_entry_age() { + let root = tempfile::TempDir::new().unwrap(); + let package = midenc_codegen_masm::intrinsics::load(); + let codec_crate = root.path().join("codec"); + fs::create_dir(&codec_crate).unwrap(); + + let staged = stage_note_package(root.path(), &codec_crate, &package).unwrap(); + let package_path = staged.cache_dir.join(package_cache::package_file_name(&package.name)); + let old = std::time::SystemTime::now() - (NOTE_PACKAGE_CACHE_MAX_AGE * 2); + fs::File::options() + .write(true) + .open(&package_path) + .unwrap() + .set_modified(old) + .unwrap(); + + // A cache hit must refresh the package mtime used by the GC freshness rule. + let reused = stage_note_package(root.path(), &codec_crate, &package).unwrap(); + assert_eq!(reused.cache_dir, staged.cache_dir); + let package_age = + fs::metadata(&package_path).unwrap().modified().unwrap().elapsed().unwrap(); + assert!( + package_age < NOTE_PACKAGE_CACHE_MAX_AGE, + "the package age was not refreshed: {package_age:?}" + ); + let metadata = fs::metadata(&staged.cache_dir).unwrap(); + let age = newest_cache_entry_mtime(&staged.cache_dir, &metadata) + .unwrap() + .elapsed() + .unwrap(); + assert!(age < NOTE_PACKAGE_CACHE_MAX_AGE, "the entry age was not refreshed: {age:?}"); + } + + #[test] + fn note_codec_cargo_errors_are_always_attributed() { + let error = + note_codec_cargo_error(Report::msg("rustc failed"), Path::new("/codec/Cargo.toml")) + .to_string(); + + assert_eq!( + error, + "the nested note codec build for '/codec/Cargo.toml' failed: rustc failed" + ); + } + + #[test] + fn note_codec_staging_is_content_addressed() { + let root = tempfile::TempDir::new().unwrap(); + let package = midenc_codegen_masm::intrinsics::load(); + let codec_crate = root.path().join("codec"); + fs::create_dir(&codec_crate).unwrap(); + + let first = stage_note_package(root.path(), &codec_crate, &package).unwrap(); + let second = stage_note_package(root.path(), &codec_crate, &package).unwrap(); + assert_eq!(first.cache_dir, second.cache_dir); + assert_eq!(first.cache_dir.parent(), Some(root.path().join("package-cache").as_path())); + assert_eq!(first.cache_dir.file_name().unwrap().len(), 64); + let package_path = first.cache_dir.join(package_cache::package_file_name(&package.name)); + assert_eq!(fs::read(package_path).unwrap(), package.to_bytes()); + + let mut dotted_package = (*package).clone(); + dotted_package.name = miden_mast_package::PackageId::from("miden.note.with.dots"); + let dotted = stage_note_package(root.path(), &codec_crate, &dotted_package).unwrap(); + let dotted_path = + dotted.cache_dir.join(package_cache::package_file_name(&dotted_package.name)); + assert_eq!(dotted_path.file_name().unwrap(), "miden.note.with.dots.masp"); + assert_eq!(fs::read(&dotted_path).unwrap(), dotted_package.to_bytes()); + fs::write(&dotted_path, b"different package bytes").unwrap(); + let error = stage_note_package(root.path(), &codec_crate, &dotted_package) + .err() + .expect("the probe must reject different bytes at the writer path") + .to_string(); + assert!(error.contains("contains different bytes"), "unexpected error: {error}"); + } + + #[test] + fn oversized_codec_components_fail_producer_validation() { + let oversized = vec![0u8; miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES + 1]; + let error = validate_note_codec_component(&oversized).unwrap_err().to_string(); + assert!( + error.contains("fails the structural limits consumers enforce"), + "unexpected error: {error}" + ); + assert!(error.contains("pre-compilation limit"), "unexpected error: {error}"); + assert!( + error.contains(&miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES.to_string()), + "the limit is not named: {error}" + ); + } + + #[test] + fn codec_components_outside_the_wasm_feature_policy_fail_producer_validation() { + let component = wat::parse_str( + r#"(component + (core module + (func (export "simd") + v128.const i32x4 0 0 0 0 + drop)))"#, + ) + .unwrap(); + + let error = validate_note_codec_component(&component).unwrap_err().to_string(); + + assert!( + error.contains("uses a Wasm feature the policy rejects"), + "unexpected error: {error}" + ); + assert!(error.contains("SIMD"), "the rejected proposal is not named: {error}"); + } + + #[test] + fn structurally_oversized_codec_components_fail_producer_validation() { + let globals = "(global i32 (i32.const 0))".repeat(1_001); + let component = wat::parse_str(format!("(component (core module {globals}))")).unwrap(); + + let error = validate_note_codec_component(&component).unwrap_err().to_string(); + + assert!( + error.contains("fails the structural limits consumers enforce"), + "unexpected error: {error}" + ); + assert!(error.contains("1001 globals"), "the observed count is not named: {error}"); + } + + #[test] + fn note_codec_cargo_command_pins_the_guest_build_flags() { + let command = note_codec_cargo_command( + Path::new("cargo"), + None, + Path::new("/codec"), + Path::new("/codec/Cargo.toml"), + Path::new("/codec/target/cargo-target"), + Path::new("/codec/target/package-cache"), + ); + + let environment = command.get_envs().collect::>(); + assert!( + environment.contains(&( + OsStr::new("RUSTFLAGS"), + Some(OsStr::new(miden_note_schema::NOTE_CODEC_GUEST_RUSTFLAGS)), + )), + "the guest rustflags are not pinned: {environment:?}" + ); + assert!( + environment.contains(&(OsStr::new("CARGO_ENCODED_RUSTFLAGS"), None)), + "the outer rustflags are not scrubbed: {environment:?}" + ); + + let args = command.get_args().collect::>(); + let target = args + .windows(2) + .find(|window| window[0] == OsStr::new("--target")) + .expect("the nested build does not select a target"); + assert_eq!(target[1], OsStr::new(NOTE_CODEC_TARGET)); + } + + /// Resolves the codec interface from one complete WIT document. + fn resolve_codec_interface(wit: &str) -> (Resolve, wit_parser::InterfaceId) { + let mut resolve = Resolve::default(); + let package_id = resolve.push_str("note-codec-test.wit", wit).unwrap(); + let world_id = resolve.packages[package_id].worlds["note-codec"]; + let (_, interface_id) = codec_world_export(&resolve, world_id).unwrap(); + (resolve, interface_id) + } + + /// Returns the package-qualified identity of one resolved codec interface. + fn codec_interface_identity( + resolve: &Resolve, + interface_id: wit_parser::InterfaceId, + ) -> String { + let interface = &resolve.interfaces[interface_id]; + let package = &resolve.packages[interface.package.unwrap()].name; + package.interface_id(interface.name.as_deref().unwrap()) + } +} diff --git a/midenc-compile/src/compiler.rs b/midenc-compile/src/compiler.rs index b1d52e2b34..b7ba5bdc89 100644 --- a/midenc-compile/src/compiler.rs +++ b/midenc-compile/src/compiler.rs @@ -1,6 +1,8 @@ #[cfg(feature = "std")] +use alloc::sync::Arc; +#[cfg(feature = "std")] use alloc::{borrow::ToOwned, format, string::ToString, vec}; -use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}; +use alloc::{boxed::Box, string::String, vec::Vec}; #[cfg(feature = "std")] use std::ffi::OsString; @@ -8,10 +10,11 @@ use std::ffi::OsString; use clap::{Parser, builder::ArgPredicate}; use miden_mast_package::TargetType; use midenc_session::{ - ColorChoice, DebugInfo, InputFile, IrFilter, LinkLibrary, OptLevel, Options, OutputFile, - OutputTypeSpec, OutputTypes, PathBuf, RemapPathPrefix, Session, Verbosity, Warnings, - add_target_link_libraries, diagnostics::Emitter, + ColorChoice, DebugInfo, IrFilter, LinkLibrary, OptLevel, Options, OutputFile, OutputTypeSpec, + OutputTypes, PathBuf, RemapPathPrefix, Verbosity, Warnings, add_target_link_libraries, }; +#[cfg(feature = "std")] +use midenc_session::{InputFile, Session, diagnostics::Emitter}; /// Compile a program from WebAssembly or Miden IR, to Miden Assembly. #[derive(Debug, Clone)] @@ -383,6 +386,12 @@ pub struct Compiler { arg(long, short = 'p', value_name = "SPEC", conflicts_with("workspace"),) )] pub package: Vec, + /// Require Cargo.lock to remain unchanged in Cargo builds. + #[cfg_attr(feature = "std", arg(long, help_heading = "Compiler"))] + pub locked: bool, + /// Prevent Cargo builds from accessing the network. + #[cfg_attr(feature = "std", arg(long, help_heading = "Compiler"))] + pub offline: bool, /// Path to the package/project manifest /// /// If unspecified, the compiler will create a virtual manifest for the given input file, if @@ -710,6 +719,8 @@ impl Compiler { release: _, workspace, package, + locked, + offline, manifest_path, remap_path_prefixes, } = self; @@ -777,6 +788,8 @@ impl Compiler { options.entrypoint = entrypoint; options.workspace = workspace; options.packages = package; + options.cargo_locked = locked; + options.cargo_offline = offline; options.stop_after = stop_after; options.parse_only = parse_only; options.analyze_only = analyze_only; @@ -806,6 +819,7 @@ impl Compiler { } /// Use this configuration to obtain a [Session] used for compilation + #[cfg(feature = "std")] fn into_session( options: Box, input: Option, @@ -847,9 +861,11 @@ fn format_error(err: clap::Error) -> clap::Error { err.format(&mut cmd) } +#[cfg(feature = "std")] #[derive(Clone)] struct TargetTypeValueParser; +#[cfg(feature = "std")] impl clap::builder::TypedValueParser for TargetTypeValueParser { type Value = TargetType; @@ -909,4 +925,12 @@ mod tests { fn no_stop_after_means_no_cap() { assert_eq!(options(&[]).stop_after, None); } + + /// Cargo resolution policy flags reach the Rust project build through the session options. + #[test] + fn cargo_resolution_policy_reaches_the_options() { + let options = options(&["--locked", "--offline"]); + assert!(options.cargo_locked); + assert!(options.cargo_offline); + } } diff --git a/midenc-compile/src/lib.rs b/midenc-compile/src/lib.rs index ce851985a2..1338a3826f 100644 --- a/midenc-compile/src/lib.rs +++ b/midenc-compile/src/lib.rs @@ -14,21 +14,21 @@ pub mod pipeline; #[cfg(feature = "std")] pub mod rust; +#[cfg(feature = "std")] use alloc::rc::Rc; pub use midenc_hir::Context; +#[cfg(feature = "std")] use midenc_hir::Op; +use midenc_session::diagnostics::{Diagnostic, Report, miette}; #[cfg(feature = "std")] use midenc_session::{OutputFile, OutputType}; -use midenc_session::{ - OutputMode, - diagnostics::{Diagnostic, Report, WrapErr, miette}, -}; +#[cfg(feature = "std")] +use midenc_session::{OutputMode, diagnostics::WrapErr}; -pub use self::{ - compiler::Compiler, - pipeline::artifacts::{CodegenOutput, CompiledArtifact, MidenComponent}, -}; +pub use self::compiler::Compiler; +#[cfg(feature = "std")] +pub use self::pipeline::artifacts::{CodegenOutput, CompiledArtifact, MidenComponent}; pub type CompilerResult = Result; @@ -39,6 +39,7 @@ pub type CompilerResult = Result; pub struct CompilerStopped(&'static str); /// Run the compiler using the provided [midenc_session::Session] +#[cfg(feature = "std")] pub fn compile(context: Rc) -> CompilerResult<()> { use midenc_hir::formatter::DisplayHex; @@ -87,6 +88,7 @@ pub fn compile(context: Rc) -> CompilerResult<()> { } /// Same as `compile`, but return compiled artifacts to the caller +#[cfg(feature = "std")] pub fn compile_to_memory(context: Rc) -> CompilerResult { let session = context.session_rc(); let input = session.input.clone().ok_or_else(|| Report::msg("no inputs"))?; @@ -121,6 +123,7 @@ pub fn compile_to_memory(context: Rc) -> CompilerResult( link_output: MidenComponent, pre_assembly_stage: F, @@ -167,6 +170,7 @@ where /// emitted is nonetheless decided by the session: `Pipeline::compile` attaches an observer that /// renders the selected target's artifacts through the route's own declarations, and /// `Session::emit` writes only the output types the session asked for. +#[cfg(feature = "std")] fn run_pipeline( session: Rc, input: midenc_session::InputFile, @@ -197,6 +201,7 @@ fn run_pipeline( /// /// `CompiledArtifact::Lowered` is therefore unreachable from here. It survives because it is /// part of a public enum with an external consumer, and narrowing that is a separate change. +#[cfg(feature = "std")] fn artifact_from_outcome( outcome: pipeline::Outcome, stop: Option, @@ -219,6 +224,7 @@ fn artifact_from_outcome( Ok(CompiledArtifact::Assembled(package)) } +#[cfg(feature = "std")] pub(crate) fn emit_hir_if_requested( op: &midenc_hir::Operation, context: Rc, diff --git a/midenc-compile/src/pipeline/assembly.rs b/midenc-compile/src/pipeline/assembly.rs index 2771df6c6d..544ee6a510 100644 --- a/midenc-compile/src/pipeline/assembly.rs +++ b/midenc-compile/src/pipeline/assembly.rs @@ -2,7 +2,8 @@ //! //! [`prepare_assembler`] runs before assembly, applying the session's link inputs; //! [`post_process_package`] runs after it, attaching to the assembled package the sections -//! and advice-map entries that codegen produced but the assembler knows nothing about. +//! and advice-map entries that codegen produced but the assembler knows nothing about. For note +//! targets, it also builds, validates, and attaches an author codec declared by project metadata. //! //! Both are shared: the [`Pipeline`](super::Pipeline) driver prepares its own assembler //! through the first, and every frontend that lowers HIR post-processes through the second — @@ -16,10 +17,13 @@ use alloc::vec::Vec; +use miden_assembly::TargetAssemblyContext; use miden_mast_package::Package; use midenc_codegen_masm::{MasmComponent, intrinsics}; use midenc_session::{Session, diagnostics::Report}; +use crate::cargo::NOTE_CODEC_TABLE_NAME; + /// Apply the session's link inputs to `assembler` before a project is assembled with it. pub(crate) fn prepare_assembler( assembler: &mut miden_assembly::Assembler, @@ -59,23 +63,37 @@ pub(crate) fn prepare_assembler( Ok(()) } +/// Attaches frontend metadata, advice-map data, and target-specific sections after assembly. +/// +/// The author codec is attached only when the note is the root target of the build, as +/// [`attaches_note_codec`] decides. Every other section is attached for every role. pub(crate) fn post_process_package( package: &mut Package, component: &MasmComponent, sections: &midenc_frontend_wasm_metadata::PackageSections, - target: &midenc_session::miden_project::Target, - registry: &dyn miden_package_registry::PackageRegistryAndProvider, + context: &TargetAssemblyContext<'_>, + role: crate::pipeline::observer::TargetRole, ) -> Result<(), Report> { use miden_assembly::serde::Serializable; use miden_mast_package::{Section, SectionId}; use midenc_session::miden_project::TargetType; - attach_account_component_metadata(package, sections.account_component_metadata.as_deref()); - attach_component_wit(package, sections.component_wit.as_deref()); + let has_note_codec = crate::cargo::has_project_note_codec(context.package.metadata()); + validate_note_codec_declaration( + has_note_codec, + package_has_note_target(&context.package), + context.target.ty, + sections.note_storage_schema.is_some(), + context.target.name.inner(), + )?; + + attach_account_component_metadata(package, sections.account_component_metadata.as_deref())?; + attach_component_wit(package, sections.component_wit.as_deref())?; + attach_note_storage_schema(package, sections.note_storage_schema.as_deref())?; extend_rodata_advice_map(package, &component.rodata); // Embed the kernel in note/transaction script packages, if not already embedded - if matches!(target.ty, TargetType::Note | TargetType::TransactionScript) + if matches!(context.target.ty, TargetType::Note | TargetType::TransactionScript) && !package.sections.iter().any(|section| section.id == SectionId::KERNEL) && let Ok(Some(kernel_dep)) = package.kernel_runtime_dependency() { @@ -83,12 +101,138 @@ pub(crate) fn post_process_package( kernel_dep.version().clone(), kernel_dep.digest, ); - let kernel_package = registry.load_package(kernel_dep.id(), &version)?; + let kernel_package = context.package_registry.load_package(kernel_dep.id(), &version)?; package .sections .push(Section::new(SectionId::KERNEL, kernel_package.to_bytes())); } + if attaches_note_codec(has_note_codec, context.target.ty, role) { + // Run after schema and kernel attachment. The codec stages this package state and hashes it. + attach_note_codec(package, context)?; + } + + Ok(()) +} + +/// Returns true when the assembled package must carry an author codec section. +/// +/// The codec is built only for a note that is the root target of the build. A note that is a +/// source dependency of another project gets no codec section, for two reasons. The codec +/// crate's inputs are outside the build provenance, so a dependency copy that is kept in a +/// package store can carry a stale codec after the codec crate changes. And every dependency +/// assembly would otherwise run a nested cargo build. A consumer that needs the codec loads the +/// note's own package. This is also the behavior of the package post-processor that will take +/// over the codec build, because a post-processor never runs on a dependency. +fn attaches_note_codec( + has_note_codec: bool, + target_type: midenc_session::miden_project::TargetType, + role: crate::pipeline::observer::TargetRole, +) -> bool { + use midenc_session::miden_project::TargetType; + + has_note_codec && target_type == TargetType::Note && role.is_root() +} + +/// Validates the target and schema required by an author codec declaration. +fn validate_note_codec_declaration( + has_note_codec: bool, + package_has_note_target: bool, + target_type: midenc_session::miden_project::TargetType, + has_note_storage_schema: bool, + target_name: &str, +) -> Result<(), Report> { + use midenc_session::miden_project::TargetType; + + if has_note_codec && !package_has_note_target { + return Err(Report::msg(format!( + "`[package.metadata.{NOTE_CODEC_TABLE_NAME}]` requires a note target, but the package \ + that contains target '{target_name}' defines no note target" + ))); + } + if has_note_codec && target_type == TargetType::Note && !has_note_storage_schema { + return Err(Report::msg(format!( + "note target '{target_name}' declares `[package.metadata.{NOTE_CODEC_TABLE_NAME}]` \ + but emitted no note storage schema; add one named-field `#[note]` struct" + ))); + } + Ok(()) +} + +/// Returns true when a package defines at least one note target. +fn package_has_note_target(package: &midenc_session::miden_project::Package) -> bool { + use midenc_session::miden_project::TargetType; + + package + .library_target() + .is_some_and(|target| target.inner().ty == TargetType::Note) + || package + .executable_targets() + .iter() + .any(|target| target.inner().ty == TargetType::Note) +} + +/// Builds and attaches the note codec declared by the current project package. +fn attach_note_codec( + package: &mut Package, + context: &TargetAssemblyContext<'_>, +) -> Result<(), Report> { + let Some(component) = crate::cargo::build_project_note_codec( + context.package.as_ref(), + context.manifest_path, + context.project_root.as_ref(), + package, + )? + else { + return Ok(()); + }; + + let section_id = midenc_frontend_wasm_metadata::package_note_codec_section_id(); + set_unique_section(package, section_id, component, "note codec") +} + +/// Attach the note storage schema to the assembled package. +fn attach_note_storage_schema( + package: &mut Package, + note_storage_schema: Option<&[u8]>, +) -> Result<(), Report> { + if let Some(bytes) = note_storage_schema { + validate_note_storage_schema(bytes)?; + let section_id = midenc_frontend_wasm_metadata::package_note_storage_schema_section_id(); + set_unique_section(package, section_id, bytes.to_vec(), "note storage schema")?; + } + Ok(()) +} + +/// Validates a note storage schema with the rules every consumer applies on read. +/// +/// The check makes the producer build fail with the consumer diagnostic. Without it, a +/// package could publish a schema that every reader rejects, and the author would learn +/// about the defect from a consumer instead of the build. +fn validate_note_storage_schema(bytes: &[u8]) -> Result<(), Report> { + let text = core::str::from_utf8(bytes) + .map_err(|error| Report::msg(format!("the note storage schema is not UTF-8: {error}")))?; + miden_note_schema::NoteStorageSchema::from_wit_text(text).map_err(|error| { + Report::msg(format!("the note storage schema fails consumer validation: {error}")) + })?; + Ok(()) +} + +/// Adds one package section and rejects an existing section with the same identifier. +fn set_unique_section( + package: &mut Package, + section_id: miden_mast_package::SectionId, + bytes: Vec, + description: &str, +) -> Result<(), Report> { + use miden_mast_package::Section; + + if package.sections.iter().any(|section| section.id == section_id) { + return Err(Report::msg(format!( + "cannot attach {description}: package already contains section `{section_id}`" + ))); + } + package.sections.push(Section::new(section_id, bytes)); Ok(()) } @@ -96,22 +240,29 @@ pub(crate) fn post_process_package( fn attach_account_component_metadata( package: &mut Package, account_component_metadata_bytes: Option<&[u8]>, -) { - use miden_mast_package::{Section, SectionId}; +) -> Result<(), Report> { + use miden_mast_package::SectionId; if let Some(bytes) = account_component_metadata_bytes { - package - .sections - .push(Section::new(SectionId::ACCOUNT_COMPONENT_METADATA, bytes.to_vec())); + set_unique_section( + package, + SectionId::ACCOUNT_COMPONENT_METADATA, + bytes.to_vec(), + "account component metadata", + )?; } + Ok(()) } /// Attach the component's public WIT source to the assembled package. -fn attach_component_wit(package: &mut Package, component_wit_bytes: Option<&[u8]>) { - use miden_mast_package::Section; +fn attach_component_wit( + package: &mut Package, + component_wit_bytes: Option<&[u8]>, +) -> Result<(), Report> { if let Some(bytes) = component_wit_bytes { let id = midenc_frontend_wasm_metadata::package_wit_section_id(); - package.sections.push(Section::new(id, bytes.to_vec())); + set_unique_section(package, id, bytes.to_vec(), "component WIT")?; } + Ok(()) } /// Extend the package advice map with the component's rodata segments. @@ -123,3 +274,95 @@ fn extend_rodata_advice_map(package: &mut Package, rodata: &[midenc_codegen_masm let advice_map = rodata.iter().map(|segment| (segment.digest, segment.to_elements())).collect(); package.extend_advice_map(advice_map); } + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use miden_mast_package::SectionId; + use midenc_session::miden_project::TargetType; + + use super::*; + + #[test] + fn attach_rejects_a_schema_that_consumers_cannot_read() { + let mut package = (*midenc_codegen_masm::intrinsics::load()).clone(); + + let error = attach_note_storage_schema(&mut package, Some(b"not wit at all")) + .unwrap_err() + .to_string(); + + assert!(error.contains("fails consumer validation"), "unexpected error: {error}"); + } + + #[test] + fn attach_accepts_a_schema_that_consumers_can_read() { + let mut package = (*midenc_codegen_masm::intrinsics::load()).clone(); + let schema = b"package test:note-schema@1.0.0; + +interface note-storage { + record note { + value: u64, + } + + type storage = note; +} +"; + + attach_note_storage_schema(&mut package, Some(schema.as_slice())).unwrap(); + + let id = midenc_frontend_wasm_metadata::package_note_storage_schema_section_id(); + assert!(package.sections.iter().any(|section| section.id == id)); + } + + #[test] + fn unique_sections_reject_an_existing_identifier() { + let mut package = (*midenc_codegen_masm::intrinsics::load()).clone(); + let id = SectionId::custom("test_unique_section").unwrap(); + + set_unique_section(&mut package, id.clone(), vec![1], "test section").unwrap(); + let error = set_unique_section(&mut package, id, vec![2], "test section") + .unwrap_err() + .to_string(); + + assert!(error.contains("already contains section `test_unique_section`")); + } + + #[test] + fn codec_metadata_skips_non_note_target_when_package_has_note_target() { + validate_note_codec_declaration(true, true, TargetType::Library, false, "library").unwrap(); + } + + #[test] + fn codec_metadata_requires_a_note_target_in_the_package() { + let error = + validate_note_codec_declaration(true, false, TargetType::Library, false, "library") + .unwrap_err() + .to_string(); + assert!(error.contains("defines no note target")); + } + + #[test] + fn codec_metadata_requires_a_schema_on_the_note_target() { + let missing_schema = + validate_note_codec_declaration(true, true, TargetType::Note, false, "schema-less") + .unwrap_err() + .to_string(); + assert!(missing_schema.contains("emitted no note storage schema")); + + validate_note_codec_declaration(true, true, TargetType::Note, true, "note").unwrap(); + validate_note_codec_declaration(false, false, TargetType::Library, false, "library") + .unwrap(); + } + + #[test] + fn codec_attaches_only_to_a_root_note_that_declares_one() { + use crate::pipeline::observer::TargetRole; + + assert!(attaches_note_codec(true, TargetType::Note, TargetRole::Root)); + assert!(!attaches_note_codec(true, TargetType::Note, TargetRole::Dependency)); + assert!(!attaches_note_codec(true, TargetType::Note, TargetRole::RequiredLibrary)); + assert!(!attaches_note_codec(true, TargetType::Library, TargetRole::Root)); + assert!(!attaches_note_codec(false, TargetType::Note, TargetRole::Root)); + } +} diff --git a/midenc-compile/src/pipeline/backend.rs b/midenc-compile/src/pipeline/backend.rs index 1db71fdc83..1e944859a1 100644 --- a/midenc-compile/src/pipeline/backend.rs +++ b/midenc-compile/src/pipeline/backend.rs @@ -51,7 +51,8 @@ use crate::{CodegenOutput, CompilerResult, MidenComponent}; /// asks the advice provider for the data behind it, so a package assembled without the /// advice map fails at run time, in the VM, with nothing in the build to point at. /// - [`sections`](LoweredTarget::sections) carries the out-of-band payloads — the serialized -/// account-component metadata and the component's public WIT — that become package sections. +/// account-component metadata, the component's public WIT, and the note storage schema — that +/// become package sections. /// - [`source_provenance`](LoweredTarget::source_provenance) is what the assembler hashes to /// decide whether a cached build of this target is still current. /// @@ -830,8 +831,8 @@ mod tests { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), + cx.role(), ) } diff --git a/midenc-compile/src/pipeline/frontends/hir.rs b/midenc-compile/src/pipeline/frontends/hir.rs index 0942260190..8dacbd3b1b 100644 --- a/midenc-compile/src/pipeline/frontends/hir.rs +++ b/midenc-compile/src/pipeline/frontends/hir.rs @@ -393,8 +393,8 @@ impl Frontend for HirFrontend { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), + cx.role(), ) } diff --git a/midenc-compile/src/pipeline/frontends/rust.rs b/midenc-compile/src/pipeline/frontends/rust.rs index efceda88c8..201b66149f 100644 --- a/midenc-compile/src/pipeline/frontends/rust.rs +++ b/midenc-compile/src/pipeline/frontends/rust.rs @@ -621,7 +621,14 @@ trim-paths = [\"diagnostics\", \"object\"] std::fs::write(&manifest_path, &cargo_toml) .map_err(|err| Report::msg(format!("failed to generate temporary Cargo.toml: {err}")))?; - let cargo_build_args = build_cargo_args(&manifest_path, options); + let cargo_build_args = build_cargo_args( + Some(&manifest_path), + options.profile == "release", + options.workspace, + &options.packages, + (options.cargo_locked, options.cargo_offline), + "\"s\"", + ); // The same mandatory set and inherited-flag precedence as the manifest route: a present // `CARGO_ENCODED_RUSTFLAGS` is authoritative over plain `RUSTFLAGS`. @@ -674,7 +681,7 @@ trim-paths = [\"diagnostics\", \"object\"] cargo.args(&cargo_build_args); // Handle the target for buildable commands - crate::rust::install_wasm32_target(wasi, toolchain.as_deref())?; + crate::rust::install_wasm32_target(wasi, toolchain.as_deref(), options.cargo_offline)?; cargo.arg("--target").arg(format!("wasm32-{wasi}")); @@ -822,7 +829,15 @@ fn rustc( Ok(output_file) } -fn build_cargo_args(manifest_path: &Path, options: &Options) -> Vec { +/// Builds the shared argument vector for a nested `cargo build` invocation. +pub(super) fn build_cargo_args( + manifest_path: Option<&Path>, + release: bool, + workspace: bool, + packages: &[P], + cargo_policy: (bool, bool), + release_opt_level: &str, +) -> Vec { let mut args = vec!["build".to_string()]; // Add build-std flags required for Miden compilation @@ -844,7 +859,7 @@ fn build_cargo_args(manifest_path: &Path, options: &Options) -> Vec { ("profile.dev.overflow-checks", "false"), ("profile.dev.debug", "true"), ("profile.dev.debug-assertions", "false"), - ("profile.release.opt-level", "\"s\""), + ("profile.release.opt-level", release_opt_level), ("profile.release.lto", "true"), ("profile.release.codegen-units", "1"), ("profile.release.panic", "\"abort\""), @@ -864,18 +879,23 @@ fn build_cargo_args(manifest_path: &Path, options: &Options) -> Vec { } // Forward cargo-specific options - if options.profile == "release" { + if release { args.push("--release".to_string()); } + args.extend( + crate::cargo::apply_cargo_policy(cargo_policy.0, cargo_policy.1).map(ToString::to_string), + ); - args.push("--manifest-path".to_string()); - args.push(manifest_path.to_string_lossy().to_string()); + if let Some(manifest_path) = manifest_path { + args.push("--manifest-path".to_string()); + args.push(manifest_path.to_string_lossy().to_string()); + } - if options.workspace { + if workspace { args.push("--workspace".to_string()); } - for package in &options.packages { + for package in packages { args.push("--package".to_string()); args.push(package.to_string()); } @@ -1632,8 +1652,8 @@ impl Frontend for RustProjectFrontend { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), + cx.role(), ) } } @@ -1986,7 +2006,14 @@ pub(crate) mod manifest { _source_manager: Arc, ) -> CompilerResult { let rustup_toolchain = crate::rust::rustup_toolchain(); - let cargo_build_args = build_cargo_args(cargo_opts, compiler_opts.optimize); + let cargo_build_args = super::build_cargo_args( + cargo_opts.manifest_path.as_deref(), + cargo_opts.release, + cargo_opts.workspace, + &cargo_opts.packages, + (cargo_opts.locked, cargo_opts.offline), + cargo_profile_opt_level(compiler_opts.optimize), + ); let inherited_encoded = std::env::var_os("CARGO_ENCODED_RUSTFLAGS"); let inherited_plain = std::env::var_os("RUSTFLAGS"); @@ -2014,6 +2041,7 @@ pub(crate) mod manifest { &cargo_build_args, env, cargo_target_dir.as_deref(), + cargo_opts.offline, )?; assert_eq!(wasm_outputs.len(), 1, "expected only one Wasm artifact"); @@ -2139,76 +2167,13 @@ pub(crate) mod manifest { } } - /// Builds the argument vector for the underlying `cargo build` invocation. - fn build_cargo_args(cargo_opts: &CargoOptions, opt_level: OptLevel) -> Vec { - let mut args = vec!["build".to_string()]; - - // Add build-std flags required for Miden compilation - args.extend( - [ - "-Z", - "build-std=core,alloc,panic_abort", - "-Z", - "build-std-features=optimize_for_size", - ] - .into_iter() - .map(|s| s.to_string()), - ); - - // Configure profile settings - let cfg_pairs: Vec<(&str, &str)> = vec![ - ("profile.dev.panic", "\"abort\""), - ("profile.dev.opt-level", "1"), - ("profile.dev.overflow-checks", "false"), - ("profile.dev.debug", "true"), - ("profile.dev.debug-assertions", "false"), - ("profile.release.opt-level", cargo_profile_opt_level(opt_level)), - ("profile.release.lto", "true"), - ("profile.release.codegen-units", "1"), - ("profile.release.panic", "\"abort\""), - // The guest Wasm needs DWARF (`debug = true` above) so the compiler can turn - // it into Miden debug information — but the host-side units of this nested - // build (build scripts, proc macros, and their whole native Miden dependency - // cone) do not. When a profile sets `debug` explicitly, the host units - // inherit it, which was measured at ~94% of a 310 MB proc-macro artifact, - // repeated in every build directory a test suite uses. - ("profile.dev.build-override.debug", "false"), - ("profile.release.build-override.debug", "false"), - ]; - - for (key, value) in cfg_pairs { - args.push("--config".to_string()); - args.push(format!("{key}={value}")); - } - - // Forward cargo-specific options - if cargo_opts.release { - args.push("--release".to_string()); - } - - if let Some(ref manifest_path) = cargo_opts.manifest_path { - args.push("--manifest-path".to_string()); - args.push(manifest_path.to_string_lossy().to_string()); - } - - if cargo_opts.workspace { - args.push("--workspace".to_string()); - } - - for package in &cargo_opts.packages { - args.push("--package".to_string()); - args.push(package.to_string()); - } - - args - } - fn run_cargo( wasi: &str, toolchain: Option<&str>, spawn_args: &[String], env: E, cargo_target_dir: Option<&Path>, + offline: bool, ) -> CompilerResult> where E: IntoIterator, @@ -2233,7 +2198,7 @@ pub(crate) mod manifest { cargo.args(spawn_args); // Handle the target for buildable commands - crate::rust::install_wasm32_target(wasi, None)?; + crate::rust::install_wasm32_target(wasi, None, offline)?; cargo.arg("--target").arg(format!("wasm32-{wasi}")); @@ -3892,6 +3857,27 @@ path = "lib.rs" assert_eq!(merged, vec!["--cfg".to_string(), "miden".to_string()]); } + /// The root Cargo policy is forwarded to the manifest build command. + #[test] + fn the_cargo_resolution_policy_is_handed_to_the_nested_build() { + let options = crate::cargo::CargoOptions { + locked: true, + offline: true, + ..Default::default() + }; + let args = build_cargo_args( + options.manifest_path.as_deref(), + options.release, + options.workspace, + &options.packages, + (options.locked, options.offline), + "2", + ); + + assert!(args.iter().any(|arg| arg == "--locked")); + assert!(args.iter().any(|arg| arg == "--offline")); + } + /// A WebAssembly module with a body, for the lowering half of the entry point. const MANIFEST_WAT: &str = r#" (module diff --git a/midenc-compile/src/pipeline/frontends/wasm.rs b/midenc-compile/src/pipeline/frontends/wasm.rs index af3d2bd7a6..c2a3218bfb 100644 --- a/midenc-compile/src/pipeline/frontends/wasm.rs +++ b/midenc-compile/src/pipeline/frontends/wasm.rs @@ -591,8 +591,8 @@ impl Frontend for WasmFrontend { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), + cx.role(), ) } diff --git a/midenc-compile/src/pipeline/seed.rs b/midenc-compile/src/pipeline/seed.rs index afed7bedb0..00f2e242a5 100644 --- a/midenc-compile/src/pipeline/seed.rs +++ b/midenc-compile/src/pipeline/seed.rs @@ -438,8 +438,8 @@ impl Frontend for SeedFrontend { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), + cx.role(), ) } @@ -605,8 +605,8 @@ mod tests { package, &found.component, &found.sections, - cx.assembly().target, - cx.assembly().package_registry, + cx.assembly(), + cx.role(), ) } diff --git a/midenc-compile/src/pipeline/testing.rs b/midenc-compile/src/pipeline/testing.rs index bad2f5c6da..4f1734577d 100644 --- a/midenc-compile/src/pipeline/testing.rs +++ b/midenc-compile/src/pipeline/testing.rs @@ -215,6 +215,7 @@ pub(crate) fn component_in_namespace( sections: midenc_frontend_wasm_metadata::PackageSections { account_component_metadata: metadata, component_wit: None, + note_storage_schema: None, }, source_provenance: ProjectSourceProvenanceInputs { root: SourceFileProvenance { diff --git a/midenc-compile/src/rust.rs b/midenc-compile/src/rust.rs index 3a6170f0d2..ffc2d08e36 100644 --- a/midenc-compile/src/rust.rs +++ b/midenc-compile/src/rust.rs @@ -13,7 +13,16 @@ use midenc_hir::Report; use crate::CompilerResult; -pub fn install_wasm32_target(wasi: &str, toolchain: Option<&str>) -> CompilerResult<()> { +/// Ensures that the requested Wasm target is installed for the selected Rust toolchain. +/// +/// The target is detected directly in the selected toolchain's sysroot. When `offline` is true, +/// this fails with installation guidance if the target is absent; it never invokes rustup to +/// install the target. +pub fn install_wasm32_target( + wasi: &str, + toolchain: Option<&str>, + offline: bool, +) -> CompilerResult<()> { let Some(toolchain) = toolchain.map(ToString::to_string).or_else(rustup_toolchain) else { return Err(Report::msg(format!( "failed to find the `wasm32-{wasi}` target and `rustup` is not available. If you're \ @@ -24,15 +33,21 @@ pub fn install_wasm32_target(wasi: &str, toolchain: Option<&str>) -> CompilerRes log::info!(target: "driver", "verifying wasm32-{wasi} target is installed for the {toolchain} toolchain.."); + let target = format!("wasm32-{wasi}"); let sysroot = get_sysroot(Some(&toolchain))?; if sysroot.join(format!("lib/rustlib/wasm32-{wasi}")).exists() { log::info!(target: "driver", "wasm32-{wasi} is available"); return Ok(()); } + if offline { + return Err(Report::msg(format!( + "the `{target}` target is not installed for the `{toolchain}` toolchain; install it \ + with `rustup target add --toolchain {toolchain} {target}` or drop `--offline`" + ))); + } log::info!(target: "driver", "installing wasm32-{wasi} target"); - let target = format!("wasm32-{wasi}"); let output = Command::new("rustup") .arg("target") .arg("add") @@ -87,7 +102,38 @@ pub fn get_sysroot(toolchain: Option<&str>) -> CompilerResult { Ok(sysroot) } -pub fn spawn_cargo(mut cmd: Command, cargo: &Path) -> CompilerResult> { +/// Runs a Cargo command and exits the process with Cargo's status when the build fails. +/// +/// The frontend build paths use this so a failed user build ends the tool with Cargo's own +/// exit code. Callers that must report the failure themselves use [`run_cargo`]. +pub fn spawn_cargo(cmd: Command, cargo: &Path) -> CompilerResult> { + let (status, artifacts) = run_cargo_inner(cmd, cargo)?; + if !status.success() { + std::process::exit(status.code().unwrap_or(1)); + } + Ok(artifacts) +} + +/// Runs a Cargo command and returns an error when the build fails. +/// +/// Nested builds such as the note codec build use this so their error context and recovery +/// guidance reach the user instead of the process ending with Cargo's status. +pub fn run_cargo(cmd: Command, cargo: &Path) -> CompilerResult> { + let (status, artifacts) = run_cargo_inner(cmd, cargo)?; + if !status.success() { + return Err(Report::msg(format!( + "`{cargo}` failed with {status}", + cargo = cargo.display() + ))); + } + Ok(artifacts) +} + +/// Spawns a Cargo command and collects its Wasm artifacts together with the exit status. +fn run_cargo_inner( + mut cmd: Command, + cargo: &Path, +) -> CompilerResult<(std::process::ExitStatus, Vec)> { use std::io::BufRead; log::debug!(target: "driver", "spawning command {cmd:?}"); @@ -142,11 +188,7 @@ pub fn spawn_cargo(mut cmd: Command, cargo: &Path) -> CompilerResult Option { @@ -227,3 +269,19 @@ impl BuildOutput { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(unix)] + fn run_cargo_reports_a_failed_status_instead_of_exiting() { + let mut cmd = Command::new("sh"); + cmd.args(["-c", "exit 3"]).stdout(std::process::Stdio::piped()); + + let error = run_cargo(cmd, Path::new("sh")).unwrap_err().to_string(); + + assert!(error.contains("failed with"), "unexpected error: {error}"); + } +} diff --git a/midenc-session/src/options/mod.rs b/midenc-session/src/options/mod.rs index 78e914da36..9716f8364c 100644 --- a/midenc-session/src/options/mod.rs +++ b/midenc-session/src/options/mod.rs @@ -34,6 +34,15 @@ pub struct Options { pub workspace: bool, /// Build the specified packages in the current workspace (used by `cargo miden`) pub packages: Vec, + /// Require Cargo.lock to remain unchanged in the Rust project build. + /// + /// The note codec build does not read this option. That build is session-free, and it + /// resolves its own crate under its own lockfile. + pub cargo_locked: bool, + /// Prevent network access in the Rust project build. + /// + /// The note codec build does not read this option, for the same reason as `cargo_locked`. + pub cargo_offline: bool, /// The name of the current project target being compiled pub target: Option, /// The type of target that was requested @@ -162,6 +171,8 @@ impl Options { profile: "dev".to_string(), workspace: false, packages: vec![], + cargo_locked: false, + cargo_offline: false, target: None, target_type: target, entrypoint: None, diff --git a/sdk/.cargo/config.toml b/sdk/.cargo/config.toml deleted file mode 100644 index 6b509f5b70..0000000000 --- a/sdk/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "wasm32-wasip1" diff --git a/sdk/alloc/.cargo/config.toml b/sdk/alloc/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/alloc/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/base-macros/.cargo/config.toml b/sdk/base-macros/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/base-macros/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/base-macros/Cargo.toml b/sdk/base-macros/Cargo.toml index 423d53d689..5a964540db 100644 --- a/sdk/base-macros/Cargo.toml +++ b/sdk/base-macros/Cargo.toml @@ -20,7 +20,7 @@ default = [] internal-wit-emit = ["dep:wit-component"] [dependencies] -proc-macro2.workspace = true +proc-macro2 = { workspace = true, features = ["span-locations"] } quote.workspace = true miden-assembly-syntax.workspace = true miden-mast-package.workspace = true @@ -33,8 +33,8 @@ syn = { workspace = true, features = ["full"] } heck.workspace = true miden-formatting.workspace = true midenc-frontend-wasm-metadata.workspace = true -wit-bindgen-core = "0.57" -wit-bindgen-rust = { version = "0.57", default-features = false } +wit-bindgen-core.workspace = true +wit-bindgen-rust.workspace = true wit-component = { workspace = true, optional = true } [dev-dependencies] @@ -43,6 +43,8 @@ miden-assembly = { workspace = true, features = ["std"] } miden-protocol = { workspace = true, features = ["std"] } miden-field.workspace = true miden-field-repr.workspace = true +midenc-expect-test.workspace = true +tempfile.workspace = true wit-component.workspace = true [package.metadata.docs.rs] diff --git a/sdk/base-macros/src/component_macro/mod.rs b/sdk/base-macros/src/component_macro/mod.rs index 6039e40a7e..16e9a92017 100644 --- a/sdk/base-macros/src/component_macro/mod.rs +++ b/sdk/base-macros/src/component_macro/mod.rs @@ -400,9 +400,8 @@ fn expand_component_storage( let component_metadata = acc_builder.build(call_site_span.into())?; - let mut metadata_bytes = component_metadata.to_bytes(); - let padded_len = metadata_bytes.len().div_ceil(16) * 16; - metadata_bytes.resize(padded_len, 0); + let metadata_bytes = + midenc_frontend_wasm_metadata::pad_to_link_section_alignment(component_metadata.to_bytes()); let link_section = generate_link_section(&metadata_bytes); let runtime_boilerplate = runtime_boilerplate(); @@ -1465,6 +1464,7 @@ mod tests { wit_name: "struct-a".into(), is_custom: true, path: vec!["StructA".into()], + leading_colon: false, dependencies: Vec::new(), }; @@ -1480,6 +1480,7 @@ mod tests { wit_name: "struct-a".into(), is_custom: true, path: vec!["StructA".into()], + leading_colon: false, dependencies: Vec::new(), }; let prefix = vec!["foo".to_string(), "bar".to_string()]; @@ -1499,6 +1500,7 @@ mod tests { wit_name: "struct-a".into(), is_custom: true, path: vec!["super".into(), "StructA".into()], + leading_colon: false, dependencies: Vec::new(), }; let prefix = vec!["foo".to_string(), "bar".to_string()]; @@ -1527,6 +1529,7 @@ mod tests { #[test] fn build_custom_with_entries_prefers_custom_paths() { let exported_types = vec![ExportedTypeDef { + docs: Vec::new(), rust_name: "StructA".into(), wit_name: "struct-a".into(), kind: ExportedTypeKind::Record { fields: Vec::new() }, diff --git a/sdk/base-macros/src/export_type.rs b/sdk/base-macros/src/export_type.rs index 70f2a9fc6e..41f55a7211 100644 --- a/sdk/base-macros/src/export_type.rs +++ b/sdk/base-macros/src/export_type.rs @@ -1,9 +1,32 @@ +//! Type export and identity guard expansion for `#[export_type]`. + use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::{Item, parse_macro_input}; -use crate::types::{exported_type_from_enum, exported_type_from_struct, register_export_type}; +use crate::types::{ + ExportedTypeDef, custom_type_shape_assertions, export_type_shape_const, + exported_type_from_enum, exported_type_from_struct, nominal_type_identity_guards, + register_export_type, registered_export_type_map, +}; + +/// Builds the guard and identity items emitted next to one exported type. +fn export_type_identity_items( + def: &ExportedTypeDef, + generics: &syn::Generics, + span: proc_macro2::Span, +) -> Result { + let guards = nominal_type_identity_guards(def, span)?; + register_export_type(def.clone(), span)?; + // The registry lookup runs after registration so a self-referential type sees itself. + let registry = registered_export_type_map(); + let assertions = custom_type_shape_assertions(def, ®istry, span)?; + let shape_const = export_type_shape_const(def, generics, span); + Ok(quote! { #guards #shape_const #assertions }) +} +/// Expands `#[export_type]` and registers the annotated record or enum for schema emission. pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> TokenStream { if !attr.is_empty() { return syn::Error::new_spanned( @@ -19,21 +42,19 @@ pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> TokenStream { match item { Item::Struct(item_struct) => { let span = item_struct.ident.span(); - match exported_type_from_struct(&item_struct) { - Ok(def) => match register_export_type(def, span) { - Ok(()) => quote! { #item_struct }.into(), - Err(err) => err.to_compile_error().into(), - }, + match exported_type_from_struct(&item_struct) + .and_then(|def| export_type_identity_items(&def, &item_struct.generics, span)) + { + Ok(items) => quote! { #item_struct #items }.into(), Err(err) => err.to_compile_error().into(), } } Item::Enum(item_enum) => { let span = item_enum.ident.span(); - match exported_type_from_enum(&item_enum) { - Ok(def) => match register_export_type(def, span) { - Ok(()) => quote! { #item_enum }.into(), - Err(err) => err.to_compile_error().into(), - }, + match exported_type_from_enum(&item_enum) + .and_then(|def| export_type_identity_items(&def, &item_enum.generics, span)) + { + Ok(items) => quote! { #item_enum #items }.into(), Err(err) => err.to_compile_error().into(), } } diff --git a/sdk/base-macros/src/lib.rs b/sdk/base-macros/src/lib.rs index 29cce44958..7225a806f5 100644 --- a/sdk/base-macros/src/lib.rs +++ b/sdk/base-macros/src/lib.rs @@ -75,6 +75,7 @@ mod fpi; mod generate; mod manifest_paths; mod note; +mod note_schema; mod script; #[cfg(test)] mod test_support; @@ -285,8 +286,11 @@ pub fn account_procedure( component_macro::expand_account_procedure(attr, item) } -/// Generates an equvalent type in the WIT interface. -/// Required for every type mentioned in the public methods of an account component. +/// Generates an equivalent type in the WIT interface. +/// +/// Use this macro for every custom type in the public methods of an account component. Exported +/// records and enums can also appear in note storage schemas. The macro emits a hidden shape +/// constant that lets `#[note]` confirm the exact Rust type at compile time. /// /// Intended to be used together with `#[component]` attribute macro. #[proc_macro_attribute] @@ -304,6 +308,20 @@ pub fn export_type( /// - the associated inherent `impl` block that contains an entrypoint method annotated with /// `#[note_script]` /// +/// # Note storage schema +/// +/// A named-field `#[note]` struct emits a WIT storage schema into the package's +/// `note_storage_schema` section. Field doc comments are copied into the schema. +/// +/// A note struct must use named fields or no fields. A tuple struct is a compile error. A storage +/// field cannot use `Vec`. Apply `#[export_type]` to each nested custom type, and place that type +/// definition before the `#[note]` struct. Each field must use the exact registered +/// `#[export_type]` Rust type. A different type with the same name fails the hidden shape check. +/// +/// There can be only one `#[note]` struct per linked artifact, so a note crate cannot depend on +/// another note crate. A second struct fails at link time because it defines the duplicate +/// `__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD` symbol. +/// /// # Foreign Procedure Invocation (FPI) /// /// Use `#[account(...)]` on an empty struct to generate typed active and foreign account wrappers diff --git a/sdk/base-macros/src/note.rs b/sdk/base-macros/src/note.rs index ab9ec8749f..e40171e888 100644 --- a/sdk/base-macros/src/note.rs +++ b/sdk/base-macros/src/note.rs @@ -1,3 +1,5 @@ +//! Note script and note storage struct expansion for `#[note]`. + use std::collections::BTreeSet; use heck::{ToKebabCase, ToSnakeCase}; @@ -11,10 +13,11 @@ use syn::{ use crate::{ boilerplate::runtime_boilerplate, + note_schema::{expand_note_storage_schema, note_storage_schema_uniqueness_guard}, types::{TypeRef, map_type_to_type_ref, registered_export_type_map}, util::{ - generate_frontend_link_section, generate_wit_link_section, is_type_named, - is_unit_return_type, + NOTE_NAMED_FIELDS_ERROR, generate_frontend_link_section, generate_wit_link_section, + is_type_named, is_unit_return_type, }, wit_builder::WitBuilder, wit_world::{ManifestPackage, write_world_block}, @@ -123,8 +126,10 @@ fn expand_method_marker_attr( quote!(#item_fn) } +/// Expands a note input struct with felt encoding, decoding, and schema metadata. fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { let struct_ident = &item_struct.ident; + let uniqueness_guard = note_storage_schema_uniqueness_guard(); if !item_struct.generics.params.is_empty() { return syn::Error::new( @@ -133,11 +138,14 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { ) .into_compile_error(); } + if let syn::Fields::Unnamed(fields) = &item_struct.fields { + return syn::Error::new(fields.span(), NOTE_NAMED_FIELDS_ERROR).into_compile_error(); + } let to_felt_repr_impl = note_storage_encoding(&item_struct); - let from_impl = match &item_struct.fields { + let (from_impl, schema_static) = match &item_struct.fields { syn::Fields::Unit => { - quote! { + let from_impl = quote! { impl ::core::convert::TryFrom<&[::miden::Felt]> for #struct_ident { type Error = ::miden::felt_repr::FeltReprError; @@ -148,9 +156,18 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { Ok(Self) } } - } + }; + (from_impl, quote! {}) } syn::Fields::Named(fields) => { + // A schema error replaces the schema static only. Everything else is computable from + // the struct alone, so the macro still emits it. This keeps the schema diagnostic + // alone: neither the use sites nor the `#[note]` `impl` block add "cannot find type" + // or trait-bound errors on top of it. + let schema_static = match expand_note_storage_schema(&item_struct) { + Ok(schema_static) => schema_static, + Err(err) => err.into_compile_error(), + }; let field_inits = fields.named.iter().map(|field| { let ident = field.ident.as_ref().expect("named fields must have identifiers"); let ty = &field.ty; @@ -159,7 +176,7 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { } }); - quote! { + let from_impl = quote! { impl ::core::convert::TryFrom<&[::miden::Felt]> for #struct_ident { type Error = ::miden::felt_repr::FeltReprError; @@ -171,30 +188,10 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { Ok(value) } } - } - } - syn::Fields::Unnamed(fields) => { - let field_inits = fields.unnamed.iter().map(|field| { - let ty = &field.ty; - quote! { - <#ty as ::miden::felt_repr::FromFeltRepr>::from_felt_repr(&mut reader)? - } - }); - - quote! { - impl ::core::convert::TryFrom<&[::miden::Felt]> for #struct_ident { - type Error = ::miden::felt_repr::FeltReprError; - - #[inline(always)] - fn try_from(felts: &[::miden::Felt]) -> Result { - let mut reader = ::miden::felt_repr::FeltReader::new(felts); - let value = Self(#(#field_inits),*); - reader.ensure_eof()?; - Ok(value) - } - } - } + }; + (from_impl, schema_static) } + syn::Fields::Unnamed(_) => unreachable!("tuple note structs are rejected above"), }; quote! { @@ -203,6 +200,8 @@ fn expand_note_struct(item_struct: ItemStruct) -> TokenStream2 { #to_felt_repr_impl impl ::miden::active_note::ActiveNote for #struct_ident {} + #schema_static + #uniqueness_guard } } @@ -226,17 +225,9 @@ fn note_storage_encoding(item_struct: &ItemStruct) -> TokenStream2 { } }) .collect(), - syn::Fields::Unnamed(fields) => fields - .unnamed - .iter() - .enumerate() - .map(|(index, _)| { - let index = syn::Index::from(index); - quote! { - ::miden::felt_repr::ToFeltRepr::write_felt_repr(&self.#index, writer); - } - }) - .collect(), + syn::Fields::Unnamed(_) => { + unreachable!("tuple note structs are rejected before storage encoding") + } }; let writer_ident = if field_writes.is_empty() { @@ -355,8 +346,7 @@ fn expand_note_impl(item_impl: ItemImpl) -> TokenStream2 { Ok(metadata) => metadata, Err(err) => return err.to_compile_error(), }; - let component_package = - format!("miden:{}", metadata.package.name().into_inner().to_kebab_case()); + let component_package = metadata.component_package(); let interface_name = component_package.to_kebab_case(); let world_name = format!("{interface_name}-world"); let interface_module = interface_name.to_snake_case(); @@ -371,7 +361,7 @@ fn expand_note_impl(item_impl: ItemImpl) -> TokenStream2 { let inline_wit = build_note_script_wit( &component_package, - metadata.package.version().inner(), + metadata.component_version(), &interface_name, &world_name, &export_name, @@ -1196,6 +1186,194 @@ mod tests { use syn::parse_quote; use super::*; + use crate::{ + test_support::compile_rust_source, + types::{lock_export_type_registry_for_tests, reset_export_type_registry_for_tests}, + }; + + #[test] + fn named_note_struct_emits_storage_schema_static() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let item_struct: ItemStruct = parse_quote! { + struct PaymentNote { + target: AccountId, + } + }; + + let tokens = expand_note_struct(item_struct).to_string(); + + assert!(tokens.contains("__MIDEN_NOTE_STORAGE_SCHEMA_BYTES")); + assert!(tokens.contains(crate::note_schema::NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD_SYMBOL)); + assert!(tokens.contains("miden_note_schema")); + } + + #[test] + fn unit_note_struct_does_not_emit_storage_schema() { + let item_struct: ItemStruct = parse_quote!( + struct EmptyNote; + ); + + let tokens = expand_note_struct(item_struct).to_string(); + + assert!(!tokens.contains("__MIDEN_NOTE_STORAGE_SCHEMA_BYTES")); + assert!(tokens.contains(crate::note_schema::NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD_SYMBOL)); + } + + #[test] + fn schema_failure_keeps_the_note_struct_next_to_the_error() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let item_struct: ItemStruct = parse_quote! { + struct VecNote { + values: Vec, + } + }; + + let tokens = expand_note_struct(item_struct).to_string(); + + assert!(tokens.contains("compile_error"), "the schema error must be reported: {tokens}"); + assert!(tokens.contains("struct VecNote"), "the struct must survive the error: {tokens}"); + assert!( + !tokens.contains("__MIDEN_NOTE_STORAGE_SCHEMA_BYTES"), + "no schema metadata is generated for a failed schema: {tokens}" + ); + } + + /// Stand-in for the SDK items that a `#[note]` expansion references. + /// + /// `compile_rust_source` runs a bare `rustc` without external crates, so the compiled source + /// declares the SDK surface itself. + const MIDEN_SDK_STUB: &str = r#" +extern crate self as miden; + +#[derive(Debug)] +pub struct Felt; + +pub mod felt_repr { + use super::Felt; + + #[derive(Debug)] + pub struct FeltReprError; + + pub struct FeltReader<'a>(#[allow(dead_code)] &'a [Felt]); + + impl<'a> FeltReader<'a> { + pub fn new(felts: &'a [Felt]) -> Self { + Self(felts) + } + + pub fn ensure_eof(&self) -> Result<(), FeltReprError> { + Ok(()) + } + } + + pub struct FeltWriter<'a>(#[allow(dead_code)] &'a mut Vec); + + pub trait FromFeltRepr: Sized { + fn from_felt_repr(reader: &mut FeltReader<'_>) -> Result; + } + + pub trait ToFeltRepr { + fn write_felt_repr(&self, writer: &mut FeltWriter<'_>); + } + + impl FromFeltRepr for Vec { + fn from_felt_repr(_reader: &mut FeltReader<'_>) -> Result { + Ok(Vec::new()) + } + } + + impl ToFeltRepr for Vec { + fn write_felt_repr(&self, _writer: &mut FeltWriter<'_>) {} + } +} + +pub mod active_note { + use super::Felt; + + pub trait ActiveNote { + fn get_sender(&self) -> Felt { + Felt + } + } + + pub fn get_storage() -> Vec { + Vec::new() + } +} +"#; + + #[test] + fn schema_failure_reports_only_the_schema_diagnostic() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let item_struct: ItemStruct = parse_quote! { + struct VecNote { + values: Vec, + } + }; + let expansion = expand_note_struct(item_struct); + // The whole `#[note]` `impl` expansion cannot compile outside a real SDK crate, because it + // calls the `miden::generate!` and `bindings::export!` proc macros. The note-script body + // below is built with the same generator that the `impl` expansion uses, so the decoding + // and `ActiveNote` use sites are the ones a real note crate gets. + let note_ty: syn::TypePath = parse_quote!(VecNote); + let note_init = note_instantiation(¬e_ty); + let source = format!( + r#" +{MIDEN_SDK_STUB} +mod user {{ + use ::miden::active_note::ActiveNote as _; + + {expansion} + + pub fn takes_note(_note: VecNote) {{}} + + pub fn note_script() {{ + {note_init} + let _ = __miden_note.get_sender(); + }} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!(!output.status.success(), "a failed note schema must fail the compilation"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("`Vec` is not supported in note storage schemas yet"), + "the schema diagnostic is missing: +{stderr}" + ); + assert!( + !stderr.contains("cannot find type"), + "the schema diagnostic must not cascade into missing-type errors: +{stderr}" + ); + assert!( + !stderr.contains("the trait bound"), + "the schema diagnostic must not cascade into trait-bound errors: +{stderr}" + ); + assert!( + !stderr.contains("is not implemented"), + "the schema diagnostic must not cascade into missing-impl errors: +{stderr}" + ); + } + + #[test] + fn tuple_note_struct_requires_named_fields() { + let item_struct: ItemStruct = parse_quote!( + struct TupleNote(Felt); + ); + + let tokens = expand_note_struct(item_struct).to_string(); + + assert!(tokens.contains(NOTE_NAMED_FIELDS_ERROR)); + } #[test] fn entrypoint_signature_allows_non_run_name() { diff --git a/sdk/base-macros/src/note_schema.rs b/sdk/base-macros/src/note_schema.rs new file mode 100644 index 0000000000..b6bd1ddc0a --- /dev/null +++ b/sdk/base-macros/src/note_schema.rs @@ -0,0 +1,1325 @@ +//! Note storage schema generation for `#[note]` structs. + +use std::collections::{BTreeSet, HashMap, HashSet}; + +use heck::ToKebabCase; +use midenc_frontend_wasm_metadata::{ + WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME, pad_to_link_section_alignment, +}; +use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; +use quote::quote; +use semver::Version; +use syn::{ItemStruct, Type, spanned::Spanned}; +use wit_bindgen_core::wit_parser::Resolve; + +use crate::{ + manifest_paths::SDK_WIT_SOURCE, + types::{ + ExportedField, ExportedTypeDef, ExportedTypeKind, TypeRef, custom_type_shape_assertions, + doc_comments, map_type_to_type_ref, nominal_type_identity_guards, registered_export_types, + }, + util::NOTE_NAMED_FIELDS_ERROR, + wit_builder::{WitBody, WitBuilder}, + wit_world::ManifestPackage, +}; + +/// Fully qualified SDK core-types interface imported by generated schemas. +const CORE_TYPES_PACKAGE: &str = "miden:base/core-types@1.0.0"; +/// SDK core-types package name used for the embedded dependency package. +const CORE_TYPES_PACKAGE_NAME: &str = "miden:base"; +/// SDK interface copied into generated schemas. +const CORE_TYPES_INTERFACE: &str = "core-types"; +/// Source name reported for generated schema validation errors. +const NOTE_STORAGE_SCHEMA_SOURCE_NAME: &str = "note-storage-schema.wit"; +/// Storage types accepted by the note schema diagnostic. +const NOTE_STORAGE_SUPPORTED_TYPES: &str = "`u64`, `u32`, `u8`, `bool`, SDK core-type records, \ + `#[export_type]` records or enums, and `Option` \ + over a supported type"; +/// Linker symbol used to reject multiple note storage schemas in one crate. +pub(crate) const NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD_SYMBOL: &str = + "__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD"; + +/// A rendered note storage schema and the Rust definitions that produced it. +struct RenderedNoteStorageSchema { + span: Span, + source: String, + definitions: Vec, +} + +/// Generates the note storage schema custom-section static for a named-field note struct. +pub(crate) fn expand_note_storage_schema( + item_struct: &ItemStruct, +) -> Result { + let package = ManifestPackage::load_or_default(item_struct.ident.span())?; + let registry = registered_export_types(); + let rendered = render_note_storage_schema_with_registry_model( + item_struct, + &package.component_package(), + package.component_version(), + ®istry, + )?; + validate_rendered_note_storage_schema(&rendered)?; + // Identity items reference types as they are written at this `#[note]` site, so only the + // root definition (whose field types are in scope here) is checked. Registry definitions + // carry their own identity items at their `#[export_type]` sites. + let root_definition = rendered + .definitions + .last() + .expect("a rendered schema always contains its root definition"); + let identity_guards = nominal_type_identity_guards(root_definition, rendered.span)?; + let registry_by_rust_name = registry + .iter() + .cloned() + .map(|definition| (definition.rust_name.clone(), definition)) + .collect(); + let shape_assertions = + custom_type_shape_assertions(root_definition, ®istry_by_rust_name, rendered.span)?; + + let bytes = pad_to_link_section_alignment(rendered.source.into_bytes()); + + let bytes_len = bytes.len(); + let encoded_bytes = Literal::byte_string(&bytes); + let macos_link_section = macos_note_storage_schema_link_section(); + + Ok(quote! { + #identity_guards + #shape_assertions + + // Mach-O limits section names to 16 bytes. Wasm uses the canonical section name below. + #[cfg_attr(target_os = "macos", unsafe(link_section = #macos_link_section))] + #[cfg_attr( + not(target_os = "macos"), + unsafe(link_section = #WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME) + )] + #[doc(hidden)] + #[allow(clippy::octal_escapes)] + // No `#[used]`: the section survives without it, and on this target the + // attribute additionally lands the bytes in the module's data segments, + // growing every package by the size of its own schema. + pub static __MIDEN_NOTE_STORAGE_SCHEMA_BYTES: [u8; #bytes_len] = *#encoded_bytes; + }) +} + +/// Derives the Mach-O link-section name from the canonical Wasm custom-section name. +fn macos_note_storage_schema_link_section() -> String { + let (segment, section) = WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME + .split_once(',') + .expect("the note storage schema section name must include a segment"); + let section = section + .get(..section.len().min(16)) + .expect("the note storage schema section name must be ASCII"); + format!("{segment},{section}") +} + +/// Emits a fixed linker symbol that permits one note struct per crate. +pub(crate) fn note_storage_schema_uniqueness_guard() -> TokenStream2 { + quote! { + const _: () = { + // A crate may contain exactly one `#[note]` struct. Reusing a fixed symbol name lets + // the linker reject duplicates across modules. + #[doc(hidden)] + #[used] + #[unsafe(export_name = #NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD_SYMBOL)] + static __miden_note_storage_schema_uniqueness_guard: u8 = 0; + }; + } +} + +/// Renders the note storage schema WIT document for a named-field note struct. +#[cfg(test)] +fn render_note_storage_schema( + item_struct: &ItemStruct, + component_package: &str, + component_version: &Version, +) -> Result { + let registry = registered_export_types(); + Ok(render_note_storage_schema_with_registry_model( + item_struct, + component_package, + component_version, + ®istry, + )? + .source) +} + +/// Renders a note storage schema against one export-type registry snapshot. +#[cfg(test)] +fn render_note_storage_schema_with_registry( + item_struct: &ItemStruct, + component_package: &str, + component_version: &Version, + registry: &[ExportedTypeDef], +) -> Result { + Ok(render_note_storage_schema_with_registry_model( + item_struct, + component_package, + component_version, + registry, + )? + .source) +} + +/// Renders a note storage schema and retains its source-definition context. +fn render_note_storage_schema_with_registry_model( + item_struct: &ItemStruct, + component_package: &str, + component_version: &Version, + registry: &[ExportedTypeDef], +) -> Result { + let registry_by_rust_name = registry + .iter() + .cloned() + .map(|definition| (definition.rust_name.clone(), definition)) + .collect(); + let root = note_root_type(item_struct, ®istry_by_rust_name)?; + let custom_types = referenced_custom_types(&root, registry, item_struct.ident.span())?; + for definition in custom_types.iter().chain([&root]) { + validate_note_storage_definition(definition, item_struct.ident.span())?; + } + let core_imports = required_core_type_imports(&root, &custom_types); + let schema_package = schema_package_name(component_package); + + let mut wit = WitBuilder::new("#[note]", &schema_package, component_version); + if !core_imports.is_empty() { + wit.use_path(CORE_TYPES_PACKAGE); + wit.blank_line(); + } + wit.interface("note-storage", |interface| { + if !core_imports.is_empty() { + interface.line(&format!( + "use core-types.{{{}}};", + core_imports.iter().cloned().collect::>().join(", ") + )); + interface.blank_line(); + } + + for custom_type in &custom_types { + render_type_definition(interface, custom_type); + interface.blank_line(); + } + render_type_definition(interface, &root); + interface.blank_line(); + interface.line(&format!("type storage = {};", root.wit_name)); + }); + wit.blank_line(); + render_core_types_package(&mut wit)?; + + let mut definitions = custom_types; + definitions.push(root); + Ok(RenderedNoteStorageSchema { + span: item_struct.ident.span(), + source: wit.finish(), + definitions, + }) +} + +/// Builds the exported type definition for the note storage root. +fn note_root_type( + item_struct: &ItemStruct, + registry: &HashMap, +) -> Result { + let syn::Fields::Named(named) = &item_struct.fields else { + return Err(syn::Error::new(item_struct.fields.span(), NOTE_NAMED_FIELDS_ERROR)); + }; + + let mut fields = Vec::with_capacity(named.named.len()); + for field in &named.named { + let ident = field.ident.as_ref().expect("named fields must have identifiers"); + let context = format!("field `{ident}` in type `{}`", item_struct.ident); + let ty = map_note_field_type(&field.ty, registry, &context)?; + validate_note_storage_type_ref(&ty, field.ty.span(), &context)?; + fields.push(ExportedField { + docs: doc_comments(&field.attrs), + name: ident.to_string(), + ty, + }); + } + + Ok(ExportedTypeDef { + docs: doc_comments(&item_struct.attrs), + rust_name: item_struct.ident.to_string(), + wit_name: item_struct.ident.to_string().to_kebab_case(), + kind: ExportedTypeKind::Record { fields }, + }) +} + +/// Checks one record or variant against the supported note storage type surface. +fn validate_note_storage_definition( + definition: &ExportedTypeDef, + span: Span, +) -> Result<(), syn::Error> { + match &definition.kind { + ExportedTypeKind::Record { fields } => { + for field in fields { + validate_note_storage_type_ref( + &field.ty, + span, + &format!("field `{}` in type `{}`", field.name, definition.rust_name), + )?; + } + } + ExportedTypeKind::Variant { variants } => { + for variant in variants { + if let Some(payload) = &variant.payload { + validate_note_storage_type_ref( + payload, + span, + &format!( + "variant `{}` in type `{}`", + variant.wit_name, definition.rust_name + ), + )?; + } + } + } + } + Ok(()) +} + +/// Checks one type reference against the supported note storage type surface. +fn validate_note_storage_type_ref( + type_ref: &TypeRef, + span: Span, + context: &str, +) -> Result<(), syn::Error> { + if type_ref.is_custom + || type_ref.is_sdk_core_record() + || matches!(type_ref.wit_name.as_str(), "u64" | "u32" | "u8" | "bool") + { + return Ok(()); + } + + if type_ref.path.last().is_some_and(|segment| segment == "Option") { + let [inner] = type_ref.dependencies.as_slice() else { + return Err(unsupported_note_storage_type(type_ref, span, context)); + }; + return validate_note_storage_type_ref(inner, span, context); + } + + Err(unsupported_note_storage_type(type_ref, span, context)) +} + +/// Builds an actionable error for a type outside the note storage surface. +fn unsupported_note_storage_type(type_ref: &TypeRef, span: Span, context: &str) -> syn::Error { + syn::Error::new( + span, + format!( + "`#[note]` storage {context} uses unsupported WIT type `{}`; supported types are \ + {NOTE_STORAGE_SUPPORTED_TYPES}", + type_ref.wit_name, + ), + ) +} + +/// Resolves the generated WIT before it is embedded in the guest binary. +fn validate_rendered_note_storage_schema( + rendered: &RenderedNoteStorageSchema, +) -> Result<(), syn::Error> { + let mut resolve = Resolve::default(); + resolve + .push_str(NOTE_STORAGE_SCHEMA_SOURCE_NAME, &rendered.source) + .map(|_| ()) + .map_err(|error| { + let message = format!("{error:#}"); + let (span, context) = rendered_schema_error_context(rendered, &message); + syn::Error::new( + span, + format!("failed to resolve note storage schema for {context}: {message}"), + ) + }) +} + +/// Finds the Rust definition associated with a WIT parser diagnostic. +fn rendered_schema_error_context( + rendered: &RenderedNoteStorageSchema, + error: &str, +) -> (Span, String) { + let fallback = rendered + .definitions + .last() + .map(|definition| (rendered.span, format!("type `{}`", definition.rust_name))) + .unwrap_or_else(|| (Span::call_site(), "the `#[note]` storage type".to_string())); + let Some(error_line) = wit_error_line(error).and_then(|line| line.checked_sub(1)) else { + return fallback; + }; + let Some(error_line) = rendered.source.lines().nth(error_line) else { + return fallback; + }; + let error_line = error_line.trim(); + for definition in &rendered.definitions { + let keyword = match &definition.kind { + ExportedTypeKind::Record { .. } => "record", + ExportedTypeKind::Variant { .. } => "variant", + }; + let header = format!("{keyword} {} {{", definition.wit_name); + if error_line == header { + return (rendered.span, format!("type `{}`", definition.rust_name)); + } + match &definition.kind { + ExportedTypeKind::Record { fields } => { + for field in fields { + let field_prefix = format!("{}:", field.name.to_kebab_case()); + if error_line.starts_with(&field_prefix) { + return ( + rendered.span, + format!( + "field `{}` of type `{}` in type `{}`", + field.name, field.ty.wit_name, definition.rust_name + ), + ); + } + } + } + ExportedTypeKind::Variant { variants } => { + for variant in variants { + if error_line.starts_with(&variant.wit_name) { + let payload = variant + .payload + .as_ref() + .map(|payload| format!(" with payload type `{}`", payload.wit_name)) + .unwrap_or_default(); + return ( + rendered.span, + format!( + "variant `{}`{payload} in type `{}`", + variant.wit_name, definition.rust_name + ), + ); + } + } + } + } + } + + fallback +} + +/// Reads the one-based source line from a highlighted WIT parser diagnostic. +fn wit_error_line(error: &str) -> Option { + let marker = format!("{NOTE_STORAGE_SCHEMA_SOURCE_NAME}:"); + error.lines().find_map(|line| { + let (_, location) = line.split_once(&marker)?; + location.split(':').next()?.parse().ok() + }) +} + +/// Maps a Rust field type to WIT syntax with note-specific diagnostics. +fn map_note_field_type( + ty: &Type, + registry: &HashMap, + context: &str, +) -> Result { + if contains_vec(ty) { + return Err(syn::Error::new( + ty.span(), + "`Vec` is not supported in note storage schemas yet", + )); + } + + map_type_to_type_ref(ty, registry).map_err(|err| { + syn::Error::new( + ty.span(), + format!( + "`#[note]` storage {context} is not supported: {err}; supported types are \ + {NOTE_STORAGE_SUPPORTED_TYPES}" + ), + ) + }) +} + +/// Returns true when a path type names `Vec`, either directly or in an angle-bracketed +/// type argument at any nesting depth. +/// +/// The search looks through groups and parentheses. All other type forms, such as +/// references, arrays, slices, tuples, and pointers, give false. +fn contains_vec(ty: &Type) -> bool { + match ty { + Type::Group(group) => contains_vec(&group.elem), + Type::Paren(paren) => contains_vec(&paren.elem), + Type::Path(path) => path.path.segments.iter().any(|segment| { + if segment.ident == "Vec" { + return true; + } + let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else { + return false; + }; + arguments.args.iter().any( + |argument| matches!(argument, syn::GenericArgument::Type(ty) if contains_vec(ty)), + ) + }), + _ => false, + } +} + +/// Returns the registry definitions reachable from the root, in registry order. +fn referenced_custom_types( + root: &ExportedTypeDef, + registry: &[ExportedTypeDef], + span: proc_macro2::Span, +) -> Result, syn::Error> { + let by_wit_name = registry + .iter() + .map(|definition| (definition.wit_name.as_str(), definition)) + .collect::>(); + let mut referenced = HashSet::new(); + visit_definition_types(root, &by_wit_name, &mut referenced, span)?; + + Ok(registry + .iter() + .filter(|definition| referenced.contains(definition.wit_name.as_str())) + .cloned() + .collect()) +} + +/// Visits every type reference in an exported definition. +fn visit_definition_types( + definition: &ExportedTypeDef, + registry: &HashMap<&str, &ExportedTypeDef>, + referenced: &mut HashSet, + span: proc_macro2::Span, +) -> Result<(), syn::Error> { + match &definition.kind { + ExportedTypeKind::Record { fields } => { + for field in fields { + visit_type_ref(&field.ty, registry, referenced, span)?; + } + } + ExportedTypeKind::Variant { variants } => { + for payload in variants.iter().filter_map(|variant| variant.payload.as_ref()) { + visit_type_ref(payload, registry, referenced, span)?; + } + } + } + Ok(()) +} + +/// Visits a type reference and its custom-type dependencies. +fn visit_type_ref( + type_ref: &TypeRef, + registry: &HashMap<&str, &ExportedTypeDef>, + referenced: &mut HashSet, + span: proc_macro2::Span, +) -> Result<(), syn::Error> { + for dependency in &type_ref.dependencies { + visit_type_ref(dependency, registry, referenced, span)?; + } + if !type_ref.is_custom || referenced.contains(&type_ref.wit_name) { + return Ok(()); + } + + let definition = registry.get(type_ref.wit_name.as_str()).ok_or_else(|| { + let rust_name = type_ref.path.last().map(String::as_str).unwrap_or(&type_ref.wit_name); + syn::Error::new( + span, + format!( + "custom type `{rust_name}` in a note storage schema needs #[export_type] on its \ + definition before the #[note] struct" + ), + ) + })?; + referenced.insert(type_ref.wit_name.clone()); + visit_definition_types(definition, registry, referenced, span) +} + +/// Returns the sorted SDK core types used by the rendered schema definitions. +fn required_core_type_imports( + root: &ExportedTypeDef, + custom_types: &[ExportedTypeDef], +) -> BTreeSet { + let mut imports = BTreeSet::new(); + add_definition_core_type_imports(root, &mut imports); + for definition in custom_types { + add_definition_core_type_imports(definition, &mut imports); + } + imports +} + +/// Adds the SDK core types used by one definition to `imports`. +fn add_definition_core_type_imports(definition: &ExportedTypeDef, imports: &mut BTreeSet) { + match &definition.kind { + ExportedTypeKind::Record { fields } => { + for field in fields { + field.ty.add_required_core_type_imports(imports); + } + } + ExportedTypeKind::Variant { variants } => { + for payload in variants.iter().filter_map(|variant| variant.payload.as_ref()) { + payload.add_required_core_type_imports(imports); + } + } + } +} + +/// Writes one record or variant definition to an interface body. +fn render_type_definition(interface: &mut WitBody, definition: &ExportedTypeDef) { + render_docs(interface, &definition.docs); + match &definition.kind { + ExportedTypeKind::Record { fields } => { + interface.block(&format!("record {} {{", definition.wit_name), |record| { + for field in fields { + render_docs(record, &field.docs); + record.line(&format!("{}: {},", field.name.to_kebab_case(), field.ty.wit_name)); + } + }); + } + ExportedTypeKind::Variant { variants } => { + interface.block(&format!("variant {} {{", definition.wit_name), |variant_body| { + for variant in variants { + render_docs(variant_body, &variant.docs); + match &variant.payload { + Some(payload) => variant_body + .line(&format!("{}({}),", variant.wit_name, payload.wit_name)), + None => variant_body.line(&format!("{},", variant.wit_name)), + } + } + }); + } + } +} + +/// Writes Rust doc attribute text as WIT doc comments. +fn render_docs(body: &mut WitBody, docs: &[String]) { + for doc in docs { + for line in doc.split('\n') { + let line = line.strip_suffix('\r').unwrap_or(line); + let line = line.strip_prefix(' ').unwrap_or(line); + if line.is_empty() { + body.line("///"); + } else { + body.line(&format!("/// {line}")); + } + } + } +} + +/// Appends `-schema` to a component package name before any version suffix. +fn schema_package_name(component_package: &str) -> String { + match component_package.split_once('@') { + Some((name, version)) => format!("{name}-schema@{version}"), + None => format!("{component_package}-schema"), + } +} + +/// Writes the embedded SDK `core-types` interface as a braced dependency package. +fn render_core_types_package(wit: &mut WitBuilder) -> Result<(), syn::Error> { + let body = extract_interface_body(SDK_WIT_SOURCE, CORE_TYPES_INTERFACE).ok_or_else(|| { + syn::Error::new( + proc_macro2::Span::call_site(), + "failed to find the core-types interface in the embedded SDK WIT", + ) + })?; + let body = body.strip_prefix('\n').unwrap_or(body); + let body = body.strip_suffix('\n').unwrap_or(body); + + wit.package_block(CORE_TYPES_PACKAGE_NAME, &Version::new(1, 0, 0), |package| { + package.line("interface core-types {"); + for line in body.split('\n') { + package.line(line); + } + package.line("}"); + }); + Ok(()) +} + +/// Extracts a brace-balanced interface body without changing its text. +fn extract_interface_body<'a>(source: &'a str, interface_name: &str) -> Option<&'a str> { + let header = format!("interface {interface_name} {{"); + let body_start = source.find(&header)? + header.len(); + let mut depth = 1usize; + let body = &source[body_start..]; + let bytes = body.as_bytes(); + let mut offset = 0usize; + let mut in_line_comment = false; + let mut block_comment_depth = 0usize; + while offset < bytes.len() { + if in_line_comment { + if bytes[offset] == b'\n' { + in_line_comment = false; + } + offset += 1; + continue; + } + if block_comment_depth > 0 { + if bytes[offset..].starts_with(b"/*") { + block_comment_depth += 1; + offset += 2; + } else if bytes[offset..].starts_with(b"*/") { + block_comment_depth -= 1; + offset += 2; + } else { + offset += 1; + } + continue; + } + if bytes[offset..].starts_with(b"//") { + in_line_comment = true; + offset += 2; + continue; + } + if bytes[offset..].starts_with(b"/*") { + block_comment_depth = 1; + offset += 2; + continue; + } + match bytes[offset] { + b'{' => depth += 1, + b'}' => { + depth -= 1; + if depth == 0 { + return Some(&source[body_start..body_start + offset]); + } + } + _ => {} + } + offset += 1; + } + None +} + +#[cfg(test)] +mod tests { + use midenc_expect_test::expect; + use syn::parse_quote; + use wit_bindgen_core::wit_parser::{Resolve, Type as WitType, TypeDefKind}; + + use super::*; + use crate::types::{ + ExportedVariant, exported_type_from_struct, lock_export_type_registry_for_tests, + map_type_to_type_ref, reset_export_type_registry_for_tests, + }; + + /// Checks that the schema package contains the expected root alias. + fn assert_schema_root(source: &str, expected_root: &str) { + let mut resolve = Resolve::default(); + let package_id = + resolve.push_str("note-schema.wit", source).expect("note schema must parse"); + let package = &resolve.packages[package_id]; + let interface_id = package.interfaces["note-storage"]; + let interface = &resolve.interfaces[interface_id]; + let storage_id = interface.types["storage"]; + let TypeDefKind::Type(WitType::Id(root_id)) = resolve.types[storage_id].kind else { + panic!("storage must be a named type alias"); + }; + assert_eq!(resolve.types[root_id].name.as_deref(), Some(expected_root)); + } + + #[test] + fn extracts_nested_interface_body_verbatim() { + let source = "package test:a;\ninterface target {\n record nested {\n value: \ + u32,\n }\n}\n"; + assert_eq!( + extract_interface_body(source, "target"), + Some("\n record nested {\n value: u32,\n }\n") + ); + assert!(extract_interface_body(SDK_WIT_SOURCE, CORE_TYPES_INTERFACE).is_some()); + } + + #[test] + fn ignores_comment_braces_while_extracting_interface_body() { + let source = + "package test:a;\ninterface core-types {\n /// A closing } before { opening \ + brace.\n /* Another } before { pair. */\n record nested {\n value: \ + u32,\n }\n}\n"; + let source_without_braces = source.replace("} before {", "before"); + let body = extract_interface_body(source, CORE_TYPES_INTERFACE).unwrap(); + let body_without_braces = + extract_interface_body(&source_without_braces, CORE_TYPES_INTERFACE).unwrap(); + + assert_eq!(body.replace("} before {", "before"), body_without_braces); + assert!(body.contains("/// A closing } before { opening brace.")); + assert!(body.contains("/* Another } before { pair. */")); + } + + #[test] + fn uniqueness_guard_expansion_matches_golden() { + let tokens = note_storage_schema_uniqueness_guard().to_string(); + + expect![[r#"const _ : () = { # [doc (hidden)] # [used] # [unsafe (export_name = "__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD")] static __miden_note_storage_schema_uniqueness_guard : u8 = 0 ; } ;"#]].assert_eq(&tokens); + } + + #[test] + fn derives_macos_note_storage_schema_link_section() { + assert_eq!(macos_note_storage_schema_link_section(), "rodata,miden_note_schem"); + } + + #[test] + fn renders_p2id_shaped_schema() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct P2idNote { + target_account_id: AccountId, + } + }; + let source = render_note_storage_schema(¬e, "miden:p2id", &Version::new(0, 1, 0)) + .expect("schema must render"); + + expect![[r#" + // This file is auto-generated by the `#[note]` macro. + // Do not edit this file manually. + + package miden:p2id-schema@0.1.0; + + use miden:base/core-types@1.0.0; + + interface note-storage { + use core-types.{account-id}; + + record p2id-note { + target-account-id: account-id, + } + + type storage = p2id-note; + } + + package miden:base@1.0.0 { + interface core-types { + /// Represents an on-chain felt. + /// + /// Field modulus M = 2^64 - 2^32 + 1. + record felt { + /// The backing type is `f32` which will be treated as a felt by the compiler. + /// We're basically hijacking the Wasm `f32` type and treat as felt. + inner: f32, + } + + + /// A group of four field elements in the Miden base field. + record word { + a: felt, + b: felt, + c: felt, + d: felt, + } + + /// A cryptographic digest representing a 256-bit hash value. + /// This is a wrapper around `word` which contains 4 field elements. + record digest { + inner: word + } + + /// Unique identifier of an account. + /// + /// # Layout + /// + /// An `AccountId` consists of two field elements, where the first is called the prefix and the + /// second is called the suffix. It is laid out as follows: + /// + /// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] + /// suffix: [zero bit | hash (55 bits) | 8 zero bits] + record account-id { + prefix: felt, + suffix: felt + } + + /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) + record recipient { + inner: word + } + + record tag { + inner: felt + } + + /// A fungible or a non-fungible asset. + /// + /// In protocol v0.14 assets are encoded as two words: an asset key and an asset value. + /// + /// The methodology for constructing fungible and non-fungible assets is described below. + /// + /// # Fungible assets + /// - `key`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `[amount, 0, 0, 0]` + /// + /// # Non-fungible assets + /// - `key`: `[hash0, hash1, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `DATA_HASH` + record asset { + key: word, + value: word, + } + + /// A validated fungible asset amount, at most 2^63 - 2^31. + record asset-amount { + inner: felt + } + + /// Account nonce + record nonce { + inner: felt + } + + /// A block height in the chain + record block-number { + inner: felt + } + + /// Account hash + record account-hash { + inner: word + } + + /// Block hash + record block-hash { + inner: word + } + + /// Storage value + record storage-value { + inner: word + } + + /// Account storage root + record storage-root { + inner: word + } + + /// Account code root + record account-code-root { + inner: word + } + + /// Commitment to the account vault + record vault-commitment { + inner: word + } + + /// An index of the created note + record note-idx { + inner: felt + } + + record note-type { + inner: felt + } + + record note-execution-hint { + inner: felt + } + + } + } + "#]].assert_eq(&source); + assert_schema_root(&source, "p2id-note"); + } + + #[test] + fn renders_nested_record_and_enum_schema() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let destination: syn::ItemStruct = parse_quote! { + /// Destination details. + struct Destination { + /// Destination account. + account_id: AccountId, + } + }; + let route: syn::ItemEnum = parse_quote! { + /// Route selection. + enum Route { + /// Send directly. + Direct, + /// Send through a destination. + Via(Destination), + } + }; + let destination = exported_type_from_struct(&destination).expect("record must map"); + let destination_type: Type = parse_quote!(Destination); + let route_payload = map_type_to_type_ref( + &destination_type, + &HashMap::from([(destination.rust_name.clone(), destination.clone())]), + ) + .expect("enum payload must map"); + let route = ExportedTypeDef { + docs: doc_comments(&route.attrs), + rust_name: route.ident.to_string(), + wit_name: route.ident.to_string().to_kebab_case(), + kind: ExportedTypeKind::Variant { + variants: vec![ + ExportedVariant { + docs: doc_comments(&route.variants[0].attrs), + wit_name: "direct".into(), + payload: None, + }, + ExportedVariant { + docs: doc_comments(&route.variants[1].attrs), + wit_name: "via".into(), + payload: Some(route_payload), + }, + ], + }, + }; + let note: ItemStruct = parse_quote! { + /// A routed note. + struct RoutedNote { + /// Selected route. + route: Route, + } + }; + let source = render_note_storage_schema_with_registry( + ¬e, + "miden:routed-note", + &Version::new(2, 3, 4), + &[destination, route], + ) + .expect("schema must render"); + + expect![[r#" + // This file is auto-generated by the `#[note]` macro. + // Do not edit this file manually. + + package miden:routed-note-schema@2.3.4; + + use miden:base/core-types@1.0.0; + + interface note-storage { + use core-types.{account-id}; + + /// Destination details. + record destination { + /// Destination account. + account-id: account-id, + } + + /// Route selection. + variant route { + /// Send directly. + direct, + /// Send through a destination. + via(destination), + } + + /// A routed note. + record routed-note { + /// Selected route. + route: route, + } + + type storage = routed-note; + } + + package miden:base@1.0.0 { + interface core-types { + /// Represents an on-chain felt. + /// + /// Field modulus M = 2^64 - 2^32 + 1. + record felt { + /// The backing type is `f32` which will be treated as a felt by the compiler. + /// We're basically hijacking the Wasm `f32` type and treat as felt. + inner: f32, + } + + + /// A group of four field elements in the Miden base field. + record word { + a: felt, + b: felt, + c: felt, + d: felt, + } + + /// A cryptographic digest representing a 256-bit hash value. + /// This is a wrapper around `word` which contains 4 field elements. + record digest { + inner: word + } + + /// Unique identifier of an account. + /// + /// # Layout + /// + /// An `AccountId` consists of two field elements, where the first is called the prefix and the + /// second is called the suffix. It is laid out as follows: + /// + /// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] + /// suffix: [zero bit | hash (55 bits) | 8 zero bits] + record account-id { + prefix: felt, + suffix: felt + } + + /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) + record recipient { + inner: word + } + + record tag { + inner: felt + } + + /// A fungible or a non-fungible asset. + /// + /// In protocol v0.14 assets are encoded as two words: an asset key and an asset value. + /// + /// The methodology for constructing fungible and non-fungible assets is described below. + /// + /// # Fungible assets + /// - `key`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `[amount, 0, 0, 0]` + /// + /// # Non-fungible assets + /// - `key`: `[hash0, hash1, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `DATA_HASH` + record asset { + key: word, + value: word, + } + + /// A validated fungible asset amount, at most 2^63 - 2^31. + record asset-amount { + inner: felt + } + + /// Account nonce + record nonce { + inner: felt + } + + /// A block height in the chain + record block-number { + inner: felt + } + + /// Account hash + record account-hash { + inner: word + } + + /// Block hash + record block-hash { + inner: word + } + + /// Storage value + record storage-value { + inner: word + } + + /// Account storage root + record storage-root { + inner: word + } + + /// Account code root + record account-code-root { + inner: word + } + + /// Commitment to the account vault + record vault-commitment { + inner: word + } + + /// An index of the created note + record note-idx { + inner: felt + } + + record note-type { + inner: felt + } + + record note-execution-hint { + inner: felt + } + + } + } + "#]].assert_eq(&source); + assert_schema_root(&source, "routed-note"); + } + + #[test] + fn renders_multiline_doc_attributes_as_separate_wit_comments() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + #[doc = " First note line.\n Second note line."] + struct DocumentedNote { + #[doc = " First field line.\n Second field line."] + value: u64, + } + }; + let source = + render_note_storage_schema(¬e, "miden:documented-note", &Version::new(1, 0, 0)) + .expect("multiline docs must render"); + + assert!(source.contains("/// First note line.\n /// Second note line.")); + assert!(source.contains("/// First field line.\n /// Second field line.")); + assert_schema_root(&source, "documented-note"); + } + + #[test] + fn rejects_signed_integer_storage_fields() { + for (rust_type, wit_type) in [("i8", "s8"), ("i16", "s16"), ("i32", "s32"), ("i64", "s64")] + { + assert_unsupported_note_field_type(rust_type, wit_type); + } + } + + #[test] + fn rejects_u16_storage_fields() { + assert_unsupported_note_field_type("u16", "u16"); + } + + #[test] + fn rejects_result_storage_fields() { + assert_unsupported_note_field_type("Result", "result"); + } + + #[test] + fn rejects_other_types_outside_the_storage_allow_list() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct FloatNote { + value: f32, + } + }; + let err = render_note_storage_schema_with_registry( + ¬e, + "miden:float-note", + &Version::new(1, 0, 0), + &[], + ) + .expect_err("types outside the note storage surface must fail"); + + let message = err.to_string(); + assert!(message.contains("field `value` in type `FloatNote`")); + assert!(message.contains("`f32` is not supported")); + assert!(message.contains("supported types are")); + } + + #[test] + fn rejects_unsupported_types_nested_in_options() { + assert_unsupported_note_field_type("Option", "s16"); + } + + #[test] + fn rejects_unsupported_fields_in_nested_records() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let nested: syn::ItemStruct = parse_quote! { + struct Nested { + count: u16, + } + }; + let nested = exported_type_from_struct(&nested).expect("record must map"); + let note: ItemStruct = parse_quote! { + struct NestedNote { + nested: Nested, + } + }; + let err = render_note_storage_schema_with_registry( + ¬e, + "miden:nested-note", + &Version::new(1, 0, 0), + &[nested], + ) + .expect_err("unsupported nested fields must fail"); + + let message = err.to_string(); + assert!(message.contains("field `count` in type `Nested`")); + assert!(message.contains("unsupported WIT type `u16`")); + } + + #[test] + fn expansion_surfaces_wit_parser_errors_with_type_context() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct Type { + value: u64, + } + }; + let err = expand_note_storage_schema(¬e) + .expect_err("a WIT keyword cannot be used as a record name"); + + let message = err.to_string(); + assert!(message.contains("failed to resolve note storage schema")); + assert!(message.contains("type `Type`")); + } + + #[test] + fn expansion_surfaces_wit_parser_errors_with_field_context() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct InvalidFieldNote { + type_: u64, + } + }; + let err = expand_note_storage_schema(¬e) + .expect_err("a WIT keyword cannot be used as a field name"); + + let message = err.to_string(); + assert!(message.contains("failed to resolve note storage schema")); + assert!(message.contains("field `type_` of type `u64`")); + } + + #[test] + fn rejects_tuple_note_structs() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote!( + struct TupleNote(Felt); + ); + let err = render_note_storage_schema(¬e, "miden:tuple-note", &Version::new(1, 0, 0)) + .expect_err("tuple notes must fail"); + + assert_eq!(err.to_string(), NOTE_NAMED_FIELDS_ERROR); + } + + #[test] + fn rejects_vec_fields() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct VecNote { + values: Vec, + } + }; + let err = render_note_storage_schema(¬e, "miden:vec-note", &Version::new(1, 0, 0)) + .expect_err("Vec fields must fail"); + + assert_eq!(err.to_string(), "`Vec` is not supported in note storage schemas yet"); + } + + #[test] + fn rejects_custom_types_registered_after_the_note() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let note: ItemStruct = parse_quote! { + struct CustomNote { + value: MissingType, + } + }; + let err = render_note_storage_schema(¬e, "miden:custom-note", &Version::new(1, 0, 0)) + .expect_err("unregistered custom types must fail"); + + let message = err.to_string(); + assert!(message.contains("#[export_type]")); + assert!(message.contains("before the #[note] struct")); + } + + /// Checks one Rust field type against the note-specific allow-list diagnostic. + fn assert_unsupported_note_field_type(rust_type: &str, expected_wit_type: &str) { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let note: ItemStruct = + syn::parse_str(&format!("struct UnsupportedNote {{ value: {rust_type}, }}")) + .expect("test note must parse"); + let err = render_note_storage_schema_with_registry( + ¬e, + "miden:unsupported-note", + &Version::new(1, 0, 0), + &[], + ) + .expect_err("unsupported note field must fail"); + + let message = err.to_string(); + assert!(message.contains("field `value` in type `UnsupportedNote`")); + assert!(message.contains(&format!("unsupported WIT type `{expected_wit_type}`"))); + assert!(message.contains("supported types are")); + } +} diff --git a/sdk/base-macros/src/test_support.rs b/sdk/base-macros/src/test_support.rs index 3aa27cacfa..904959ba46 100644 --- a/sdk/base-macros/src/test_support.rs +++ b/sdk/base-macros/src/test_support.rs @@ -1,6 +1,12 @@ //! Shared fixtures for base-macros unit tests. -use std::{fs, path::Path, sync::Arc}; +use std::{ + env, fs, + io::Write, + path::Path, + process::{Command, Output, Stdio}, + sync::Arc, +}; use miden_assembly::{Assembler, DefaultSourceManager, ModuleParser, ast::ModuleKind}; use miden_mast_package::Package; @@ -38,3 +44,29 @@ pub(crate) fn write_masp_fixture(package_path: &Path, package_id: &str, wit: Opt .expect("package directory must be created"); fs::write(package_path, package.to_bytes()).expect("package fixture must be written"); } + +/// Compiles one standalone Rust source string and returns the rustc result. +/// +/// The source is compiled to metadata only, so a test can assert on the diagnostics that a macro +/// expansion produces in a real compilation. +pub(crate) fn compile_rust_source(source: &str) -> Output { + let output_dir = tempfile::tempdir().expect("failed to create rustc output directory"); + let output_path = output_dir.path().join("macro_expansion.rmeta"); + let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); + let mut child = Command::new(rustc) + .args(["--crate-name", "macro_expansion", "--edition=2024", "--emit=metadata", "-o"]) + .arg(output_path) + .arg("-") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to start rustc for a macro expansion test"); + child + .stdin + .take() + .expect("rustc stdin must be piped") + .write_all(source.as_bytes()) + .expect("failed to write the macro expansion source"); + child.wait_with_output().expect("failed to wait for rustc") +} diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index 3739f6f669..e05b3bfb6d 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -1,22 +1,34 @@ +//! Rust-to-WIT type mapping and per-crate export registration. + use std::{ collections::{HashMap, HashSet}, sync::{Mutex, OnceLock}, }; -static EXPORTED_TYPES: OnceLock>> = OnceLock::new(); - -use heck::ToKebabCase; -use proc_macro2::Span; -use syn::{ItemStruct, Type, spanned::Spanned}; +use heck::{ToKebabCase, ToUpperCamelCase}; +use proc_macro2::{Span, TokenStream}; +use quote::quote_spanned; +use syn::{Attribute, ItemStruct, Type, spanned::Spanned}; use wit_bindgen_core::wit_parser::Type as WitType; use crate::manifest_paths::SDK_WIT_SOURCE; +/// Exported types grouped by the crate currently being expanded. +static EXPORTED_TYPES: OnceLock>>> = + OnceLock::new(); + +/// Guard that serializes tests which share the process-wide exported-type registry. +#[cfg(test)] +static EXPORTED_TYPES_TEST_LOCK: Mutex<()> = Mutex::new(()); + +/// One Rust type as it was written at the expansion site, with its WIT identity. #[derive(Clone, Debug)] pub(crate) struct TypeRef { pub(crate) wit_name: String, pub(crate) is_custom: bool, pub(crate) path: Vec, + /// True when the written path starts with `::`. + pub(crate) leading_colon: bool, pub(crate) dependencies: Vec, } @@ -35,16 +47,23 @@ impl TypeRef { dependency.add_required_core_type_imports(imports); } } + + /// Returns true when this type is an SDK core-type record. + pub(crate) fn is_sdk_core_record(&self) -> bool { + !self.is_custom && sdk_core_record_names().contains(&self.wit_name) + } } #[derive(Clone, Debug)] pub(crate) struct ExportedField { + pub(crate) docs: Vec, pub(crate) name: String, pub(crate) ty: TypeRef, } #[derive(Clone, Debug)] pub(crate) struct ExportedVariant { + pub(crate) docs: Vec, pub(crate) wit_name: String, pub(crate) payload: Option, } @@ -57,11 +76,34 @@ pub(crate) enum ExportedTypeKind { #[derive(Clone, Debug)] pub(crate) struct ExportedTypeDef { + pub(crate) docs: Vec, pub(crate) rust_name: String, pub(crate) wit_name: String, pub(crate) kind: ExportedTypeKind, } +/// Returns the text stored in `#[doc = "..."]` attributes. +pub(crate) fn doc_comments(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter_map(|attr| { + if !attr.path().is_ident("doc") { + return None; + } + let syn::Meta::NameValue(meta) = &attr.meta else { + return None; + }; + let syn::Expr::Lit(expr) = &meta.value else { + return None; + }; + let syn::Lit::Str(value) = &expr.lit else { + return None; + }; + Some(value.value()) + }) + .collect() +} + /// Represents the types that can be used as storage fields. /// /// During macro expansion struct field types correspond to strings, as types haven't been @@ -72,20 +114,460 @@ pub(crate) enum StorageFieldType { StorageValue, } -pub(crate) fn register_export_type(def: ExportedTypeDef, _span: Span) -> Result<(), syn::Error> { - let registry = EXPORTED_TYPES.get_or_init(|| Mutex::new(Vec::new())); +/// One exported-type registration together with the source location that produced it. +#[derive(Clone, Debug)] +struct RegisteredExportType { + def: ExportedTypeDef, + location: ExpansionLocation, +} + +/// Source location of one macro expansion. +/// +/// The location tells a stale re-expansion of an edited item (same location) from a real +/// conflict between two items (different locations). +// Keep this registry identity/replacement policy aligned with +// sdk/note-codec/macros/src/registry.rs; changes must land in both. +type ExpansionLocation = (String, usize, usize); + +/// Returns the (file, line, column) location of one expansion span. +fn expansion_location(span: Span) -> ExpansionLocation { + let start = span.start(); + (span.file(), start.line, start.column) +} + +/// Returns the key of the crate whose macro expansion is running. +/// +/// Long-lived macro hosts such as the rust-analyzer proc-macro server expand many crates in +/// one process; the key keeps their registrations apart. +fn macro_invocation_crate_key() -> String { + std::env::var("CARGO_MANIFEST_DIR") + .or_else(|_| std::env::var("CARGO_PKG_NAME")) + .unwrap_or_default() +} + +/// Registers one exported type while preserving the first definition seen by the macro process. +pub(crate) fn register_export_type(def: ExportedTypeDef, span: Span) -> Result<(), syn::Error> { + let registry = EXPORTED_TYPES.get_or_init(|| Mutex::new(HashMap::new())); let mut registry = registry.lock().expect("mutex poisoned"); - if let Some(existing) = registry.iter_mut().find(|existing| existing.wit_name == def.wit_name) { - *existing = def; + let entries = registry.entry(macro_invocation_crate_key()).or_default(); + register_export_type_in(entries, def, span, expansion_location(span)) +} + +/// Applies exported-type identity rules to one registry snapshot. +fn register_export_type_in( + registry: &mut Vec, + def: ExportedTypeDef, + span: Span, + location: ExpansionLocation, +) -> Result<(), syn::Error> { + if let Some(existing) = + registry.iter_mut().find(|existing| existing.def.wit_name == def.wit_name) + { + if existing.location == location { + if existing.def.rust_name == def.rust_name + && exported_type_shapes_match(&existing.def, &def) + { + // rust-analyzer can expand the same attribute more than once in one process. + return Ok(()); + } + // A long-lived macro host re-expanded an edited item; replace the stale shape. + existing.def = def; + return Ok(()); + } + // Two different items that map to one WIT name are ambiguous even with equal + // shapes: a note field referring to the shared name cannot say which one it means. + + let identity = if existing.def.rust_name == def.rust_name { + format!("Rust type `{}`", def.rust_name) + } else { + format!("Rust types `{}` and `{}` both map to", existing.def.rust_name, def.rust_name) + }; + return Err(syn::Error::new( + span, + format!( + "conflicting #[export_type] registration: {identity} WIT type `{}` with different \ + identity or shape; the earlier registration is `{}`, while this registration is \ + `{}`. Rename one type or make both registrations structurally identical. If this \ + error appears in your IDE after an edit, restart the rust-analyzer proc-macro \ + server", + def.wit_name, + describe_exported_type_shape(&existing.def), + describe_exported_type_shape(&def), + ), + )); + } + registry.push(RegisteredExportType { def, location }); + Ok(()) +} + +/// Returns true when two definitions render the same structural WIT type. +fn exported_type_shapes_match(left: &ExportedTypeDef, right: &ExportedTypeDef) -> bool { + match (&left.kind, &right.kind) { + ( + ExportedTypeKind::Record { + fields: left_fields, + }, + ExportedTypeKind::Record { + fields: right_fields, + }, + ) => { + left_fields.len() == right_fields.len() + && left_fields.iter().zip(right_fields).all(|(left, right)| { + left.name.to_kebab_case() == right.name.to_kebab_case() + && type_ref_shapes_match(&left.ty, &right.ty) + }) + } + ( + ExportedTypeKind::Variant { + variants: left_variants, + }, + ExportedTypeKind::Variant { + variants: right_variants, + }, + ) => { + left_variants.len() == right_variants.len() + && left_variants.iter().zip(right_variants).all(|(left, right)| { + left.wit_name == right.wit_name + && match (&left.payload, &right.payload) { + (Some(left), Some(right)) => type_ref_shapes_match(left, right), + (None, None) => true, + _ => false, + } + }) + } + _ => false, + } +} + +/// Returns true when two references resolve to the same WIT identity and generic shape. +fn type_ref_shapes_match(left: &TypeRef, right: &TypeRef) -> bool { + left.wit_name == right.wit_name + && left.is_custom == right.is_custom + && left.dependencies.len() == right.dependencies.len() + && left + .dependencies + .iter() + .zip(&right.dependencies) + .all(|(left, right)| type_ref_shapes_match(left, right)) +} + +/// Formats the canonical structural shape of one exported definition. +/// +/// The text serves conflict diagnostics and the compile-time shape checks that pin a written +/// type to its `#[export_type]` registration, so it must stay stable and structural. +pub(crate) fn describe_exported_type_shape(def: &ExportedTypeDef) -> String { + match &def.kind { + ExportedTypeKind::Record { fields } => format!( + "record {} {{ {} }}", + def.wit_name, + fields + .iter() + .map(|field| format!("{}: {}", field.name.to_kebab_case(), field.ty.wit_name)) + .collect::>() + .join(", ") + ), + ExportedTypeKind::Variant { variants } => format!( + "variant {} {{ {} }}", + def.wit_name, + variants + .iter() + .map(|variant| match &variant.payload { + Some(payload) => format!("{}({})", variant.wit_name, payload.wit_name), + None => variant.wit_name.clone(), + }) + .collect::>() + .join(", ") + ), + } +} + +/// Emits nominal identity checks for names the classifier trusts without resolution. +/// +/// Procedural macros cannot resolve identifiers. Two classes of names are pinned: +/// SDK core-type names (a genuine `miden::Word` import passes, a local same-named type +/// fails unless registered with `#[export_type]`), and the builtin names `Option`, +/// `Result`, and the primitives (proven to BE the `::core` definitions). Both checks stop +/// the emitted WIT shape from drifting from the encoded Rust type. +pub(crate) fn nominal_type_identity_guards( + definition: &ExportedTypeDef, + span: Span, +) -> Result { + let mut guarded = HashSet::new(); + let mut guards = TokenStream::new(); + visit_exported_type_refs(definition, &mut |type_ref| { + collect_sdk_core_type_identity_guard(type_ref, span, &mut guarded, &mut guards)?; + collect_builtin_type_identity_guard(type_ref, span, &mut guarded, &mut guards) + })?; + Ok(guards) +} + +/// Appends one nominal builtin identity check when a builtin-named reference is not guarded. +/// +/// `Option`, `Result`, and the primitive names are classified by their last path segment, so +/// a same-named foreign or shadowing type could silently change the encoded layout. The check +/// proves the written type IS the `::core` builtin, mirroring the SDK core-type guard. +fn collect_builtin_type_identity_guard( + type_ref: &TypeRef, + span: Span, + guarded: &mut HashSet<(String, String)>, + guards: &mut TokenStream, +) -> Result<(), syn::Error> { + let Some(canonical) = builtin_canonical_type_text(type_ref) else { + return Ok(()); + }; + let written = written_type_text(type_ref); + if !guarded.insert((written.clone(), canonical.clone())) { return Ok(()); } - registry.push(def); + let written = parse_reconstructed_type(&written, span)?; + let canonical = parse_reconstructed_type(&canonical, span)?; + guards.extend(quote_spanned! {span=> + const _: fn() = || { + fn __miden_builtin_type_name_collision_use_the_core_type( + _: ::core::marker::PhantomData, + _: ::core::marker::PhantomData, + ) {} + __miden_builtin_type_name_collision_use_the_core_type( + ::core::marker::PhantomData::<#written>, + ::core::marker::PhantomData::<#canonical>, + ); + }; + }); + Ok(()) +} + +/// Returns the `::core` spelling of a builtin-named reference, or None for other types. +fn builtin_canonical_type_text(type_ref: &TypeRef) -> Option { + if type_ref.is_custom { + return None; + } + let last = type_ref.path.last()?; + match (last.as_str(), type_ref.dependencies.as_slice()) { + ("Option", [inner]) => { + Some(format!("::core::option::Option<{}>", written_type_text(inner))) + } + ("Result", [ok, err]) => Some(format!( + "::core::result::Result<{}, {}>", + written_type_text(ok), + written_type_text(err) + )), + (ident, []) if rust_type_to_wit_type(ident).is_some() => { + Some(format!("::core::primitive::{ident}")) + } + _ => None, + } +} + +/// Reconstructs the Rust source text of a reference as it was written. +fn written_type_text(type_ref: &TypeRef) -> String { + // The unit type is recorded with an empty path (see the tuple arm of + // `map_type_to_type_ref`); render it as `()` so reconstructed generics parse. + if type_ref.path.is_empty() { + return "()".to_string(); + } + let path = written_path(type_ref); + match (type_ref.path.last().map(String::as_str), type_ref.dependencies.as_slice()) { + (Some("Option"), [inner]) => format!("{path}<{}>", written_type_text(inner)), + (Some("Result"), [ok, err]) => { + format!("{path}<{}, {}>", written_type_text(ok), written_type_text(err)) + } + _ => path, + } +} + +/// Returns the path of a reference as it was written, keeping any leading `::`. +fn written_path(type_ref: &TypeRef) -> String { + let path = type_ref.path.join("::"); + if type_ref.leading_colon { + format!("::{path}") + } else { + path + } +} + +/// Parses one reconstructed type text back into a type for guard emission. +fn parse_reconstructed_type(text: &str, span: Span) -> Result { + syn::parse_str::(text).map_err(|error| { + syn::Error::new( + span, + format!("failed to reconstruct type `{text}` for an identity check: {error}"), + ) + }) +} + +/// Visits every type reference contained in one exported definition. +fn visit_exported_type_refs( + definition: &ExportedTypeDef, + visitor: &mut impl FnMut(&TypeRef) -> Result<(), syn::Error>, +) -> Result<(), syn::Error> { + match &definition.kind { + ExportedTypeKind::Record { fields } => { + for field in fields { + visit_type_ref_dependencies(&field.ty, visitor)?; + } + } + ExportedTypeKind::Variant { variants } => { + for payload in variants.iter().filter_map(|variant| variant.payload.as_ref()) { + visit_type_ref_dependencies(payload, visitor)?; + } + } + } + Ok(()) +} + +/// Visits one type reference and every nested generic dependency. +fn visit_type_ref_dependencies( + type_ref: &TypeRef, + visitor: &mut impl FnMut(&TypeRef) -> Result<(), syn::Error>, +) -> Result<(), syn::Error> { + visitor(type_ref)?; + for dependency in &type_ref.dependencies { + visit_type_ref_dependencies(dependency, visitor)?; + } + Ok(()) +} + +/// Appends one nominal SDK identity check when a core-type path has not already been guarded. +fn collect_sdk_core_type_identity_guard( + type_ref: &TypeRef, + span: Span, + guarded: &mut HashSet<(String, String)>, + guards: &mut TokenStream, +) -> Result<(), syn::Error> { + if !type_ref.requires_core_type_import() { + return Ok(()); + } + + let rust_path = written_path(type_ref); + if !guarded.insert((rust_path.clone(), type_ref.wit_name.clone())) { + return Ok(()); + } + let rust_path = syn::parse_str::(&rust_path).map_err(|error| { + syn::Error::new( + span, + format!("failed to reconstruct SDK core-type path for an identity check: {error}"), + ) + })?; + let sdk_ident = syn::Ident::new(&type_ref.wit_name.to_upper_camel_case(), span); + guards.extend(quote_spanned! {span=> + const _: fn() = || { + fn __miden_core_type_name_collision_use_sdk_type_or_add_export_type( + _: ::core::marker::PhantomData, + _: ::core::marker::PhantomData, + ) {} + __miden_core_type_name_collision_use_sdk_type_or_add_export_type( + ::core::marker::PhantomData::<#rust_path>, + ::core::marker::PhantomData::<::miden::#sdk_ident>, + ); + }; + }); + Ok(()) +} + +/// Emits the hidden constant that records the structural shape of an exported type. +/// +/// Compile-time checks read this constant through a written type path, so a type that only +/// shares the registered name cannot pass for the registered type. +pub(crate) fn export_type_shape_const( + def: &ExportedTypeDef, + generics: &syn::Generics, + span: Span, +) -> TokenStream { + let ident = syn::Ident::new(&def.rust_name, span); + let shape = describe_exported_type_shape(def); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + quote_spanned! {span=> + impl #impl_generics #ident #ty_generics #where_clause { + #[doc(hidden)] + pub const __MIDEN_EXPORT_TYPE_SHAPE: &'static str = #shape; + } + } +} + +/// Emits compile-time checks that pin written custom types to their registrations. +/// +/// Each check reads the shape constant through the type path as it is written at the +/// expansion site. A type that is not the registered type fails to compile, either because +/// it has no shape constant or because its shape text differs. +pub(crate) fn custom_type_shape_assertions( + definition: &ExportedTypeDef, + registry: &HashMap, + span: Span, +) -> Result { + let mut asserted = HashSet::new(); + let mut checks = TokenStream::new(); + visit_exported_type_refs(definition, &mut |type_ref| { + collect_custom_type_shape_assertion(type_ref, registry, span, &mut asserted, &mut checks) + })?; + Ok(checks) +} + +/// Appends one shape check when a custom type path has not already been checked. +fn collect_custom_type_shape_assertion( + type_ref: &TypeRef, + registry: &HashMap, + span: Span, + asserted: &mut HashSet, + checks: &mut TokenStream, +) -> Result<(), syn::Error> { + if !type_ref.is_custom { + return Ok(()); + } + let written_path = written_path(type_ref); + if !asserted.insert(written_path.clone()) { + return Ok(()); + } + let Some(rust_name) = type_ref.path.last() else { + return Ok(()); + }; + let Some(registered) = registry.get(rust_name) else { + // The schema resolution path reports unregistered custom types with full context. + return Ok(()); + }; + let expected = describe_exported_type_shape(registered); + let path = syn::parse_str::(&written_path).map_err(|error| { + syn::Error::new( + span, + format!("failed to reconstruct the path of custom type `{written_path}`: {error}"), + ) + })?; + let message = format!( + "type `{written_path}` does not match the #[export_type] registration named `{}`; write \ + the registered type here or rename one of the types", + registered.rust_name + ); + checks.extend(quote_spanned! {span=> + const _: () = { + const fn __miden_shape_text_eq(left: &str, right: &str) -> bool { + let (left, right) = (left.as_bytes(), right.as_bytes()); + if left.len() != right.len() { + return false; + } + let mut index = 0; + while index < left.len() { + if left[index] != right[index] { + return false; + } + index += 1; + } + true + } + assert!( + __miden_shape_text_eq(<#path>::__MIDEN_EXPORT_TYPE_SHAPE, #expected), + #message + ); + }; + }); Ok(()) } pub(crate) fn registered_export_types() -> Vec { - let registry = EXPORTED_TYPES.get_or_init(|| Mutex::new(Vec::new())); - registry.lock().expect("mutex poisoned").clone() + let registry = EXPORTED_TYPES.get_or_init(|| Mutex::new(HashMap::new())); + let registry = registry.lock().expect("mutex poisoned"); + registry + .get(¯o_invocation_crate_key()) + .map(|entries| entries.iter().map(|entry| entry.def.clone()).collect()) + .unwrap_or_default() } pub(crate) fn registered_export_type_map() -> HashMap { @@ -120,6 +602,7 @@ pub(crate) fn map_type_to_type_ref( let path_segments: Vec = path.path.segments.iter().map(|segment| segment.ident.to_string()).collect(); + let leading_colon = path.path.leading_colon.is_some(); reject_unsupported_component_primitive(&ident, last.span())?; @@ -133,6 +616,7 @@ pub(crate) fn map_type_to_type_ref( wit_name, is_custom: false, path: path_segments, + leading_colon, dependencies: vec![inner], }); } @@ -147,6 +631,7 @@ pub(crate) fn map_type_to_type_ref( wit_name, is_custom: false, path: path_segments, + leading_colon, dependencies: vec![ok, err], }); } @@ -164,6 +649,7 @@ pub(crate) fn map_type_to_type_ref( wit_name: wit_type_name(wit_type).to_string(), is_custom: false, path: path_segments, + leading_colon, dependencies: Vec::new(), }); } @@ -173,6 +659,7 @@ pub(crate) fn map_type_to_type_ref( wit_name, is_custom: true, path: path_segments, + leading_colon, dependencies: Vec::new(), }); } @@ -182,6 +669,7 @@ pub(crate) fn map_type_to_type_ref( wit_name, is_custom: false, path: path_segments, + leading_colon, dependencies: Vec::new(), }); } @@ -190,6 +678,7 @@ pub(crate) fn map_type_to_type_ref( wit_name, is_custom: true, path: path_segments, + leading_colon, dependencies: Vec::new(), }) } @@ -247,6 +736,7 @@ fn map_result_argument_type_to_type_ref( wit_name: "_".to_string(), is_custom: false, path: Vec::new(), + leading_colon: false, dependencies: Vec::new(), }), _ => map_type_to_type_ref(ty, exported_types), @@ -308,6 +798,17 @@ fn sdk_core_type_names() -> &'static HashSet { NAMES.get_or_init(|| parse_wit_type_names(SDK_WIT_SOURCE)) } +/// Returns the record names declared by the SDK core-types WIT document. +fn sdk_core_record_names() -> &'static HashSet { + static NAMES: OnceLock> = OnceLock::new(); + NAMES.get_or_init(|| { + SDK_WIT_SOURCE + .lines() + .filter_map(|line| extract_wit_type_name(line.trim_start(), "record")) + .collect() + }) +} + fn parse_wit_type_names(source: &str) -> HashSet { let mut names = HashSet::new(); for line in source.lines() { @@ -367,18 +868,21 @@ pub(crate) fn exported_type_from_struct( })?; let field_ty = map_type_to_type_ref(&field.ty, &known_exported)?; fields.push(ExportedField { + docs: doc_comments(&field.attrs), name: field_ident.to_string(), ty: field_ty, }); } Ok(ExportedTypeDef { + docs: doc_comments(&item_struct.attrs), rust_name: item_struct.ident.to_string(), wit_name: item_struct.ident.to_string().to_kebab_case(), kind: ExportedTypeKind::Record { fields }, }) } syn::Fields::Unit => Ok(ExportedTypeDef { + docs: doc_comments(&item_struct.attrs), rust_name: item_struct.ident.to_string(), wit_name: item_struct.ident.to_string().to_kebab_case(), kind: ExportedTypeKind::Record { fields: Vec::new() }, @@ -421,10 +925,15 @@ pub(crate) fn exported_type_from_enum( } }; - variants.push(ExportedVariant { wit_name, payload }); + variants.push(ExportedVariant { + docs: doc_comments(&variant.attrs), + wit_name, + payload, + }); } Ok(ExportedTypeDef { + docs: doc_comments(&item_enum.attrs), rust_name: item_enum.ident.to_string(), wit_name: item_enum.ident.to_string().to_kebab_case(), kind: ExportedTypeKind::Variant { variants }, @@ -456,6 +965,14 @@ pub(crate) fn ensure_custom_type_defined( Ok(()) } +#[cfg(test)] +/// Serializes tests that mutate the process-global exported-type registry. +pub(crate) fn lock_export_type_registry_for_tests() -> std::sync::MutexGuard<'static, ()> { + EXPORTED_TYPES_TEST_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + #[cfg(test)] pub(crate) fn reset_export_type_registry_for_tests() { if let Some(registry) = EXPORTED_TYPES.get() { diff --git a/sdk/base-macros/src/types/tests.rs b/sdk/base-macros/src/types/tests.rs index e0759c727a..5c325fb6d5 100644 --- a/sdk/base-macros/src/types/tests.rs +++ b/sdk/base-macros/src/types/tests.rs @@ -3,9 +3,11 @@ use std::collections::HashSet; use syn::parse_quote; use super::*; +use crate::test_support::compile_rust_source; #[test] fn emits_hint_for_missing_export_type() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("LocalType").unwrap(); let exported = HashMap::new(); @@ -21,6 +23,7 @@ fn emits_hint_for_missing_export_type() { #[test] fn allows_sdk_type_without_export_attribute() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("Asset").unwrap(); let exported = HashMap::new(); @@ -35,6 +38,7 @@ fn allows_sdk_type_without_export_attribute() { #[test] fn allows_block_number_without_export_attribute() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("BlockNumber").unwrap(); let exported = HashMap::new(); @@ -49,6 +53,7 @@ fn allows_block_number_without_export_attribute() { #[test] fn allows_nonce_without_export_attribute() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("Nonce").unwrap(); let exported = HashMap::new(); @@ -63,6 +68,7 @@ fn allows_nonce_without_export_attribute() { #[test] fn allows_asset_amount_without_export_attribute() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("AssetAmount").unwrap(); let exported = HashMap::new(); @@ -77,6 +83,7 @@ fn allows_asset_amount_without_export_attribute() { #[test] fn allows_wit_primitive_type_without_export_attribute() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let ty: Type = syn::parse_str("u64").unwrap(); let exported = HashMap::new(); @@ -91,6 +98,7 @@ fn allows_wit_primitive_type_without_export_attribute() { #[test] fn struct_fields_allow_wit_primitive_types() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let item: syn::ItemStruct = parse_quote! { struct Foo { @@ -113,8 +121,42 @@ fn struct_fields_allow_wit_primitive_types() { } } +#[test] +fn exported_types_capture_doc_attributes() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let item_struct: syn::ItemStruct = parse_quote! { + /// Record documentation. + struct DocumentedRecord { + /// Field documentation. + value: Felt, + } + }; + let record = exported_type_from_struct(&item_struct).expect("record definition should parse"); + assert_eq!(record.docs, vec![" Record documentation."]); + let ExportedTypeKind::Record { fields } = record.kind else { + panic!("expected record kind"); + }; + assert_eq!(fields[0].docs, vec![" Field documentation."]); + + let item_enum: syn::ItemEnum = parse_quote! { + /// Variant documentation. + enum DocumentedVariant { + /// Case documentation. + Case, + } + }; + let variant = exported_type_from_enum(&item_enum).expect("variant definition should parse"); + assert_eq!(variant.docs, vec![" Variant documentation."]); + let ExportedTypeKind::Variant { variants } = variant.kind else { + panic!("expected variant kind"); + }; + assert_eq!(variants[0].docs, vec![" Case documentation."]); +} + #[test] fn maps_rust_primitive_types_to_wit_types() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let exported_names = HashSet::new(); @@ -144,6 +186,7 @@ fn maps_rust_primitive_types_to_wit_types() { #[test] fn rejects_unsupported_component_primitives() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); @@ -161,6 +204,7 @@ fn rejects_unsupported_component_primitives() { #[test] fn rejects_unsupported_component_primitives_nested_in_option_or_result() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); @@ -178,6 +222,7 @@ fn rejects_unsupported_component_primitives_nested_in_option_or_result() { #[test] fn maps_rust_option_type_to_wit_option() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let exported_names = HashSet::new(); @@ -193,6 +238,7 @@ fn maps_rust_option_type_to_wit_option() { #[test] fn option_type_tracks_nested_core_type_imports() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let ty: Type = syn::parse_str("Option").unwrap(); @@ -207,6 +253,7 @@ fn option_type_tracks_nested_core_type_imports() { #[test] fn option_type_validates_nested_custom_type() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let ty: Type = syn::parse_str("Option").unwrap(); @@ -223,6 +270,7 @@ fn option_type_validates_nested_custom_type() { #[test] fn maps_rust_result_type_to_wit_result() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let exported_names = HashSet::new(); @@ -238,6 +286,7 @@ fn maps_rust_result_type_to_wit_result() { #[test] fn result_type_tracks_nested_core_type_imports() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let ty: Type = syn::parse_str("Result").unwrap(); @@ -252,6 +301,7 @@ fn result_type_tracks_nested_core_type_imports() { #[test] fn result_type_validates_nested_custom_type() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let ty: Type = syn::parse_str("Result").unwrap(); @@ -268,6 +318,7 @@ fn result_type_validates_nested_custom_type() { #[test] fn result_type_maps_unit_argument_to_wit_placeholder() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let exported = HashMap::new(); let ty: Type = syn::parse_str("Result<(), Felt>").unwrap(); @@ -278,6 +329,7 @@ fn result_type_maps_unit_argument_to_wit_placeholder() { #[test] fn struct_field_missing_export_type_hint() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let item: syn::ItemStruct = parse_quote! { struct Foo { @@ -300,6 +352,7 @@ fn struct_field_missing_export_type_hint() { #[test] fn enum_payload_missing_export_type_hint() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let item: syn::ItemEnum = parse_quote! { enum Foo { @@ -326,6 +379,7 @@ fn enum_payload_missing_export_type_hint() { #[test] fn forward_reference_between_export_types_is_allowed() { + let _registry_guard = lock_export_type_registry_for_tests(); reset_export_type_registry_for_tests(); let first: syn::ItemStruct = parse_quote! { @@ -351,3 +405,495 @@ fn forward_reference_between_export_types_is_allowed() { panic!("expected record kind"); } } + +#[test] +fn rejects_same_name_different_shape_export_type_registration() { + let first: syn::ItemStruct = parse_quote! { + struct Fee { + amount: u64, + } + }; + let second: syn::ItemStruct = parse_quote! { + struct Fee { + amount: Word, + } + }; + let mut registry = Vec::new(); + register_export_type_in( + &mut registry, + exported_type_from_struct(&first).unwrap(), + Span::call_site(), + ("tests.rs".to_string(), 1, 0), + ) + .unwrap(); + + let error = register_export_type_in( + &mut registry, + exported_type_from_struct(&second).unwrap(), + Span::call_site(), + ("tests.rs".to_string(), 2, 0), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains("conflicting #[export_type] registration")); + assert!(error.contains("record fee { amount: u64 }")); + assert!(error.contains("record fee { amount: word }")); + assert!(error.contains("Rename one type or make both registrations structurally identical")); + assert!(error.contains("restart the rust-analyzer proc-macro server")); + assert_eq!(registry.len(), 1); +} + +#[test] +fn allows_same_shape_export_type_reregistration() { + let first: syn::ItemStruct = parse_quote! { + /// Documentation from rustc's expansion. + struct Fee { + amount: u64, + } + }; + let second: syn::ItemStruct = parse_quote! { + /// Documentation from rust-analyzer's expansion. + struct Fee { + amount: u64, + } + }; + let mut registry = Vec::new(); + + register_export_type_in( + &mut registry, + exported_type_from_struct(&first).unwrap(), + Span::call_site(), + ("tests.rs".to_string(), 1, 0), + ) + .unwrap(); + register_export_type_in( + &mut registry, + exported_type_from_struct(&second).unwrap(), + Span::call_site(), + ("tests.rs".to_string(), 1, 0), + ) + .unwrap(); + + assert_eq!(registry.len(), 1); + assert_eq!(registry[0].def.docs, vec![" Documentation from rustc's expansion."]); +} + +#[test] +fn same_shape_registrations_from_different_locations_conflict() { + let first: syn::ItemStruct = parse_quote! { + struct Fee { + amount: u64, + } + }; + let second: syn::ItemStruct = parse_quote! { + struct Fee { + amount: u64, + } + }; + let mut registry = Vec::new(); + + register_export_type_in( + &mut registry, + exported_type_from_struct(&first).unwrap(), + Span::call_site(), + ("tests.rs".to_string(), 1, 0), + ) + .unwrap(); + // A second item with the same name is ambiguous even when the shapes are equal. + let error = register_export_type_in( + &mut registry, + exported_type_from_struct(&second).unwrap(), + Span::call_site(), + ("tests.rs".to_string(), 2, 0), + ) + .unwrap_err() + .to_string(); + + assert!(error.contains("conflicting #[export_type] registration")); + assert_eq!(registry.len(), 1); +} + +#[test] +fn same_location_reregistration_replaces_a_stale_shape() { + let first: syn::ItemStruct = parse_quote! { + struct Fee { + amount: u64, + } + }; + let second: syn::ItemStruct = parse_quote! { + struct Fee { + amount: u32, + } + }; + let mut registry = Vec::new(); + let location = ("tests.rs".to_string(), 1, 0); + + register_export_type_in( + &mut registry, + exported_type_from_struct(&first).unwrap(), + Span::call_site(), + location.clone(), + ) + .unwrap(); + // A long-lived macro host re-expands the edited item from the same location. + register_export_type_in( + &mut registry, + exported_type_from_struct(&second).unwrap(), + Span::call_site(), + location, + ) + .unwrap(); + + assert_eq!(registry.len(), 1); + assert_eq!(describe_exported_type_shape(®istry[0].def), "record fee { amount: u32 }"); +} + +#[test] +fn bare_core_type_name_collision_fails_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + value: Word, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +extern crate self as miden; +pub struct Word; +mod user {{ + pub struct Word; + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!(!output.status.success(), "a local `Word` must fail the SDK identity guard"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("__miden_core_type_name_collision_use_sdk_type_or_add_export_type"), + "identity diagnostic is not actionable:\n{stderr}" + ); +} + +#[test] +fn bare_sdk_core_type_import_passes_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + value: Word, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +extern crate self as miden; +pub struct Word; +mod user {{ + use crate::Word; + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!( + output.status.success(), + "a genuine SDK import must pass the identity guard:\n{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn unregistered_same_named_type_fails_the_shape_check() { + let registered: syn::ItemStruct = parse_quote! { + struct Price { + amount: u64, + } + }; + let registered_def = exported_type_from_struct(®istered).unwrap(); + let root: syn::ItemStruct = parse_quote! { + struct Root { + price: other::Price, + } + }; + let root_def = exported_type_from_struct(&root).unwrap(); + let registry = HashMap::from([("Price".to_string(), registered_def.clone())]); + let shape_const = + export_type_shape_const(®istered_def, &syn::Generics::default(), Span::call_site()); + let assertions = custom_type_shape_assertions(&root_def, ®istry, Span::call_site()).unwrap(); + let source = format!( + r#" +mod registered {{ + pub struct Price {{ pub amount: u64 }} + {shape_const} +}} +mod other {{ + pub struct Price {{ pub amount: u32 }} +}} +{assertions} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!( + !output.status.success(), + "a type without #[export_type] must fail the shape check" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("__MIDEN_EXPORT_TYPE_SHAPE"), + "the missing shape constant is not reported: +{stderr}" + ); +} + +#[test] +fn same_named_type_with_a_different_shape_fails_the_shape_check() { + let registered: syn::ItemStruct = parse_quote! { + struct Price { + amount: u64, + } + }; + let impostor: syn::ItemStruct = parse_quote! { + struct Price { + amount: u32, + } + }; + let registered_def = exported_type_from_struct(®istered).unwrap(); + let impostor_def = exported_type_from_struct(&impostor).unwrap(); + let root: syn::ItemStruct = parse_quote! { + struct Root { + price: other::Price, + } + }; + let root_def = exported_type_from_struct(&root).unwrap(); + let registry = HashMap::from([("Price".to_string(), registered_def.clone())]); + let registered_const = + export_type_shape_const(®istered_def, &syn::Generics::default(), Span::call_site()); + let impostor_const = + export_type_shape_const(&impostor_def, &syn::Generics::default(), Span::call_site()); + let assertions = custom_type_shape_assertions(&root_def, ®istry, Span::call_site()).unwrap(); + let source = format!( + r#" +mod registered {{ + pub struct Price {{ pub amount: u64 }} + {registered_const} +}} +mod other {{ + pub struct Price {{ pub amount: u32 }} + {impostor_const} +}} +{assertions} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!(!output.status.success(), "a different shape must fail the shape check"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("does not match the #[export_type] registration"), + "the shape diagnostic is not actionable: +{stderr}" + ); +} + +#[test] +fn the_registered_type_passes_the_shape_check() { + let registered: syn::ItemStruct = parse_quote! { + struct Price { + amount: u64, + } + }; + let registered_def = exported_type_from_struct(®istered).unwrap(); + let root: syn::ItemStruct = parse_quote! { + struct Root { + price: registered::Price, + } + }; + let root_def = exported_type_from_struct(&root).unwrap(); + let registry = HashMap::from([("Price".to_string(), registered_def.clone())]); + let shape_const = + export_type_shape_const(®istered_def, &syn::Generics::default(), Span::call_site()); + let assertions = custom_type_shape_assertions(&root_def, ®istry, Span::call_site()).unwrap(); + let source = format!( + r#" +mod registered {{ + pub struct Price {{ pub amount: u64 }} + {shape_const} +}} +{assertions} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!( + output.status.success(), + "the registered type must pass the shape check: +{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn foreign_option_type_fails_the_builtin_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + value: fake::Option, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +mod fake {{ + #[allow(dead_code)] + pub struct Option(pub T); +}} +mod user {{ + use crate::fake; + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!(!output.status.success(), "a foreign `Option` must fail the builtin guard"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("__miden_builtin_type_name_collision_use_the_core_type"), + "builtin diagnostic is not actionable: +{stderr}" + ); +} + +#[test] +fn a_shadowed_primitive_fails_the_builtin_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + value: u64, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +mod user {{ + #[allow(non_camel_case_types, dead_code)] + pub struct u64; + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!(!output.status.success(), "a shadowed primitive must fail the builtin guard"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("__miden_builtin_type_name_collision_use_the_core_type"), + "builtin diagnostic is not actionable: +{stderr}" + ); +} + +#[test] +fn unit_result_shapes_pass_the_builtin_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + done: Result<(), u32>, + partial: Result, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +mod user {{ + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!( + output.status.success(), + "unit result shapes must pass the builtin guard: +{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn genuine_builtin_types_pass_the_builtin_identity_guard() { + let item: syn::ItemStruct = parse_quote! { + struct NoteFields { + maybe: Option, + either: core::result::Result, + flag: bool, + } + }; + let definition = exported_type_from_struct(&item).unwrap(); + let guards = nominal_type_identity_guards(&definition, Span::call_site()).unwrap(); + let source = format!( + r#" +mod user {{ + {guards} +}} +fn main() {{}} +"# + ); + + let output = compile_rust_source(&source); + assert!( + output.status.success(), + "genuine builtins must pass the builtin guard: +{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn leading_colon_path_round_trips() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let exported = HashMap::new(); + + let ty: Type = syn::parse_str("::core::primitive::u64").unwrap(); + let type_ref = map_type_to_type_ref(&ty, &exported).expect("primitive should resolve"); + assert!(type_ref.leading_colon); + assert_eq!(written_type_text(&type_ref), "::core::primitive::u64"); + // A fully qualified primitive reconstructs to the canonical text, so the builtin + // identity guard compares one type with itself. + assert_eq!( + builtin_canonical_type_text(&type_ref).as_deref(), + Some("::core::primitive::u64") + ); + + let ty: Type = syn::parse_str("u64").unwrap(); + let type_ref = map_type_to_type_ref(&ty, &exported).expect("primitive should resolve"); + assert!(!type_ref.leading_colon); + assert_eq!(written_type_text(&type_ref), "u64"); +} + +#[test] +fn leading_colon_generic_path_round_trips() { + let _registry_guard = lock_export_type_registry_for_tests(); + reset_export_type_registry_for_tests(); + let exported = HashMap::new(); + + let ty: Type = syn::parse_str("::core::option::Option<::core::primitive::u32>").unwrap(); + let type_ref = map_type_to_type_ref(&ty, &exported).expect("option should resolve"); + + assert!(type_ref.leading_colon); + assert_eq!(written_type_text(&type_ref), "::core::option::Option<::core::primitive::u32>"); +} diff --git a/sdk/base-macros/src/util.rs b/sdk/base-macros/src/util.rs index 625aec103d..0596cd9d5a 100644 --- a/sdk/base-macros/src/util.rs +++ b/sdk/base-macros/src/util.rs @@ -19,6 +19,9 @@ const FRONTEND_METADATA_BYTES_STATIC_IDENT: &str = "__miden_frontend_metadata_by /// Linker symbol used to reject multiple frontend-marked procedures in one project. pub(crate) const FRONTEND_METADATA_UNIQUENESS_GUARD_SYMBOL: &str = "__MIDEN_FRONTEND_METADATA_UNIQUENESS_GUARD"; +/// Diagnostic emitted when `#[note]` is applied to a tuple struct. +pub(crate) const NOTE_NAMED_FIELDS_ERROR: &str = + "#[note] requires named fields; tuple structs are no longer supported"; /// Returns true if a function's return type is unit. pub(crate) fn is_unit_return_type(output: &syn::ReturnType) -> bool { diff --git a/sdk/base-macros/src/wit_builder.rs b/sdk/base-macros/src/wit_builder.rs index 0f9ee25cc3..9feb76414e 100644 --- a/sdk/base-macros/src/wit_builder.rs +++ b/sdk/base-macros/src/wit_builder.rs @@ -49,6 +49,20 @@ impl WitBuilder { result } + /// Writes a braced `package` block and returns the closure result. + pub(crate) fn package_block( + &mut self, + name: &str, + version: &Version, + build: impl FnOnce(&mut WitBody) -> T, + ) -> T { + let mut body = WitBody::new(); + let result = build(&mut body); + let package = package_with_version(name, version); + self.push_block(format!("package {package} {{"), body.finish()); + result + } + /// Finishes rendering and returns the final WIT source. pub(crate) fn finish(self) -> String { self.source.to_string() + "\n" @@ -154,6 +168,12 @@ mod tests { wit.world("foo-world", |world| { world.line("export foo;"); }); + wit.blank_line(); + wit.package_block("miden:dependency", &Version::new(2, 0, 0), |package| { + package.block("interface dependency {", |interface| { + interface.line("type value = u32;"); + }); + }); let expected = r#"// This file is auto-generated by the `#[test]` macro. // Do not edit this file manually. @@ -173,6 +193,12 @@ interface foo { world foo-world { export foo; } + +package miden:dependency@2.0.0 { + interface dependency { + type value = u32; + } +} "#; assert_eq!(wit.finish(), expected); diff --git a/sdk/base-macros/tests/note_trailing_data.rs b/sdk/base-macros/tests/note_trailing_data.rs index 310e54100f..f44e798a9f 100644 --- a/sdk/base-macros/tests/note_trailing_data.rs +++ b/sdk/base-macros/tests/note_trailing_data.rs @@ -16,10 +16,6 @@ pub mod active_note { pub trait ActiveNote {} } -#[derive(Debug)] -#[note] -struct UnitNote; - #[derive(Debug)] #[note] struct OneFeltNote { @@ -27,14 +23,6 @@ struct OneFeltNote { a: miden::Felt, } -#[test] -fn unit_note_rejects_trailing_data() { - let felts = [miden::Felt::new(0).unwrap()]; - - let err = UnitNote::try_from(felts.as_slice()).unwrap_err(); - assert_eq!(err, miden::felt_repr::FeltReprError::TrailingData { pos: 0, len: 1 }); -} - #[test] fn note_struct_rejects_trailing_data() { let felts = [miden::Felt::new(1).unwrap(), miden::Felt::new(2).unwrap()]; diff --git a/sdk/base-macros/tests/unit_note_trailing_data.rs b/sdk/base-macros/tests/unit_note_trailing_data.rs new file mode 100644 index 0000000000..34f4cb0656 --- /dev/null +++ b/sdk/base-macros/tests/unit_note_trailing_data.rs @@ -0,0 +1,33 @@ +//! Tests trailing-data rejection for unit note structs. +//! +//! This stays separate because one crate may hold only one `#[note]` struct. + +use core::convert::TryFrom; + +use miden_base_macros::note; + +extern crate self as miden; + +pub use miden_field::Felt; + +pub mod felt_repr { + pub use miden_field_repr::{FeltReader, FeltReprError, FeltWriter, FromFeltRepr, ToFeltRepr}; +} + +pub mod active_note { + /// Minimal stand-in for the SDK `ActiveNote` trait implemented by the `#[note]` struct + /// expansion. + pub trait ActiveNote {} +} + +#[derive(Debug)] +#[note] +struct UnitNote; + +#[test] +fn unit_note_rejects_trailing_data() { + let felts = [miden::Felt::new(0).unwrap()]; + + let err = UnitNote::try_from(felts.as_slice()).unwrap_err(); + assert_eq!(err, miden::felt_repr::FeltReprError::TrailingData { pos: 0, len: 1 }); +} diff --git a/sdk/base-macros/wit/miden.wit b/sdk/base-macros/wit/miden.wit index 719aae4e5f..20f25ecbf9 100644 --- a/sdk/base-macros/wit/miden.wit +++ b/sdk/base-macros/wit/miden.wit @@ -40,9 +40,6 @@ interface core-types { suffix: felt } - /// Creates a new account ID from a field element. - //account-id-from-felt: func(felt: felt) -> account-id; - /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) record recipient { inner: word diff --git a/sdk/base-sys/.cargo/config.toml b/sdk/base-sys/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/base-sys/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/base/.cargo/config.toml b/sdk/base/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/base/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/field-repr/derive/.cargo/config.toml b/sdk/field-repr/derive/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/field-repr/derive/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/field-repr/derive/src/lib.rs b/sdk/field-repr/derive/src/lib.rs index 236c5e9074..5a0050a69e 100644 --- a/sdk/field-repr/derive/src/lib.rs +++ b/sdk/field-repr/derive/src/lib.rs @@ -130,7 +130,7 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::{ - Data, DeriveInput, Error, Field, Fields, Index, Variant, parse_macro_input, + Data, DeriveInput, Error, Field, Fields, Index, LitStr, Variant, parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::Comma, }; @@ -249,6 +249,9 @@ fn ensure_no_explicit_discriminants( /// Enums are encoded as a `u32` tag (variant ordinal, starting from `0`) /// followed by the selected variant payload encoded in declaration order. /// +/// Use `#[felt_repr(crate_path = "path::to::felt_repr")]` on the type to override the generated +/// runtime crate path. +/// /// # Example /// /// ```ignore @@ -260,15 +263,13 @@ fn ensure_no_explicit_discriminants( /// pub suffix: Felt, /// } /// ``` -#[proc_macro_derive(DeriveFromFeltRepr)] +#[proc_macro_derive(DeriveFromFeltRepr, attributes(felt_repr))] pub fn derive_from_felt_repr(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - - let expanded = derive_from_felt_repr_impl( - &input, - quote!(miden_field_repr), - quote!(miden_field_repr::Felt), - ); + let expanded = felt_repr_crate_path(&input).and_then(|felt_repr_crate| { + let felt_ty = quote!(#felt_repr_crate::Felt); + derive_from_felt_repr_impl(&input, felt_repr_crate, felt_ty) + }); match expanded { Ok(ts) => ts, Err(err) => err.into_compile_error().into(), @@ -411,6 +412,9 @@ fn derive_from_felt_repr_impl( /// Enums are encoded as a `u32` tag (variant ordinal, starting from `0`) /// followed by the selected variant payload encoded in declaration order. /// +/// Use `#[felt_repr(crate_path = "path::to::felt_repr")]` on the type to override the generated +/// runtime crate path. +/// /// # Example /// /// ```ignore @@ -422,16 +426,38 @@ fn derive_from_felt_repr_impl( /// pub suffix: Felt, /// } /// ``` -#[proc_macro_derive(DeriveToFeltRepr)] +#[proc_macro_derive(DeriveToFeltRepr, attributes(felt_repr))] pub fn derive_to_felt_repr(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - match derive_to_felt_repr_impl(&input, quote!(miden_field_repr)) { + match felt_repr_crate_path(&input) + .and_then(|felt_repr_crate| derive_to_felt_repr_impl(&input, felt_repr_crate)) + { Ok(ts) => ts, Err(err) => err.into_compile_error().into(), } } +/// Returns the felt representation crate path selected by a generated facade. +fn felt_repr_crate_path(input: &DeriveInput) -> Result { + let mut selected = None; + for attr in input.attrs.iter().filter(|attr| attr.path().is_ident("felt_repr")) { + attr.parse_nested_meta(|meta| { + if !meta.path.is_ident("crate_path") { + return Err(meta.error("expected `crate_path = \"path\"`")); + } + if selected.is_some() { + return Err(meta.error("`crate_path` is already set")); + } + let literal: LitStr = meta.value()?.parse()?; + let path: syn::Path = literal.parse()?; + selected = Some(quote!(#path)); + Ok(()) + })?; + } + Ok(selected.unwrap_or_else(|| quote!(miden_field_repr))) +} + fn derive_to_felt_repr_impl( input: &DeriveInput, felt_repr_crate: TokenStream2, diff --git a/sdk/field-repr/repr/.cargo/config.toml b/sdk/field-repr/repr/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/field-repr/repr/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/field-repr/tests/.cargo/config.toml b/sdk/field-repr/tests/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/field-repr/tests/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/note-bindings/Cargo.toml b/sdk/note-bindings/Cargo.toml new file mode 100644 index 0000000000..4392696adc --- /dev/null +++ b/sdk/note-bindings/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "miden-note-bindings" +description = "Typed host bindings for Miden note storage schemas" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true + +[lib] +doctest = false + +[dependencies] +miden-field.workspace = true +miden-field-repr.workspace = true +miden-note-bindings-macros.workspace = true +miden-note-schema.workspace = true +miden-protocol = { workspace = true, features = ["std"] } + +[dev-dependencies] +miden-mast-package = { workspace = true, features = ["std"] } +midenc-frontend-wasm-metadata.workspace = true +midenc-integration-test-support.workspace = true +tempfile.workspace = true diff --git a/sdk/note-bindings/macros/Cargo.toml b/sdk/note-bindings/macros/Cargo.toml new file mode 100644 index 0000000000..522c421b45 --- /dev/null +++ b/sdk/note-bindings/macros/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "miden-note-bindings-macros" +description = "Procedural macros for typed Miden note storage bindings" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true + +[lib] +proc-macro = true +doctest = false + +[dependencies] +miden-note-schema.workspace = true +miden-note-schema-codegen.workspace = true +midenc-frontend-wasm-metadata.workspace = true +proc-macro2.workspace = true +proc-macro-crate = { workspace = true } +quote.workspace = true +syn.workspace = true + +[dev-dependencies] +midenc-expect-test.workspace = true +prettyplease = { workspace = true } diff --git a/sdk/note-bindings/macros/expected/custom.rs b/sdk/note-bindings/macros/expected/custom.rs new file mode 100644 index 0000000000..9cdf61093a --- /dev/null +++ b/sdk/note-bindings/macros/expected/custom.rs @@ -0,0 +1,649 @@ +#[doc(hidden)] +mod __miden_note_bindings_a3280bdaca3ec21e { + #[doc(hidden)] + trait __MidenNoteEncode { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()>; + } + #[doc(hidden)] + trait __MidenNoteDecode: ::core::marker::Sized { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result; + } + impl __MidenNoteEncode for u64 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for u64 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u64), + ), + ) + }) + } + } + impl __MidenNoteEncode for u32 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for u32 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u32), + ), + ) + }) + } + } + impl __MidenNoteEncode for u8 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for u8 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u8), + ), + ) + }) + } + } + impl __MidenNoteEncode for bool { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for bool { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(bool), + ), + ) + }) + } + } + impl __MidenNoteEncode for ::miden_note_bindings::__private::miden_field::Felt { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for ::miden_note_bindings::__private::miden_field::Felt { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_note_bindings::__private::miden_field::Felt), + ), + ) + }) + } + } + impl __MidenNoteEncode for ::miden_note_bindings::__private::miden_field::Word { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for ::miden_note_bindings::__private::miden_field::Word { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_note_bindings::__private::miden_field::Word), + ), + ) + }) + } + } + impl __MidenNoteEncode + for ::miden_note_bindings::__private::miden_protocol::account::AccountId { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + writer.write(self.prefix().as_felt()); + writer.write(self.suffix()); + Ok(()) + } + } + impl __MidenNoteDecode + for ::miden_note_bindings::__private::miden_protocol::account::AccountId { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let prefix = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode account-id prefix: {error}"), + ) + })?; + let suffix = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode account-id suffix: {error}"), + ) + })?; + ::miden_note_bindings::__private::miden_protocol::account::AccountId::try_from_elements( + suffix, + prefix, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid account-id in note storage: {error}"), + ) + }) + } + } + impl __MidenNoteEncode + for ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::from(*self)); + Ok(()) + } + } + impl __MidenNoteDecode + for ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let value = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode asset-amount: {error}"), + ) + })?; + ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount::try_from( + value, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid asset-amount in note storage: {error}"), + ) + }) + } + } + impl __MidenNoteEncode for ::core::option::Option + where + T: __MidenNoteEncode, + { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + match self { + None => { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::ZERO) + } + Some(value) => { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::ONE); + value.__write_note_felts(writer)?; + } + } + Ok(()) + } + } + impl __MidenNoteDecode for ::core::option::Option + where + T: __MidenNoteDecode, + { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let tag = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode option tag: {error}"), + ) + })?; + match tag.as_canonical_u64() { + 0 => Ok(None), + 1 => Ok(Some(T::__read_note_felts(reader)?)), + tag => { + Err( + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid option tag {tag}; expected 0 or 1"), + ), + ) + } + } + } + } + ///Rust binding for WIT type `example:dex-schema/note-storage@1.0.0.dex-note`. + #[derive( + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, + )] + pub struct DexNote { + ///Value of the WIT `target` field. + pub target: ::miden_note_bindings::__private::miden_protocol::account::AccountId, + ///Value of the WIT `kind` field. + pub kind: OrderKind, + } + impl DexNote { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.dex-note"; + } + impl __MidenNoteEncode for DexNote { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + self.target.__write_note_felts(writer)?; + self.kind.__write_note_felts(writer)?; + Ok(()) + } + } + impl __MidenNoteDecode for DexNote { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + Ok(Self { + target: <::miden_note_bindings::__private::miden_protocol::account::AccountId as __MidenNoteDecode>::__read_note_felts( + reader, + )?, + kind: ::__read_note_felts(reader)?, + }) + } + } + ///Selects order execution. + #[derive( + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr, + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr, + )] + #[felt_repr(crate_path = "::miden_note_bindings::__private::miden_field_repr")] + pub enum OrderKind { + ///WIT `market` case. + Market, + ///WIT `limit` case. + Limit(LimitPrice), + } + impl OrderKind { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.order-kind"; + } + impl __MidenNoteEncode for OrderKind { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + match self { + Self::Market => { + writer + .write( + ::miden_note_bindings::__private::miden_field::Felt::from_u32( + 0u32, + ), + ); + } + Self::Limit(value) => { + writer + .write( + ::miden_note_bindings::__private::miden_field::Felt::from_u32( + 1u32, + ), + ); + value.__write_note_felts(writer)?; + } + } + Ok(()) + } + } + impl __MidenNoteDecode for OrderKind { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let tag = reader + .read_u32() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} tag: {error}", stringify!(OrderKind), + ), + ) + })?; + match tag { + 0u32 => Ok(Self::Market), + 1u32 => { + Ok( + Self::Limit( + ::__read_note_felts(reader)?, + ), + ) + } + tag => { + Err( + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "invalid {} tag {tag}; expected a declaration ordinal below {}", + stringify!(OrderKind), 2usize, + ), + ), + ) + } + } + } + } + ///A ratio used as an order limit. + #[derive( + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr, + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr, + )] + #[felt_repr(crate_path = "::miden_note_bindings::__private::miden_field_repr")] + pub struct LimitPrice { + ///Value of the WIT `numerator` field. + pub numerator: u64, + ///Value of the WIT `denominator` field. + pub denominator: u64, + } + impl LimitPrice { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:dex-schema/note-storage@1.0.0.limit-price"; + } + impl __MidenNoteEncode for LimitPrice { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + self.numerator.__write_note_felts(writer)?; + self.denominator.__write_note_felts(writer)?; + Ok(()) + } + } + impl __MidenNoteDecode for LimitPrice { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + Ok(Self { + numerator: ::__read_note_felts(reader)?, + denominator: ::__read_note_felts(reader)?, + }) + } + } + #[doc(hidden)] + const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = "\npackage example:dex-schema@1.0.0;\n\nuse miden:base/core-types@1.0.0;\n\ninterface note-storage {\n use core-types.{account-id};\n\n /// A ratio used as an order limit.\n record limit-price {\n numerator: u64,\n denominator: u64,\n }\n\n /// Selects order execution.\n variant order-kind {\n market,\n limit(limit-price),\n }\n\n record dex-note {\n target: account-id,\n kind: order-kind,\n }\n\n type storage = dex-note;\n}\n\npackage miden:base@1.0.0 {\n interface core-types {\n record felt { inner: f32 }\n record account-id { prefix: felt, suffix: felt }\n }\n}\n"; + /// Returns the resolved schema, which is parsed once for the whole process. + #[doc(hidden)] + fn __miden_note_storage_schema() -> ::miden_note_bindings::__private::miden_note_schema::Result< + &'static ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, + > { + static __MIDEN_NOTE_STORAGE_SCHEMA: ::std::sync::OnceLock< + ::miden_note_bindings::__private::miden_note_schema::Result< + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, + >, + > = ::std::sync::OnceLock::new(); + __MIDEN_NOTE_STORAGE_SCHEMA + .get_or_init(|| { + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) + }) + .as_ref() + .map_err(::core::clone::Clone::clone) + } + impl DexNote { + /// Encodes this typed value as note storage in WIT declaration order. + pub fn to_note_storage( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::miden_note_bindings::__private::miden_note_schema::NoteStorage, + > { + let mut felts = ::std::vec::Vec::new(); + self.__write_note_felts( + &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter::new( + &mut felts, + ), + )?; + ::miden_note_bindings::__private::miden_note_schema::NoteStorage::new(felts) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to create note storage: {error}"), + ) + }) + } + /// Decodes this typed value from complete note storage. + pub fn from_note_storage( + storage: &::miden_note_bindings::__private::miden_note_schema::NoteStorage, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let mut reader = ::miden_note_bindings::__private::miden_field_repr::FeltReader::new( + storage.items(), + ); + let value = Self::__read_note_felts(&mut reader)?; + reader + .ensure_eof() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("note storage has trailing data: {error}"), + ) + })?; + Ok(value) + } + /// Builds a typed value with a caller-provided codec registry. + pub fn from_str_values_with( + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let schema = __miden_note_storage_schema()?; + let mut builder = schema.builder_with_registry(codecs); + for (path, value) in values { + builder = builder.set(path, value)?; + } + let storage = builder.build()?; + Self::from_note_storage(&storage) + } + /// Builds a typed value with the standard codec registry. + pub fn from_str_values( + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + Self::from_str_values_with(values, &codecs) + } + /// Validates this value with structural rules and caller-provided codecs. + pub fn validate_with( + &self, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + let storage = self.to_note_storage()?; + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(()) + } + /// Validates this value with the standard codec registry. + pub fn validate( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.validate_with(&codecs) + } + /// Displays this value with caller-provided codecs and structural fallbacks. + pub fn display_with( + &self, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::std::string::String, + > { + let storage = self.to_note_storage()?; + let decoded = __miden_note_storage_schema()? + .decode_with_registry(&storage, codecs)?; + Ok(decoded.to_string()) + } + /// Displays this value with standard codecs and structural fallbacks. + pub fn display( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::std::string::String, + > { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.display_with(&codecs) + } + } +} +pub use __miden_note_bindings_a3280bdaca3ec21e::{DexNote, OrderKind, LimitPrice}; diff --git a/sdk/note-bindings/macros/expected/p2id.rs b/sdk/note-bindings/macros/expected/p2id.rs new file mode 100644 index 0000000000..eac74f37ef --- /dev/null +++ b/sdk/note-bindings/macros/expected/p2id.rs @@ -0,0 +1,515 @@ +#[doc(hidden)] +mod __miden_note_bindings_f74ea5e7a6e77b2d { + #[doc(hidden)] + trait __MidenNoteEncode { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()>; + } + #[doc(hidden)] + trait __MidenNoteDecode: ::core::marker::Sized { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result; + } + impl __MidenNoteEncode for u64 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for u64 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u64), + ), + ) + }) + } + } + impl __MidenNoteEncode for u32 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for u32 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u32), + ), + ) + }) + } + } + impl __MidenNoteEncode for u8 { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for u8 { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(u8), + ), + ) + }) + } + } + impl __MidenNoteEncode for bool { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for bool { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(bool), + ), + ) + }) + } + } + impl __MidenNoteEncode for ::miden_note_bindings::__private::miden_field::Felt { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for ::miden_note_bindings::__private::miden_field::Felt { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_note_bindings::__private::miden_field::Felt), + ), + ) + }) + } + } + impl __MidenNoteEncode for ::miden_note_bindings::__private::miden_field::Word { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + ::miden_note_bindings::__private::miden_field_repr::ToFeltRepr::write_felt_repr( + self, + writer, + ); + Ok(()) + } + } + impl __MidenNoteDecode for ::miden_note_bindings::__private::miden_field::Word { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + ::miden_note_bindings::__private::miden_field_repr::FromFeltRepr::from_felt_repr( + reader, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!( + "failed to decode {} from note storage: {error}", + stringify!(::miden_note_bindings::__private::miden_field::Word), + ), + ) + }) + } + } + impl __MidenNoteEncode + for ::miden_note_bindings::__private::miden_protocol::account::AccountId { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + writer.write(self.prefix().as_felt()); + writer.write(self.suffix()); + Ok(()) + } + } + impl __MidenNoteDecode + for ::miden_note_bindings::__private::miden_protocol::account::AccountId { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let prefix = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode account-id prefix: {error}"), + ) + })?; + let suffix = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode account-id suffix: {error}"), + ) + })?; + ::miden_note_bindings::__private::miden_protocol::account::AccountId::try_from_elements( + suffix, + prefix, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid account-id in note storage: {error}"), + ) + }) + } + } + impl __MidenNoteEncode + for ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::from(*self)); + Ok(()) + } + } + impl __MidenNoteDecode + for ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let value = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode asset-amount: {error}"), + ) + })?; + ::miden_note_bindings::__private::miden_protocol::asset::AssetAmount::try_from( + value, + ) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid asset-amount in note storage: {error}"), + ) + }) + } + } + impl __MidenNoteEncode for ::core::option::Option + where + T: __MidenNoteEncode, + { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + match self { + None => { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::ZERO) + } + Some(value) => { + writer + .write(::miden_note_bindings::__private::miden_field::Felt::ONE); + value.__write_note_felts(writer)?; + } + } + Ok(()) + } + } + impl __MidenNoteDecode for ::core::option::Option + where + T: __MidenNoteDecode, + { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let tag = reader + .read() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to decode option tag: {error}"), + ) + })?; + match tag.as_canonical_u64() { + 0 => Ok(None), + 1 => Ok(Some(T::__read_note_felts(reader)?)), + tag => { + Err( + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("invalid option tag {tag}; expected 0 or 1"), + ), + ) + } + } + } + } + ///Rust binding for WIT type `example:p2id-schema/note-storage@1.0.0.p2id-note`. + #[derive( + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, + )] + pub struct P2idNote { + ///Value of the WIT `target-account-id` field. + pub target_account_id: ::miden_note_bindings::__private::miden_protocol::account::AccountId, + } + impl P2idNote { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = "example:p2id-schema/note-storage@1.0.0.p2id-note"; + } + impl __MidenNoteEncode for P2idNote { + fn __write_note_felts( + &self, + writer: &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + self.target_account_id.__write_note_felts(writer)?; + Ok(()) + } + } + impl __MidenNoteDecode for P2idNote { + fn __read_note_felts( + reader: &mut ::miden_note_bindings::__private::miden_field_repr::FeltReader< + '_, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + Ok(Self { + target_account_id: <::miden_note_bindings::__private::miden_protocol::account::AccountId as __MidenNoteDecode>::__read_note_felts( + reader, + )?, + }) + } + } + #[doc(hidden)] + const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = "\npackage example:p2id-schema@1.0.0;\n\nuse miden:base/core-types@1.0.0;\n\ninterface note-storage {\n use core-types.{account-id};\n\n record p2id-note {\n target-account-id: account-id,\n }\n\n type storage = p2id-note;\n}\n\npackage miden:base@1.0.0 {\n interface core-types {\n record felt { inner: f32 }\n record account-id { prefix: felt, suffix: felt }\n }\n}\n"; + /// Returns the resolved schema, which is parsed once for the whole process. + #[doc(hidden)] + fn __miden_note_storage_schema() -> ::miden_note_bindings::__private::miden_note_schema::Result< + &'static ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, + > { + static __MIDEN_NOTE_STORAGE_SCHEMA: ::std::sync::OnceLock< + ::miden_note_bindings::__private::miden_note_schema::Result< + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema, + >, + > = ::std::sync::OnceLock::new(); + __MIDEN_NOTE_STORAGE_SCHEMA + .get_or_init(|| { + ::miden_note_bindings::__private::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) + }) + .as_ref() + .map_err(::core::clone::Clone::clone) + } + impl P2idNote { + /// Encodes this typed value as note storage in WIT declaration order. + pub fn to_note_storage( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::miden_note_bindings::__private::miden_note_schema::NoteStorage, + > { + let mut felts = ::std::vec::Vec::new(); + self.__write_note_felts( + &mut ::miden_note_bindings::__private::miden_field_repr::FeltWriter::new( + &mut felts, + ), + )?; + ::miden_note_bindings::__private::miden_note_schema::NoteStorage::new(felts) + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("failed to create note storage: {error}"), + ) + }) + } + /// Decodes this typed value from complete note storage. + pub fn from_note_storage( + storage: &::miden_note_bindings::__private::miden_note_schema::NoteStorage, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let mut reader = ::miden_note_bindings::__private::miden_field_repr::FeltReader::new( + storage.items(), + ); + let value = Self::__read_note_felts(&mut reader)?; + reader + .ensure_eof() + .map_err(|error| { + ::miden_note_bindings::__private::miden_note_schema::Error::new( + format!("note storage has trailing data: {error}"), + ) + })?; + Ok(value) + } + /// Builds a typed value with a caller-provided codec registry. + pub fn from_str_values_with( + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let schema = __miden_note_storage_schema()?; + let mut builder = schema.builder_with_registry(codecs); + for (path, value) in values { + builder = builder.set(path, value)?; + } + let storage = builder.build()?; + Self::from_note_storage(&storage) + } + /// Builds a typed value with the standard codec registry. + pub fn from_str_values( + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + Self::from_str_values_with(values, &codecs) + } + /// Validates this value with structural rules and caller-provided codecs. + pub fn validate_with( + &self, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + let storage = self.to_note_storage()?; + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(()) + } + /// Validates this value with the standard codec registry. + pub fn validate( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result<()> { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.validate_with(&codecs) + } + /// Displays this value with caller-provided codecs and structural fallbacks. + pub fn display_with( + &self, + codecs: &::miden_note_bindings::__private::miden_note_schema::CodecRegistry, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::std::string::String, + > { + let storage = self.to_note_storage()?; + let decoded = __miden_note_storage_schema()? + .decode_with_registry(&storage, codecs)?; + Ok(decoded.to_string()) + } + /// Displays this value with standard codecs and structural fallbacks. + pub fn display( + &self, + ) -> ::miden_note_bindings::__private::miden_note_schema::Result< + ::std::string::String, + > { + let codecs = ::miden_note_bindings::__private::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.display_with(&codecs) + } + } +} +pub use __miden_note_bindings_f74ea5e7a6e77b2d::P2idNote; diff --git a/sdk/note-bindings/macros/src/lib.rs b/sdk/note-bindings/macros/src/lib.rs new file mode 100644 index 0000000000..c0838b96ad --- /dev/null +++ b/sdk/note-bindings/macros/src/lib.rs @@ -0,0 +1,262 @@ +//! Procedural macros that generate typed host bindings for Miden note storage. + +#![deny(missing_docs)] + +extern crate proc_macro; + +use miden_note_schema::{NotePackageArtifact, NotePackageResolver, NoteStorageSchema}; +use miden_note_schema_codegen::{RuntimePaths, generate_host_types}; +use proc_macro::TokenStream; +use proc_macro_crate::{FoundCrate, crate_name}; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use syn::{LitStr, parse_macro_input}; + +/// Generates typed bindings from the package built by a Miden project. +/// +/// The project path is relative to `CARGO_MANIFEST_DIR`. Build the note project with +/// `cargo miden build` before compiling the consumer. +#[proc_macro] +pub fn from_project(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand_from_project(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Generates typed bindings from one exact Miden package path. +/// +/// A relative path is resolved against `CARGO_MANIFEST_DIR`. +#[proc_macro] +pub fn from_package(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand_from_package(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Generates typed bindings from WIT text for internal tests. +#[doc(hidden)] +#[proc_macro] +pub fn from_wit_text(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand_from_wit_text(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Expands a project-relative note binding request. +fn expand_from_project(input: &LitStr) -> syn::Result { + let artifact = NotePackageResolver::new("miden-note-bindings") + .from_project(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_package_artifact(&artifact, input.span()) +} + +/// Expands an exact package binding request. +fn expand_from_package(input: &LitStr) -> syn::Result { + let artifact = NotePackageResolver::new("miden-note-bindings") + .from_package(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_package_artifact(&artifact, input.span()) +} + +/// Expands bindings from a loaded package and tracks the artifact as a macro input. +fn expand_package_artifact( + artifact: &NotePackageArtifact, + span: Span, +) -> syn::Result { + let scope_key = artifact.path().to_string_lossy(); + let bindings = expand_schema(artifact.schema(), span, &scope_key)?; + let tracked_path = artifact.path().to_string_lossy(); + let tracked_manifests = artifact + .tracked_inputs() + .iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect::>(); + let package_cache_env = midenc_frontend_wasm_metadata::package_cache::PACKAGE_CACHE_ENV; + Ok(quote! { + // These constants exist only to register the package file, the manifests that named it, + // and the cache path as proc-macro rebuild inputs. + const _: &[u8] = ::core::include_bytes!(#tracked_path); + #(const _: &[u8] = ::core::include_bytes!(#tracked_manifests);)* + const _: ::core::option::Option<&str> = ::core::option_env!(#package_cache_env); + #bindings + }) +} + +/// Expands bindings from a WIT string literal. +fn expand_from_wit_text(input: &LitStr) -> syn::Result { + let schema = NoteStorageSchema::from_wit_text(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_schema(&schema, input.span(), &input.value()) +} + +/// Adds the typed consumer API to shared generated host types. +fn expand_schema( + schema: &NoteStorageSchema, + span: Span, + scope_key: &str, +) -> syn::Result { + let facade_path = binding_facade(); + let runtime_paths = RuntimePaths::through_facade(quote!(#facade_path)); + let generated = generate_host_types(schema, &runtime_paths) + .map_err(|error| syn::Error::new(span, error.to_string()))?; + let type_tokens = generated.tokens(); + let root_ident = generated.root_ident(); + let type_idents = generated.type_idents(); + let wit_text = schema.wit_text(); + let scope_ident = format_ident!("__miden_note_bindings_{:016x}", stable_hash(scope_key)); + let runtime = quote!(#facade_path::__private); + + Ok(quote! { + #[doc(hidden)] + mod #scope_ident { + #type_tokens + + #[doc(hidden)] + const __MIDEN_NOTE_STORAGE_SCHEMA_WIT: &str = #wit_text; + + /// Returns the resolved schema, which is parsed once for the whole process. + #[doc(hidden)] + fn __miden_note_storage_schema() -> #runtime::miden_note_schema::Result< + &'static #runtime::miden_note_schema::NoteStorageSchema, + > { + static __MIDEN_NOTE_STORAGE_SCHEMA: ::std::sync::OnceLock< + #runtime::miden_note_schema::Result< + #runtime::miden_note_schema::NoteStorageSchema, + >, + > = ::std::sync::OnceLock::new(); + __MIDEN_NOTE_STORAGE_SCHEMA + .get_or_init(|| { + #runtime::miden_note_schema::NoteStorageSchema::from_wit_text( + __MIDEN_NOTE_STORAGE_SCHEMA_WIT, + ) + }) + .as_ref() + .map_err(::core::clone::Clone::clone) + } + + impl #root_ident { + /// Encodes this typed value as note storage in WIT declaration order. + pub fn to_note_storage( + &self, + ) -> #runtime::miden_note_schema::Result<#runtime::miden_note_schema::NoteStorage> { + let mut felts = ::std::vec::Vec::new(); + self.__write_note_felts( + &mut #runtime::miden_field_repr::FeltWriter::new(&mut felts), + )?; + #runtime::miden_note_schema::NoteStorage::new(felts).map_err(|error| { + #runtime::miden_note_schema::Error::new(format!( + "failed to create note storage: {error}" + )) + }) + } + + /// Decodes this typed value from complete note storage. + pub fn from_note_storage( + storage: &#runtime::miden_note_schema::NoteStorage, + ) -> #runtime::miden_note_schema::Result { + let mut reader = #runtime::miden_field_repr::FeltReader::new(storage.items()); + let value = Self::__read_note_felts(&mut reader)?; + reader.ensure_eof().map_err(|error| { + #runtime::miden_note_schema::Error::new(format!( + "note storage has trailing data: {error}" + )) + })?; + Ok(value) + } + + /// Builds a typed value with a caller-provided codec registry. + pub fn from_str_values_with( + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, + codecs: &#runtime::miden_note_schema::CodecRegistry, + ) -> #runtime::miden_note_schema::Result { + let schema = __miden_note_storage_schema()?; + let mut builder = schema.builder_with_registry(codecs); + for (path, value) in values { + builder = builder.set(path, value)?; + } + let storage = builder.build()?; + Self::from_note_storage(&storage) + } + + /// Builds a typed value with the standard codec registry. + pub fn from_str_values( + values: &::std::collections::BTreeMap< + ::std::string::String, + ::std::string::String, + >, + ) -> #runtime::miden_note_schema::Result { + let codecs = + #runtime::miden_note_schema::CodecRegistry::with_standard_codecs(); + Self::from_str_values_with(values, &codecs) + } + + /// Validates this value with structural rules and caller-provided codecs. + pub fn validate_with( + &self, + codecs: &#runtime::miden_note_schema::CodecRegistry, + ) -> #runtime::miden_note_schema::Result<()> { + let storage = self.to_note_storage()?; + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(()) + } + + /// Validates this value with the standard codec registry. + pub fn validate(&self) -> #runtime::miden_note_schema::Result<()> { + let codecs = + #runtime::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.validate_with(&codecs) + } + + /// Displays this value with caller-provided codecs and structural fallbacks. + pub fn display_with( + &self, + codecs: &#runtime::miden_note_schema::CodecRegistry, + ) -> #runtime::miden_note_schema::Result<::std::string::String> { + let storage = self.to_note_storage()?; + let decoded = + __miden_note_storage_schema()?.decode_with_registry(&storage, codecs)?; + Ok(decoded.to_string()) + } + + /// Displays this value with standard codecs and structural fallbacks. + pub fn display( + &self, + ) -> #runtime::miden_note_schema::Result<::std::string::String> { + let codecs = + #runtime::miden_note_schema::CodecRegistry::with_standard_codecs(); + self.display_with(&codecs) + } + } + } + + pub use #scope_ident::{#(#type_idents),*}; + }) +} + +/// Resolves the bindings facade path in the consuming crate. +fn binding_facade() -> syn::Path { + match crate_name("miden-note-bindings") { + Ok(FoundCrate::Itself) => syn::parse_quote!(crate), + Ok(FoundCrate::Name(name)) => { + let ident = syn::Ident::new(&name, Span::call_site()); + syn::parse_quote!(::#ident) + } + Err(_) => syn::parse_quote!(::miden_note_bindings), + } +} + +/// Returns a deterministic FNV-1a scope suffix for one macro input. +fn stable_hash(value: &str) -> u64 { + value.bytes().fold(0xcbf2_9ce4_8422_2325, |hash, byte| { + (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3) + }) +} + +#[cfg(test)] +mod tests; diff --git a/sdk/note-bindings/macros/src/tests.rs b/sdk/note-bindings/macros/src/tests.rs new file mode 100644 index 0000000000..af56307522 --- /dev/null +++ b/sdk/note-bindings/macros/src/tests.rs @@ -0,0 +1,83 @@ +//! Tests for macro expansion. + +use midenc_expect_test::expect_file; +use syn::LitStr; + +use crate::expand_from_wit_text; + +const P2ID_SCHEMA: &str = r#" +package example:p2id-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id}; + + record p2id-note { + target-account-id: account-id, + } + + type storage = p2id-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record account-id { prefix: felt, suffix: felt } + } +} +"#; + +const CUSTOM_SCHEMA: &str = r#" +package example:dex-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id}; + + /// A ratio used as an order limit. + record limit-price { + numerator: u64, + denominator: u64, + } + + /// Selects order execution. + variant order-kind { + market, + limit(limit-price), + } + + record dex-note { + target: account-id, + kind: order-kind, + } + + type storage = dex-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record account-id { prefix: felt, suffix: felt } + } +} +"#; + +/// Formats one WIT-text macro expansion as Rust source. +fn expand(wit: &str) -> String { + let literal = LitStr::new(wit, proc_macro2::Span::call_site()); + let tokens = expand_from_wit_text(&literal).unwrap(); + let file: syn::File = syn::parse2(tokens).unwrap(); + prettyplease::unparse(&file) +} + +#[test] +fn expands_p2id_schema_golden() { + expect_file!["../expected/p2id.rs"].assert_eq(&expand(P2ID_SCHEMA)); +} + +#[test] +fn expands_custom_schema_golden() { + expect_file!["../expected/custom.rs"].assert_eq(&expand(CUSTOM_SCHEMA)); +} diff --git a/sdk/note-bindings/src/lib.rs b/sdk/note-bindings/src/lib.rs new file mode 100644 index 0000000000..5487f6ada7 --- /dev/null +++ b/sdk/note-bindings/src/lib.rs @@ -0,0 +1,29 @@ +//! Typed host bindings for Miden note storage schemas. +//! +//! Generate bindings from a built package, and then use the generated storage type. +//! +//! ```ignore +//! miden_note_bindings::from_package!("../p2id-note/target/miden/release/p2id.masp"); +//! +//! fn decode(storage: &miden_note_bindings::NoteStorage) -> P2idNote { +//! P2idNote::from_note_storage(storage).unwrap() +//! } +//! ``` + +#![deny(missing_docs)] + +pub use miden_field_repr::*; +#[doc(hidden)] +pub use miden_note_bindings_macros::from_wit_text; +pub use miden_note_bindings_macros::{from_package, from_project}; +pub use miden_note_schema::{CodecRegistry, Error, NoteStorage, Result}; +pub use miden_protocol::{account, address, asset}; + +/// Support used by generated typed bindings. +#[doc(hidden)] +pub mod __private { + pub use miden_field; + pub use miden_field_repr; + pub use miden_note_schema; + pub use miden_protocol; +} diff --git a/sdk/note-bindings/tests/generated_custom.rs b/sdk/note-bindings/tests/generated_custom.rs new file mode 100644 index 0000000000..103c13423b --- /dev/null +++ b/sdk/note-bindings/tests/generated_custom.rs @@ -0,0 +1,134 @@ +//! Type-checks and exercises bindings generated from custom WIT types. + +use std::collections::BTreeMap; + +use miden_note_bindings::{CodecRegistry, Felt, FromFeltRepr, ToFeltRepr}; + +miden_note_bindings::from_wit_text!( + r#" +package example:custom-schema@1.0.0; + +interface note-storage { + record limit-price { + numerator: u64, + denominator: u64, + } + + variant order-kind { + market, + limit(limit-price), + } + + record custom-note { + price: limit-price, + kind: order-kind, + } + + type storage = custom-note; +} +"# +); + +/// Requires a type to carry both native felt-repr traits. +fn assert_native_felt_repr() {} + +#[test] +fn custom_types_have_native_repr_and_typed_round_trips() { + assert_native_felt_repr::(); + assert_native_felt_repr::(); + assert_native_felt_repr::(); + + let value = CustomNote { + price: LimitPrice { + numerator: 3, + denominator: 2, + }, + kind: OrderKind::Limit(LimitPrice { + numerator: 5, + denominator: 4, + }), + }; + let storage = value.to_note_storage().unwrap(); + assert_eq!( + storage.items(), + &[ + Felt::from_u32(3), + Felt::ZERO, + Felt::from_u32(2), + Felt::ZERO, + Felt::ONE, + Felt::from_u32(5), + Felt::ZERO, + Felt::from_u32(4), + Felt::ZERO, + ] + ); + assert_eq!(CustomNote::from_note_storage(&storage).unwrap(), value); + + let codecs = CodecRegistry::default(); + value.validate_with(&codecs).unwrap(); + assert_eq!( + value.display_with(&codecs).unwrap(), + "{price: {numerator: 3, denominator: 2}, kind: limit({numerator: 5, denominator: 4})}" + ); + value.validate().unwrap(); + assert_eq!(value.display().unwrap(), value.display_with(&codecs).unwrap()); +} + +#[test] +fn custom_record_string_paths_use_the_registry_parameter() { + let mut values = BTreeMap::new(); + values.insert("price.numerator".to_owned(), "7".to_owned()); + values.insert("price.denominator".to_owned(), "6".to_owned()); + values.insert("kind".to_owned(), "market".to_owned()); + + let value = CustomNote::from_str_values_with(&values, &CodecRegistry::default()).unwrap(); + assert_eq!( + value, + CustomNote { + price: LimitPrice { + numerator: 7, + denominator: 6, + }, + kind: OrderKind::Market, + } + ); +} + +/// Bindings whose WIT type and field names collide with Rust prelude names. +/// +/// The generated items shadow the prelude inside the generated module, so the test only compiles +/// when every prelude name in the generated code is fully qualified. +mod prelude_names { + miden_note_bindings::from_wit_text!( + r#" +package example:prelude-schema@1.0.0; + +interface note-storage { + record %option { + %string: u64, + } + + record %vec { + %string: u32, + %result: %option, + maybe: option, + } + + type storage = %vec; +} +"# + ); + + #[test] + fn prelude_named_types_round_trip() { + let value = Vec { + string: 9, + result: Option { string: 4 }, + maybe: ::core::option::Option::Some(1), + }; + let storage = value.to_note_storage().unwrap(); + assert_eq!(Vec::from_note_storage(&storage).unwrap(), value); + value.validate().unwrap(); + } +} diff --git a/sdk/note-bindings/tests/p2id_consumer.rs b/sdk/note-bindings/tests/p2id_consumer.rs new file mode 100644 index 0000000000..cd9d52d2e4 --- /dev/null +++ b/sdk/note-bindings/tests/p2id_consumer.rs @@ -0,0 +1,193 @@ +//! End-to-end test for package discovery and generated p2id consumer bindings. + +use std::{env, fs, path::Path, process::Command}; + +use miden_mast_package::Section; +use midenc_frontend_wasm_metadata::package_note_storage_schema_section_id; +use midenc_integration_test_support::{ + compile_project, scrub_nested_cargo_env, workspace_root, write_masp_file_atomic, +}; + +/// Returns the native rustc host target. +fn host_target() -> String { + let output = Command::new(env::var_os("RUSTC").unwrap_or_else(|| "rustc".into())) + .arg("-vV") + .output() + .expect("failed to query the rustc host target"); + assert!(output.status.success(), "rustc -vV failed"); + String::from_utf8(output.stdout) + .unwrap() + .lines() + .find_map(|line| line.strip_prefix("host: ")) + .expect("rustc -vV did not report a host target") + .to_owned() +} + +/// Copies the workspace patch table into an isolated consumer manifest. +/// +/// The consumer crate must resolve the same patched dependencies as the workspace. A +/// workspace without a patch table has nothing to copy, and the function returns an empty +/// string. +fn workspace_patch_section(workspace: &Path) -> String { + let manifest = fs::read_to_string(workspace.join("Cargo.toml")).unwrap(); + let mut section = String::new(); + let mut copying = false; + for line in manifest.lines() { + let trimmed = line.trim(); + if trimmed == "[patch.crates-io]" { + copying = true; + } else if copying && trimmed.starts_with('[') { + break; + } + if copying { + section.push_str(line); + section.push('\n'); + } + } + section +} + +#[test] +fn generated_p2id_bindings_compile_and_run_in_a_consumer_crate() { + let workspace = workspace_root(); + let examples = workspace.join("examples"); + let wallet_dir = examples.join("basic-wallet"); + let wallet = compile_project(&wallet_dir); + write_masp_file_atomic(&wallet, wallet_dir.join("target/miden/release")) + .expect("failed to persist the basic-wallet dependency package"); + + let p2id_dir = examples.join("p2id-note"); + let p2id = compile_project(&p2id_dir); + let package_dir = p2id_dir.join("target/miden/release"); + write_masp_file_atomic(&p2id, &package_dir).expect("failed to persist the p2id package"); + let package_path = package_dir.join("p2id.masp"); + + let temp = tempfile::tempdir().unwrap(); + fs::create_dir_all(temp.path().join("src")).unwrap(); + // Seed the workspace lockfile so the offline build resolves to the versions that the + // workspace already downloaded instead of racing the registry index. + fs::copy(workspace.join("Cargo.lock"), temp.path().join("Cargo.lock")).unwrap(); + let second_package_dir = temp.path().join("packages/counter"); + fs::create_dir_all(&second_package_dir).unwrap(); + let mut second_package = (*p2id).clone(); + let schema_id = package_note_storage_schema_section_id(); + second_package.sections.retain(|section| section.id != schema_id); + second_package.sections.push(Section::new( + schema_id, + br#"package example:counter-schema@1.0.0; + +interface note-storage { + record counter-note { value: u64 } + type storage = counter-note; +} +"# + .to_vec(), + )); + write_masp_file_atomic(&second_package, &second_package_dir) + .expect("failed to persist the second schema package"); + let second_package_path = second_package_dir.join("p2id.masp"); + + let bindings_dir = workspace.join("sdk/note-bindings"); + let patches = workspace_patch_section(&workspace); + fs::write( + temp.path().join("Cargo.toml"), + format!( + r#"[package] +name = "note-bindings-consumer" +version = "0.1.0" +edition = "2024" + +[workspace] + +# Keep only file/line debug information: this project exists to prove the +# generated bindings compile and run, and full DWARF for the native Miden +# dependency cone costs gigabytes in the shared test build directory. +[profile.dev] +debug = "line-tables-only" + +[dependencies] +miden-note-bindings = {{ path = {bindings_dir:?} }} + +{patches} +"#, + bindings_dir = bindings_dir.to_string_lossy(), + ), + ) + .unwrap(); + + let source = format!( + r#"use std::collections::BTreeMap; + +use miden_note_bindings::{{account::AccountId, address::NetworkId}}; + +mod project_bindings {{ + miden_note_bindings::from_project!({project_dir:?}); +}} + +mod package_bindings {{ + miden_note_bindings::from_package!({package_path:?}); + miden_note_bindings::from_package!({second_package_path:?}); +}} + +fn main() {{ + let account_id = + AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); + let bech32 = account_id.to_bech32(NetworkId::Mainnet); + let mut values = BTreeMap::new(); + values.insert("target_account_id".to_owned(), bech32.clone()); + + let typed = project_bindings::P2idNote::from_str_values(&values).unwrap(); + assert_eq!(typed.target_account_id, account_id); + let storage = typed.to_note_storage().unwrap(); + assert_eq!( + storage.items(), + &[account_id.prefix().as_felt(), account_id.suffix()], + ); + typed.validate().unwrap(); + assert_eq!( + typed.display().unwrap(), + format!("{{{{target-account-id: {{bech32}}}}}}"), + ); + + let decoded = project_bindings::P2idNote::from_note_storage(&storage).unwrap(); + assert_eq!(decoded, typed); + + let exact = package_bindings::P2idNote::from_note_storage(&storage).unwrap(); + assert_eq!(exact.target_account_id, account_id); + + let counter = package_bindings::CounterNote {{ value: 9 }}; + let counter_storage = counter.to_note_storage().unwrap(); + assert_eq!(counter_storage.items()[0].as_canonical_u64(), 9); + assert_eq!(counter_storage.items()[1].as_canonical_u64(), 0); +}} +"#, + project_dir = p2id_dir.to_string_lossy(), + package_path = package_path.to_string_lossy(), + second_package_path = second_package_path.to_string_lossy(), + ); + fs::write(temp.path().join("src/main.rs"), source).unwrap(); + + let mut cargo = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); + cargo + .args(["run", "--quiet", "--target"]) + .arg(host_target()) + .arg("--manifest-path") + .arg(temp.path().join("Cargo.toml")) + .current_dir(temp.path()) + .env("CARGO_TARGET_DIR", workspace.join("target/note-bindings-consumer")) + // Share the hash-keyed intermediates with the other test builds; only the + // name-keyed final artifacts stay in the directory above. Convention from + // `tests/support`. + .env("CARGO_BUILD_BUILD_DIR", workspace.join("target/miden_build_cache")) + .env("CARGO_NET_OFFLINE", "true"); + scrub_nested_cargo_env(&mut cargo); + let output = cargo + .output() + .expect("failed to spawn Cargo for the generated bindings consumer"); + assert!( + output.status.success(), + "generated bindings consumer failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} diff --git a/sdk/note-codec/Cargo.toml b/sdk/note-codec/Cargo.toml new file mode 100644 index 0000000000..fc5b949284 --- /dev/null +++ b/sdk/note-codec/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "miden-note-codec" +description = "Author-side codecs for typed Miden note storage" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true + +[lib] +doctest = false + +[dependencies] +miden-field.workspace = true +miden-field-repr.workspace = true +miden-note-codec-macros.workspace = true +miden-note-codec-wit.workspace = true +miden-protocol.workspace = true +wit-bindgen = { workspace = true } + +[dev-dependencies] +miden-note-schema.workspace = true +midenc-frontend-wasm-metadata.workspace = true +tempfile.workspace = true +wit-component.workspace = true +wit-parser.workspace = true diff --git a/sdk/note-codec/macros/Cargo.toml b/sdk/note-codec/macros/Cargo.toml new file mode 100644 index 0000000000..372b2015d4 --- /dev/null +++ b/sdk/note-codec/macros/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "miden-note-codec-macros" +description = "Procedural macros for author-side Miden note codecs" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true + +[lib] +proc-macro = true +doctest = false + +[dependencies] +miden-note-codec-wit.workspace = true +miden-note-schema.workspace = true +miden-note-schema-codegen.workspace = true +midenc-frontend-wasm-metadata.workspace = true +proc-macro2 = { workspace = true, features = ["span-locations"] } +proc-macro-crate = { workspace = true } +quote.workspace = true +syn.workspace = true + +[dev-dependencies] +prettyplease = { workspace = true } diff --git a/sdk/note-codec/macros/src/expand.rs b/sdk/note-codec/macros/src/expand.rs new file mode 100644 index 0000000000..b301058050 --- /dev/null +++ b/sdk/note-codec/macros/src/expand.rs @@ -0,0 +1,287 @@ +//! Macro expansion for generated author types and component dispatch. + +use miden_note_codec_wit::NOTE_CODEC_WIT; +use miden_note_schema::{NotePackageArtifact, NotePackageResolver, NoteStorageSchema}; +use miden_note_schema_codegen::{RuntimePaths, generate_host_types}; +use proc_macro_crate::{FoundCrate, crate_name}; +use proc_macro2::{Span, TokenStream}; +use quote::{ToTokens, quote}; +use syn::{ItemImpl, LitStr, Type, spanned::Spanned}; + +use crate::registry::{register_codec, register_schema, registered_codecs}; + +/// Expands a project-relative type generation request. +pub(crate) fn from_project(input: &LitStr) -> syn::Result { + let artifact = NotePackageResolver::new("miden-note-codec") + .from_project(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_package_artifact(&artifact, input.span()) +} + +/// Expands an exact package type generation request. +pub(crate) fn from_package(input: &LitStr) -> syn::Result { + let artifact = NotePackageResolver::new("miden-note-codec") + .from_package(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_package_artifact(&artifact, input.span()) +} + +/// Expands a WIT string literal for internal tests. +pub(crate) fn from_wit_text(input: &LitStr) -> syn::Result { + let schema = NoteStorageSchema::from_wit_text(&input.value()) + .map_err(|error| syn::Error::new(input.span(), error.to_string()))?; + expand_schema(&schema, input.span()) +} + +/// Loads one package, tracks it as an input, and expands its schema types. +fn expand_package_artifact(artifact: &NotePackageArtifact, span: Span) -> syn::Result { + let types = expand_schema(artifact.schema(), span)?; + let tracked_path = artifact.path().to_string_lossy(); + let tracked_manifests = artifact + .tracked_inputs() + .iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect::>(); + let package_cache_env = midenc_frontend_wasm_metadata::package_cache::PACKAGE_CACHE_ENV; + Ok(quote! { + // These constants exist only to register the package file, the manifests that named it, + // and the cache path as proc-macro rebuild inputs. + const _: &[u8] = ::core::include_bytes!(#tracked_path); + #(const _: &[u8] = ::core::include_bytes!(#tracked_manifests);)* + const _: ::core::option::Option<&str> = ::core::option_env!(#package_cache_env); + #types + }) +} + +/// Generates host-profile types and records their WIT identities. +fn expand_schema(schema: &NoteStorageSchema, span: Span) -> syn::Result { + let facade = note_codec_facade(); + let runtime = RuntimePaths::through_facade(quote!(#facade)); + let generated = generate_host_types(schema, &runtime) + .map_err(|error| syn::Error::new(span, error.to_string()))?; + register_schema(schema, span)?; + Ok(generated.tokens().clone()) +} + +/// Validates and records one marked author codec implementation. +pub(crate) fn note_codec(args: TokenStream, item: ItemImpl) -> syn::Result { + if !args.is_empty() { + return Err(syn::Error::new_spanned(args, "#[note_codec] does not accept arguments")); + } + let (_, trait_path, _) = item.trait_.as_ref().ok_or_else(|| { + syn::Error::new_spanned(&item.self_ty, "#[note_codec] needs an AuthorTypeCodec impl") + })?; + if !trait_path + .segments + .last() + .is_some_and(|segment| segment.ident == "AuthorTypeCodec") + { + return Err(syn::Error::new_spanned( + trait_path, + "#[note_codec] can only mark an AuthorTypeCodec impl", + )); + } + if !item.generics.params.is_empty() { + return Err(syn::Error::new_spanned( + &item.generics, + "#[note_codec] does not support generic implementations", + )); + } + let rust_name = rust_type_name(&item.self_ty)?; + register_codec(&rust_name, item.self_ty.to_token_stream().to_string(), item.self_ty.span())?; + Ok(quote!(#item)) +} + +/// Generates native dispatch and Wasm-only component export glue. +pub(crate) fn export_codecs(input: TokenStream) -> syn::Result { + if !input.is_empty() { + return Err(syn::Error::new_spanned(input, "export_codecs! does not accept arguments")); + } + let codecs = registered_codecs(Span::call_site())?; + let facade = note_codec_facade(); + let registrations = codecs + .iter() + .map(|codec| { + let fqn = &codec.fqn; + let ty = syn::parse_str::(&codec.rust_type).map_err(|error| { + syn::Error::new( + Span::call_site(), + format!("failed to restore codec type `{}`: {error}", codec.rust_type), + ) + })?; + Ok((fqn, ty)) + }) + .collect::>>()?; + let fqns = registrations.iter().map(|(fqn, _)| fqn).collect::>(); + let parse_arms = registrations.iter().map(|(fqn, ty)| { + quote! { + #fqn => { + let value = <#ty as #facade::AuthorTypeCodec>::parse(value)?; + let mut felts = ::std::vec::Vec::new(); + <#ty as __MidenNoteEncode>::__write_note_felts( + &value, + &mut #facade::__private::miden_field_repr::FeltWriter::new( + &mut felts, + ), + ) + .map_err(|error| format!( + "failed to encode codec type `{}`: {error}", + #fqn, + ))?; + Ok(#facade::felts_to_u64(&felts)) + } + } + }); + let display_arms = registrations.iter().map(|(fqn, ty)| { + quote! { + #fqn => { + let felts = #facade::felts_from_u64(value)?; + let mut reader = + #facade::__private::miden_field_repr::FeltReader::new(&felts); + let value = <#ty as __MidenNoteDecode>::__read_note_felts(&mut reader) + .map_err(|error| format!( + "failed to decode codec type `{}`: {error}", + #fqn, + ))?; + reader.ensure_eof().map_err(|error| format!( + "codec type `{}` has trailing felt data: {error}", + #fqn, + ))?; + Ok(<#ty as #facade::AuthorTypeCodec>::display(&value)) + } + } + }); + let validate_arms = registrations.iter().map(|(fqn, ty)| { + quote! { + #fqn => { + let felts = #facade::felts_from_u64(value)?; + let mut reader = + #facade::__private::miden_field_repr::FeltReader::new(&felts); + let value = <#ty as __MidenNoteDecode>::__read_note_felts(&mut reader) + .map_err(|error| format!( + "failed to decode codec type `{}`: {error}", + #fqn, + ))?; + reader.ensure_eof().map_err(|error| format!( + "codec type `{}` has trailing felt data: {error}", + #fqn, + ))?; + <#ty as #facade::AuthorTypeCodec>::validate(&value) + } + } + }); + let wit = NOTE_CODEC_WIT; + // `quote` prints `::` paths with spaces around each separator. `generate!` reads this value + // as one path, so the spaces come out. + let wit_runtime_path = LitStr::new( + &format!("{facade}::__private::wit_bindgen::rt", facade = quote!(#facade)).replace(' ', ""), + Span::call_site(), + ); + + Ok(quote! { + /// Native dispatch used by the note codec component adapter. + #[doc(hidden)] + pub mod __miden_note_codec_dispatch { + use super::*; + + /// Returns all supported canonical WIT FQNs. + pub fn supported_types() -> ::std::vec::Vec<::std::string::String> { + vec![#(#fqns.to_owned()),*] + } + + /// Parses one value through its marked author codec. + pub fn parse( + type_fqn: &str, + value: &str, + ) -> ::core::result::Result<::std::vec::Vec, ::std::string::String> { + match type_fqn { + #(#parse_arms,)* + _ => Err(format!( + "no note codec is registered for WIT type `{type_fqn}`" + )), + } + } + + /// Displays one value through its marked author codec. + pub fn display( + type_fqn: &str, + value: &[u64], + ) -> ::core::result::Result<::std::string::String, ::std::string::String> { + match type_fqn { + #(#display_arms,)* + _ => Err(format!( + "no note codec is registered for WIT type `{type_fqn}`" + )), + } + } + + /// Validates one value through its marked author codec. + pub fn validate( + type_fqn: &str, + value: &[u64], + ) -> ::core::result::Result<(), ::std::string::String> { + match type_fqn { + #(#validate_arms,)* + _ => Err(format!( + "no note codec is registered for WIT type `{type_fqn}`" + )), + } + } + } + + #[cfg(target_family = "wasm")] + mod __miden_note_codec_component { + #facade::__private::wit_bindgen::generate!({ + inline: #wit, + world: "note-codec", + runtime_path: #wit_runtime_path, + }); + + struct Component; + + impl exports::miden::note_codec::codec::Guest for Component { + fn supported_types() -> Vec { + super::__miden_note_codec_dispatch::supported_types() + } + + fn parse(type_fqn: String, value: String) -> Result, String> { + super::__miden_note_codec_dispatch::parse(&type_fqn, &value) + } + + fn display(type_fqn: String, value: Vec) -> Result { + super::__miden_note_codec_dispatch::display(&type_fqn, &value) + } + + fn validate(type_fqn: String, value: Vec) -> Result<(), String> { + super::__miden_note_codec_dispatch::validate(&type_fqn, &value) + } + } + + export!(Component); + } + }) +} + +/// Returns the final identifier of one concrete impl self type. +fn rust_type_name(ty: &Type) -> syn::Result { + let Type::Path(path) = ty else { + return Err(syn::Error::new_spanned(ty, "#[note_codec] needs a concrete generated type")); + }; + path.path + .segments + .last() + .map(|segment| segment.ident.to_string()) + .ok_or_else(|| syn::Error::new_spanned(ty, "#[note_codec] type path is empty")) +} + +/// Resolves the note codec facade path in the consuming crate. +fn note_codec_facade() -> syn::Path { + match crate_name("miden-note-codec") { + Ok(FoundCrate::Itself) => syn::parse_quote!(crate), + Ok(FoundCrate::Name(name)) => { + let ident = syn::Ident::new(&name, Span::call_site()); + syn::parse_quote!(::#ident) + } + Err(_) => syn::parse_quote!(::miden_note_codec), + } +} diff --git a/sdk/note-codec/macros/src/lib.rs b/sdk/note-codec/macros/src/lib.rs new file mode 100644 index 0000000000..e335919f57 --- /dev/null +++ b/sdk/note-codec/macros/src/lib.rs @@ -0,0 +1,68 @@ +//! Procedural macros for author-side note codec components. + +#![deny(missing_docs)] + +extern crate proc_macro; + +mod expand; +mod registry; + +use proc_macro::TokenStream; +use syn::{ItemImpl, LitStr, parse_macro_input}; + +/// Generates host-profile note types from the package built by a Miden project. +/// +/// The project path is relative to `CARGO_MANIFEST_DIR`. Build the note project with +/// `cargo miden build` before compiling the codec crate. +#[proc_macro] +pub fn from_project(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand::from_project(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Generates host-profile note types from one exact Miden package path. +/// +/// A relative path is resolved against `CARGO_MANIFEST_DIR`. +#[proc_macro] +pub fn from_package(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand::from_package(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Generates host-profile note types from WIT text for internal tests. +#[doc(hidden)] +#[proc_macro] +pub fn from_wit_text(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + expand::from_wit_text(&input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Marks an [`miden_note_codec::AuthorTypeCodec`] implementation for component export. +#[proc_macro_attribute] +pub fn note_codec(args: TokenStream, input: TokenStream) -> TokenStream { + let item = parse_macro_input!(input as ItemImpl); + expand::note_codec(args.into(), item) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +/// Exports all codecs marked earlier in the crate through the `miden:note-codec` component world. +/// +/// Place this macro after the generated schema types and every `#[note_codec]` implementation. +/// Procedural macros register codecs as they expand; supported types are exported in canonical FQN +/// order. +#[proc_macro] +pub fn export_codecs(input: TokenStream) -> TokenStream { + expand::export_codecs(input.into()) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +#[cfg(test)] +mod tests; diff --git a/sdk/note-codec/macros/src/registry.rs b/sdk/note-codec/macros/src/registry.rs new file mode 100644 index 0000000000..08815d97d7 --- /dev/null +++ b/sdk/note-codec/macros/src/registry.rs @@ -0,0 +1,244 @@ +//! Process-global schema and codec registration for one macro expansion process. + +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::{Mutex, OnceLock}, +}; + +use miden_note_schema::{NoteStorageSchema, SchemaCase, SchemaType, SchemaTypeKind}; +use miden_note_schema_codegen::generated_type_ident; +use proc_macro2::Span; + +/// One marked author codec. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct CodecRegistration { + /// Fully qualified WIT name implemented by the codec. + pub(crate) fqn: String, + /// Rust type path that implements the codec. + pub(crate) rust_type: String, +} + +/// One marked codec together with the source location that produced it. +#[derive(Clone, Debug)] +struct RegisteredCodec { + registration: CodecRegistration, + location: ExpansionLocation, +} + +/// Schema types and marked codecs registered by earlier macro expansions. +#[derive(Default)] +struct Registry { + /// Registered schema source and the expansion that supplied it. + schema: Option<(String, ExpansionLocation)>, + /// Generated Rust type name to WIT FQN, used by `#[note_codec]` lookup. + types: BTreeMap, + /// Marked codecs keyed by the WIT FQN they implement. + codecs: BTreeMap, +} + +/// The registrations of every crate this macro process expanded, keyed by crate. +/// +/// Procedural macros register as they expand, so `#[note_codec]` and `export_codecs!` read what +/// an earlier invocation in the same crate wrote. +static REGISTRY: OnceLock>> = OnceLock::new(); + +/// Source location of one macro expansion. +/// +/// The location tells a stale re-expansion of an edited invocation (same location) from a +/// real conflict between two invocations (different locations). +// Keep this registry identity/replacement policy aligned with sdk/base-macros/src/types.rs; +// changes must land in both. +type ExpansionLocation = (String, usize, usize); + +/// Returns the (file, line, column) location of one expansion span. +fn expansion_location(span: Span) -> ExpansionLocation { + let start = span.start(); + (span.file(), start.line, start.column) +} + +/// Returns the key of the crate whose macro expansion is running. +/// +/// Long-lived macro hosts such as the rust-analyzer proc-macro server expand many crates in one +/// process; the key keeps their registrations apart. One Cargo package builds many crates, such +/// as a library and its integration tests, so the key names the crate as well as the package +/// directory. +fn macro_invocation_crate_key() -> String { + let package = std::env::var("CARGO_MANIFEST_DIR") + .or_else(|_| std::env::var("CARGO_PKG_NAME")) + .unwrap_or_default(); + let crate_name = std::env::var("CARGO_CRATE_NAME").unwrap_or_default(); + format!("{package}\u{1f}{crate_name}") +} + +/// Returns the shared registry map keyed by expanding crate. +fn registry() -> &'static Mutex> { + REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new())) +} + +/// Records every generated named type, including the storage root. +pub(crate) fn register_schema(schema: &NoteStorageSchema, span: Span) -> syn::Result<()> { + let mut bindings = BTreeMap::new(); + collect_type_bindings(schema.root(), &mut BTreeSet::new(), &mut bindings)?; + let bindings = index_by_rust_type_name(&bindings, span)?; + + let mut registries = registry() + .lock() + .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; + let registry = registries.entry(macro_invocation_crate_key()).or_default(); + let location = expansion_location(span); + match ®istry.schema { + Some((existing, _)) if existing == schema.wit_text() => return Ok(()), + Some((_, existing_location)) if *existing_location == location => { + // A long-lived macro host re-expanded an edited invocation; replace the stale + // schema and drop the codecs that were registered against it. + registry.codecs.clear(); + } + Some(_) => { + return Err(syn::Error::new( + span, + "miden-note-codec supports one note schema per crate; remove the second distinct \ + from_project!, from_package!, or from_wit_text! invocation. If this error \ + appears in your IDE after an edit, restart the rust-analyzer proc-macro server", + )); + } + None => {} + } + registry.schema = Some((schema.wit_text().to_owned(), location)); + registry.types = bindings; + Ok(()) +} + +/// Resolves and records a marked codec implementation by generated Rust type name. +pub(crate) fn register_codec(rust_name: &str, rust_type: String, span: Span) -> syn::Result<()> { + let mut registries = registry() + .lock() + .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; + let registry = registries.entry(macro_invocation_crate_key()).or_default(); + let fqn = registry.types.get(rust_name).ok_or_else(|| { + syn::Error::new( + span, + format!( + "type `{rust_name}` is not part of a registered note schema; invoke \ + miden_note_codec::from_project! or from_package! before #[note_codec]" + ), + ) + })?; + let fqn = fqn.clone(); + let location = expansion_location(span); + let registration = CodecRegistration { + fqn: fqn.clone(), + rust_type, + }; + if let Some(existing) = registry.codecs.get(&fqn) + && existing.registration != registration + // A re-registration from the same source location replaces a stale entry. + && existing.location != location + { + return Err(syn::Error::new( + span, + format!( + "WIT type `{fqn}` already has a different #[note_codec] implementation. If this \ + error appears in your IDE after an edit, restart the rust-analyzer proc-macro \ + server" + ), + )); + } + registry.codecs.insert( + fqn, + RegisteredCodec { + registration, + location, + }, + ); + Ok(()) +} + +/// Returns marked codecs in canonical FQN order. +pub(crate) fn registered_codecs(span: Span) -> syn::Result> { + let mut registries = registry() + .lock() + .map_err(|_| syn::Error::new(span, "note codec registry mutex is poisoned"))?; + let registry = registries.entry(macro_invocation_crate_key()).or_default(); + if registry.codecs.is_empty() { + return Err(syn::Error::new( + span, + "export_codecs! found no registered codecs; place it after from_project! or \ + from_package! and after every #[note_codec] implementation because procedural macros \ + register as they expand; supported types are exported in canonical FQN order", + )); + } + Ok(registry.codecs.values().map(|codec| codec.registration.clone()).collect()) +} + +/// Indexes WIT FQNs by the Rust type name the generator gives them. +/// +/// Two WIT types of different interfaces may share a local name. The generator rejects that +/// collision, and this index rejects it the same way instead of dropping one binding. +fn index_by_rust_type_name( + bindings: &BTreeMap, + span: Span, +) -> syn::Result> { + let mut index = BTreeMap::new(); + for (fqn, rust_name) in bindings { + if let Some(existing) = index.insert(rust_name.clone(), fqn.clone()) { + return Err(syn::Error::new( + span, + format!("WIT types `{existing}` and `{fqn}` both map to Rust type `{rust_name}`"), + )); + } + } + Ok(index) +} + +/// Collects reachable generated record and variant bindings, keyed by WIT FQN. +fn collect_type_bindings( + ty: &SchemaType, + seen: &mut BTreeSet, + bindings: &mut BTreeMap, +) -> syn::Result<()> { + if ty.standard_leaf().is_some() { + return Ok(()); + } + if matches!(ty.kind(), SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_)) { + let fqn = ty.fqn().ok_or_else(|| { + syn::Error::new(Span::call_site(), "a generated note codec type has no WIT FQN") + })?; + let name = ty.name().ok_or_else(|| { + syn::Error::new( + Span::call_site(), + format!("generated WIT type `{fqn}` has no local name"), + ) + })?; + if !seen.insert(fqn.to_owned()) { + return Ok(()); + } + bindings.insert(fqn.to_owned(), generated_type_ident(name)); + } + + match ty.kind() { + SchemaTypeKind::Record(fields) => { + for field in fields { + collect_type_bindings(field.ty(), seen, bindings)?; + } + } + SchemaTypeKind::Option(payload) => collect_type_bindings(payload, seen, bindings)?, + SchemaTypeKind::Variant(cases) => { + for payload in cases.iter().filter_map(SchemaCase::payload) { + collect_type_bindings(payload, seen, bindings)?; + } + } + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {} + } + Ok(()) +} + +/// Clears the process-wide macro registry between unit tests. +/// +/// The registry lives for the whole proc-macro process, so a test that expands macros must start +/// from an empty registry. Tests serialize on the registry test lock before they call this. +#[cfg(test)] +pub(crate) fn reset_for_tests() { + if let Some(registry) = REGISTRY.get() { + registry.lock().expect("mutex poisoned").clear(); + } +} diff --git a/sdk/note-codec/macros/src/tests.rs b/sdk/note-codec/macros/src/tests.rs new file mode 100644 index 0000000000..751d22870b --- /dev/null +++ b/sdk/note-codec/macros/src/tests.rs @@ -0,0 +1,219 @@ +//! Tests for codec macro registration and expansion. + +use std::sync::{Mutex, MutexGuard}; + +use proc_macro2::Span; +use quote::quote; +use syn::{ItemImpl, LitStr}; + +use crate::{expand, registry::reset_for_tests}; + +static REGISTRY_TEST_LOCK: Mutex<()> = Mutex::new(()); + +/// Serializes tests that exercise the process-global procedural-macro registry. +fn lock_registry() -> MutexGuard<'static, ()> { + REGISTRY_TEST_LOCK.lock().expect("registry test mutex is poisoned") +} + +const SCHEMA: &str = r#" +package example:codec-schema@1.0.0; + +interface note-storage { + record ratio { + numerator: u64, + denominator: u64, + } + + record codec-note { + ratio: ratio, + } + + type storage = codec-note; +} +"#; + +const EMBEDDED_CORE_SCHEMA: &str = r#" +package example:embedded-core-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{digest}; + record embedded-core-note { value: digest } + type storage = embedded-core-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record word { a: felt, b: felt, c: felt, d: felt } + record digest { inner: word } + } +} +"#; + +#[test] +fn schema_and_codec_registration_generate_native_and_wasm_dispatch() { + let _guard = lock_registry(); + reset_for_tests(); + let schema = LitStr::new(SCHEMA, Span::call_site()); + let generated = expand::from_wit_text(&schema).unwrap(); + let item: ItemImpl = syn::parse2(quote! { + impl miden_note_codec::AuthorTypeCodec for Ratio { + fn parse(_value: &str) -> Result { todo!() } + fn display(&self) -> String { todo!() } + fn validate(&self) -> Result<(), String> { todo!() } + } + }) + .unwrap(); + expand::note_codec(quote!(), item).unwrap(); + let root_item: ItemImpl = syn::parse2(quote! { + impl miden_note_codec::AuthorTypeCodec for CodecNote { + fn parse(_value: &str) -> Result { todo!() } + fn display(&self) -> String { todo!() } + fn validate(&self) -> Result<(), String> { todo!() } + } + }) + .unwrap(); + expand::note_codec(quote!(), root_item).unwrap(); + let exported = expand::export_codecs(quote!()).unwrap(); + let source = prettyplease::unparse(&syn::parse2(quote!(#generated #exported)).unwrap()); + + assert!(source.contains("pub struct CodecNote")); + assert!(source.contains("pub struct Ratio")); + assert!(source.contains("::miden_note_codec::__private::miden_field_repr")); + assert!(source.contains("felt_repr(crate_path")); + assert!(!source.contains("extern crate")); + assert!(source.contains("example:codec-schema/note-storage@1.0.0.codec-note")); + assert!(source.contains("example:codec-schema/note-storage@1.0.0.ratio")); + assert!(source.contains("cfg(target_family = \"wasm\")")); + assert!(source.contains("exports::miden::note_codec::codec::Guest")); +} + +const RESERVED_NAME_SCHEMA: &str = r#" +package example:reserved-schema@1.0.0; + +interface note-storage { + record %self { + value: u64, + } + + record reserved-note { + inner: %self, + } + + type storage = reserved-note; +} +"#; + +#[test] +fn a_reserved_wit_name_registers_under_the_generated_rust_name() { + let _guard = lock_registry(); + reset_for_tests(); + let schema = LitStr::new(RESERVED_NAME_SCHEMA, Span::call_site()); + let generated = expand::from_wit_text(&schema).unwrap(); + // The generator escapes the Rust reserved word, so the registry key must match `Self_`. + let item: ItemImpl = syn::parse2(quote! { + impl miden_note_codec::AuthorTypeCodec for Self_ { + fn parse(_value: &str) -> Result { todo!() } + fn display(&self) -> String { todo!() } + fn validate(&self) -> Result<(), String> { todo!() } + } + }) + .unwrap(); + expand::note_codec(quote!(), item).unwrap(); + let exported = expand::export_codecs(quote!()).unwrap(); + let source = prettyplease::unparse(&syn::parse2(quote!(#generated #exported)).unwrap()); + + assert!(source.contains("pub struct Self_")); + assert!(source.contains("example:reserved-schema/note-storage@1.0.0.self")); +} + +#[test] +fn a_crate_cannot_register_two_distinct_schemas() { + let _guard = lock_registry(); + reset_for_tests(); + let (first_span, second_span) = distinct_expansion_spans(); + let first = LitStr::new(SCHEMA, first_span); + expand::from_wit_text(&first).unwrap(); + + let second_schema = SCHEMA + .replace("example:codec-schema", "example:other-schema") + .replace("codec-note", "other-note"); + let second = LitStr::new(&second_schema, second_span); + let error = expand::from_wit_text(&second).unwrap_err().to_string(); + + assert!(error.contains("one note schema per crate")); + assert!(error.contains("second distinct")); +} + +#[test] +fn same_location_reregistration_replaces_a_stale_schema() { + let _guard = lock_registry(); + reset_for_tests(); + let first = LitStr::new(SCHEMA, Span::call_site()); + expand::from_wit_text(&first).unwrap(); + + // A long-lived macro host re-expands the edited invocation from the same location. + let second_schema = SCHEMA + .replace("example:codec-schema", "example:other-schema") + .replace("codec-note", "other-note"); + let second = LitStr::new(&second_schema, Span::call_site()); + expand::from_wit_text(&second).unwrap(); +} + +/// Returns two spans with distinct source locations. +fn distinct_expansion_spans() -> (Span, Span) { + let tokens: proc_macro2::TokenStream = "first +second" + .parse() + .expect("the span fixture must parse"); + let mut tokens = tokens.into_iter(); + let first = tokens.next().expect("first fixture token").span(); + let second = tokens.next().expect("second fixture token").span(); + assert_ne!(first.start().line, second.start().line, "fixture spans must differ"); + (first, second) +} + +#[test] +fn export_requires_earlier_codec_declarations() { + let _guard = lock_registry(); + reset_for_tests(); + let error = expand::export_codecs(quote!()).unwrap_err().to_string(); + + assert!(error.contains("found no registered codecs")); + assert!(error.contains("register as they expand")); + assert!(error.contains("canonical FQN order")); + assert!(error.contains("#[note_codec]")); +} + +#[test] +fn codec_registry_uses_canonical_standard_leaf_definition() { + let _guard = lock_registry(); + reset_for_tests(); + let schema = LitStr::new(EMBEDDED_CORE_SCHEMA, Span::call_site()); + let generated = expand::from_wit_text(&schema).unwrap(); + let digest_impl: ItemImpl = syn::parse2(quote! { + impl miden_note_codec::AuthorTypeCodec for Digest { + fn parse(_value: &str) -> Result { todo!() } + fn display(&self) -> String { todo!() } + fn validate(&self) -> Result<(), String> { todo!() } + } + }) + .unwrap(); + expand::note_codec(quote!(), digest_impl).unwrap(); + + let word_impl: ItemImpl = syn::parse2(quote! { + impl miden_note_codec::AuthorTypeCodec for Word { + fn parse(_value: &str) -> Result { todo!() } + fn display(&self) -> String { todo!() } + fn validate(&self) -> Result<(), String> { todo!() } + } + }) + .unwrap(); + let error = expand::note_codec(quote!(), word_impl).unwrap_err().to_string(); + + let source = prettyplease::unparse(&syn::parse2(quote!(#generated)).unwrap()); + assert!(source.contains("pub struct Digest")); + assert!(error.contains("not part of a registered note schema")); +} diff --git a/sdk/note-codec/src/lib.rs b/sdk/note-codec/src/lib.rs new file mode 100644 index 0000000000..e55fdfb77d --- /dev/null +++ b/sdk/note-codec/src/lib.rs @@ -0,0 +1,194 @@ +//! Author-side support for typed note storage codecs. +//! +//! Codec components exchange field elements as canonical `u64` values. This crate checks every +//! integer before it enters [`Felt`], so a component cannot introduce a non-canonical (unreduced) +//! or ambiguous field representation. +//! +//! Call `from_project!` or `from_package!` first. Then declare every `#[note_codec]` +//! implementation. Call `export_codecs!` last. Macros register as they expand, while supported +//! types are exported in canonical FQN order. +//! +//! ```ignore +//! use miden_note_codec::AuthorTypeCodec; +//! +//! miden_note_codec::from_project!("../my-note"); +//! +//! #[miden_note_codec::note_codec] +//! impl AuthorTypeCodec for Ratio { +//! // Implement parse, display, and validate. +//! } +//! +//! miden_note_codec::export_codecs!(); +//! ``` +//! +//! The note project opts in to the codec build with package-level metadata in its +//! `miden-project.toml`. The `crate` directory is relative to that manifest: +//! +//! ```toml +//! [package.metadata.midenc.note-codec] +//! crate = "../my-note-codec" +//! ``` +//! +//! The codec is embedded only when the note is built as its own project. A note that another +//! project pulls in as a source dependency carries no codec section, because the codec crate's +//! inputs are outside the build provenance, so a stored dependency copy could keep a stale codec +//! after the codec crate changes. Load the note's own package to get the codec. + +#![deny(missing_docs)] + +use miden_field::Felt; +use miden_field_repr::{FeltReader, FromFeltRepr, ToFeltRepr}; +#[doc(hidden)] +pub use miden_note_codec_macros::from_wit_text; +pub use miden_note_codec_macros::{export_codecs, from_package, from_project, note_codec}; +pub use miden_note_codec_wit::NOTE_CODEC_WIT; +pub use miden_protocol::{account, asset}; + +/// Parses, displays, and validates one author-defined note storage type. +pub trait AuthorTypeCodec: Sized { + /// Parses text into a typed value. + fn parse(value: &str) -> Result; + + /// Displays a typed value. + fn display(&self) -> String; + + /// Validates semantic constraints that are not part of the felt layout. + fn validate(&self) -> Result<(), String>; +} + +/// Converts a field element to its canonical component-boundary integer. +pub fn felt_to_u64(value: Felt) -> u64 { + value.as_canonical_u64() +} + +/// Converts a component-boundary integer to a canonical field element. +pub fn felt_from_u64(value: u64) -> Result { + Felt::new(value).map_err(|error| format!("invalid component felt `{value}`: {error}")) +} + +/// Converts field elements to canonical component-boundary integers. +pub fn felts_to_u64(values: &[Felt]) -> Vec { + values.iter().copied().map(felt_to_u64).collect() +} + +/// Converts component-boundary integers to canonical field elements. +pub fn felts_from_u64(values: &[u64]) -> Result, String> { + values + .iter() + .copied() + .enumerate() + .map(|(index, value)| { + felt_from_u64(value) + .map_err(|error| format!("component felt at index {index} is invalid: {error}")) + }) + .collect() +} + +/// Encodes a native felt-repr value for the component boundary. +pub fn encode_felt_repr(value: &impl ToFeltRepr) -> Vec { + felts_to_u64(&value.to_felt_repr()) +} + +/// Decodes one complete native felt-repr value from the component boundary. +pub fn decode_felt_repr(values: &[u64]) -> Result { + let felts = felts_from_u64(values)?; + let mut reader = FeltReader::new(&felts); + let value = T::from_felt_repr(&mut reader) + .map_err(|error| format!("invalid felt representation: {error}"))?; + reader + .ensure_eof() + .map_err(|error| format!("invalid felt representation: {error}"))?; + Ok(value) +} + +/// Support used by generated host-profile types. +#[doc(hidden)] +pub mod __private { + pub use miden_field; + pub use miden_field_repr; + pub use miden_protocol; + pub use wit_bindgen; + + /// Lightweight error support required by shared generated type helpers. + pub mod miden_note_schema { + use core::fmt; + + /// A generated type conversion error. + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct Error(String); + + impl Error { + /// Creates a generated type conversion error. + pub fn new(message: impl Into) -> Self { + Self(message.into()) + } + } + + impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } + } + + impl std::error::Error for Error {} + + /// A generated type conversion result. + pub type Result = core::result::Result; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A sample author type used to test the native boundary glue. + #[derive(Clone, Debug, Eq, PartialEq, FromFeltRepr, ToFeltRepr)] + struct Ratio { + numerator: u64, + denominator: u64, + } + + impl AuthorTypeCodec for Ratio { + fn parse(value: &str) -> Result { + let (numerator, denominator) = value + .split_once('/') + .ok_or_else(|| "a ratio must use `numerator/denominator`".to_owned())?; + Ok(Self { + numerator: numerator.parse::().map_err(|error| error.to_string())?, + denominator: denominator.parse::().map_err(|error| error.to_string())?, + }) + } + + fn display(&self) -> String { + format!("{}/{}", self.numerator, self.denominator) + } + + fn validate(&self) -> Result<(), String> { + if self.denominator == 0 { + Err("the denominator must not be zero".to_owned()) + } else { + Ok(()) + } + } + } + + #[test] + fn author_trait_values_round_trip_through_boundary_glue() { + let ratio = Ratio::parse("3/2").unwrap(); + ratio.validate().unwrap(); + let encoded = encode_felt_repr(&ratio); + assert_eq!(encoded, [3, 0, 2, 0]); + let decoded = decode_felt_repr::(&encoded).unwrap(); + assert_eq!(decoded, ratio); + assert_eq!(decoded.display(), "3/2"); + } + + #[test] + fn boundary_rejects_noncanonical_field_integers() { + let maximum = Felt::ORDER - 1; + assert_eq!(felt_to_u64(felt_from_u64(maximum).unwrap()), maximum); + assert!(felt_from_u64(Felt::ORDER).unwrap_err().contains("exceeds the felt modulus")); + assert!(felt_from_u64(u64::MAX).unwrap_err().contains("exceeds the felt modulus")); + assert!(felts_from_u64(&[0, Felt::ORDER]).unwrap_err().contains("index 1")); + } +} diff --git a/sdk/note-codec/tests/account_id_dispatch.rs b/sdk/note-codec/tests/account_id_dispatch.rs new file mode 100644 index 0000000000..d86454ac72 --- /dev/null +++ b/sdk/note-codec/tests/account_id_dispatch.rs @@ -0,0 +1,68 @@ +//! Verifies structural codec dispatch for a type that contains a protocol leaf. + +use miden_note_codec::{AuthorTypeCodec, export_codecs, from_wit_text, note_codec}; +use miden_protocol::account::AccountId; + +from_wit_text!( + r#" +package example:account-codec@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id}; + + record account-label { + account: account-id, + serial: u64, + } + + record account-note { + label: account-label, + } + + type storage = account-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record account-id { prefix: felt, suffix: felt } + } +} +"# +); + +#[note_codec] +impl AuthorTypeCodec for AccountLabel { + fn parse(value: &str) -> Result { + let (account, serial) = value + .split_once(',') + .ok_or_else(|| "an account label must use `account-id,serial`".to_owned())?; + let (account, _) = AccountId::parse(account).map_err(|error| error.to_string())?; + let serial = serial.parse::().map_err(|error| error.to_string())?; + Ok(Self { account, serial }) + } + + fn display(&self) -> String { + format!("{},{}", self.account.to_hex(), self.serial) + } + + fn validate(&self) -> Result<(), String> { + Ok(()) + } +} + +export_codecs!(); + +#[test] +fn account_id_codec_uses_generated_structural_traits() { + let account = AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); + let input = format!("{},7", account.to_hex()); + let fqn = AccountLabel::WIT_FQN; + + let encoded = __miden_note_codec_dispatch::parse(fqn, &input).unwrap(); + assert_eq!(encoded, [account.prefix().as_u64(), account.suffix().as_canonical_u64(), 7, 0]); + __miden_note_codec_dispatch::validate(fqn, &encoded).unwrap(); + assert_eq!(__miden_note_codec_dispatch::display(fqn, &encoded).unwrap(), input); +} diff --git a/sdk/note-codec/tests/component_export.rs b/sdk/note-codec/tests/component_export.rs new file mode 100644 index 0000000000..fb2ea70bb2 --- /dev/null +++ b/sdk/note-codec/tests/component_export.rs @@ -0,0 +1,206 @@ +//! Component export test for the author codec world. + +use std::{ + env, fs, + path::{Path, PathBuf}, + process::{Command, Output}, +}; + +use midenc_frontend_wasm_metadata::NESTED_CARGO_SCRUB_ENV; +use tempfile::TempDir; +use wit_component::DecodedWasm; +use wit_parser::WorldItem; + +const WASM_TARGET: &str = "wasm32-wasip2"; + +#[test] +fn minimal_codec_crate_builds_to_wasi_only_component() { + if !wasm_target_is_installed() { + eprintln!("skipping component export test: {WASM_TARGET} is not installed"); + return; + } + + let fixture = TempDir::new().expect("failed to create temporary codec crate"); + write_fixture(fixture.path()); + let target_dir = workspace_root().join("target/note-codec-component-test"); + let mut command = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); + command + .args([ + "build", + "--manifest-path", + fixture.path().join("Cargo.toml").to_str().unwrap(), + "--release", + "--target", + WASM_TARGET, + "--offline", + ]) + .env("CARGO_TARGET_DIR", &target_dir) + // Share the hash-keyed intermediates with the other test builds; only the + // name-keyed final artifacts stay in the directory above. Convention from + // `tests/support`. + .env("CARGO_BUILD_BUILD_DIR", workspace_root().join("target/miden_build_cache")); + for &variable in NESTED_CARGO_SCRUB_ENV { + command.env_remove(variable); + } + // Pin the guest rustflags after the scrub, the way the compiler builds a codec crate. + command.env("RUSTFLAGS", miden_note_schema::NOTE_CODEC_GUEST_RUSTFLAGS); + let output = command.output().expect("failed to run cargo for the component fixture"); + assert_command_succeeded("building the component fixture", &output); + + let component = fs::read( + target_dir.join(format!("{WASM_TARGET}/release/note_codec_component_fixture.wasm")), + ) + .expect("component fixture did not produce a Wasm component"); + // A codec crate must build to a component every consumer accepts. + miden_note_schema::validate_note_codec_component( + &component, + miden_note_schema::MAX_NOTE_CODEC_COMPONENT_BYTES, + ) + .expect("the built component must pass the note codec load policy"); + let DecodedWasm::Component(resolve, world_id) = + wit_component::decode(&component).expect("failed to decode the built component") + else { + panic!("the fixture build did not produce a component"); + }; + let world = &resolve.worlds[world_id]; + for (key, _) in world.imports.iter() { + let name = resolve.name_world_key(key); + assert!(name.starts_with("wasi:"), "unexpected non-WASI import `{name}`"); + } + assert_eq!(world.exports.len(), 1); + + let interface_id = world + .exports + .values() + .find_map(|item| match item { + WorldItem::Interface { id, .. } => Some(*id), + _ => None, + }) + .expect("the component does not export the note codec interface"); + let interface = &resolve.interfaces[interface_id]; + assert_eq!(interface.name.as_deref(), Some("codec")); + let package_id = interface.package.expect("codec interface has no package"); + let package = &resolve.packages[package_id].name; + assert_eq!(package.namespace, "miden"); + assert_eq!(package.name, "note-codec"); + assert_eq!(package.version.as_ref().map(ToString::to_string).as_deref(), Some("1.0.0")); + assert_eq!( + interface.functions.keys().map(String::as_str).collect::>(), + ["supported-types", "parse", "display", "validate"] + ); +} + +/// Writes the minimal author codec crate used by the componentization test. +fn write_fixture(root: &Path) { + fs::create_dir(root.join("src")).expect("failed to create fixture source directory"); + // Seed the workspace lockfile so the offline build resolves to the versions that the + // workspace already downloaded instead of racing the registry index. + fs::copy(workspace_root().join("Cargo.lock"), root.join("Cargo.lock")) + .expect("failed to seed the fixture with the workspace lockfile"); + let codec_path = Path::new(env!("CARGO_MANIFEST_DIR")); + let manifest = format!( + r#"[package] +name = "note-codec-component-fixture" +version = "0.0.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +codec-facade = {{ package = "miden-note-codec", path = {:?} }} + +[workspace] +"#, + codec_path + ); + fs::write(root.join("Cargo.toml"), manifest).expect("failed to write fixture manifest"); + fs::write(root.join("src/lib.rs"), FIXTURE_SOURCE).expect("failed to write fixture source"); +} + +/// Returns the compiler workspace root. +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("note-codec must be inside sdk/") + .to_owned() +} + +/// Reports all command output when one fixture command fails. +fn assert_command_succeeded(action: &str, output: &Output) { + assert!( + output.status.success(), + "failed while {action}\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} + +/// Local copy of the sysroot probe in tests/support to keep this test dependency-light. +fn wasm_target_is_installed() -> bool { + let output = match Command::new("rustc").args(["--print", "sysroot"]).output() { + Ok(output) if output.status.success() => output, + Ok(output) => { + eprintln!( + "`rustc --print sysroot` failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + return false; + } + Err(error) => { + eprintln!("could not run `rustc --print sysroot` (rustup may be unavailable): {error}"); + return false; + } + }; + let sysroot = Path::new(String::from_utf8_lossy(&output.stdout).trim()).to_path_buf(); + sysroot.join("lib").join("rustlib").join(WASM_TARGET).exists() +} + +const FIXTURE_SOURCE: &str = r##" +use codec_facade::AuthorTypeCodec; + +codec_facade::from_wit_text!(r#" +package example:codec-schema@1.0.0; + +interface note-storage { + record ratio { + numerator: u64, + denominator: u64, + } + + record codec-note { + ratio: ratio, + } + + type storage = codec-note; +} +"#); + +#[codec_facade::note_codec] +impl AuthorTypeCodec for Ratio { + fn parse(value: &str) -> Result { + let (numerator, denominator) = value + .split_once('/') + .ok_or_else(|| "a ratio must use `numerator/denominator`".to_owned())?; + Ok(Self { + numerator: numerator.parse::().map_err(|error| error.to_string())?, + denominator: denominator.parse::().map_err(|error| error.to_string())?, + }) + } + + fn display(&self) -> String { + format!("{}/{}", self.numerator, self.denominator) + } + + fn validate(&self) -> Result<(), String> { + if self.denominator == 0 { + Err("the denominator must not be zero".to_owned()) + } else { + Ok(()) + } + } +} + +codec_facade::export_codecs!(); +"##; diff --git a/sdk/note-codec/tests/dispatch.rs b/sdk/note-codec/tests/dispatch.rs new file mode 100644 index 0000000000..11fc943609 --- /dev/null +++ b/sdk/note-codec/tests/dispatch.rs @@ -0,0 +1,72 @@ +//! Native tests for generated author codec dispatch. + +use miden_note_codec::AuthorTypeCodec; + +miden_note_codec::from_wit_text!( + r#" +package example:codec-schema@1.0.0; + +interface note-storage { + record ratio { + numerator: u64, + denominator: u64, + } + + record codec-note { + ratio: ratio, + } + + type storage = codec-note; +} +"# +); + +#[miden_note_codec::note_codec] +impl AuthorTypeCodec for Ratio { + fn parse(value: &str) -> Result { + let (numerator, denominator) = value + .split_once('/') + .ok_or_else(|| "a ratio must use `numerator/denominator`".to_owned())?; + Ok(Self { + numerator: numerator.parse::().map_err(|error| error.to_string())?, + denominator: denominator.parse::().map_err(|error| error.to_string())?, + }) + } + + fn display(&self) -> String { + format!("{}/{}", self.numerator, self.denominator) + } + + fn validate(&self) -> Result<(), String> { + if self.denominator == 0 { + Err("the denominator must not be zero".to_owned()) + } else { + Ok(()) + } + } +} + +miden_note_codec::export_codecs!(); + +#[test] +fn marked_codec_dispatches_by_canonical_wit_fqn() { + const RATIO_FQN: &str = "example:codec-schema/note-storage@1.0.0.ratio"; + + assert_eq!(__miden_note_codec_dispatch::supported_types(), [RATIO_FQN]); + let encoded = __miden_note_codec_dispatch::parse(RATIO_FQN, "3/2").unwrap(); + assert_eq!(encoded, [3, 0, 2, 0]); + assert_eq!(__miden_note_codec_dispatch::display(RATIO_FQN, &encoded).unwrap(), "3/2"); + __miden_note_codec_dispatch::validate(RATIO_FQN, &encoded).unwrap(); + + let invalid = __miden_note_codec_dispatch::parse(RATIO_FQN, "3/0").unwrap(); + assert!( + __miden_note_codec_dispatch::validate(RATIO_FQN, &invalid) + .unwrap_err() + .contains("denominator") + ); + assert!( + __miden_note_codec_dispatch::parse("example:unknown/type", "3/2") + .unwrap_err() + .contains("no note codec is registered") + ); +} diff --git a/sdk/note-codec/wit-crate/Cargo.toml b/sdk/note-codec/wit-crate/Cargo.toml new file mode 100644 index 0000000000..401e48fcf6 --- /dev/null +++ b/sdk/note-codec/wit-crate/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "miden-note-codec-wit" +description = "Canonical WIT contract for Miden note codecs" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true + +[lib] +doctest = false diff --git a/sdk/note-codec/wit-crate/src/lib.rs b/sdk/note-codec/wit-crate/src/lib.rs new file mode 100644 index 0000000000..f0a72daf5e --- /dev/null +++ b/sdk/note-codec/wit-crate/src/lib.rs @@ -0,0 +1,8 @@ +//! Canonical WIT contract for Miden note codecs. + +#![deny(missing_docs)] +#![no_std] + +/// The WIT document implemented by note codec components. +pub const NOTE_CODEC_WIT: &str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/wit/note-codec.wit")); diff --git a/sdk/note-codec/wit-crate/wit/note-codec.wit b/sdk/note-codec/wit-crate/wit/note-codec.wit new file mode 100644 index 0000000000..950aa77cb9 --- /dev/null +++ b/sdk/note-codec/wit-crate/wit/note-codec.wit @@ -0,0 +1,28 @@ +// Mirror note: the wit-crate file is canonical; sdk/note-schema/wit/note-codec.wit exists because +// Wasmtime bindgen!(path:) needs package-local WIT, and an equality test guards both files. +package miden:note-codec@1.0.0; + +/// Parses, displays, and validates custom note storage types. +interface codec { + /// A canonical Miden field element at the component boundary. + /// + /// This is a `u64`, not the core-types `felt` record, because that record models the + /// compiler's guest felt value and does not expose its canonical integer representation. + type felt = u64; + + /// Returns the fully-qualified WIT names handled by this component. + supported-types: func() -> list; + + /// Parses text into a type's structural felt representation. + parse: func(type-fqn: string, value: string) -> result, string>; + + /// Displays a type's structural felt representation. + display: func(type-fqn: string, value: list) -> result; + + /// Validates a type's structural felt representation. + validate: func(type-fqn: string, value: list) -> result<_, string>; +} + +world note-codec { + export codec; +} diff --git a/sdk/note-schema/Cargo.toml b/sdk/note-schema/Cargo.toml new file mode 100644 index 0000000000..2c827f3183 --- /dev/null +++ b/sdk/note-schema/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "miden-note-schema" +description = "Host-side reader and codec registry for Miden note storage schemas" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true + +[features] +default = [] +codec-component = ["dep:wasmtime"] + +[lib] +doctest = false + +[dependencies] +miden-field.workspace = true +miden-field-repr.workspace = true +miden-mast-package = { workspace = true, features = ["std"] } +miden-protocol = { workspace = true, features = ["std"] } +midenc-frontend-wasm-metadata.workspace = true +toml.workspace = true +wasmparser.workspace = true +wit-parser.workspace = true +wasmtime = { workspace = true, optional = true } + +[dev-dependencies] +miden-core.workspace = true +miden-note-codec-wit.workspace = true +midenc-integration-test-support.workspace = true +tempfile.workspace = true +wat.workspace = true diff --git a/sdk/note-schema/codegen/Cargo.toml b/sdk/note-schema/codegen/Cargo.toml new file mode 100644 index 0000000000..fc681dfe46 --- /dev/null +++ b/sdk/note-schema/codegen/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "miden-note-schema-codegen" +description = "Rust code generator for Miden note storage schemas" +version = "0.14.0" +rust-version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +edition.workspace = true + +[lib] +doctest = false + +[dependencies] +heck.workspace = true +miden-note-schema.workspace = true +proc-macro2.workspace = true +quote.workspace = true + +[dev-dependencies] +prettyplease = { workspace = true } +syn.workspace = true diff --git a/sdk/note-schema/codegen/src/lib.rs b/sdk/note-schema/codegen/src/lib.rs new file mode 100644 index 0000000000..f26c1c97ca --- /dev/null +++ b/sdk/note-schema/codegen/src/lib.rs @@ -0,0 +1,781 @@ +//! Shared Rust code generation for resolved note storage schemas. + +#![deny(missing_docs)] + +use std::{ + collections::{BTreeMap, BTreeSet, HashMap}, + fmt, +}; + +use heck::{ToSnakeCase, ToUpperCamelCase}; +use miden_note_schema::{ + NoteStorageSchema, PrimitiveType, SchemaCase, SchemaField, SchemaType, SchemaTypeKind, + StandardLeaf, +}; +use proc_macro2::{Ident, Literal, Span, TokenStream}; +use quote::{format_ident, quote}; + +/// An error reported while generating Rust bindings. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CodegenError { + message: String, +} + +impl CodegenError { + /// Creates a code-generation error. + fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } +} + +impl fmt::Display for CodegenError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for CodegenError {} + +/// Rust host types and metadata produced from one note storage schema. +pub struct GeneratedTypes { + tokens: TokenStream, + root_ident: Ident, + type_idents: Vec, + has_nested_named_types: bool, +} + +impl GeneratedTypes { + /// Returns the generated Rust items. + pub const fn tokens(&self) -> &TokenStream { + &self.tokens + } + + /// Returns the Rust identifier for the root storage type. + pub const fn root_ident(&self) -> &Ident { + &self.root_ident + } + + /// Returns every generated Rust type identifier in emission order. + pub fn type_idents(&self) -> &[Ident] { + &self.type_idents + } + + /// Returns true when the schema has a generated named type below its storage root. + pub const fn has_nested_named_types(&self) -> bool { + self.has_nested_named_types + } +} + +/// Runtime crate paths used by generated host-profile code. +#[derive(Clone)] +pub struct RuntimePaths { + miden_field: TokenStream, + miden_field_repr: TokenStream, + miden_note_schema: TokenStream, + miden_protocol: TokenStream, +} + +impl RuntimePaths { + /// Creates a runtime path set from four crate or module paths. + pub fn new( + miden_field: TokenStream, + miden_field_repr: TokenStream, + miden_note_schema: TokenStream, + miden_protocol: TokenStream, + ) -> Self { + Self { + miden_field, + miden_field_repr, + miden_note_schema, + miden_protocol, + } + } + + /// Creates paths through a facade crate's hidden runtime re-exports. + pub fn through_facade(facade: TokenStream) -> Self { + Self::new( + quote!(#facade::__private::miden_field), + quote!(#facade::__private::miden_field_repr), + quote!(#facade::__private::miden_note_schema), + quote!(#facade::__private::miden_protocol), + ) + } +} + +impl Default for RuntimePaths { + fn default() -> Self { + Self::new( + quote!(::miden_field), + quote!(::miden_field_repr), + quote!(::miden_note_schema), + quote!(::miden_protocol), + ) + } +} + +/// Generates Rust host-profile types and structural felt conversion helpers. +pub fn generate_host_types( + schema: &NoteStorageSchema, + runtime: &RuntimePaths, +) -> Result { + schema + .validate_native_leaf_shapes() + .map_err(|error| CodegenError::new(error.to_string()))?; + let root = schema.root(); + let root_fqn = root + .fqn() + .ok_or_else(|| CodegenError::new("the note storage root does not have a WIT FQN"))?; + let root_name = root + .name() + .ok_or_else(|| CodegenError::new("the note storage root does not have a WIT name"))?; + + let mut definitions = Vec::new(); + let mut seen = BTreeSet::new(); + collect_named_types(root, &mut seen, &mut definitions)?; + + let mut rust_names = BTreeMap::new(); + let mut used_names = BTreeMap::::new(); + for definition in &definitions { + let fqn = definition.fqn().expect("collected named types always have a FQN"); + let name = definition.name().expect("collected named types always have a name"); + let ident = type_ident(name); + if let Some(existing) = used_names.insert(ident.to_string(), fqn.to_owned()) + && existing != fqn + { + return Err(CodegenError::new(format!( + "WIT types `{existing}` and `{fqn}` both map to Rust type `{ident}`" + ))); + } + rust_names.insert(fqn.to_owned(), ident); + } + + let root_ident = rust_names.get(root_fqn).cloned().unwrap_or_else(|| type_ident(root_name)); + let helper_traits = generate_helper_traits(runtime); + let mut felt_repr_support = FeltReprSupport::default(); + let items = definitions + .iter() + .map(|definition| generate_type(definition, &rust_names, runtime, &mut felt_repr_support)) + .collect::, _>>()?; + let type_idents = definitions + .iter() + .map(|definition| { + rust_names + .get(definition.fqn().expect("generated types have an FQN")) + .expect("generated types have a Rust name") + .clone() + }) + .collect(); + let has_nested_named_types = definitions + .iter() + .any(|definition| definition.fqn().is_some_and(|fqn| fqn != root_fqn)); + + Ok(GeneratedTypes { + tokens: quote! { + #helper_traits + #(#items)* + }, + root_ident, + type_idents, + has_nested_named_types, + }) +} + +/// Collects reachable named records and variants, excluding mapped protocol leaves. +fn collect_named_types<'a>( + ty: &'a SchemaType, + seen: &mut BTreeSet, + definitions: &mut Vec<&'a SchemaType>, +) -> Result<(), CodegenError> { + if ty.standard_leaf().is_some() { + return Ok(()); + } + + let is_definition = matches!(ty.kind(), SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_)); + if is_definition { + let fqn = ty.fqn().ok_or_else(|| { + CodegenError::new("anonymous WIT records and variants cannot be emitted as Rust types") + })?; + ty.name().ok_or_else(|| { + CodegenError::new(format!("WIT type `{fqn}` does not have a local name")) + })?; + if !seen.insert(fqn.to_owned()) { + return Ok(()); + } + definitions.push(ty); + } + + match ty.kind() { + SchemaTypeKind::Record(fields) => { + for field in fields { + collect_named_types(field.ty(), seen, definitions)?; + } + } + SchemaTypeKind::Option(payload) => collect_named_types(payload, seen, definitions)?, + SchemaTypeKind::Variant(cases) => { + for payload in cases.iter().filter_map(SchemaCase::payload) { + collect_named_types(payload, seen, definitions)?; + } + } + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {} + } + Ok(()) +} + +/// Generates the private traits that keep protocol-leaf order separate from foreign trait impls. +fn generate_helper_traits(runtime: &RuntimePaths) -> TokenStream { + let RuntimePaths { + miden_field, + miden_field_repr, + miden_note_schema, + miden_protocol, + } = runtime; + let primitive_impls = [ + quote!(u64), + quote!(u32), + quote!(u8), + quote!(bool), + quote!(#miden_field::Felt), + quote!(#miden_field::Word), + ] + .into_iter() + .map(|ty| { + quote! { + impl __MidenNoteEncode for #ty { + fn __write_note_felts( + &self, + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { + #miden_field_repr::ToFeltRepr::write_felt_repr(self, writer); + Ok(()) + } + } + + impl __MidenNoteDecode for #ty { + fn __read_note_felts( + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { + #miden_field_repr::FromFeltRepr::from_felt_repr(reader).map_err(|error| { + #miden_note_schema::Error::new(format!( + "failed to decode {} from note storage: {error}", + stringify!(#ty), + )) + }) + } + } + } + }); + + quote! { + #[doc(hidden)] + trait __MidenNoteEncode { + fn __write_note_felts( + &self, + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()>; + } + + #[doc(hidden)] + trait __MidenNoteDecode: ::core::marker::Sized { + fn __read_note_felts( + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result; + } + + #(#primitive_impls)* + + impl __MidenNoteEncode for #miden_protocol::account::AccountId { + fn __write_note_felts( + &self, + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { + // The WIT record declares prefix before suffix. + writer.write(self.prefix().as_felt()); + writer.write(self.suffix()); + Ok(()) + } + } + + impl __MidenNoteDecode for #miden_protocol::account::AccountId { + fn __read_note_felts( + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { + let prefix = reader.read().map_err(|error| { + #miden_note_schema::Error::new(format!( + "failed to decode account-id prefix: {error}" + )) + })?; + let suffix = reader.read().map_err(|error| { + #miden_note_schema::Error::new(format!( + "failed to decode account-id suffix: {error}" + )) + })?; + // WIT declares prefix before suffix, but the constructor takes suffix first. + #miden_protocol::account::AccountId::try_from_elements(suffix, prefix).map_err( + |error| { + #miden_note_schema::Error::new(format!( + "invalid account-id in note storage: {error}" + )) + }, + ) + } + } + + impl __MidenNoteEncode for #miden_protocol::asset::AssetAmount { + fn __write_note_felts( + &self, + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { + writer.write(#miden_field::Felt::from(*self)); + Ok(()) + } + } + + impl __MidenNoteDecode for #miden_protocol::asset::AssetAmount { + fn __read_note_felts( + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { + let value = reader.read().map_err(|error| { + #miden_note_schema::Error::new(format!( + "failed to decode asset-amount: {error}" + )) + })?; + #miden_protocol::asset::AssetAmount::try_from(value).map_err(|error| { + #miden_note_schema::Error::new(format!( + "invalid asset-amount in note storage: {error}" + )) + }) + } + } + + impl __MidenNoteEncode for ::core::option::Option + where + T: __MidenNoteEncode, + { + fn __write_note_felts( + &self, + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { + match self { + None => writer.write(#miden_field::Felt::ZERO), + Some(value) => { + writer.write(#miden_field::Felt::ONE); + value.__write_note_felts(writer)?; + } + } + Ok(()) + } + } + + impl __MidenNoteDecode for ::core::option::Option + where + T: __MidenNoteDecode, + { + fn __read_note_felts( + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { + let tag = reader.read().map_err(|error| { + #miden_note_schema::Error::new(format!( + "failed to decode option tag: {error}" + )) + })?; + match tag.as_canonical_u64() { + 0 => Ok(None), + 1 => Ok(Some(T::__read_note_felts(reader)?)), + tag => Err(#miden_note_schema::Error::new(format!( + "invalid option tag {tag}; expected 0 or 1" + ))), + } + } + } + } +} + +/// Generates one Rust record or variant and its structural conversion helpers. +fn generate_type( + definition: &SchemaType, + rust_names: &BTreeMap, + runtime: &RuntimePaths, + felt_repr_support: &mut FeltReprSupport, +) -> Result { + let fqn = definition.fqn().expect("generated type definitions always have a FQN"); + let ident = rust_names.get(fqn).expect("every generated type has a Rust identifier"); + let docs = type_docs(definition, fqn); + // Every trait is named through its absolute path. A generated type takes the name of its + // WIT type, so a schema may declare a type called `Clone`, `Debug`, or `Sized`. + let derives = if felt_repr_support.supports_native_felt_repr(definition) { + let miden_field_repr = &runtime.miden_field_repr; + // `quote` prints `::` paths with spaces around each separator. The attribute value must + // be one path the derive macro can parse, so the spaces come out. + let crate_path = Literal::string(&miden_field_repr.to_string().replace(' ', "")); + quote! { + #[derive( + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, + #miden_field_repr::ToFeltRepr, + #miden_field_repr::FromFeltRepr, + )] + #[felt_repr(crate_path = #crate_path)] + } + } else { + quote! { + #[derive( + ::core::clone::Clone, + ::core::fmt::Debug, + ::core::cmp::PartialEq, + ::core::cmp::Eq, + )] + } + }; + + let (item, encode_impl, decode_impl) = match definition.kind() { + SchemaTypeKind::Record(fields) => { + generate_record(ident, fields, rust_names, &docs, &derives, runtime)? + } + SchemaTypeKind::Variant(cases) => { + generate_variant(ident, cases, rust_names, &docs, &derives, runtime)? + } + _ => { + return Err(CodegenError::new(format!( + "named WIT type `{fqn}` is not a record or variant" + ))); + } + }; + + Ok(quote! { + #item + + impl #ident { + /// The canonical fully-qualified WIT name for this type. + pub const WIT_FQN: &'static str = #fqn; + } + + #encode_impl + #decode_impl + }) +} + +/// Generates a Rust struct in WIT declaration order. +fn generate_record( + ident: &Ident, + fields: &[SchemaField], + rust_names: &BTreeMap, + docs: &TokenStream, + derives: &TokenStream, + runtime: &RuntimePaths, +) -> Result<(TokenStream, TokenStream, TokenStream), CodegenError> { + let miden_field_repr = &runtime.miden_field_repr; + let miden_note_schema = &runtime.miden_note_schema; + let rust_fields = fields + .iter() + .map(|field| { + let ident = value_ident(field.name()); + let ty = rust_type(field.ty(), rust_names, runtime)?; + let docs = field_docs(field); + Ok((ident, ty, docs)) + }) + .collect::, CodegenError>>()?; + let field_idents = rust_fields.iter().map(|(ident, ..)| ident).collect::>(); + let field_types = rust_fields.iter().map(|(_, ty, _)| ty).collect::>(); + let field_docs = rust_fields.iter().map(|(_, _, docs)| docs).collect::>(); + + let item = quote! { + #docs + #derives + pub struct #ident { + #( + #field_docs + pub #field_idents: #field_types, + )* + } + }; + let encode_impl = quote! { + impl __MidenNoteEncode for #ident { + fn __write_note_felts( + &self, + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { + #(self.#field_idents.__write_note_felts(writer)?;)* + Ok(()) + } + } + }; + let decode_impl = quote! { + impl __MidenNoteDecode for #ident { + fn __read_note_felts( + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { + Ok(Self { + #(#field_idents: <#field_types as __MidenNoteDecode>::__read_note_felts(reader)?,)* + }) + } + } + }; + Ok((item, encode_impl, decode_impl)) +} + +/// Generates a Rust enum with declaration-ordinal tags. +fn generate_variant( + ident: &Ident, + cases: &[SchemaCase], + rust_names: &BTreeMap, + docs: &TokenStream, + derives: &TokenStream, + runtime: &RuntimePaths, +) -> Result<(TokenStream, TokenStream, TokenStream), CodegenError> { + let miden_field = &runtime.miden_field; + let miden_field_repr = &runtime.miden_field_repr; + let miden_note_schema = &runtime.miden_note_schema; + let rust_cases = cases + .iter() + .map(|case| { + let ident = type_ident(case.name()); + let payload = + case.payload().map(|ty| rust_type(ty, rust_names, runtime)).transpose()?; + let docs = case_docs(case); + Ok((ident, payload, docs)) + }) + .collect::, CodegenError>>()?; + + let declarations = rust_cases.iter().map(|(case, payload, docs)| match payload { + Some(payload) => quote! { + #docs + #case(#payload), + }, + None => quote! { + #docs + #case, + }, + }); + let encode_arms = rust_cases.iter().enumerate().map(|(ordinal, (case, payload, _))| { + let ordinal = ordinal as u32; + match payload { + Some(_) => quote! { + Self::#case(value) => { + writer.write(#miden_field::Felt::from_u32(#ordinal)); + value.__write_note_felts(writer)?; + } + }, + None => quote! { + Self::#case => { + writer.write(#miden_field::Felt::from_u32(#ordinal)); + } + }, + } + }); + let decode_arms = rust_cases.iter().enumerate().map(|(ordinal, (case, payload, _))| { + let ordinal = ordinal as u32; + match payload { + Some(payload) => quote! { + #ordinal => Ok(Self::#case( + <#payload as __MidenNoteDecode>::__read_note_felts(reader)?, + )), + }, + None => quote! { #ordinal => Ok(Self::#case), }, + } + }); + let case_count = cases.len(); + + let item = quote! { + #docs + #derives + pub enum #ident { + #(#declarations)* + } + }; + let encode_impl = quote! { + impl __MidenNoteEncode for #ident { + fn __write_note_felts( + &self, + writer: &mut #miden_field_repr::FeltWriter<'_>, + ) -> #miden_note_schema::Result<()> { + match self { + #(#encode_arms)* + } + Ok(()) + } + } + }; + let decode_impl = quote! { + impl __MidenNoteDecode for #ident { + fn __read_note_felts( + reader: &mut #miden_field_repr::FeltReader<'_>, + ) -> #miden_note_schema::Result { + let tag = reader.read_u32().map_err(|error| { + #miden_note_schema::Error::new(format!( + "failed to decode {} tag: {error}", + stringify!(#ident), + )) + })?; + match tag { + #(#decode_arms)* + tag => Err(#miden_note_schema::Error::new(format!( + "invalid {} tag {tag}; expected a declaration ordinal below {}", + stringify!(#ident), + #case_count, + ))), + } + } + } + }; + Ok((item, encode_impl, decode_impl)) +} + +/// Maps one schema type to its host-profile Rust type. +fn rust_type( + ty: &SchemaType, + rust_names: &BTreeMap, + runtime: &RuntimePaths, +) -> Result { + if let Some(mapped) = mapped_leaf(ty, runtime) { + return Ok(mapped); + } + if let Some(fqn) = ty.fqn() + && let Some(ident) = rust_names.get(fqn) + { + return Ok(quote!(#ident)); + } + + match ty.kind() { + SchemaTypeKind::Primitive(PrimitiveType::U64) => Ok(quote!(u64)), + SchemaTypeKind::Primitive(PrimitiveType::U32) => Ok(quote!(u32)), + SchemaTypeKind::Primitive(PrimitiveType::U8) => Ok(quote!(u8)), + SchemaTypeKind::Primitive(PrimitiveType::Bool) => Ok(quote!(bool)), + SchemaTypeKind::Option(payload) => { + let payload = rust_type(payload, rust_names, runtime)?; + Ok(quote!(::core::option::Option<#payload>)) + } + SchemaTypeKind::Record(_) | SchemaTypeKind::Variant(_) => Err(CodegenError::new(format!( + "named WIT type `{}` was not collected for Rust generation", + ty.fqn().or(ty.name()).unwrap_or("") + ))), + SchemaTypeKind::Felt => { + let miden_field = &runtime.miden_field; + Ok(quote!(#miden_field::Felt)) + } + } +} + +/// Returns the native Rust type for one standard WIT leaf. +fn mapped_leaf(ty: &SchemaType, runtime: &RuntimePaths) -> Option { + let RuntimePaths { + miden_field, + miden_protocol, + .. + } = runtime; + match ty.standard_leaf()? { + StandardLeaf::Felt => Some(quote!(#miden_field::Felt)), + StandardLeaf::Word => Some(quote!(#miden_field::Word)), + StandardLeaf::AccountId => Some(quote!(#miden_protocol::account::AccountId)), + StandardLeaf::AssetAmount => Some(quote!(#miden_protocol::asset::AssetAmount)), + } +} + +/// Memoized answers for the felt-repr support walk, keyed by resolved node identity. +#[derive(Default)] +struct FeltReprSupport { + answers: HashMap<*const SchemaType, bool>, +} + +impl FeltReprSupport { + /// Returns true when all fields implement the native felt-repr traits without protocol + /// adapters. + /// + /// The schema model is a DAG that shares one node per resolved WIT type, so the answers are + /// memoized by node identity. The walk is then linear in the number of distinct types instead + /// of the size of the expanded tree. + fn supports_native_felt_repr(&mut self, ty: &SchemaType) -> bool { + let key = ty as *const SchemaType; + if let Some(answer) = self.answers.get(&key) { + return *answer; + } + let answer = self.compute(ty); + self.answers.insert(key, answer); + answer + } + + /// Answers the felt-repr question for one node from its children. + fn compute(&mut self, ty: &SchemaType) -> bool { + if matches!(ty.standard_leaf(), Some(StandardLeaf::AccountId | StandardLeaf::AssetAmount)) { + return false; + } + match ty.kind() { + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => true, + SchemaTypeKind::Record(fields) => { + fields.iter().all(|field| self.supports_native_felt_repr(field.ty())) + } + SchemaTypeKind::Option(payload) => self.supports_native_felt_repr(payload), + SchemaTypeKind::Variant(cases) => cases.iter().all(|case| { + case.payload().is_none_or(|payload| self.supports_native_felt_repr(payload)) + }), + } + } +} + +/// Produces documentation for a generated WIT type. +fn type_docs(ty: &SchemaType, fqn: &str) -> TokenStream { + let docs = ty + .docs() + .map(str::to_owned) + .unwrap_or_else(|| format!("Rust binding for WIT type `{fqn}`.")); + quote!(#[doc = #docs]) +} + +/// Produces documentation for a generated record field. +fn field_docs(field: &SchemaField) -> TokenStream { + let docs = field + .docs() + .map(str::to_owned) + .unwrap_or_else(|| format!("Value of the WIT `{}` field.", field.name())); + quote!(#[doc = #docs]) +} + +/// Produces documentation for a generated variant case. +fn case_docs(case: &SchemaCase) -> TokenStream { + let docs = case + .docs() + .map(str::to_owned) + .unwrap_or_else(|| format!("WIT `{}` case.", case.name())); + quote!(#[doc = #docs]) +} + +/// Returns the Rust type name the generator gives one WIT type or case name. +/// +/// A caller that maps generated types back to WIT names uses this function, so the two sides +/// agree on the upper camel case conversion and on the escape of a Rust reserved word. +pub fn generated_type_ident(name: &str) -> String { + type_ident(name).to_string() +} + +/// Converts a WIT type or case name to a Rust type identifier. +fn type_ident(name: &str) -> Ident { + rust_ident(&name.to_upper_camel_case()) +} + +/// Converts a WIT field name to a Rust value identifier. +fn value_ident(name: &str) -> Ident { + rust_ident(&name.replace('-', "_").to_snake_case()) +} + +/// Creates an identifier and avoids Rust reserved words. +fn rust_ident(name: &str) -> Ident { + const RESERVED: &[&str] = &[ + "Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue", + "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "gen", "if", + "impl", "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", + "pub", "ref", "return", "self", "static", "struct", "super", "trait", "true", "try", + "type", "typeof", "union", "unsafe", "unsized", "use", "virtual", "where", "while", + "yield", + ]; + if RESERVED.contains(&name) { + format_ident!("{name}_") + } else { + Ident::new(name, Span::call_site()) + } +} + +#[cfg(test)] +mod tests; diff --git a/sdk/note-schema/codegen/src/tests.rs b/sdk/note-schema/codegen/src/tests.rs new file mode 100644 index 0000000000..2e61c417eb --- /dev/null +++ b/sdk/note-schema/codegen/src/tests.rs @@ -0,0 +1,126 @@ +//! Tests for host-profile type generation. + +use miden_note_schema::NoteStorageSchema; + +use crate::{RuntimePaths, generate_host_types}; + +const P2ID_SCHEMA: &str = r#" +package example:p2id-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id}; + + record p2id-note { + target-account-id: account-id, + } + + type storage = p2id-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record account-id { prefix: felt, suffix: felt } + } +} +"#; + +const CUSTOM_SCHEMA: &str = r#" +package example:dex-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id}; + + record limit-price { + numerator: u64, + denominator: u64, + } + + variant order-kind { + market, + limit(limit-price), + } + + record dex-note { + target: account-id, + kind: order-kind, + } + + type storage = dex-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record account-id { prefix: felt, suffix: felt } + } +} +"#; + +const EMBEDDED_CORE_SCHEMA: &str = r#" +package example:embedded-core-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{digest}; + record embedded-core-note { value: digest } + type storage = embedded-core-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record word { a: felt, b: felt, c: felt, d: felt } + record digest { inner: word } + } +} +"#; + +/// Formats generated tokens as Rust source. +fn generate(wit: &str) -> (String, bool, String) { + let schema = NoteStorageSchema::from_wit_text(wit).unwrap(); + let generated = generate_host_types(&schema, &RuntimePaths::default()).unwrap(); + let file: syn::File = syn::parse2(generated.tokens().clone()).unwrap(); + ( + prettyplease::unparse(&file), + generated.has_nested_named_types(), + generated.root_ident().to_string(), + ) +} + +#[test] +fn maps_protocol_leaf_and_root_type() { + let (source, has_nested_named_types, root) = generate(P2ID_SCHEMA); + assert_eq!(root, "P2idNote"); + assert!(!has_nested_named_types); + assert!(source.contains("pub target_account_id: ::miden_protocol::account::AccountId")); + assert!(source.contains("pub const WIT_FQN")); + assert!(source.contains("self.prefix().as_felt()")); +} + +#[test] +fn derives_native_repr_for_custom_records_and_variants() { + let (source, has_nested_named_types, root) = generate(CUSTOM_SCHEMA); + assert_eq!(root, "DexNote"); + assert!(has_nested_named_types); + assert!(source.contains("pub struct LimitPrice")); + assert!(source.contains("pub enum OrderKind")); + assert!(source.matches("::miden_field_repr::ToFeltRepr").count() >= 2); + assert!(source.contains("example:dex-schema/note-storage@1.0.0.limit-price")); +} + +#[test] +fn generates_nonstandard_embedded_core_record_as_schema_owned_type() { + let (source, has_nested_named_types, root) = generate(EMBEDDED_CORE_SCHEMA); + + assert_eq!(root, "EmbeddedCoreNote"); + assert!(has_nested_named_types); + assert!(source.contains("pub struct Digest")); + assert!(source.contains("pub value: Digest")); + assert!(!source.contains("pub struct Word")); +} diff --git a/sdk/note-schema/src/artifact.rs b/sdk/note-schema/src/artifact.rs new file mode 100644 index 0000000000..61f8ba08cc --- /dev/null +++ b/sdk/note-schema/src/artifact.rs @@ -0,0 +1,497 @@ +//! Miden package discovery for note schema macros. + +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use miden_mast_package::Package; +use midenc_frontend_wasm_metadata::package_cache; + +use crate::{Error, NoteStorageSchema, Result}; + +/// A loaded package artifact and its note storage schema. +pub struct NotePackageArtifact { + path: PathBuf, + schema: NoteStorageSchema, + tracked_inputs: Vec, +} + +impl NotePackageArtifact { + /// Returns the exact package path used to load the schema. + pub fn path(&self) -> &Path { + &self.path + } + + /// Returns the schema loaded from the package. + pub const fn schema(&self) -> &NoteStorageSchema { + &self.schema + } + + /// Returns every manifest the resolver read to derive the package file name. + /// + /// A caller registers these files as rebuild inputs next to the package itself. A rename of + /// the note package changes a manifest but leaves the old package file in place, so nothing + /// else tells the caller that the selection changed. + pub fn tracked_inputs(&self) -> &[PathBuf] { + &self.tracked_inputs + } +} + +/// Resolves package artifacts for one note macro crate. +pub struct NotePackageResolver<'a> { + macro_crate: &'a str, +} + +impl<'a> NotePackageResolver<'a> { + /// Creates a resolver whose diagnostics name `macro_crate`. + pub const fn new(macro_crate: &'a str) -> Self { + Self { macro_crate } + } + + /// Loads the package built by a Miden project. + pub fn from_project(&self, project: &str) -> Result { + let project_dir = self.resolve_manifest_path(project)?; + if !project_dir.is_dir() { + return Err(Error::new(format!( + "{}: note project directory '{}' does not exist", + self.macro_crate, + project_dir.display() + ))); + } + let identity = project_identity(&project_dir); + let stems = &identity.stems; + let cache_dir = package_cache_dir() + .map_err(|error| Error::new(format!("{}: {error}", self.macro_crate)))?; + let package_path = resolve_project_package(&project_dir, stems, cache_dir.as_deref()) + .map_err(|error| Error::new(format!("{}: {error}", self.macro_crate)))? + .ok_or_else(|| { + Error::new(missing_project_package_message( + self.macro_crate, + &project_dir, + cache_dir.as_deref(), + stems, + )) + })?; + self.load_package(package_path, identity.manifests) + } + + /// Loads one exact Miden package path. + pub fn from_package(&self, package: &str) -> Result { + let package_path = self.resolve_manifest_path(package)?; + if !package_path.is_file() { + return Err(Error::new(format!( + "{}: Miden package '{}' does not exist", + self.macro_crate, + package_path.display() + ))); + } + self.load_package(package_path, Vec::new()) + } + + /// Resolves one path relative to the consuming crate manifest. + fn resolve_manifest_path(&self, value: &str) -> Result { + let path = PathBuf::from(value); + if path.is_absolute() { + return Ok(path); + } + let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| { + Error::new(format!( + "{}: CARGO_MANIFEST_DIR is not set during macro expansion", + self.macro_crate + )) + })?; + Ok(PathBuf::from(manifest_dir).join(path)) + } + + /// Loads the package and its unique schema section. + fn load_package( + &self, + path: PathBuf, + tracked_inputs: Vec, + ) -> Result { + let path = path.canonicalize().map_err(|error| { + Error::new(format!( + "{}: failed to resolve Miden package '{}': {error}", + self.macro_crate, + path.display() + )) + })?; + let package = Package::deserialize_from_file(&path).map_err(|error| { + Error::new(format!( + "{}: failed to read Miden package '{}': {error}", + self.macro_crate, + path.display() + )) + })?; + let schema = NoteStorageSchema::from_package(&package).map_err(|error| { + Error::new(format!( + "{}: failed to read note storage schema from '{}': {error}", + self.macro_crate, + path.display() + )) + })?; + Ok(NotePackageArtifact { + path, + schema, + tracked_inputs, + }) + } +} + +/// Resolves one project package by package identity and output-directory priority. +/// +/// A set package-cache directory replaces the search: the build owns that directory, so the +/// output directories of the project are not consulted. +fn resolve_project_package( + project_dir: &Path, + stems: &[String], + cache_dir: Option<&Path>, +) -> Result> { + if let Some(cache_dir) = cache_dir { + return Ok(find_project_package_in_cache(cache_dir, stems)); + } + + let profiles = candidate_profiles(); + for output_dir in project_output_dirs(project_dir, &profiles) { + if let Some(package) = find_project_package_in_dir(&output_dir, stems)? { + return Ok(Some(package)); + } + } + Ok(None) +} + +/// Returns the configured package cache directory, treating an empty value as unset. +fn package_cache_dir() -> Result> { + env::var_os(package_cache::PACKAGE_CACHE_ENV) + .filter(|value| !value.is_empty()) + .map(|value| absolutize(PathBuf::from(value))) + .transpose() +} + +/// Returns Cargo and Miden profile names in lookup order. +fn candidate_profiles() -> Vec { + let mut profiles = Vec::new(); + if let Ok(profile) = env::var("PROFILE") { + push_profile(&mut profiles, profile); + } + push_profile(&mut profiles, "release".to_owned()); + push_profile(&mut profiles, "debug".to_owned()); + push_profile(&mut profiles, "dev".to_owned()); + profiles +} + +/// Returns candidate output directories for one project. +fn project_output_dirs(project_dir: &Path, profiles: &[String]) -> Vec { + let mut dirs = Vec::new(); + + // Keep this policy in parity with base-macros/src/dependency_package.rs. The crates cannot + // share the implementation without adding the full SDK macro dependency graph. + push_profile_dirs(&mut dirs, project_dir.join("target"), profiles); + push_manifest_ancestor_target_profile_dirs(&mut dirs, project_dir, profiles); + push_ancestor_target_profile_dirs(&mut dirs, project_dir, profiles); + + if let Some(target_dir) = env::var_os("CARGO_TARGET_DIR") { + push_profile_dirs(&mut dirs, PathBuf::from(target_dir), profiles); + } + if let Some(out_dir) = env::var_os("OUT_DIR") { + for ancestor in Path::new(&out_dir).ancestors() { + push_profile_dirs(&mut dirs, ancestor.to_path_buf(), profiles); + } + } + if let Ok(current_dir) = env::current_dir() { + push_profile_dirs(&mut dirs, current_dir.join("target"), profiles); + push_manifest_ancestor_target_profile_dirs(&mut dirs, ¤t_dir, profiles); + push_ancestor_target_profile_dirs(&mut dirs, ¤t_dir, profiles); + } + dirs +} + +/// Adds `target/miden/` directories while preserving order. +fn push_profile_dirs(dirs: &mut Vec, target_root: PathBuf, profiles: &[String]) { + for profile in profiles { + let dir = target_root.join("miden").join(profile); + if !dirs.contains(&dir) { + dirs.push(dir); + } + } +} + +/// Adds target directories found among the ancestors of `path`. +fn push_ancestor_target_profile_dirs(dirs: &mut Vec, path: &Path, profiles: &[String]) { + for ancestor in path.ancestors() { + if ancestor.file_name().is_some_and(|name| name == "target") { + push_profile_dirs(dirs, ancestor.to_path_buf(), profiles); + } + } +} + +/// Adds target directories for Cargo manifest ancestors of `path`. +fn push_manifest_ancestor_target_profile_dirs( + dirs: &mut Vec, + path: &Path, + profiles: &[String], +) { + for ancestor in path.ancestors() { + if ancestor.join("Cargo.toml").is_file() || ancestor.join("Cargo.lock").is_file() { + push_profile_dirs(dirs, ancestor.join("target"), profiles); + } + } +} + +/// Finds a project package in the build-owned package cache. +fn find_project_package_in_cache(dir: &Path, stems: &[String]) -> Option { + stems + .iter() + .map(|stem| dir.join(package_cache::package_file_name(stem))) + .find(|path| path.is_file()) +} + +/// Finds a package in one output directory by ordered package identity. +fn find_project_package_in_dir(dir: &Path, stems: &[String]) -> Result> { + if !dir.is_dir() { + return Ok(None); + } + + let packages = fs::read_dir(dir) + .map_err(|error| Error::new(format!("failed to read '{}': {error}", dir.display())))? + .collect::, _>>() + .map_err(|error| { + Error::new(format!("failed to read an entry in '{}': {error}", dir.display())) + })? + .into_iter() + .map(|entry| entry.path()) + .filter(|path| { + path.is_file() && path.extension().is_some_and(|extension| extension == "masp") + }) + .collect::>(); + + // The stems are ordered by identity. The canonical miden-project package name comes first, + // followed by legacy aliases. This matches base-macros/src/dependency_package.rs and prevents + // a newer legacy artifact from shadowing the canonical package. + for stem in stems { + if let Some(package) = packages + .iter() + .find(|path| path.file_stem().and_then(|value| value.to_str()) == Some(stem.as_str())) + { + return Ok(Some(package.clone())); + } + } + Ok(None) +} + +/// The package file name stems of one project and the manifests they came from. +struct ProjectIdentity { + /// Ordered package file name stems, canonical identity first. + stems: Vec, + /// The manifests that were read, in the order they were read. + manifests: Vec, +} + +/// Returns ordered package filename stems for one project, and the manifests they came from. +fn project_identity(project_dir: &Path) -> ProjectIdentity { + let mut stems = Vec::new(); + let mut manifests = Vec::new(); + for manifest in [project_dir.join("miden-project.toml"), project_dir.join("Cargo.toml")] { + if let Some(name) = package_name_from_manifest(&manifest) { + push_package_stem(&mut stems, &name); + manifests.push(manifest); + } + } + if let Some(name) = project_dir.file_name().and_then(|name| name.to_str()) { + push_package_stem(&mut stems, name); + } + ProjectIdentity { stems, manifests } +} + +/// Reads a package name from one TOML manifest. +fn package_name_from_manifest(manifest_path: &Path) -> Option { + let manifest = fs::read_to_string(manifest_path).ok()?; + let manifest = manifest.parse::().ok()?; + manifest + .get("package") + .and_then(toml::Value::as_table) + .and_then(|package| package.get("name")) + .and_then(toml::Value::as_str) + .map(ToOwned::to_owned) +} + +/// Adds a package filename stem and its underscore alias. +fn push_package_stem(stems: &mut Vec, name: &str) { + if !name.is_empty() && !stems.iter().any(|existing| existing == name) { + stems.push(name.to_owned()); + } + let normalized = name.replace('-', "_"); + if !normalized.is_empty() && !stems.contains(&normalized) { + stems.push(normalized); + } +} + +/// Makes a cache path absolute for generated `include_bytes!` inputs. +fn absolutize(path: PathBuf) -> Result { + if path.is_absolute() { + return Ok(path); + } + Ok(env::current_dir() + .map_err(|error| Error::new(format!("failed to resolve current directory: {error}")))? + .join(path)) +} + +/// Adds one profile name once. +fn push_profile(profiles: &mut Vec, profile: String) { + if !profile.is_empty() && !profiles.contains(&profile) { + profiles.push(profile); + } +} + +/// Formats the diagnostic for a project without a built package. +fn missing_project_package_message( + macro_crate: &str, + project_dir: &Path, + cache_dir: Option<&Path>, + stems: &[String], +) -> String { + let manifest = project_dir.join("Cargo.toml"); + let build = if manifest.is_file() { + format!("cargo miden build --manifest-path {} --release", manifest.display()) + } else { + "cargo miden build --release".to_owned() + }; + if let Some(cache_dir) = cache_dir { + let expected_files = stems + .iter() + .map(|stem| format!("'{}'", package_cache::package_file_name(stem))) + .collect::>() + .join(", "); + return format!( + "{macro_crate} could not find a built `.masp` package for note project '{}'. Expected \ + one of these package names: {expected_files}. Searched {} directory '{}'. Build the \ + note project first with `{build}` so the package is available during macro expansion.", + project_dir.display(), + package_cache::PACKAGE_CACHE_ENV, + cache_dir.display(), + ); + } + format!( + "{macro_crate} could not find a built `.masp` package under '{}'. Build the note project \ + first with `{build}`.", + project_dir.join("target/miden/").display() + ) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use super::{ + find_project_package_in_cache, find_project_package_in_dir, + missing_project_package_message, project_identity, project_output_dirs, + }; + + #[test] + fn canonical_project_identity_wins_over_legacy_aliases() { + let temp = tempfile::tempdir().unwrap(); + fs::write( + temp.path().join("miden-project.toml"), + "[package]\nname='canonical-note'\nversion='0.1.0'", + ) + .unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='legacy-note'\nversion='0.1.0'") + .unwrap(); + let output = temp.path().join("output"); + fs::create_dir(&output).unwrap(); + fs::write(output.join("legacy-note.masp"), b"newer legacy package").unwrap(); + fs::write(output.join("canonical-note.masp"), b"canonical package").unwrap(); + + let stems = project_identity(temp.path()).stems; + let selected = find_project_package_in_dir(&output, &stems).unwrap().unwrap(); + assert_eq!(selected, output.join("canonical-note.masp")); + } + + #[test] + fn tracked_inputs_name_the_manifests_that_were_read() { + let temp = tempfile::tempdir().unwrap(); + fs::write( + temp.path().join("miden-project.toml"), + "[package]\nname='canonical-note'\nversion='0.1.0'", + ) + .unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='legacy-note'\nversion='0.1.0'") + .unwrap(); + + let identity = project_identity(temp.path()); + + assert_eq!( + identity.manifests, + [temp.path().join("miden-project.toml"), temp.path().join("Cargo.toml")] + ); + } + + #[test] + fn tracked_inputs_skip_a_manifest_that_is_absent() { + let temp = tempfile::tempdir().unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='legacy-note'\nversion='0.1.0'") + .unwrap(); + + let identity = project_identity(temp.path()); + + assert_eq!(identity.manifests, [temp.path().join("Cargo.toml")]); + } + + #[test] + fn manifest_ancestor_target_directories_are_candidates() { + let temp = tempfile::tempdir().unwrap(); + fs::write(temp.path().join("Cargo.lock"), "").unwrap(); + let project = temp.path().join("crates/note"); + fs::create_dir_all(&project).unwrap(); + let profiles = vec!["release".to_owned()]; + + let dirs = project_output_dirs(&project, &profiles); + + assert!(dirs.contains(&temp.path().join("target/miden/release"))); + } + + #[test] + fn missing_package_diagnostic_names_macro_and_build_command() { + let temp = tempfile::tempdir().unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='note'\nversion='0.1.0'") + .unwrap(); + let stems = project_identity(temp.path()).stems; + let message = missing_project_package_message("test-note-macro", temp.path(), None, &stems); + assert!(message.contains("test-note-macro")); + assert!(message.contains("cargo miden build --manifest-path")); + assert!(message.contains("--release")); + } + + #[test] + fn missing_cached_package_diagnostic_names_cache_and_expected_files() { + let temp = tempfile::tempdir().unwrap(); + fs::write(temp.path().join("Cargo.toml"), "[package]\nname='test-note'\nversion='0.1.0'") + .unwrap(); + let cache_dir = temp.path().join("package-cache"); + let stems = project_identity(temp.path()).stems; + + let message = missing_project_package_message( + "test-note-macro", + temp.path(), + Some(&cache_dir), + &stems, + ); + + assert!(message.contains(&cache_dir.display().to_string())); + assert!(message.contains("'test-note.masp'")); + assert!(message.contains("MIDENC_PACKAGE_CACHE")); + assert!(!message.contains("target/miden/")); + } + + #[test] + fn cache_lookup_uses_the_shared_package_file_name() { + let temp = tempfile::tempdir().unwrap(); + let package = temp.path().join("note.with.dot.masp"); + fs::write(&package, b"package").unwrap(); + + let found = find_project_package_in_cache(temp.path(), &["note.with.dot".to_owned()]); + + assert_eq!(found, Some(package)); + } +} diff --git a/sdk/note-schema/src/builder.rs b/sdk/note-schema/src/builder.rs new file mode 100644 index 0000000000..59a54438f3 --- /dev/null +++ b/sdk/note-schema/src/builder.rs @@ -0,0 +1,333 @@ +//! String-path note storage builder. + +use std::collections::BTreeMap; + +use miden_field_repr::FeltWriter; + +use crate::{ + CodecRegistry, Error, Felt, NoteStorage, NoteStorageSchema, PrimitiveType, Result, SchemaField, + SchemaType, SchemaTypeKind, + codec::{parse_felt, parse_unsigned, write_repr}, + schema::normalize_name, + value::validate_encoding, +}; + +/// Builds note storage from normalized dotted string paths. +pub struct NoteStorageBuilder<'a> { + schema: &'a NoteStorageSchema, + registry: &'a CodecRegistry, + values: BTreeMap, +} + +impl<'a> NoteStorageBuilder<'a> { + /// Creates an empty builder for a schema and codec registry. + pub(crate) fn new(schema: &'a NoteStorageSchema, registry: &'a CodecRegistry) -> Self { + Self { + schema, + registry, + values: BTreeMap::new(), + } + } + + /// Sets a leaf value by kebab-case or snake_case dotted path. + /// + /// Options accept `none` or `some()`. Variants accept a case name or + /// `case-name()` when the selected case has a payload. + pub fn set(mut self, path: &str, value: impl AsRef) -> Result { + let segments = normalize_path(path)?; + let normalized = segments.join("."); + let ty = resolve_path(self.schema.root(), &segments)?; + let value = value.as_ref(); + reject_unsupported_constructor_path(ty, &normalized, value, self.registry)?; + + if let Some(conflict) = self.values.keys().find(|existing| { + *existing == &normalized + || existing.starts_with(&format!("{normalized}.")) + || normalized.starts_with(&format!("{existing}.")) + }) { + return Err(Error::new(format!( + "path `{normalized}` conflicts with the existing value at `{conflict}`" + ))); + } + self.values.insert(normalized, value.to_owned()); + Ok(self) + } + + /// Checks completeness and returns note storage in declaration order. + pub fn build(self) -> Result { + let mut felts = Vec::new(); + encode_type( + self.schema.root(), + "", + &self.values, + self.registry, + &mut FeltWriter::new(&mut felts), + )?; + validate_encoding(self.schema.root(), &felts) + .map_err(|err| err.context("built note storage does not match its schema"))?; + NoteStorage::new(felts) + .map_err(|err| Error::new(format!("failed to create note storage: {err}"))) + } +} + +/// Rejects structural constructor shapes that need nested record text parsing. +fn reject_unsupported_constructor_path( + ty: &SchemaType, + path: &str, + value: &str, + registry: &CodecRegistry, +) -> Result<()> { + if ty.fqn().is_some_and(|fqn| registry.contains(fqn)) { + return Ok(()); + } + match ty.kind() { + SchemaTypeKind::Option(payload) + if matches!(payload.kind(), SchemaTypeKind::Record(_)) + && value.trim() != "none" + && !payload.fqn().is_some_and(|fqn| registry.contains(fqn)) => + { + Err(Error::new(format!( + "path `{path}` selects an option with record payload `{}`; the string builder \ + does not support nested record constructors without a codec for that payload", + payload.fqn().or(payload.name()).unwrap_or("") + ))) + } + SchemaTypeKind::Variant(cases) => { + let (case_name, _) = parse_constructor(value)?; + let case_name = normalize_name(case_name); + let unsupported = cases.iter().find(|case| case.name() == case_name).and_then(|case| { + let payload = case.payload()?; + (matches!(payload.kind(), SchemaTypeKind::Record(_)) + && !payload.fqn().is_some_and(|fqn| registry.contains(fqn))) + .then_some(case.name()) + }); + if let Some(case_name) = unsupported { + Err(Error::new(format!( + "path `{path}` selects variant case `{case_name}` with a record payload; the \ + string builder does not support nested record constructors without a codec \ + for that payload" + ))) + } else { + Ok(()) + } + } + _ => Ok(()), + } +} + +/// Normalizes and validates a dotted field path. +fn normalize_path(path: &str) -> Result> { + // `split` always yields one segment, so only an empty segment can fail here. + let segments = path.split('.').map(normalize_name).collect::>(); + if segments.iter().any(String::is_empty) { + return Err(Error::new(format!("invalid empty note storage path `{path}`"))); + } + Ok(segments) +} + +/// Resolves a dotted path through structural record fields. +fn resolve_path<'a>(mut ty: &'a SchemaType, segments: &[String]) -> Result<&'a SchemaType> { + let mut resolved = Vec::with_capacity(segments.len()); + for segment in segments { + let SchemaTypeKind::Record(fields) = ty.kind() else { + return Err(Error::new(format!( + "path `{}` continues through non-record type `{}`", + resolved.join("."), + ty.fqn().or(ty.name()).unwrap_or("") + ))); + }; + let field = fields.iter().find(|field| field.name() == segment).ok_or_else(|| { + let prefix = if resolved.is_empty() { + "".to_owned() + } else { + resolved.join(".") + }; + Error::new(format!("record `{prefix}` has no field named `{segment}`")) + })?; + resolved.push(segment.clone()); + ty = field.ty(); + } + Ok(ty) +} + +/// Encodes a schema type from complete path assignments. +fn encode_type( + ty: &SchemaType, + path: &str, + values: &BTreeMap, + registry: &CodecRegistry, + writer: &mut FeltWriter<'_>, +) -> Result<()> { + if let Some(value) = values.get(path) { + let felts = encode_text_value(ty, value, registry) + .map_err(|err| err.context(format!("value at `{path}`")))?; + for felt in felts { + writer.write(felt); + } + return Ok(()); + } + + if let SchemaTypeKind::Record(fields) = ty.kind() { + let Some((fqn, codec)) = ty.fqn().and_then(|fqn| Some((fqn, registry.codec(fqn)?))) else { + return encode_record_fields(fields, path, values, registry, writer); + }; + // A record assembled from child-path values must still satisfy its registered + // codec, or the builder would produce storage that the same registry rejects on + // decode. Encode the subtree separately so the codec can validate it as one value. + let mut subtree = Vec::new(); + encode_record_fields(fields, path, values, registry, &mut FeltWriter::new(&mut subtree))?; + codec.validate(&subtree).map_err(|err| { + err.context(format!( + "codec `{fqn}` validation failed for the values assigned under `{}`", + if path.is_empty() { "" } else { path } + )) + })?; + for felt in subtree { + writer.write(felt); + } + return Ok(()); + } + + Err(Error::new(format!( + "missing note storage value for `{}`", + if path.is_empty() { "" } else { path } + ))) +} + +/// Encodes every field of a record from complete path assignments. +fn encode_record_fields( + fields: &[SchemaField], + path: &str, + values: &BTreeMap, + registry: &CodecRegistry, + writer: &mut FeltWriter<'_>, +) -> Result<()> { + for field in fields { + let field_path = if path.is_empty() { + field.name().to_owned() + } else { + format!("{path}.{}", field.name()) + }; + encode_type(field.ty(), &field_path, values, registry, writer)?; + } + Ok(()) +} + +/// Encodes one direct string value. +fn encode_text_value(ty: &SchemaType, value: &str, registry: &CodecRegistry) -> Result> { + if let Some(fqn) = ty.fqn() + && let Some(codec) = registry.codec(fqn) + { + let felts = codec.parse(value)?; + codec + .validate(&felts) + .map_err(|err| err.context(format!("codec `{fqn}` validation failed")))?; + validate_encoding(ty, &felts) + .map_err(|err| err.context(format!("codec `{fqn}` changed the structural layout")))?; + return Ok(felts); + } + + match ty.kind() { + SchemaTypeKind::Felt => Ok(write_repr(&parse_felt(value)?)), + SchemaTypeKind::Primitive(primitive) => encode_primitive(*primitive, value), + SchemaTypeKind::Option(payload) => encode_option(payload, value, registry), + SchemaTypeKind::Variant(cases) => { + let (case_name, payload_text) = parse_constructor(value)?; + let case_name = normalize_name(case_name); + let (ordinal, case) = + cases.iter().enumerate().find(|(_, case)| case.name() == case_name).ok_or_else( + || { + Error::new(format!( + "variant `{}` has no case named `{case_name}`", + ty.fqn().or(ty.name()).unwrap_or("") + )) + }, + )?; + let ordinal = u32::try_from(ordinal) + .map_err(|_| Error::new("variant has more than u32::MAX cases"))?; + let mut felts = write_repr(&ordinal); + match (case.payload(), payload_text) { + (None, None) => {} + (None, Some(_)) => { + return Err(Error::new(format!( + "variant case `{case_name}` does not accept a payload" + ))); + } + (Some(_), None) => { + return Err(Error::new(format!("variant case `{case_name}` needs a payload"))); + } + (Some(payload), Some(payload_text)) => { + felts.extend(encode_text_value(payload, payload_text, registry)?); + } + } + Ok(felts) + } + SchemaTypeKind::Record(_) => Err(Error::new(format!( + "type `{}` has no codec; set its leaf fields with dotted paths", + ty.fqn().or(ty.name()).unwrap_or("") + ))), + } +} + +/// Encodes an option tag and optional direct payload. +fn encode_option(payload: &SchemaType, value: &str, registry: &CodecRegistry) -> Result> { + let value = value.trim(); + if value == "none" { + return Ok(write_repr(&0u32)); + } + let (constructor, payload_text) = parse_constructor(value)?; + if normalize_name(constructor) != "some" { + return Err(Error::new("an option value must be `none` or `some()`")); + } + let payload_text = + payload_text.ok_or_else(|| Error::new("an option `some` value needs a payload"))?; + let mut felts = write_repr(&1u32); + felts.extend(encode_text_value(payload, payload_text, registry)?); + Ok(felts) +} + +/// Encodes one supported primitive through `miden-field-repr`. +fn encode_primitive(primitive: PrimitiveType, value: &str) -> Result> { + match primitive { + PrimitiveType::U64 => Ok(write_repr(&parse_unsigned(value, "u64")?)), + PrimitiveType::U32 => { + let value = u32::try_from(parse_unsigned(value, "u32")?) + .map_err(|_| Error::new(format!("u32 value `{value}` is out of range")))?; + Ok(write_repr(&value)) + } + PrimitiveType::U8 => { + let value = u8::try_from(parse_unsigned(value, "u8")?) + .map_err(|_| Error::new(format!("u8 value `{value}` is out of range")))?; + Ok(write_repr(&value)) + } + PrimitiveType::Bool => { + let value = match value.trim() { + "true" | "1" => true, + "false" | "0" => false, + value => { + return Err(Error::new(format!( + "invalid bool `{value}`; expected true, false, 1, or 0" + ))); + } + }; + Ok(write_repr(&value)) + } + } +} + +/// Splits `name` or `name(payload)` text without interpreting the payload. +fn parse_constructor(value: &str) -> Result<(&str, Option<&str>)> { + let value = value.trim(); + let Some(open) = value.find('(') else { + return Ok((value, None)); + }; + if !value.ends_with(')') { + return Err(Error::new(format!("value `{value}` has an unclosed payload"))); + } + let name = value[..open].trim(); + let payload = value[open + 1..value.len() - 1].trim(); + if name.is_empty() || payload.is_empty() { + return Err(Error::new(format!("value `{value}` has an empty constructor or payload"))); + } + Ok((name, Some(payload))) +} diff --git a/sdk/note-schema/src/codec.rs b/sdk/note-schema/src/codec.rs new file mode 100644 index 0000000000..28c244f3ff --- /dev/null +++ b/sdk/note-schema/src/codec.rs @@ -0,0 +1,355 @@ +//! String codecs for named WIT leaf types. + +use std::{collections::BTreeMap, sync::Arc}; + +use miden_field::{Felt, Word}; +use miden_field_repr::{FeltReader, FeltWriter, FromFeltRepr, ToFeltRepr}; +use miden_protocol::{account::AccountId, address::NetworkId, asset::AssetAmount}; + +use crate::{Error, Result}; + +/// The canonical WIT FQN for `felt`. +pub const FELT_FQN: &str = "miden:base/core-types@1.0.0.felt"; +/// The canonical WIT FQN for `word`. +pub const WORD_FQN: &str = "miden:base/core-types@1.0.0.word"; +/// The canonical WIT FQN for `account-id`. +pub const ACCOUNT_ID_FQN: &str = "miden:base/core-types@1.0.0.account-id"; +/// The canonical WIT FQN for `asset-amount`. +pub const ASSET_AMOUNT_FQN: &str = "miden:base/core-types@1.0.0.asset-amount"; + +/// A protocol type whose schema leaf maps directly to an existing host type and standard codec. +/// +/// This is the canonical standard-leaf definition used by schema traversal, Rust code generation, +/// and author-codec registration. Named types outside this set remain schema-owned, including +/// other records in the `miden:base/core-types` interface. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum StandardLeaf { + /// The one-element Miden base-field type. + Felt, + /// A group of four Miden base-field elements. + Word, + /// A two-element protocol account identifier. + AccountId, + /// A validated fungible-asset amount. + AssetAmount, +} + +impl StandardLeaf { + /// Every standard leaf in canonical registry order. + pub const ALL: [Self; 4] = [Self::Felt, Self::Word, Self::AccountId, Self::AssetAmount]; + + /// Returns the canonical WIT FQN for this standard leaf. + pub const fn fqn(self) -> &'static str { + match self { + Self::Felt => FELT_FQN, + Self::Word => WORD_FQN, + Self::AccountId => ACCOUNT_ID_FQN, + Self::AssetAmount => ASSET_AMOUNT_FQN, + } + } + + /// Classifies a canonical WIT FQN as a standard leaf. + pub fn from_fqn(fqn: &str) -> Option { + Self::ALL.into_iter().find(|leaf| leaf.fqn() == fqn) + } +} + +/// Parses, displays, and validates one fully-qualified WIT leaf type. +pub trait ConsumerTypeCodec: Send + Sync { + /// Parses a string into its structural felt representation. + fn parse(&self, value: &str) -> Result>; + + /// Displays a structurally valid felt representation. + fn display(&self, felts: &[Felt]) -> Result; + + /// Validates the felt representation and any semantic type constraints. + fn validate(&self, felts: &[Felt]) -> Result<()>; +} + +/// A registry of codecs keyed by canonical WIT fully-qualified type name. +/// +/// The canonical form is `:/@.`. The version follows +/// the interface, matching WIT interface identifiers. When a package has no version, the +/// `@` part is omitted, for example `miden:base/core-types.account-id`. +/// The standard account ID codec displays the canonical mainnet bech32 form because the network +/// identifier is not part of an account ID's felt representation. +#[derive(Clone)] +pub struct CodecRegistry { + codecs: BTreeMap>, +} + +impl CodecRegistry { + /// Creates an empty codec registry. + pub fn empty() -> Self { + Self { + codecs: BTreeMap::new(), + } + } + + /// Creates a registry containing all standard note storage codecs. + pub fn with_standard_codecs() -> Self { + let mut registry = Self::empty(); + for leaf in StandardLeaf::ALL { + match leaf { + StandardLeaf::Felt => registry.register(leaf.fqn(), FeltCodec), + StandardLeaf::Word => registry.register(leaf.fqn(), WordCodec), + StandardLeaf::AccountId => registry.register(leaf.fqn(), AccountIdCodec), + StandardLeaf::AssetAmount => registry.register(leaf.fqn(), AssetAmountCodec), + } + } + registry + } + + /// Registers or replaces a codec under its canonical WIT FQN. + pub fn register(&mut self, fqn: impl Into, codec: impl ConsumerTypeCodec + 'static) { + self.register_shared(fqn, Arc::new(codec)); + } + + /// Registers or replaces a shared codec under its canonical WIT FQN. + pub fn register_shared(&mut self, fqn: impl Into, codec: Arc) { + self.codecs.insert(fqn.into(), codec); + } + + /// Returns the codec registered for a canonical WIT FQN. + pub fn codec(&self, fqn: &str) -> Option<&dyn ConsumerTypeCodec> { + self.codecs.get(fqn).map(Arc::as_ref) + } + + /// Returns true when the canonical WIT FQN has a codec. + pub fn contains(&self, fqn: &str) -> bool { + self.codecs.contains_key(fqn) + } +} + +impl Default for CodecRegistry { + fn default() -> Self { + Self::with_standard_codecs() + } +} + +/// Parses and displays one field element. +struct FeltCodec; + +impl ConsumerTypeCodec for FeltCodec { + fn parse(&self, value: &str) -> Result> { + let felt = parse_felt(value)?; + Ok(write_repr(&felt)) + } + + fn display(&self, felts: &[Felt]) -> Result { + read_repr::(felts).map(|felt| felt.as_canonical_u64().to_string()) + } + + fn validate(&self, felts: &[Felt]) -> Result<()> { + read_repr::(felts).map(|_| ()) + } +} + +/// Parses and displays a four-felt word. +struct WordCodec; + +impl ConsumerTypeCodec for WordCodec { + fn parse(&self, value: &str) -> Result> { + let value = value.trim(); + let word = if value.starts_with("0x") || value.starts_with("0X") { + Word::parse(value).map_err(|err| Error::new(format!("invalid word hex: {err}")))? + } else { + let values = value.trim_matches(['[', ']']); + let felts = values.split(',').map(parse_felt).collect::>>()?; + let elements: [Felt; 4] = felts.try_into().map_err(|felts: Vec| { + Error::new(format!( + "a word needs four comma-separated felts, found {}", + felts.len() + )) + })?; + Word::new(elements) + }; + Ok(write_repr(&word)) + } + + fn display(&self, felts: &[Felt]) -> Result { + read_repr::(felts).map(|word| word.to_hex()) + } + + fn validate(&self, felts: &[Felt]) -> Result<()> { + read_repr::(felts).map(|_| ()) + } +} + +/// Parses and displays a protocol account ID. +struct AccountIdCodec; + +impl ConsumerTypeCodec for AccountIdCodec { + fn parse(&self, value: &str) -> Result> { + let (account_id, _) = AccountId::parse(value) + .map_err(|err| Error::new(format!("invalid account-id: {err}")))?; + let mut felts = Vec::with_capacity(2); + let mut writer = FeltWriter::new(&mut felts); + writer.write(account_id.prefix().as_felt()); + writer.write(account_id.suffix()); + Ok(felts) + } + + fn display(&self, felts: &[Felt]) -> Result { + read_account_id(felts).map(|account_id| account_id.to_bech32(NetworkId::Mainnet)) + } + + fn validate(&self, felts: &[Felt]) -> Result<()> { + read_account_id(felts).map(|_| ()) + } +} + +/// Parses and displays a validated asset amount. +struct AssetAmountCodec; + +impl ConsumerTypeCodec for AssetAmountCodec { + fn parse(&self, value: &str) -> Result> { + let amount = value + .trim() + .parse::() + .map_err(|err| Error::new(format!("invalid asset amount: {err}")))?; + let amount = AssetAmount::new(amount) + .map_err(|err| Error::new(format!("invalid asset amount: {err}")))?; + let felt = Felt::from(amount); + Ok(write_repr(&felt)) + } + + fn display(&self, felts: &[Felt]) -> Result { + read_asset_amount(felts).map(|amount| amount.to_string()) + } + + fn validate(&self, felts: &[Felt]) -> Result<()> { + read_asset_amount(felts).map(|_| ()) + } +} + +/// Parses a canonical felt from decimal or hexadecimal text. +pub(crate) fn parse_felt(value: &str) -> Result { + let value = parse_unsigned(value, "felt")?; + Felt::new(value).map_err(|err| Error::new(format!("invalid felt: {err}"))) +} + +/// Parses a decimal or hexadecimal unsigned integer. +pub(crate) fn parse_unsigned(value: &str, ty: &str) -> Result { + let value = value.trim(); + let parsed = match value.strip_prefix("0x").or_else(|| value.strip_prefix("0X")) { + Some(digits) => u64::from_str_radix(digits, 16), + None => value.parse::(), + }; + parsed.map_err(|err| Error::new(format!("invalid {ty}: {err}"))) +} + +/// Encodes a felt-repr value through the shared writer. +pub(crate) fn write_repr(value: &impl ToFeltRepr) -> Vec { + let mut felts = Vec::new(); + value.write_felt_repr(&mut FeltWriter::new(&mut felts)); + felts +} + +/// Decodes one felt-repr value and rejects trailing elements. +fn read_repr(felts: &[Felt]) -> Result { + let mut reader = FeltReader::new(felts); + let value = T::from_felt_repr(&mut reader) + .map_err(|err| Error::new(format!("invalid felt representation: {err}")))?; + reader + .ensure_eof() + .map_err(|err| Error::new(format!("invalid felt representation: {err}")))?; + Ok(value) +} + +/// Decodes and validates an account ID in WIT record field order. +fn read_account_id(felts: &[Felt]) -> Result { + let mut reader = FeltReader::new(felts); + let prefix = reader + .read() + .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?; + let suffix = reader + .read() + .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?; + reader + .ensure_eof() + .map_err(|err| Error::new(format!("invalid account-id representation: {err}")))?; + // WIT declares prefix before suffix, but the constructor takes suffix first. + AccountId::try_from_elements(suffix, prefix) + .map_err(|err| Error::new(format!("invalid account-id representation: {err}"))) +} + +/// Decodes and validates an asset amount. +fn read_asset_amount(felts: &[Felt]) -> Result { + let felt = read_repr::(felts)?; + AssetAmount::try_from(felt) + .map_err(|err| Error::new(format!("invalid asset amount representation: {err}"))) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Returns a valid account ID and its mainnet bech32 form. + fn account_id() -> (AccountId, String) { + let account_id = + AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); + let bech32 = account_id.to_bech32(NetworkId::Mainnet); + (account_id, bech32) + } + + #[test] + fn standard_registry_contains_canonical_versioned_fqns() { + let registry = CodecRegistry::default(); + + for leaf in StandardLeaf::ALL { + assert!(registry.contains(leaf.fqn())); + } + assert!(!CodecRegistry::empty().contains(FELT_FQN)); + } + + #[test] + fn standard_leaf_definition_is_pinned() { + assert_eq!( + StandardLeaf::ALL.map(StandardLeaf::fqn), + [FELT_FQN, WORD_FQN, ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN] + ); + for leaf in StandardLeaf::ALL { + assert_eq!(StandardLeaf::from_fqn(leaf.fqn()), Some(leaf)); + assert!(CodecRegistry::default().contains(leaf.fqn())); + } + assert_eq!(StandardLeaf::from_fqn("miden:base/core-types@1.0.0.digest"), None); + } + + #[test] + fn felt_codec_accepts_decimal_and_hex() { + let codec = FeltCodec; + + assert_eq!(codec.parse("42").unwrap(), codec.parse("0x2a").unwrap()); + assert_eq!(codec.display(&codec.parse("42").unwrap()).unwrap(), "42"); + } + + #[test] + fn word_codec_accepts_hex_and_four_felts() { + let codec = WordCodec; + let from_felts = codec.parse("[1, 2, 3, 4]").unwrap(); + let from_hex = codec.parse(&codec.display(&from_felts).unwrap()).unwrap(); + + assert_eq!(from_hex, from_felts); + } + + #[test] + fn account_id_codec_uses_prefix_suffix_field_order() { + let codec = AccountIdCodec; + let (account_id, bech32) = account_id(); + let felts = codec.parse(&bech32).unwrap(); + let hex_felts = codec.parse(&account_id.to_hex()).unwrap(); + + assert_eq!(felts, [account_id.prefix().as_felt(), account_id.suffix()]); + assert_eq!(hex_felts, felts); + assert_eq!(codec.display(&felts).unwrap(), bech32); + } + + #[test] + fn asset_amount_codec_enforces_protocol_limit() { + let codec = AssetAmountCodec; + + assert!(codec.parse(&AssetAmount::MAX.as_u64().to_string()).is_ok()); + assert!(codec.parse(&(AssetAmount::MAX.as_u64() + 1).to_string()).is_err()); + } +} diff --git a/sdk/note-schema/src/codec_component.rs b/sdk/note-schema/src/codec_component.rs new file mode 100644 index 0000000000..1057dd00af --- /dev/null +++ b/sdk/note-schema/src/codec_component.rs @@ -0,0 +1,1308 @@ +//! Consumer adapters for author codec components. + +use std::{collections::HashSet, sync::Arc}; + +use miden_field::Felt; +use miden_mast_package::Package; +use midenc_frontend_wasm_metadata::{PACKAGE_NOTE_CODEC_SECTION_ID, package_note_codec_section_id}; +use wasmtime::{ + Config, Engine, ResourceLimiter, Store, StoreLimits, StoreLimitsBuilder, Trap, + component::{Component, Instance, Linker, WasmList, WasmStr}, +}; + +use crate::{ + CodecFailure, CodecRegistry, ConsumerTypeCodec, Error, NoteStorageSchema, Result, + codec_structure::{MAX_CORE_INSTANCES, MAX_INSTANTIATED_MEMORIES, MAX_INSTANTIATED_TABLES}, + validate_note_codec_component, +}; + +/// Maximum bytes of Wasm stack available to one component call. +const MAX_WASM_STACK_BYTES: usize = 512 * 1024; + +/// The versioned interface a note codec component exports. +const CODEC_INTERFACE: &str = "miden:note-codec/codec@1.0.0"; + +/// The discovery function of the codec interface. +const SUPPORTED_TYPES: &str = "supported-types"; + +/// Maximum instances one codec call store may hold. +/// +/// The value is the structural budget for the core instances one instantiation creates. The load +/// policy counts what instantiation creates, so a component that loads instantiates within this +/// count. +const MAX_STORE_INSTANCES: usize = MAX_CORE_INSTANCES; + +/// Maximum tables one codec call store may hold. +/// +/// The value is the structural budget for the tables one instantiation creates. +const MAX_STORE_TABLES: usize = MAX_INSTANTIATED_TABLES; + +/// Maximum linear memories one codec call store may hold. +/// +/// The value is the structural budget for the linear memories one instantiation creates. +const MAX_STORE_MEMORIES: usize = MAX_INSTANTIATED_MEMORIES; + +wasmtime::component::bindgen!({ + path: "wit", + world: "note-codec", +}); + +/// Runtime limits a host applies to bundled note codecs. Limits are host policy. +/// A package carries no limit values and cannot raise them. Hosts in one +/// deployment should run identical limits, so a codec behaves the same everywhere. +/// +/// The store counts are fixed policy instead of host configuration. One call store admits at +/// most `MAX_STORE_INSTANCES` instances, `MAX_STORE_TABLES` tables, and `MAX_STORE_MEMORIES` +/// linear memory, and one call gets `MAX_WASM_STACK_BYTES` of Wasm stack. The structural policy +/// in [`validate_note_codec_component`] counts what one instantiation creates and applies the +/// same values, so a component that loads instantiates within these counts. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CodecLimits { + /// Fuel budget for one codec call, about one unit per Wasm instruction. + pub fuel: u64, + /// Cap on one guest linear memory in bytes. Wasmtime applies the cap to each memory, and a + /// store admits `MAX_STORE_MEMORIES` of them. + pub max_memory_bytes: usize, + /// Cap on the elements of one table. Wasmtime applies the cap to each table, and a store + /// admits `MAX_STORE_TABLES` of them. + pub max_table_elements: usize, + /// Cap on the component size in bytes. + pub max_component_bytes: usize, + /// Cap on the number of types one component may report. + pub max_supported_types: usize, + /// Cap on one reported type name in bytes. + pub max_fqn_bytes: usize, + /// Cap on the felts one `parse` call may return. + pub max_returned_felts: usize, + /// Cap on one returned string in bytes. + pub max_returned_string_bytes: usize, +} + +impl Default for CodecLimits { + fn default() -> Self { + Self { + fuel: 10_000_000, + max_memory_bytes: 16 * 1024 * 1024, + max_table_elements: 4_096, + max_component_bytes: crate::schema::MAX_NOTE_CODEC_COMPONENT_BYTES, + max_supported_types: 128, + max_fqn_bytes: 512, + max_returned_felts: 4_096, + max_returned_string_bytes: 16 * 1024, + } + } +} + +impl CodecRegistry { + /// Loads the note codec component from a package under the default limits. + pub fn load_from_package(package: &Package) -> Result { + Self::load_from_package_with_limits(package, CodecLimits::default()) + } + + /// Loads the note codec component from a package and registers all reported types. + /// + /// A package without a note codec section is the common case, so it is not an error: the + /// registry comes back with the standard codecs alone, the same base the bundled-codec path + /// registers on top of. More than one note codec section is still an error. + pub fn load_from_package_with_limits(package: &Package, limits: CodecLimits) -> Result { + // The section check comes first. A package without a codec section needs no schema, and + // a unit `#[note]` struct emits neither section. + let section_id = package_note_codec_section_id(); + if !package.sections.iter().any(|section| section.id == section_id) { + return Ok(Self::default()); + } + let bytes = crate::section::unique_package_section( + package, + section_id, + PACKAGE_NOTE_CODEC_SECTION_ID, + )?; + let schema = NoteStorageSchema::from_package(package)?; + Self::load_from_component(bytes, &schema.custom_type_fqns(), limits) + } + + /// Loads a note codec component with every unresolved import stubbed as a trap. + fn load_from_component( + bytes: &[u8], + custom_type_fqns: &HashSet, + limits: CodecLimits, + ) -> Result { + let runtime = Arc::new(ComponentRuntime::new(bytes, limits)?); + let supported_types = runtime.supported_types()?; + let mut registry = Self::default(); + validate_reported_fqns(&supported_types, custom_type_fqns, ®istry)?; + + for fqn in supported_types { + registry.register_shared( + fqn.clone(), + Arc::new(ComponentCodec { + fqn, + runtime: Arc::clone(&runtime), + }), + ); + } + + Ok(registry) + } +} + +/// A compiled component used to create an isolated instance for each operation. +struct ComponentRuntime { + /// The engine that holds the accepted Wasm proposals and the fuel setting. + engine: Engine, + /// The compiled codec component. + component: Component, + /// The host limits applied to every instance and every call. + limits: CodecLimits, +} + +/// Store state that owns the component resource limits. +struct ComponentStore { + /// The inner limiter that decides every memory and table question. + limits: StoreLimits, + /// Set when a memory or table growth was refused, so a failed call reports its class. + limit_hit: bool, +} + +// The inner limiter decides every question. This wrapper only records that a growth was +// refused, which a call failure reports as `CodecFailure::LimitExceeded`. +impl ResourceLimiter for ComponentStore { + fn memory_growing( + &mut self, + current: usize, + desired: usize, + maximum: Option, + ) -> wasmtime::Result { + let allowed = self.limits.memory_growing(current, desired, maximum); + if !matches!(allowed, Ok(true)) { + self.limit_hit = true; + } + allowed + } + + fn memory_grow_failed(&mut self, error: wasmtime::Error) -> wasmtime::Result<()> { + self.limit_hit = true; + self.limits.memory_grow_failed(error) + } + + fn table_growing( + &mut self, + current: usize, + desired: usize, + maximum: Option, + ) -> wasmtime::Result { + let allowed = self.limits.table_growing(current, desired, maximum); + if !matches!(allowed, Ok(true)) { + self.limit_hit = true; + } + allowed + } + + fn table_grow_failed(&mut self, error: wasmtime::Error) -> wasmtime::Result<()> { + self.limit_hit = true; + self.limits.table_grow_failed(error) + } + + fn instances(&self) -> usize { + self.limits.instances() + } + + fn tables(&self) -> usize { + self.limits.tables() + } + + fn memories(&self) -> usize { + self.limits.memories() + } +} + +/// One isolated codec component call context. +struct ComponentInstance { + /// The store that holds the fuel budget and the resource limits of this call. + store: Store, + /// The raw instance, used by the calls that lift their results lazily. + instance: Instance, + /// The generated typed bindings of the codec world. + bindings: NoteCodec, +} + +impl ComponentRuntime { + /// Compiles a component under the explicit engine policy and the given limits. + fn new(bytes: &[u8], limits: CodecLimits) -> Result { + // One entry point applies the byte cap, the Wasm feature policy, and the structural + // limits. The producer applies the same policy when it attaches a codec. + validate_note_codec_component(bytes, limits.max_component_bytes)?; + let mut config = Config::new(); + config.wasm_component_model(true); + config.consume_fuel(true); + // Proposals the wasm32-wasip2 target emits by default. + config.wasm_bulk_memory(true); + config.wasm_multi_value(true); + // Validation policy: `NOTE_CODEC_WASM_FEATURES` decides what loads, and the call above + // already applied it. The setters below repeat that policy in the engine wherever + // wasmtime exposes a setter. Threads, garbage collection, reference types and typed + // function references have no setter here, because wasmtime gates those setters behind + // Cargo features this crate does not enable. Another crate in the same build can turn + // those proposals back on by feature unification, which is why the accepted set is + // pinned in wasmparser instead of read back from the engine. + config.wasm_simd(false); + config.wasm_relaxed_simd(false); + config.wasm_multi_memory(false); + config.wasm_memory64(false); + config.wasm_tail_call(false); + config.wasm_extended_const(false); + config.wasm_custom_page_sizes(false); + config.wasm_wide_arithmetic(false); + config.wasm_shared_everything_threads(false); + config.wasm_stack_switching(false); + config.wasm_exceptions(false); + config.wasm_component_model_error_context(false); + // Floats stay on: codecs parse and format decimal text. NaN canonicalization keeps + // float results identical across hosts. + config.cranelift_nan_canonicalization(true); + config.max_wasm_stack(MAX_WASM_STACK_BYTES); + config.wasm_backtrace(false); + let engine = Engine::new(&config) + .map_err(|error| component_error("create the Wasmtime engine", error))?; + let component = Component::new(&engine, bytes) + .map_err(|error| component_error("compile the note codec component", error))?; + Ok(Self { + engine, + component, + limits, + }) + } + + /// Instantiates the component with trapping WASI stubs and fresh per-call limits. + fn instantiate(&self) -> Result { + let mut linker = Linker::new(&self.engine); + // The wasip2 standard library imports WASI interfaces the codec never needs at + // runtime. Stub every import as a trap so nothing outside the component is callable. + linker + .define_unknown_imports_as_traps(&self.component) + .map_err(|error| component_error("stub the note codec imports", error))?; + let state = ComponentStore { + limits: component_store_limits(&self.limits), + limit_hit: false, + }; + let mut store = Store::new(&self.engine, state); + store.limiter(|state| state as &mut dyn ResourceLimiter); + store + .set_fuel(self.limits.fuel) + .map_err(|error| component_error("set the note codec fuel budget", error))?; + // Instantiation runs the guest `_initialize` and every data-segment initializer under + // the same fuel and store limits as an exported call, so it fails in the same classes. + let instance = match linker.instantiate(&mut store, &self.component) { + Ok(instance) => instance, + Err(error) => return Err(start_failure(&store, error)), + }; + let bindings = match NoteCodec::new(&mut store, &instance) { + Ok(bindings) => bindings, + Err(error) => return Err(start_failure(&store, error)), + }; + Ok(ComponentInstance { + store, + instance, + bindings, + }) + } + + /// Queries the component's supported FQNs once during registry construction. + /// + /// The call lifts its result lazily instead of going through the generated bindings. A + /// `list` is the one result a codec can inflate far past every limit: the elements + /// are pointer and length pairs that may all alias one small region, so lifting the whole + /// list into owned strings allocates before any cap applies. The per-value calls return one + /// string or one felt list, which guest memory already bounds, so they keep the bindings. + fn supported_types(&self) -> Result> { + let ComponentInstance { + mut store, + instance, + .. + } = self.instantiate()?; + let interface = + instance.get_export_index(&mut store, None, CODEC_INTERFACE).ok_or_else(|| { + Error::codec( + CodecFailure::Trapped, + format!("note codec component exports no `{CODEC_INTERFACE}`"), + ) + })?; + let export = instance + .get_export_index(&mut store, Some(&interface), SUPPORTED_TYPES) + .ok_or_else(|| { + Error::codec( + CodecFailure::Trapped, + format!( + "note codec component exports no `{SUPPORTED_TYPES}` in \ + `{CODEC_INTERFACE}`" + ), + ) + })?; + let supported_types = instance + .get_typed_func::<(), (WasmList,)>(&mut store, &export) + .map_err(|error| component_error("find `supported-types`", error))?; + let (reported,) = match supported_types.call(&mut store, ()) { + Ok(reported) => reported, + Err(error) => { + return Err(Error::codec( + classify_failure(&store, &error), + format!("note codec component failed in `supported-types`: {error:#}"), + )); + } + }; + + // Check the list length before any element is read, then take the cursors. A cursor + // holds a pointer and a length, so this copies nothing out of guest memory. + if reported.len() > self.limits.max_supported_types { + return Err(Error::codec( + CodecFailure::LimitExceeded, + format!( + "note codec component reported {} types; the limit is {}", + reported.len(), + self.limits.max_supported_types + ), + )); + } + let cursors = reported + .iter(&mut store) + .collect::>>() + .map_err(|error| component_error("read the reported type FQNs", error))?; + + let mut fqns = Vec::with_capacity(cursors.len()); + for cursor in cursors { + // `to_str` borrows guest memory for UTF-8, so the cap applies before the copy. + let fqn = cursor + .to_str(&store) + .map_err(|error| component_error("decode a reported type FQN", error))?; + ensure_returned_string_limit("type FQN", &fqn, self.limits.max_fqn_bytes)?; + fqns.push(fqn.into_owned()); + } + // The typed-func API requires the post-return call once the results are read. + supported_types + .post_return(&mut store) + .map_err(|error| component_error("finish `supported-types`", error))?; + Ok(fqns) + } +} + +/// Builds the resource limits attached to every isolated component call store. +fn component_store_limits(limits: &CodecLimits) -> StoreLimits { + StoreLimitsBuilder::new() + .memory_size(limits.max_memory_bytes) + .table_elements(limits.max_table_elements) + .instances(MAX_STORE_INSTANCES) + .tables(MAX_STORE_TABLES) + .memories(MAX_STORE_MEMORIES) + .trap_on_grow_failure(true) + .build() +} + +/// A registry entry that dispatches one FQN through isolated component instances. +struct ComponentCodec { + /// The fully qualified WIT name this entry answers for. + fqn: String, + /// The compiled component shared by every registry entry of one package. + runtime: Arc, +} + +impl ComponentCodec { + /// Calls one component operation in a fresh fuel- and memory-limited store. + fn with_runtime( + &self, + operation: &str, + call: impl FnOnce(&NoteCodec, &mut Store) -> wasmtime::Result, + ) -> Result { + let mut instance = self.runtime.instantiate()?; + match call(&instance.bindings, &mut instance.store) { + Ok(value) => Ok(value), + Err(error) => Err(self.call_failure(operation, &instance.store, error)), + } + } + + /// Reports why one component call did not return a value. + fn call_failure( + &self, + operation: &str, + store: &Store, + error: wasmtime::Error, + ) -> Error { + let fqn = &self.fqn; + match classify_failure(store, &error) { + CodecFailure::LimitExceeded => Error::codec( + CodecFailure::LimitExceeded, + format!("note codec `{fqn}` exceeded a resource limit in `{operation}`"), + ), + CodecFailure::OutOfFuel => Error::codec( + CodecFailure::OutOfFuel, + format!("note codec `{fqn}` ran out of fuel in `{operation}`"), + ), + class => Error::codec( + class, + format!("note codec `{fqn}` trapped in `{operation}`: {error:#}"), + ), + } + } + + /// Returns the limits the host applies to this codec. + fn limits(&self) -> &CodecLimits { + &self.runtime.limits + } +} + +impl ConsumerTypeCodec for ComponentCodec { + fn parse(&self, value: &str) -> Result> { + let result = self.with_runtime("parse", |bindings, store| { + bindings.miden_note_codec_codec().call_parse(store, &self.fqn, value) + })?; + let values = match result { + Ok(values) => values, + Err(message) => { + ensure_returned_string_limit( + "codec error", + &message, + self.limits().max_returned_string_bytes, + )?; + return Err(codec_rejection("parse", &self.fqn, message)); + } + }; + ensure_returned_felt_limit(&self.fqn, values.len(), self.limits().max_returned_felts)?; + component_values_to_felts(&self.fqn, &values) + } + + fn display(&self, felts: &[Felt]) -> Result { + let values = felts.iter().map(|felt| felt.as_canonical_u64()).collect::>(); + let result = self.with_runtime("display", |bindings, store| { + bindings.miden_note_codec_codec().call_display(store, &self.fqn, &values) + })?; + let display = match result { + Ok(display) => display, + Err(message) => { + ensure_returned_string_limit( + "codec error", + &message, + self.limits().max_returned_string_bytes, + )?; + return Err(codec_rejection("display", &self.fqn, message)); + } + }; + ensure_returned_string_limit( + "display value", + &display, + self.limits().max_returned_string_bytes, + )?; + Ok(display) + } + + fn validate(&self, felts: &[Felt]) -> Result<()> { + let values = felts.iter().map(|felt| felt.as_canonical_u64()).collect::>(); + let result = self.with_runtime("validate", |bindings, store| { + bindings.miden_note_codec_codec().call_validate(store, &self.fqn, &values) + })?; + match result { + Ok(()) => Ok(()), + Err(message) => { + ensure_returned_string_limit( + "codec error", + &message, + self.limits().max_returned_string_bytes, + )?; + Err(codec_rejection("validate", &self.fqn, message)) + } + } + } +} + +/// Validates the component's claimed type authority before registration. +fn validate_reported_fqns( + reported: &[String], + custom_type_fqns: &HashSet, + standard_registry: &CodecRegistry, +) -> Result<()> { + let mut seen = HashSet::new(); + for fqn in reported { + if fqn.trim().is_empty() { + return Err(Error::new("note codec component reported an empty type FQN")); + } + if !seen.insert(fqn) { + return Err(Error::new(format!( + "note codec component reported type FQN `{fqn}` more than once" + ))); + } + if standard_registry.contains(fqn) { + return Err(Error::new(format!( + "note codec component cannot replace the standard codec for `{fqn}`" + ))); + } + if !custom_type_fqns.contains(fqn) { + return Err(Error::new(format!( + "note codec component reported `{fqn}`, but that custom type does not appear in \ + the package note storage schema" + ))); + } + } + Ok(()) +} + +/// Classifies why a component call, or the instantiation that precedes it, failed. +/// +/// The store records a refused memory or table growth, and it outranks the trap the guest saw: +/// the trap is only how the refusal surfaced. +fn classify_failure(store: &Store, error: &wasmtime::Error) -> CodecFailure { + if store.data().limit_hit { + return CodecFailure::LimitExceeded; + } + match error.downcast_ref::() { + Some(&Trap::OutOfFuel) => CodecFailure::OutOfFuel, + Some(_) => CodecFailure::Trapped, + // Wasmtime applies the store instance, table, and memory counts without asking the + // limiter, so that failure carries no trap and no recorded refusal. + None if reports_store_count_limit(error) => CodecFailure::LimitExceeded, + None => CodecFailure::Trapped, + } +} + +/// Returns true when one wasmtime error reports a store count that was reached. +fn reports_store_count_limit(error: &wasmtime::Error) -> bool { + format!("{error:#}").contains("resource limit exceeded") +} + +/// Reports a component that did not reach its first exported call. +fn start_failure(store: &Store, error: wasmtime::Error) -> Error { + Error::codec( + classify_failure(store, &error), + format!("note codec failed to start: {error:#}"), + ) +} + +/// Enforces a byte-size cap on one component-returned string. +fn ensure_returned_string_limit(kind: &str, value: &str, limit: usize) -> Result<()> { + if value.len() > limit { + return Err(Error::codec( + CodecFailure::LimitExceeded, + format!( + "note codec component returned a {kind} of {} bytes; the limit is {limit}", + value.len() + ), + )); + } + Ok(()) +} + +/// Enforces the structural felt count cap on one `parse` result. +fn ensure_returned_felt_limit(fqn: &str, count: usize, limit: usize) -> Result<()> { + if count > limit { + return Err(Error::codec( + CodecFailure::LimitExceeded, + format!("codec `{fqn}` returned {count} felts from `parse`; the limit is {limit}"), + )); + } + Ok(()) +} + +/// Converts component integers into canonical field elements. +fn component_values_to_felts(fqn: &str, values: &[u64]) -> Result> { + values + .iter() + .copied() + .enumerate() + .map(|(index, value)| { + Felt::new(value).map_err(|error| { + Error::new(format!( + "codec `{fqn}` returned a noncanonical felt at index {index}: {error}" + )) + }) + }) + .collect() +} + +/// Creates a host error for a component runtime failure. +/// +/// The engine, the compilation, the import stubs, and the lifting of a returned value all fail +/// this way, and a consumer sees one class for a codec the engine did not run. +fn component_error(action: &str, error: impl core::fmt::Display) -> Error { + Error::codec(CodecFailure::Trapped, format!("failed to {action}: {error:#}")) +} + +/// Creates a host error for an author codec rejection. +fn codec_rejection(operation: &str, fqn: &str, message: String) -> Error { + Error::codec( + CodecFailure::Rejected, + format!("codec `{fqn}` rejected `{operation}`: {message}"), + ) +} + +#[cfg(test)] +pub(crate) mod tests { + use std::{ + env, fs, + path::{Path, PathBuf}, + process::{Command, Output}, + sync::{Arc, OnceLock}, + }; + + use miden_core::{ + mast::{BasicBlockNodeBuilder, DenseMastForestBuilder, MastNodeExt}, + operations::Operation, + }; + use miden_mast_package::{ + PackageExport, PackageId, PathBuf as MastPathBuf, ProcedureExport, Section, TargetType, + Version, + }; + use midenc_frontend_wasm_metadata::{ + NESTED_CARGO_SCRUB_ENV, package_note_storage_schema_section_id, + }; + use midenc_integration_test_support::wasm_target_is_installed; + use tempfile::TempDir; + + use super::*; + + const WASM_TARGET: &str = "wasm32-wasip2"; + const FIXTURE_FQN: &str = "example:codec-schema/note-storage@1.0.0.ratio"; + const DIGEST_FQN: &str = "miden:base/core-types@1.0.0.digest"; + const FIXTURE_SCHEMA: &str = r#" +package example:codec-schema@1.0.0; + +interface note-storage { + record ratio { + numerator: u64, + denominator: u64, + } + + record codec-note { + ratio: ratio, + } + + type storage = codec-note; +} +"#; + const EMBEDDED_CORE_SCHEMA: &str = r#" +package example:embedded-core-schema@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{digest}; + record embedded-core-note { value: digest } + type storage = embedded-core-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { inner: f32 } + record word { a: felt, b: felt, c: felt, d: felt } + record digest { inner: word } + } +} +"#; + + #[test] + fn local_note_codec_wit_matches_canonical_document() { + let local = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/wit/note-codec.wit")); + assert_eq!(local, miden_note_codec_wit::NOTE_CODEC_WIT); + } + + #[test] + fn component_boundary_rejects_noncanonical_felts() { + let error = + component_values_to_felts("example:test/codec.value", &[Felt::ORDER]).unwrap_err(); + + assert!(error.to_string().contains("noncanonical felt at index 0")); + } + + #[test] + fn oversized_component_is_rejected_before_compilation() { + let limits = CodecLimits::default(); + let bytes = vec![0; limits.max_component_bytes + 1]; + let error = ComponentRuntime::new(&bytes, limits.clone()) + .err() + .expect("an oversized component must fail before compilation") + .to_string(); + + assert!(error.contains("pre-compilation limit")); + assert!(error.contains(&(limits.max_component_bytes + 1).to_string())); + assert!(error.contains(&limits.max_component_bytes.to_string())); + } + + #[test] + fn default_limits_match_the_published_policy() { + let limits = CodecLimits::default(); + + assert_eq!(limits.fuel, 10_000_000); + assert_eq!(limits.max_memory_bytes, 16 * 1024 * 1024); + assert_eq!(limits.max_table_elements, 4_096); + assert_eq!(limits.max_component_bytes, crate::MAX_NOTE_CODEC_COMPONENT_BYTES); + assert_eq!(limits.max_supported_types, 128); + assert_eq!(limits.max_fqn_bytes, 512); + assert_eq!(limits.max_returned_felts, 4_096); + assert_eq!(limits.max_returned_string_bytes, 16 * 1024); + } + + #[test] + fn component_store_limits_bound_table_elements() { + let policy = CodecLimits::default(); + let mut limits = component_store_limits(&policy); + assert!( + ResourceLimiter::table_growing(&mut limits, 0, policy.max_table_elements, None) + .unwrap() + ); + + let error = + ResourceLimiter::table_growing(&mut limits, 0, policy.max_table_elements + 1, None) + .unwrap_err() + .to_string(); + assert!(error.contains("growing table"), "unexpected table-limit error: {error}"); + } + + /// Builds a component that exports the note codec interface with a chosen + /// `supported-types` body. + /// + /// `core_declarations` adds core module text, so a test can install a trapping start + /// function. The other three interface functions carry their real signatures, which the + /// generated bindings check, and share one core function that is never called. + fn codec_shaped_component(core_declarations: &str, supported_types_body: &str) -> Vec { + let text = format!( + r#"(component + (core module $m + (memory (export "memory") 1) + {core_declarations} + (func (export "supported-types") (result i32) {supported_types_body}) + (func (export "operation") (param i32 i32 i32 i32) (result i32) + (i32.const 1024)) + (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) + (i32.const 1024))) + (core instance $i (instantiate $m)) + (func $supported (result (list string)) + (canon lift (core func $i "supported-types") + (memory $i "memory") + (realloc (func $i "cabi_realloc")))) + (func $parse (param "type-fqn" string) (param "value" string) + (result (result (list u64) (error string))) + (canon lift (core func $i "operation") + (memory $i "memory") + (realloc (func $i "cabi_realloc")))) + (func $display (param "type-fqn" string) (param "value" (list u64)) + (result (result string (error string))) + (canon lift (core func $i "operation") + (memory $i "memory") + (realloc (func $i "cabi_realloc")))) + (func $validate (param "type-fqn" string) (param "value" (list u64)) + (result (result (error string))) + (canon lift (core func $i "operation") + (memory $i "memory") + (realloc (func $i "cabi_realloc")))) + (instance $codec + (export "supported-types" (func $supported)) + (export "parse" (func $parse)) + (export "display" (func $display)) + (export "validate" (func $validate))) + (export "{CODEC_INTERFACE}" (instance $codec)))"# + ); + wat::parse_str(text).unwrap() + } + + /// Returns core module text that reports `count` string descriptors, all aliasing one + /// four-byte string. + fn aliasing_descriptors(count: usize) -> String { + let length = (count as u32).to_le_bytes().map(|byte| format!("\\{byte:02x}")).concat(); + let descriptor = "\\00\\00\\00\\00\\04\\00\\00\\00".repeat(count); + format!( + r#"(data (i32.const 0) "abcd") + (data (i32.const 8) "\10\00\00\00{length}") + (data (i32.const 16) "{descriptor}")"# + ) + } + + #[test] + fn discovery_checks_the_reported_length_before_it_lifts_a_string() { + // Every descriptor points at the same four bytes, so lifting the whole list first would + // allocate far past the limit that rejects it. + let limits = CodecLimits::default(); + let reported = limits.max_supported_types + 1; + let component = codec_shaped_component(&aliasing_descriptors(reported), "(i32.const 8)"); + + let error = ComponentRuntime::new(&component, limits.clone()) + .unwrap() + .supported_types() + .expect_err("a component over the supported-type limit must be rejected"); + + assert!( + error.to_string().contains(&format!("reported {reported} types")), + "unexpected limit error: {error}" + ); + assert!( + error + .to_string() + .contains(&format!("the limit is {}", limits.max_supported_types)), + "unexpected limit error: {error}" + ); + assert_eq!(error.codec_failure(), Some(CodecFailure::LimitExceeded)); + } + + #[test] + fn a_trapping_start_function_is_classified() { + let component = codec_shaped_component( + r#"(func $boom unreachable) + (start $boom)"#, + "(i32.const 8)", + ); + + let error = ComponentRuntime::new(&component, CodecLimits::default()) + .unwrap() + .supported_types() + .expect_err("a trapping start function must fail instantiation"); + + assert!(error.to_string().contains("failed to start"), "unexpected error: {error}"); + assert_eq!(error.codec_failure(), Some(CodecFailure::Trapped)); + } + + #[test] + fn discovery_that_runs_out_of_fuel_is_classified() { + let component = codec_shaped_component("", "(loop $spin (br $spin)) (i32.const 8)"); + + let error = ComponentRuntime::new(&component, CodecLimits::default()) + .unwrap() + .supported_types() + .expect_err("an endless `supported-types` must exhaust its fuel"); + + assert!( + error.to_string().contains("failed in `supported-types`"), + "unexpected error: {error}" + ); + assert_eq!(error.codec_failure(), Some(CodecFailure::OutOfFuel)); + } + + #[test] + fn a_package_without_a_codec_section_keeps_the_standard_codecs() { + let mut package = test_package(); + package.sections.push(Section::new( + package_note_storage_schema_section_id(), + FIXTURE_SCHEMA.as_bytes().to_vec(), + )); + + let registry = CodecRegistry::load_from_package(&package).unwrap(); + + assert!(registry.contains(crate::FELT_FQN)); + assert!(registry.contains(crate::WORD_FQN)); + assert!(!registry.contains(FIXTURE_FQN)); + } + + #[test] + fn a_package_without_a_schema_or_a_codec_section_keeps_the_standard_codecs() { + // A unit `#[note]` struct emits neither section, and a host may probe any package. + let package = test_package(); + + let registry = CodecRegistry::load_from_package(&package).unwrap(); + + assert!(registry.contains(crate::FELT_FQN)); + assert!(registry.contains(crate::WORD_FQN)); + assert!(!registry.contains(FIXTURE_FQN)); + } + + #[test] + fn simd_components_do_not_load() { + let component = wat::parse_str( + r#"(component + (core module + (func (export "simd") + v128.const i32x4 0 0 0 0 + drop)))"#, + ) + .unwrap(); + + let error = ComponentRuntime::new(&component, CodecLimits::default()) + .err() + .expect("a SIMD component must not load") + .to_string(); + + assert!(error.contains("SIMD"), "unexpected engine policy error: {error}"); + } + + #[test] + fn threads_components_do_not_load() { + let component = wat::parse_str( + r#"(component + (core module + (memory 1 1 shared)))"#, + ) + .unwrap(); + + let error = ComponentRuntime::new(&component, CodecLimits::default()) + .err() + .expect("a threads component must not load") + .to_string(); + + assert!(error.contains("threads"), "unexpected engine policy error: {error}"); + } + + #[test] + fn gc_components_do_not_load() { + let component = wat::parse_str( + r#"(component + (core module + (type (struct))))"#, + ) + .unwrap(); + + let error = ComponentRuntime::new(&component, CodecLimits::default()) + .err() + .expect("a GC component must not load") + .to_string(); + + assert!(error.contains("gc"), "unexpected engine policy error: {error}"); + } + + #[test] + fn nonstandard_embedded_core_type_is_author_codec_eligible() { + let schema = NoteStorageSchema::from_wit_text(EMBEDDED_CORE_SCHEMA).unwrap(); + let custom_types = schema.custom_type_fqns(); + + assert!(custom_types.contains(DIGEST_FQN)); + assert!(!custom_types.contains(crate::FELT_FQN)); + assert!(!custom_types.contains(crate::WORD_FQN)); + validate_reported_fqns(&[DIGEST_FQN.to_owned()], &custom_types, &CodecRegistry::default()) + .unwrap(); + } + + #[test] + fn package_component_registers_and_dispatches_author_codec() { + if !wasm_target_is_installed() { + eprintln!("skipping component adapter test: {WASM_TARGET} is not installed"); + return; + } + + let component = build_fixture_component(); + let mut package = test_package(); + package.sections.push(Section::new( + package_note_storage_schema_section_id(), + FIXTURE_SCHEMA.as_bytes().to_vec(), + )); + package.sections.push(Section::new(package_note_codec_section_id(), component)); + + let registry = CodecRegistry::load_from_package(&package).unwrap(); + let codec = registry.codec(FIXTURE_FQN).expect("fixture ratio codec was not registered"); + let encoded = codec.parse("3/2").unwrap(); + assert_eq!(encoded, [Felt::new(3).unwrap(), Felt::ZERO, Felt::new(2).unwrap(), Felt::ZERO]); + codec.validate(&encoded).unwrap(); + assert_eq!(codec.display(&encoded).unwrap(), "3/2"); + + let invalid = codec.parse("3/0").unwrap(); + assert!(codec.validate(&invalid).unwrap_err().to_string().contains("denominator")); + } + + #[test] + fn component_calls_recover_after_traps_and_enforce_fuel() { + if !wasm_target_is_installed() { + eprintln!("skipping component adapter test: {WASM_TARGET} is not installed"); + return; + } + + let schema = NoteStorageSchema::from_wit_text(FIXTURE_SCHEMA).unwrap(); + let registry = CodecRegistry::load_from_component( + &build_fixture_component(), + &schema.custom_type_fqns(), + CodecLimits::default(), + ) + .unwrap(); + let codec = registry.codec(FIXTURE_FQN).unwrap(); + + let trap = codec.parse("trap").unwrap_err(); + assert!(trap.to_string().contains("trapped in `parse`"), "unexpected error: {trap}"); + assert_eq!(trap.codec_failure(), Some(CodecFailure::Trapped)); + assert_eq!(codec.display(&codec.parse("3/2").unwrap()).unwrap(), "3/2"); + + let oversized_input = "x".repeat(CodecLimits::default().max_memory_bytes + 1); + let limit = codec.parse(&oversized_input).unwrap_err(); + assert!( + limit.to_string().contains("exceeded a resource limit in `parse`"), + "unexpected error: {limit}" + ); + assert_eq!(limit.codec_failure(), Some(CodecFailure::LimitExceeded)); + assert_eq!(codec.display(&codec.parse("3/2").unwrap()).unwrap(), "3/2"); + + let fuel = codec.parse("loop").unwrap_err(); + assert!(fuel.to_string().contains("ran out of fuel"), "unexpected budget error: {fuel}"); + assert_eq!(fuel.codec_failure(), Some(CodecFailure::OutOfFuel)); + assert_eq!(codec.display(&codec.parse("3/2").unwrap()).unwrap(), "3/2"); + + // An author rejection is not a sandbox failure; it carries the codec's own message. + let rejected = codec.validate(&codec.parse("3/0").unwrap()).unwrap_err(); + assert!(rejected.to_string().contains("denominator"), "unexpected error: {rejected}"); + assert_eq!(rejected.codec_failure(), Some(CodecFailure::Rejected)); + } + + #[test] + fn limits_reject_a_component_that_reports_too_many_types() { + if !wasm_target_is_installed() { + eprintln!("skipping component adapter test: {WASM_TARGET} is not installed"); + return; + } + + let mut package = test_package(); + package.sections.push(Section::new( + package_note_storage_schema_section_id(), + FIXTURE_SCHEMA.as_bytes().to_vec(), + )); + package + .sections + .push(Section::new(package_note_codec_section_id(), build_fixture_component())); + let limits = CodecLimits { + max_supported_types: 0, + ..CodecLimits::default() + }; + + let error = CodecRegistry::load_from_package_with_limits(&package, limits) + .err() + .expect("a zero supported-type limit must reject the fixture codec") + .to_string(); + + assert!(error.contains("reported 1 types"), "unexpected limit error: {error}"); + assert!(error.contains("the limit is 0"), "unexpected limit error: {error}"); + } + + #[test] + fn reported_fqns_cannot_replace_standard_or_unrelated_types() { + let standard = CodecRegistry::default(); + let allowed = HashSet::from([FIXTURE_FQN.to_owned()]); + + let collision = + validate_reported_fqns(&[crate::ACCOUNT_ID_FQN.to_owned()], &allowed, &standard) + .unwrap_err() + .to_string(); + assert!(collision.contains("cannot replace the standard codec")); + + let unrelated = validate_reported_fqns( + &["example:other/schema@1.0.0.value".to_owned()], + &allowed, + &standard, + ) + .unwrap_err() + .to_string(); + assert!(unrelated.contains("does not appear in the package note storage schema")); + + let duplicate = validate_reported_fqns( + &[FIXTURE_FQN.to_owned(), FIXTURE_FQN.to_owned()], + &allowed, + &standard, + ) + .unwrap_err() + .to_string(); + assert!(duplicate.contains("more than once")); + } + + #[test] + fn returned_values_are_size_limited() { + let limits = CodecLimits::default(); + let long = "x".repeat(limits.max_returned_string_bytes + 1); + assert!( + ensure_returned_string_limit("display value", &long, limits.max_returned_string_bytes) + .unwrap_err() + .to_string() + .contains("the limit is") + ); + let felts = ensure_returned_felt_limit( + FIXTURE_FQN, + limits.max_returned_felts + 1, + limits.max_returned_felts, + ) + .unwrap_err(); + assert!(felts.to_string().contains("the limit is"), "unexpected error: {felts}"); + assert_eq!(felts.codec_failure(), Some(CodecFailure::LimitExceeded)); + } + + #[test] + fn package_readers_reject_duplicate_schema_and_codec_sections() { + let schema_id = package_note_storage_schema_section_id(); + let codec_id = package_note_codec_section_id(); + let mut duplicate_schema = test_package(); + duplicate_schema + .sections + .push(Section::new(schema_id.clone(), FIXTURE_SCHEMA.as_bytes().to_vec())); + duplicate_schema + .sections + .push(Section::new(schema_id, FIXTURE_SCHEMA.as_bytes().to_vec())); + assert!( + NoteStorageSchema::from_package(&duplicate_schema) + .err() + .expect("duplicate schema sections must fail") + .to_string() + .contains("more than one `note_storage_schema` section") + ); + + let mut duplicate_codec = test_package(); + duplicate_codec.sections.push(Section::new( + package_note_storage_schema_section_id(), + FIXTURE_SCHEMA.as_bytes().to_vec(), + )); + duplicate_codec.sections.push(Section::new(codec_id.clone(), Vec::new())); + duplicate_codec.sections.push(Section::new(codec_id, Vec::new())); + assert!( + CodecRegistry::load_from_package(&duplicate_codec) + .err() + .expect("duplicate codec sections must fail") + .to_string() + .contains("more than one `note_codec` section") + ); + } + + /// Builds a valid package with one procedure export. + fn test_package() -> Package { + let mut builder = DenseMastForestBuilder::new(); + let node_id = builder + .push_node(BasicBlockNodeBuilder::new(vec![Operation::Add])) + .expect("failed to build package procedure"); + builder.mark_root(node_id); + let (forest, remapping) = builder.build_with_id_map().expect("failed to build package"); + let node_id = remapping.get(node_id).expect("package root was removed"); + let export = ProcedureExport::new( + MastPathBuf::absolute("component-codec-test::run").into(), + Some(node_id), + forest[node_id].digest(), + None, + ); + + Package::create( + PackageId::from("component-codec-test"), + Version::new(0, 0, 0), + TargetType::Library, + Arc::new(forest), + [PackageExport::Procedure(export)], + [], + ) + .expect("failed to create test package") + } + + /// Builds the minimal author codec the component adapter tests dispatch through. + pub(crate) fn build_fixture_component() -> Vec { + static COMPONENT: OnceLock> = OnceLock::new(); + COMPONENT.get_or_init(build_fixture_component_uncached).clone() + } + + /// Builds the component fixture once for this test process. + fn build_fixture_component_uncached() -> Vec { + let fixture = TempDir::new().expect("failed to create component fixture directory"); + write_fixture(fixture.path()); + let target_dir = workspace_root().join("target/note-schema-component-test"); + let mut command = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into())); + command + .args([ + "build", + "--manifest-path", + fixture.path().join("Cargo.toml").to_str().unwrap(), + "--release", + "--target", + WASM_TARGET, + "--offline", + ]) + .env("CARGO_TARGET_DIR", &target_dir) + // Share the hash-keyed intermediates with the other test builds; only the + // name-keyed final artifacts stay in the directory above. Convention from + // `tests/support`. + .env("CARGO_BUILD_BUILD_DIR", workspace_root().join("target/miden_build_cache")); + for &variable in NESTED_CARGO_SCRUB_ENV { + command.env_remove(variable); + } + // Pin the guest rustflags after the scrub, the way the compiler builds a codec crate. + command.env("RUSTFLAGS", crate::NOTE_CODEC_GUEST_RUSTFLAGS); + let output = command.output().expect("failed to start fixture build"); + assert_command_succeeded("building the component adapter fixture", &output); + + fs::read( + target_dir.join(format!("{WASM_TARGET}/release/note_schema_component_fixture.wasm")), + ) + .expect("component fixture did not produce its Wasm component") + } + + /// Writes a standalone codec crate for the component adapter test. + fn write_fixture(root: &Path) { + fs::create_dir(root.join("src")).unwrap(); + // Seed the workspace lockfile so the offline build resolves to the versions that the + // workspace already downloaded instead of racing the registry index. + fs::copy(workspace_root().join("Cargo.lock"), root.join("Cargo.lock")).unwrap(); + let codec_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../note-codec"); + let manifest = format!( + r#"[package] +name = "note-schema-component-fixture" +version = "0.0.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miden-note-codec = {{ path = {:?} }} + +[workspace] +"#, + codec_path + ); + fs::write(root.join("Cargo.toml"), manifest).unwrap(); + fs::write(root.join("src/lib.rs"), fixture_source()).unwrap(); + } + + /// Returns the compiler workspace root. + fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .unwrap() + .to_owned() + } + + /// Includes both output streams when one fixture command fails. + fn assert_command_succeeded(action: &str, output: &Output) { + assert!( + output.status.success(), + "failed while {action}\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + /// Builds the fixture source around the schema shared with host-side assertions. + fn fixture_source() -> String { + [ + r##" +use miden_note_codec::AuthorTypeCodec; + +miden_note_codec::from_wit_text!(r#""##, + FIXTURE_SCHEMA, + r##""#); + +#[miden_note_codec::note_codec] +impl AuthorTypeCodec for Ratio { + fn parse(value: &str) -> Result { + if value == "trap" { + panic!("fixture trap"); + } + if value == "loop" { + loop { + std::hint::black_box(()); + } + } + let (numerator, denominator) = value + .split_once('/') + .ok_or_else(|| "a ratio must use `numerator/denominator`".to_owned())?; + Ok(Self { + numerator: numerator.parse::().map_err(|error| error.to_string())?, + denominator: denominator.parse::().map_err(|error| error.to_string())?, + }) + } + + fn display(&self) -> String { + format!("{}/{}", self.numerator, self.denominator) + } + + fn validate(&self) -> Result<(), String> { + if self.denominator == 0 { + Err("the denominator must not be zero".to_owned()) + } else { + Ok(()) + } + } +} + +miden_note_codec::export_codecs!(); +"##, + ] + .concat() + } +} diff --git a/sdk/note-schema/src/codec_structure.rs b/sdk/note-schema/src/codec_structure.rs new file mode 100644 index 0000000000..e3dcd53e10 --- /dev/null +++ b/sdk/note-schema/src/codec_structure.rs @@ -0,0 +1,1011 @@ +//! Wasm feature and structural policy for note codec components. +//! +//! Parsing, compilation, and instantiation cost work before any fuel budget applies, so a byte +//! cap alone does not bound that work. A small binary can declare thousands of tiny functions, +//! thousands of globals, or a component tree whose instantiations expand exponentially. +//! +//! The rules are fixed policy. The producer applies them when it attaches a codec, and every +//! consumer applies them when it loads one. [`validate_note_codec_component`] runs the whole +//! policy in one place. It checks that: +//! +//! - the component fits in the caller's byte budget; +//! - the component validates under [`NOTE_CODEC_WASM_FEATURES`]; +//! - the component declares no component-level start function; +//! - each core module stays under its own counts, each of its function types stays under the +//! parameter and result caps, and its code section keeps a plausible average function size; +//! - the component tree stays under its budgets for core modules and nesting depth; +//! - one instantiation of any component in the tree stays under its budgets for core +//! instantiations, component instantiations, linear memories, and tables; +//! - each kind of component-level entry stays under its cap for the whole tree. +//! +//! The walk counts what an instantiation creates, not what the tree declares. A core module that +//! is instantiated twice counts twice, and a nested component contributes what one instantiation +//! of it creates, once per instantiation. A consumer store admits the same counts, so a component +//! that passes the walk instantiates inside the store limits. +//! +//! The walk rejects only a component-level start function. A core module keeps its own start +//! function, which runs at instantiation under the consumer fuel budget and store limits. +//! +//! Nothing else is checked here. The producer checks the exported codec interface, and a +//! consumer bounds the run time of a codec call with fuel and store limits. + +use wasmparser::{ + ComponentAlias, ComponentExternalKind, ComponentInstance, ComponentOuterAliasKind, + ComponentTypeRef, Encoding, FuncValidatorAllocations, Instance, Parser, Payload, TypeRef, + ValidPayload, Validator, WasmFeatures, +}; + +use crate::{CodecFailure, Error, Result}; + +/// Wasm proposals a note codec component may use. +/// +/// The producer and every consumer validate against this constant, so the accepted set does not +/// depend on the Cargo features a host compiled its engine with. The set holds the proposals the +/// `wasm32-wasip2` target emits, the component model, and floating point. Every other proposal is +/// off, including SIMD, threads, garbage collection, typed function references, and the +/// asynchronous component-model additions. +/// +/// The value is written bit by bit instead of subtracting from `WasmFeatures::default()` or +/// `WasmFeatures::all()`, so a wasmparser upgrade cannot widen the policy. +pub const NOTE_CODEC_WASM_FEATURES: WasmFeatures = WasmFeatures::COMPONENT_MODEL + .union(WasmFeatures::FLOATS) + .union(WasmFeatures::MUTABLE_GLOBAL) + .union(WasmFeatures::SATURATING_FLOAT_TO_INT) + .union(WasmFeatures::SIGN_EXTENSION) + .union(WasmFeatures::REFERENCE_TYPES) + .union(WasmFeatures::MULTI_VALUE) + .union(WasmFeatures::BULK_MEMORY); + +/// Maximum functions in one core module, imported and defined. +const MAX_MODULE_FUNCTIONS: usize = 10_000; + +/// Maximum globals in one core module, imported and defined. +const MAX_MODULE_GLOBALS: usize = 1_000; + +/// Maximum types in one core module. +const MAX_MODULE_TYPES: usize = 1_000; + +/// Maximum tables in one core module, imported and defined. +/// +/// A module that is instantiated reaches the tighter tree-wide instantiated-table budget first. +/// This cap constrains imported tables, and the tables of a module that is never instantiated. +const MAX_MODULE_TABLES: usize = 100; + +/// Maximum linear memories in one core module, imported and defined. +const MAX_MODULE_MEMORIES: usize = 1; + +/// Maximum element segments in one core module. +const MAX_MODULE_ELEMENT_SEGMENTS: usize = 1_000; + +/// Maximum data segments in one core module. +const MAX_MODULE_DATA_SEGMENTS: usize = 1_000; + +/// Maximum imports in one core module. +const MAX_MODULE_IMPORTS: usize = 1_024; + +/// Maximum exports in one core module. +const MAX_MODULE_EXPORTS: usize = 1_024; + +/// Maximum parameters in one core function type. +const MAX_FUNCTION_PARAMS: usize = 32; + +/// Maximum results in one core function type. +const MAX_FUNCTION_RESULTS: usize = 32; + +/// Code section size, in bytes, above which the average function size applies. +/// +/// Small modules stay below it, so a short helper module is never rejected for its shape. +const MIN_METERED_CODE_SECTION_BYTES: u32 = 1_000; + +/// Minimum average bytes per function body in a metered code section. +/// +/// Compiled Wasm averages far above this value. A module below it is a compilation bomb: +/// many tiny functions that each cost a fixed amount of host work. +const MIN_AVERAGE_FUNCTION_BYTES: u32 = 40; + +/// Maximum core modules in one component, at any nesting level. +const MAX_CORE_MODULES: usize = 16; + +/// Maximum component nesting depth. +const MAX_COMPONENT_DEPTH: usize = 4; + +/// Maximum entries of one kind of component-level item in the whole component tree. +/// +/// A component may split one kind over many sections, so the cap counts each kind over the +/// whole tree: core types, component types, aliases, canonical functions, imports, and exports. +const MAX_COMPONENT_SECTION_ITEMS: usize = 256; + +/// Maximum core instances one instantiation of a component creates. +/// +/// The consumer store admits the same number of instances. +pub(crate) const MAX_CORE_INSTANCES: usize = 32; + +/// Maximum nested component instances one instantiation of a component creates. +/// +/// A `wasm32-wasip2` codec builds one nested component instance per exported interface. The +/// budget has no consumer store counterpart: a component instance holds no core instance, +/// memory, or table of its own, and what it creates is counted through those budgets. +const MAX_COMPONENT_INSTANCES: usize = 8; + +/// Maximum tables one instantiation of a component creates. +/// +/// The consumer store admits the same number of tables. +pub(crate) const MAX_INSTANTIATED_TABLES: usize = 32; + +/// Maximum linear memories one instantiation of a component creates. +/// +/// The consumer store admits the same number of memories. +pub(crate) const MAX_INSTANTIATED_MEMORIES: usize = 1; + +/// Applies the whole note codec component policy: the byte cap, the Wasm feature set, and the +/// structural limits. +/// +/// `max_bytes` is the caller's byte budget. The producer passes +/// [`MAX_NOTE_CODEC_COMPONENT_BYTES`](crate::MAX_NOTE_CODEC_COMPONENT_BYTES), and a consumer +/// passes the cap in its own limits. +pub fn validate_note_codec_component(component: &[u8], max_bytes: usize) -> Result<()> { + ensure_component_byte_limit(component.len(), max_bytes)?; + validate_note_codec_structure(component) +} + +/// Rejects a note codec component whose Wasm features or structure fall outside the policy. +/// +/// [`validate_note_codec_component`] is the entry point both sides call. This function is public +/// for a caller that bounds the byte length itself. +pub fn validate_note_codec_structure(component: &[u8]) -> Result<()> { + let mut validator = Validator::new_with_features(NOTE_CODEC_WASM_FEATURES); + // Function bodies are validated with one reusable allocation, not one per function. + let mut allocations = FuncValidatorAllocations::default(); + let mut walk = StructureWalk::default(); + for payload in Parser::new(0).parse_all(component) { + let payload = payload.map_err(malformed)?; + // Reject a start function ahead of the validator. A start function runs guest code when + // the component is instantiated, before the export call the limits are built around, and + // the validator reports only that component values are disabled. + if matches!(payload, Payload::ComponentStartSection { .. }) { + return Err(policy_rejection( + "note codec component declares a start function; the policy rejects a component \ + that runs code when it is instantiated", + )); + } + if let ValidPayload::Func(function, body) = + validator.payload(&payload).map_err(rejected_feature)? + { + let mut function = function.into_validator(allocations); + function.validate(&body).map_err(rejected_feature)?; + allocations = function.into_allocations(); + } + walk.visit(payload)?; + } + Ok(()) +} + +/// Rejects component bytes over the caller's budget, before any parsing or compilation. +fn ensure_component_byte_limit(byte_len: usize, limit: usize) -> Result<()> { + if byte_len <= limit { + return Ok(()); + } + Err(policy_rejection(format!( + "note codec component is {byte_len} bytes; the pre-compilation limit is {limit}" + ))) +} + +/// The state carried while the parser walks one component. +#[derive(Default)] +struct StructureWalk { + /// One frame per core module or component the walk entered, innermost last. + frames: Vec, + /// Core modules seen anywhere in the component. + core_modules: usize, + /// Component-level items declared anywhere in the component, one count per kind. + component_items: ComponentItemCounts, +} + +/// Component-level items counted over the whole component tree. +#[derive(Default)] +struct ComponentItemCounts { + core_types: usize, + types: usize, + aliases: usize, + canonical_functions: usize, + imports: usize, + exports: usize, +} + +/// One nesting level of the walk. +enum Frame { + /// A core module, with the counters checked when the module ends. + Module(ModuleCounts), + /// A component, with its index spaces and what one instantiation of it creates. + Component(ComponentFrame), +} + +/// One component nesting level. +/// +/// An index-space entry is `None` for a core module or a component the walk cannot read, such as +/// an imported or an aliased one. An instantiation of such an entry is rejected. +#[derive(Default)] +struct ComponentFrame { + /// The core module index space of this component. + core_modules: Vec>, + /// The component index space of this component. + components: Vec>, + /// What one instantiation of this component creates. + created: CreatedCounts, +} + +/// What one core module creates every time it is instantiated. +#[derive(Clone, Copy, Default)] +struct ModuleRuntimeCounts { + memories: usize, + tables: usize, +} + +/// What one instantiation of a component creates. +#[derive(Clone, Copy, Default)] +struct CreatedCounts { + core_instances: usize, + component_instances: usize, + memories: usize, + tables: usize, +} + +impl CreatedCounts { + /// Adds what one nested instantiation creates. + fn add(&mut self, other: Self) { + self.core_instances = self.core_instances.saturating_add(other.core_instances); + self.component_instances = + self.component_instances.saturating_add(other.component_instances); + self.memories = self.memories.saturating_add(other.memories); + self.tables = self.tables.saturating_add(other.tables); + } + + /// Checks every budget that bounds one instantiation. + fn check(&self) -> Result<()> { + ensure_created_cap("core instances", self.core_instances, MAX_CORE_INSTANCES)?; + ensure_created_cap( + "component instances", + self.component_instances, + MAX_COMPONENT_INSTANCES, + )?; + ensure_created_cap("linear memories", self.memories, MAX_INSTANTIATED_MEMORIES)?; + ensure_created_cap("tables", self.tables, MAX_INSTANTIATED_TABLES) + } +} + +/// Counters collected for one core module. +#[derive(Default)] +struct ModuleCounts { + types: usize, + functions: usize, + globals: usize, + tables: usize, + memories: usize, + element_segments: usize, + data_segments: usize, + imports: usize, + exports: usize, + /// The memories and tables one instantiation of this module creates. + runtime: ModuleRuntimeCounts, +} + +impl ModuleCounts { + /// Checks every per-module cap once the module ends. + fn check(&self) -> Result<()> { + ensure_module_cap("types", self.types, MAX_MODULE_TYPES)?; + ensure_module_cap("functions", self.functions, MAX_MODULE_FUNCTIONS)?; + ensure_module_cap("globals", self.globals, MAX_MODULE_GLOBALS)?; + ensure_module_cap("tables", self.tables, MAX_MODULE_TABLES)?; + ensure_module_cap("memories", self.memories, MAX_MODULE_MEMORIES)?; + ensure_module_cap("element segments", self.element_segments, MAX_MODULE_ELEMENT_SEGMENTS)?; + ensure_module_cap("data segments", self.data_segments, MAX_MODULE_DATA_SEGMENTS)?; + ensure_module_cap("imports", self.imports, MAX_MODULE_IMPORTS)?; + ensure_module_cap("exports", self.exports, MAX_MODULE_EXPORTS) + } +} + +impl StructureWalk { + /// Applies one parser payload to the current nesting level. + fn visit(&mut self, payload: Payload<'_>) -> Result<()> { + match payload { + Payload::Version { encoding, .. } => self.enter(encoding)?, + Payload::End(_) => self.leave()?, + Payload::TypeSection(reader) => { + self.module_counts()?.types += reader.count() as usize; + // The feature validator rejects a GC type before the walk sees the section, so + // only a core function type reaches this loop. + for ty in reader.into_iter_err_on_gc_types() { + let ty = ty.map_err(rejected_feature)?; + ensure_signature_cap("parameters", ty.params().len(), MAX_FUNCTION_PARAMS)?; + ensure_signature_cap("results", ty.results().len(), MAX_FUNCTION_RESULTS)?; + } + } + Payload::ImportSection(reader) => { + for import in reader.into_imports() { + let import = import.map_err(malformed)?; + let counts = self.module_counts()?; + counts.imports += 1; + match import.ty { + TypeRef::Func(_) | TypeRef::FuncExact(_) => counts.functions += 1, + TypeRef::Global(_) => counts.globals += 1, + TypeRef::Table(_) => counts.tables += 1, + TypeRef::Memory(_) => counts.memories += 1, + TypeRef::Tag(_) => {} + } + } + } + Payload::FunctionSection(reader) => { + self.module_counts()?.functions += reader.count() as usize; + } + Payload::GlobalSection(reader) => { + self.module_counts()?.globals += reader.count() as usize; + } + Payload::TableSection(reader) => { + let count = reader.count() as usize; + let counts = self.module_counts()?; + counts.tables += count; + counts.runtime.tables += count; + } + Payload::MemorySection(reader) => { + let count = reader.count() as usize; + let counts = self.module_counts()?; + counts.memories += count; + counts.runtime.memories += count; + } + Payload::ElementSection(reader) => { + self.module_counts()?.element_segments += reader.count() as usize; + } + Payload::DataSection(reader) => { + self.module_counts()?.data_segments += reader.count() as usize; + } + Payload::ExportSection(reader) => { + self.module_counts()?.exports += reader.count() as usize; + } + Payload::CodeSectionStart { count, size, .. } => { + ensure_average_function_size(count, size)?; + } + Payload::InstanceSection(reader) => { + for instance in reader { + self.visit_core_instance(instance.map_err(malformed)?)?; + } + } + Payload::ComponentInstanceSection(reader) => { + for instance in reader { + self.visit_component_instance(instance.map_err(malformed)?)?; + } + } + Payload::CoreTypeSection(reader) => { + self.component_items.core_types += reader.count() as usize; + ensure_component_item_cap("core types", self.component_items.core_types)?; + } + Payload::ComponentTypeSection(reader) => { + self.component_items.types += reader.count() as usize; + ensure_component_item_cap("types", self.component_items.types)?; + } + Payload::ComponentAliasSection(reader) => { + for alias in reader { + let alias = alias.map_err(malformed)?; + self.component_items.aliases += 1; + ensure_component_item_cap("aliases", self.component_items.aliases)?; + self.declare_aliased_item(&alias)?; + } + } + Payload::ComponentCanonicalSection(reader) => { + self.component_items.canonical_functions += reader.count() as usize; + ensure_component_item_cap( + "canonical functions", + self.component_items.canonical_functions, + )?; + } + Payload::ComponentImportSection(reader) => { + for import in reader { + let import = import.map_err(malformed)?; + self.component_items.imports += 1; + ensure_component_item_cap("imports", self.component_items.imports)?; + match import.ty { + ComponentTypeRef::Module(_) => self.declare_core_module(None)?, + ComponentTypeRef::Component(_) => self.declare_component(None)?, + _ => {} + } + } + } + Payload::ComponentExportSection(reader) => { + self.component_items.exports += reader.count() as usize; + ensure_component_item_cap("exports", self.component_items.exports)?; + } + _ => {} + } + Ok(()) + } + + /// Opens one nesting level and checks the component-wide caps. + fn enter(&mut self, encoding: Encoding) -> Result<()> { + match encoding { + Encoding::Module => { + self.core_modules += 1; + if self.core_modules > MAX_CORE_MODULES { + return Err(policy_rejection(format!( + "note codec component has {} core modules; the limit is {MAX_CORE_MODULES}", + self.core_modules + ))); + } + self.frames.push(Frame::Module(ModuleCounts::default())); + } + Encoding::Component => { + self.frames.push(Frame::Component(ComponentFrame::default())); + let depth = + self.frames.iter().filter(|frame| matches!(frame, Frame::Component(_))).count(); + if depth > MAX_COMPONENT_DEPTH { + return Err(policy_rejection(format!( + "note codec component nests components {depth} deep; the limit is \ + {MAX_COMPONENT_DEPTH}" + ))); + } + } + } + Ok(()) + } + + /// Closes one nesting level and records what it creates in the component that declares it. + fn leave(&mut self) -> Result<()> { + match self.frames.pop() { + Some(Frame::Module(counts)) => { + counts.check()?; + // The module now holds an index in the core module index space of the component + // that declares it. + self.declare_core_module(Some(counts.runtime)) + } + Some(Frame::Component(frame)) => self.declare_component(Some(frame.created)), + // An unbalanced end cannot happen: the parser reports one `End` for every header it + // accepted. + None => Ok(()), + } + } + + /// Counts one core instantiation and what it creates. + fn visit_core_instance(&mut self, instance: Instance<'_>) -> Result<()> { + // An instance built from exports names items other instances already created. + let created = match instance { + Instance::Instantiate { module_index, .. } => { + let module = self.instantiated_core_module(module_index)?; + CreatedCounts { + core_instances: 1, + component_instances: 0, + memories: module.memories, + tables: module.tables, + } + } + Instance::FromExports(_) => CreatedCounts { + core_instances: 1, + ..CreatedCounts::default() + }, + }; + self.record_created(created) + } + + /// Counts one component instantiation and what it creates. + fn visit_component_instance(&mut self, instance: ComponentInstance<'_>) -> Result<()> { + // An instance built from exports names items other instances already created. + let ComponentInstance::Instantiate { + component_index, .. + } = instance + else { + return Ok(()); + }; + let mut created = self.instantiated_component(component_index)?; + created.component_instances = created.component_instances.saturating_add(1); + self.record_created(created) + } + + /// Adds what one instantiation creates to the component the walk is inside. + fn record_created(&mut self, created: CreatedCounts) -> Result<()> { + let frame = self.component_frame()?; + frame.created.add(created); + frame.created.check() + } + + /// Returns what one instantiation of a core module of the current component creates. + fn instantiated_core_module(&mut self, module_index: u32) -> Result { + let frame = self.component_frame()?; + frame.core_modules.get(module_index as usize).copied().flatten().ok_or_else(|| { + policy_rejection(format!( + "note codec component instantiates core module {module_index}, which the policy \ + cannot read; a codec instantiates only the core modules it defines" + )) + }) + } + + /// Returns what one instantiation of a nested component of the current component creates. + fn instantiated_component(&mut self, component_index: u32) -> Result { + let frame = self.component_frame()?; + frame + .components + .get(component_index as usize) + .copied() + .flatten() + .ok_or_else(|| { + policy_rejection(format!( + "note codec component instantiates component {component_index}, which the \ + policy cannot read; a codec instantiates only the components it defines" + )) + }) + } + + /// Adds one core module to the index space of the component the walk is inside. + /// + /// A core module at the top level belongs to no component index space, so it is dropped. + fn declare_core_module(&mut self, created: Option) -> Result<()> { + if let Some(Frame::Component(frame)) = self.frames.last_mut() { + frame.core_modules.push(created); + } + Ok(()) + } + + /// Adds one component to the index space of the component the walk is inside. + /// + /// The root component belongs to no component index space, so it is dropped. + fn declare_component(&mut self, created: Option) -> Result<()> { + if let Some(Frame::Component(frame)) = self.frames.last_mut() { + frame.components.push(created); + } + Ok(()) + } + + /// Adds one aliased core module or component to the current index spaces. + fn declare_aliased_item(&mut self, alias: &ComponentAlias<'_>) -> Result<()> { + match aliased_item_kind(alias) { + Some(AliasedItem::CoreModule) => self.declare_core_module(None), + Some(AliasedItem::Component) => self.declare_component(None), + None => Ok(()), + } + } + + /// Returns the frame of the component the walk is inside. + /// + /// Component sections appear only inside a component. A component section anywhere else is a + /// malformed layout, and the walk fails closed instead of guessing a frame for it. + fn component_frame(&mut self) -> Result<&mut ComponentFrame> { + match self.frames.last_mut() { + Some(Frame::Component(frame)) => Ok(frame), + _ => Err(policy_rejection( + "note codec component is malformed: a component section appears outside a \ + component", + )), + } + } + + /// Returns the counters of the core module the walk is inside. + /// + /// Core sections appear only inside a core module. A core section anywhere else is a + /// malformed layout, and the walk fails closed instead of guessing a frame for it. + fn module_counts(&mut self) -> Result<&mut ModuleCounts> { + match self.frames.last_mut() { + Some(Frame::Module(counts)) => Ok(counts), + _ => Err(policy_rejection( + "note codec component is malformed: a core section appears outside a core module", + )), + } + } +} + +/// An index space one alias adds an item to. +enum AliasedItem { + CoreModule, + Component, +} + +/// Returns the index space one alias adds an item to, if the walk tracks that space. +fn aliased_item_kind(alias: &ComponentAlias<'_>) -> Option { + match alias { + ComponentAlias::InstanceExport { + kind: ComponentExternalKind::Module, + .. + } + | ComponentAlias::Outer { + kind: ComponentOuterAliasKind::CoreModule, + .. + } => Some(AliasedItem::CoreModule), + ComponentAlias::InstanceExport { + kind: ComponentExternalKind::Component, + .. + } + | ComponentAlias::Outer { + kind: ComponentOuterAliasKind::Component, + .. + } => Some(AliasedItem::Component), + _ => None, + } +} + +/// Reports a core module that is over one of its caps. +fn ensure_module_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { + if observed > limit { + return Err(policy_rejection(format!( + "note codec component has a core module with {observed} {kind}; the limit is {limit}" + ))); + } + Ok(()) +} + +/// Reports a component that creates too much when it is instantiated. +fn ensure_created_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { + if observed > limit { + return Err(policy_rejection(format!( + "note codec component creates {observed} {kind} when it is instantiated; the limit is \ + {limit}" + ))); + } + Ok(()) +} + +/// Reports a core function type that is over its parameter or result cap. +fn ensure_signature_cap(kind: &str, observed: usize, limit: usize) -> Result<()> { + if observed > limit { + return Err(policy_rejection(format!( + "note codec component has a function type with {observed} {kind}; the limit is {limit}" + ))); + } + Ok(()) +} + +/// Reports a component tree that is over the cap for one kind of component-level item. +fn ensure_component_item_cap(kind: &str, observed: usize) -> Result<()> { + if observed > MAX_COMPONENT_SECTION_ITEMS { + return Err(policy_rejection(format!( + "note codec component has {observed} component {kind}; the limit is \ + {MAX_COMPONENT_SECTION_ITEMS}" + ))); + } + Ok(()) +} + +/// Reports a code section built from many tiny functions. +fn ensure_average_function_size(count: u32, size: u32) -> Result<()> { + if size < MIN_METERED_CODE_SECTION_BYTES || count == 0 { + return Ok(()); + } + let average = size / count; + if average < MIN_AVERAGE_FUNCTION_BYTES { + return Err(policy_rejection(format!( + "note codec component has a core module with {count} functions in {size} bytes of \ + code, an average of {average} bytes; the limit is {MIN_AVERAGE_FUNCTION_BYTES} bytes \ + per function" + ))); + } + Ok(()) +} + +/// Reports bytes that do not parse as a component. +fn malformed(error: wasmparser::BinaryReaderError) -> Error { + policy_rejection(format!("note codec component is malformed: {error}")) +} + +/// Reports a component that does not validate under [`NOTE_CODEC_WASM_FEATURES`]. +fn rejected_feature(error: wasmparser::BinaryReaderError) -> Error { + policy_rejection(format!( + "note codec component uses a Wasm feature the policy rejects: {error}" + )) +} + +/// Creates an error for a component the structural load policy does not admit. +/// +/// Every rejection carries one class, so a consumer reports a codec the policy refused the same +/// way it reports a codec that ran past a host limit. +fn policy_rejection(message: impl Into) -> Error { + Error::codec(CodecFailure::LimitExceeded, message) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Wraps core module text in a component and assembles it. + fn component(core_module: &str) -> Vec { + wat::parse_str(format!("(component (core module {core_module}))")).unwrap() + } + + /// Validates one component with a byte budget that never trips. + fn validate(component: &[u8]) -> Result<()> { + validate_note_codec_component(component, usize::MAX) + } + + #[test] + fn minimal_component_passes() { + validate(&component("(func)")).unwrap(); + } + + #[test] + fn the_wasm_feature_policy_is_exact() { + // Destructuring is exhaustive on purpose: a wasmparser upgrade that adds a proposal + // fails to compile here, so the policy cannot widen without a decision. + let wasmparser::WasmFeaturesInflated { + mutable_global, + saturating_float_to_int, + sign_extension, + reference_types, + multi_value, + bulk_memory, + simd, + relaxed_simd, + threads, + shared_everything_threads, + tail_call, + floats, + multi_memory, + exceptions, + memory64, + extended_const, + component_model, + function_references, + memory_control, + gc, + custom_page_sizes, + legacy_exceptions, + gc_types, + stack_switching, + wide_arithmetic, + cm_values, + cm_nested_names, + cm_async, + cm_async_stackful, + cm_more_async_builtins, + cm_threading, + cm_error_context, + cm_fixed_length_lists, + cm_gc, + call_indirect_overlong, + bulk_memory_opt, + custom_descriptors, + compact_imports, + cm_map, + cm64, + } = NOTE_CODEC_WASM_FEATURES.inflate(); + + for (name, required) in [ + ("component model", component_model), + ("floats", floats), + ("mutable globals", mutable_global), + ("saturating float to int", saturating_float_to_int), + ("sign extension", sign_extension), + ("reference types", reference_types), + ("call-indirect overlong", call_indirect_overlong), + ("multi value", multi_value), + ("bulk memory", bulk_memory), + ("bulk memory opt", bulk_memory_opt), + ] { + assert!(required, "the policy must accept {name}"); + } + + for (name, rejected) in [ + ("simd", simd), + ("relaxed simd", relaxed_simd), + ("threads", threads), + ("shared everything threads", shared_everything_threads), + ("tail call", tail_call), + ("multi memory", multi_memory), + ("exceptions", exceptions), + ("legacy exceptions", legacy_exceptions), + ("memory64", memory64), + ("extended const", extended_const), + ("function references", function_references), + ("memory control", memory_control), + ("gc", gc), + ("gc types", gc_types), + ("custom page sizes", custom_page_sizes), + ("stack switching", stack_switching), + ("wide arithmetic", wide_arithmetic), + ("component model values", cm_values), + ("component model nested names", cm_nested_names), + ("component model async", cm_async), + ("component model stackful async", cm_async_stackful), + ("component model async builtins", cm_more_async_builtins), + ("component model threading", cm_threading), + ("component model error context", cm_error_context), + ("component model fixed length lists", cm_fixed_length_lists), + ("component model gc", cm_gc), + ("component model maps", cm_map), + ("component model 64-bit contexts", cm64), + ("custom descriptors", custom_descriptors), + ("compact imports", compact_imports), + ] { + assert!(!rejected, "the policy must reject {name}"); + } + } + + #[test] + fn oversized_components_are_rejected_before_parsing() { + let error = validate_note_codec_component(&[0; 8], 4).unwrap_err().to_string(); + + assert!(error.contains("is 8 bytes"), "unexpected error: {error}"); + assert!(error.contains("the pre-compilation limit is 4"), "unexpected error: {error}"); + } + + #[test] + fn too_many_globals_are_rejected() { + let globals = "(global i32 (i32.const 0))".repeat(MAX_MODULE_GLOBALS + 1); + let error = validate(&component(&globals)).unwrap_err().to_string(); + + assert!(error.contains("1001 globals"), "unexpected error: {error}"); + assert!(error.contains("the limit is 1000"), "unexpected error: {error}"); + } + + #[test] + fn oversized_function_signatures_are_rejected() { + let params = "i32 ".repeat(MAX_FUNCTION_PARAMS + 1); + let module = format!("(type (func (param {params})))"); + let error = validate(&component(&module)).unwrap_err().to_string(); + + assert!(error.contains("33 parameters"), "unexpected error: {error}"); + assert!(error.contains("the limit is 32"), "unexpected error: {error}"); + } + + #[test] + fn many_tiny_functions_are_rejected() { + let error = validate(&component(&"(func)".repeat(600))).unwrap_err().to_string(); + + assert!(error.contains("600 functions"), "unexpected error: {error}"); + assert!(error.contains("bytes per function"), "unexpected error: {error}"); + } + + #[test] + fn malformed_bytes_are_rejected() { + let error = validate(b"not a component").unwrap_err().to_string(); + + assert!(error.contains("note codec component is malformed"), "unexpected error: {error}"); + } + + #[test] + fn what_a_nested_component_creates_is_counted_once_per_instantiation() { + // One instantiation of the nested component creates one memory, so two instantiations + // reach the memory budget. A nested component that is never instantiated creates + // nothing. + let text = "(component + (component $inner + (core module $m (memory 1)) + (core instance (instantiate $m))) + (instance (instantiate $inner)) + (instance (instantiate $inner)))"; + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!( + error.contains("creates 2 linear memories when it is instantiated"), + "unexpected error: {error}" + ); + assert!( + error.contains(&format!("the limit is {MAX_INSTANTIATED_MEMORIES}")), + "unexpected error: {error}" + ); + } + + #[test] + fn a_component_that_is_never_instantiated_creates_nothing() { + let text = "(component + (component $unused + (core module $m (memory 1)) + (core instance (instantiate $m))) + (core module $m (memory 1)) + (core instance (instantiate $m)))"; + + validate(&wat::parse_str(text).unwrap()).unwrap(); + } + + #[test] + fn too_many_component_instances_are_rejected() { + let instantiate = "(instance (instantiate $inner))".repeat(MAX_COMPONENT_INSTANCES + 1); + let text = format!("(component (component $inner) {instantiate})"); + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!( + error.contains(&format!("creates {} component instances", MAX_COMPONENT_INSTANCES + 1)), + "unexpected error: {error}" + ); + } + + #[test] + fn instantiated_memories_are_counted_across_core_modules() { + let text = "(component + (core module $a (memory 1)) + (core module $b (memory 1)) + (core instance (instantiate $a)) + (core instance (instantiate $b)))"; + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!( + error.contains("creates 2 linear memories when it is instantiated"), + "unexpected error: {error}" + ); + assert!( + error.contains(&format!("the limit is {MAX_INSTANTIATED_MEMORIES}")), + "unexpected error: {error}" + ); + } + + #[test] + fn one_module_instantiated_twice_is_counted_twice() { + // The budgets count what instantiation creates, not what the tree declares, so one + // memory-defining module reaches the memory budget when it is instantiated twice. + let text = r#"(component + (core module $m (memory 1) (func (export "f"))) + (core instance $a (instantiate $m)) + (core instance $b (instantiate $m)))"#; + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!( + error.contains("creates 2 linear memories when it is instantiated"), + "unexpected error: {error}" + ); + } + + #[test] + fn a_core_module_the_walk_cannot_read_is_not_instantiable() { + let text = r#"(component + (import "m" (core module $m)) + (core instance (instantiate $m)))"#; + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!(error.contains("which the policy cannot read"), "unexpected error: {error}"); + } + + #[test] + fn component_items_of_one_kind_are_capped_across_sections() { + // Component sections repeat, so the cap counts one kind over the whole tree. A core + // module between the two type sections keeps them apart in the encoding. + let types = |count: usize| "(type u8)".repeat(count); + let text = format!( + "(component {first} (core module) {second})", + first = types(MAX_COMPONENT_SECTION_ITEMS), + second = types(1), + ); + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!( + error.contains(&format!("{} component types", MAX_COMPONENT_SECTION_ITEMS + 1)), + "unexpected error: {error}" + ); + assert!( + error.contains(&format!("the limit is {MAX_COMPONENT_SECTION_ITEMS}")), + "unexpected error: {error}" + ); + } + + #[test] + fn too_many_module_types_are_rejected() { + let types = "(type (func (param i32)))".repeat(MAX_MODULE_TYPES + 1); + let error = validate(&component(&types)).unwrap_err().to_string(); + + assert!( + error.contains(&format!("{} types", MAX_MODULE_TYPES + 1)), + "unexpected error: {error}" + ); + } + + #[test] + fn every_structural_rejection_carries_a_class() { + let error = validate(b"not a component").unwrap_err(); + + assert_eq!(error.codec_failure(), Some(crate::CodecFailure::LimitExceeded)); + } + + #[test] + fn component_start_functions_are_rejected() { + let text = r#"(component + (core module $m (func (export "f"))) + (core instance $i (instantiate $m)) + (func $f (canon lift (core func $i "f"))) + (start $f))"#; + let error = validate(&wat::parse_str(text).unwrap()).unwrap_err().to_string(); + + assert!(error.contains("declares a start function"), "unexpected error: {error}"); + } + + #[cfg(feature = "codec-component")] + #[test] + fn fixture_component_passes() { + if !midenc_integration_test_support::wasm_target_is_installed() { + eprintln!("skipping the structural limit fixture test: wasm32-wasip2 is not installed"); + return; + } + + validate(&crate::codec_component::tests::build_fixture_component()).unwrap(); + } +} diff --git a/sdk/note-schema/src/error.rs b/sdk/note-schema/src/error.rs new file mode 100644 index 0000000000..d134986fdb --- /dev/null +++ b/sdk/note-schema/src/error.rs @@ -0,0 +1,76 @@ +//! Error types for note storage schemas. + +use core::fmt; + +/// Why a bundled codec did not return a value. +/// +/// The class covers the whole life of a codec call: the structural load policy, the compilation +/// of the component, the instantiation that precedes the call, the call itself, and the host +/// caps applied to what the call returned. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CodecFailure { + /// The call used its whole fuel budget. + OutOfFuel, + /// The structural load policy rejected the component, or a host limit was exceeded in the + /// guest or in the returned value. + LimitExceeded, + /// The component trapped, or the engine rejected the component or the call. + Trapped, + /// The codec returned its own rejection message. + Rejected, +} + +/// An error reported while reading, encoding, or decoding a note storage schema. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Error { + message: String, + codec_failure: Option, +} + +impl Error { + /// Creates an error with an actionable message. + pub fn new(message: impl Into) -> Self { + Self { + message: message.into(), + codec_failure: None, + } + } + + /// Creates an error that reports how a bundled codec failed. + /// + /// The structural load policy and the bundled codec adapter report a failure class. + pub(crate) fn codec(kind: CodecFailure, message: impl Into) -> Self { + Self { + message: message.into(), + codec_failure: Some(kind), + } + } + + /// Returns the failure class of a bundled codec failure. + /// + /// The structural load policy classifies every rejection it reports, and the bundled codec + /// adapter classifies every compilation, instantiation, call, and host cap failure. Errors + /// from other sources, such as a schema that does not parse, return `None`. + pub fn codec_failure(&self) -> Option { + self.codec_failure + } + + /// Adds context before the current error message. + pub(crate) fn context(self, context: impl fmt::Display) -> Self { + Self { + message: format!("{context}: {}", self.message), + codec_failure: self.codec_failure, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for Error {} + +/// A result returned by note storage schema operations. +pub type Result = core::result::Result; diff --git a/sdk/note-schema/src/lib.rs b/sdk/note-schema/src/lib.rs new file mode 100644 index 0000000000..39ad7efa0a --- /dev/null +++ b/sdk/note-schema/src/lib.rs @@ -0,0 +1,51 @@ +//! Host-side access to note storage schemas embedded in Miden packages. +//! +//! # Felt layout +//! +//! The layout is structural over the resolved WIT type tree. The record +//! `miden:base/core-types@1.0.0.felt` is one felt. A `u64` uses two felts in low-then-high +//! `u32` limb order. A `u32`, `u8`, or `bool` uses one range-checked felt. An `option` uses one +//! tag felt followed by its payload when present. A variant uses one declaration-ordinal tag felt +//! followed by the selected case payload. Record fields concatenate in declaration order. +//! +//! Encoding and decoding use [`miden_field_repr::FeltReader`] and +//! [`miden_field_repr::FeltWriter`]. Codecs only parse, display, and validate values. They do not +//! change this layout. + +#![deny(missing_docs)] + +mod artifact; +mod builder; +mod codec; +#[cfg(feature = "codec-component")] +mod codec_component; +mod codec_structure; +mod error; +mod schema; +mod section; +mod value; + +#[cfg(test)] +mod tests; + +pub use artifact::{NotePackageArtifact, NotePackageResolver}; +pub use builder::NoteStorageBuilder; +pub use codec::{ + ACCOUNT_ID_FQN, ASSET_AMOUNT_FQN, CodecRegistry, ConsumerTypeCodec, FELT_FQN, StandardLeaf, + WORD_FQN, +}; +#[cfg(feature = "codec-component")] +pub use codec_component::CodecLimits; +pub use codec_structure::{ + NOTE_CODEC_WASM_FEATURES, validate_note_codec_component, validate_note_codec_structure, +}; +pub use error::{CodecFailure, Error, Result}; +pub use miden_field::Felt; +pub use miden_protocol::note::NoteStorage; +pub use schema::{ + FeltLayout, MAX_NOTE_CODEC_COMPONENT_BYTES, MAX_NOTE_STORAGE_SCHEMA_BYTES, + MAX_NOTE_STORAGE_SCHEMA_DEPTH, MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_NODES, + MAX_NOTE_STORAGE_SCHEMA_TYPES, NOTE_CODEC_GUEST_RUSTFLAGS, NoteStorageSchema, PrimitiveType, + SchemaCase, SchemaField, SchemaType, SchemaTypeKind, +}; +pub use value::{DecodedValue, DecodedValueKind}; diff --git a/sdk/note-schema/src/schema.rs b/sdk/note-schema/src/schema.rs new file mode 100644 index 0000000000..6f25057625 --- /dev/null +++ b/sdk/note-schema/src/schema.rs @@ -0,0 +1,979 @@ +//! Resolved note storage schema model. + +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +use miden_mast_package::Package; +use miden_protocol::MAX_NOTE_STORAGE_ITEMS; +use midenc_frontend_wasm_metadata::{ + PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, package_note_storage_schema_section_id, + trim_trailing_nuls, +}; +use wit_parser::{Resolve, Type, TypeDefKind, TypeId, TypeOwner}; + +use crate::{ + CodecRegistry, DecodedValue, Error, NoteStorage, NoteStorageBuilder, Result, StandardLeaf, + codec::FELT_FQN, +}; + +/// Maximum bytes accepted in an embedded note storage schema section, including alignment padding. +/// +/// The budget allows 64 bytes of schema description per protocol note-storage item. It is derived +/// from [`MAX_NOTE_STORAGE_ITEMS`] so schema parsing remains bounded with the protocol surface. +pub const MAX_NOTE_STORAGE_SCHEMA_BYTES: usize = MAX_NOTE_STORAGE_ITEMS * 64; + +/// Maximum number of resolved WIT type definitions in a note storage schema. +pub const MAX_NOTE_STORAGE_SCHEMA_TYPES: usize = MAX_NOTE_STORAGE_ITEMS; + +/// Maximum structural nesting depth accepted while resolving a note storage schema. +/// +/// Recursion is capped at one eighth of the protocol note-storage item limit, which leaves ample +/// room for legitimate models without allowing an attacker-controlled parser stack to grow to the +/// full storage width. +pub const MAX_NOTE_STORAGE_SCHEMA_DEPTH: usize = MAX_NOTE_STORAGE_ITEMS / 8; + +/// Maximum number of felts in the root note storage layout. +pub const MAX_NOTE_STORAGE_SCHEMA_FELTS: usize = MAX_NOTE_STORAGE_ITEMS; + +/// Maximum number of nodes in the expanded note storage schema tree. +/// +/// The resolved model is a DAG, but every structural walk over it expands that DAG into a tree: +/// decoding, builder validation, and Rust code generation all visit a shared type once per +/// reference. A schema of zero-felt records that names one type per level stays below the byte, +/// type, depth, and felt limits while the expanded tree doubles at each level, so this budget +/// bounds the expanded tree directly. It allows four expanded nodes per protocol note-storage +/// item, which is far above any practical model. +pub const MAX_NOTE_STORAGE_SCHEMA_NODES: usize = MAX_NOTE_STORAGE_ITEMS * 4; + +/// Default maximum bytes accepted for one note codec component before Wasmtime compilation. +/// +/// This is the producer's cap and the default of `CodecLimits::max_component_bytes`, the +/// consumer-side policy struct behind the `codec-component` feature, so a package that builds is +/// a package that consumers accept. A host may tighten its own consumer cap. +pub const MAX_NOTE_CODEC_COMPONENT_BYTES: usize = 4 * 1024 * 1024; + +/// Rustflags the nested codec build pins, so a codec crate's own cargo config cannot enable +/// `simd128` in the guest. Mirrors the VM event-handler plugin. +/// +/// The pin covers one target feature only. The full policy is +/// [`NOTE_CODEC_WASM_FEATURES`](crate::NOTE_CODEC_WASM_FEATURES), which the producer enforces at +/// build time and every consumer enforces at load time. +pub const NOTE_CODEC_GUEST_RUSTFLAGS: &str = "-C target-feature=-simd128"; + +const _: () = assert!(MAX_NOTE_STORAGE_SCHEMA_DEPTH > 0); + +/// The minimum and maximum felt count for a schema type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FeltLayout { + minimum: usize, + maximum: usize, +} + +impl FeltLayout { + /// Returns the minimum number of felts accepted by this layout. + pub const fn minimum(self) -> usize { + self.minimum + } + + /// Returns the maximum number of felts accepted by this layout. + pub const fn maximum(self) -> usize { + self.maximum + } + + /// Returns the fixed width, or `None` for a variable-width layout. + pub const fn fixed_width(self) -> Option { + if self.minimum == self.maximum { + Some(self.minimum) + } else { + None + } + } + + /// Creates a fixed-width layout. + const fn fixed(width: usize) -> Self { + Self { + minimum: width, + maximum: width, + } + } + + /// Adds two layouts in declaration order. + fn concatenate(self, other: Self) -> Result { + let minimum = self + .minimum + .checked_add(other.minimum) + .ok_or_else(|| Error::new("note storage layout minimum width is too large"))?; + let maximum = self + .maximum + .checked_add(other.maximum) + .ok_or_else(|| Error::new("note storage layout maximum width is too large"))?; + Self::bounded(minimum, maximum) + } + + /// Creates a layout within the protocol note-storage width. + /// + /// Every composed layout passes through this function, so no resolved type, and no resolved + /// root, is wider than the protocol allows. + fn bounded(minimum: usize, maximum: usize) -> Result { + if maximum > MAX_NOTE_STORAGE_SCHEMA_FELTS { + return Err(Error::new(format!( + "note storage schema layout has maximum width {maximum} felts; the protocol limit \ + is {MAX_NOTE_STORAGE_SCHEMA_FELTS}" + ))); + } + Ok(Self { minimum, maximum }) + } +} + +/// A supported primitive WIT type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PrimitiveType { + /// An unsigned 64-bit integer stored as low and high `u32` limbs. + U64, + /// An unsigned 32-bit integer stored in one felt. + U32, + /// An unsigned 8-bit integer stored in one felt. + U8, + /// A boolean stored as zero or one. + Bool, +} + +/// The structural kind of a resolved schema type. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum SchemaTypeKind { + /// The one-felt `miden:base/core-types.felt` bedrock type. + Felt, + /// A supported WIT primitive. + Primitive(PrimitiveType), + /// A record with fields in declaration order. + Record(Vec), + /// An optional payload stored after a tag felt. + Option(Arc), + /// A variant with declaration-ordinal cases. + Variant(Vec), +} + +/// A resolved WIT type used by note storage. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SchemaType { + name: Option, + fqn: Option, + docs: Option, + kind: SchemaTypeKind, + layout: FeltLayout, +} + +impl SchemaType { + /// Returns the WIT type name when this is a named type. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Returns the canonical fully-qualified WIT type name. + pub fn fqn(&self) -> Option<&str> { + self.fqn.as_deref() + } + + /// Returns the resolved WIT documentation. + pub fn docs(&self) -> Option<&str> { + self.docs.as_deref() + } + + /// Returns the structural type kind. + pub const fn kind(&self) -> &SchemaTypeKind { + &self.kind + } + + /// Returns the felt layout for this type. + pub const fn layout(&self) -> FeltLayout { + self.layout + } + + /// Classifies this type as a standard protocol leaf. + pub fn standard_leaf(&self) -> Option { + self.fqn.as_deref().and_then(StandardLeaf::from_fqn) + } +} + +/// A named record field in declaration order. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SchemaField { + name: String, + docs: Option, + ty: Arc, +} + +impl SchemaField { + /// Returns the field's kebab-case WIT name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the field-level WIT documentation. + pub fn docs(&self) -> Option<&str> { + self.docs.as_deref() + } + + /// Returns the field type. + pub fn ty(&self) -> &SchemaType { + self.ty.as_ref() + } +} + +/// A WIT variant case in declaration order. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SchemaCase { + name: String, + docs: Option, + payload: Option>, +} + +impl SchemaCase { + /// Returns the case's kebab-case WIT name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the case documentation. + pub fn docs(&self) -> Option<&str> { + self.docs.as_deref() + } + + /// Returns the optional case payload. + pub fn payload(&self) -> Option<&SchemaType> { + self.payload.as_deref() + } +} + +/// A resolved note storage schema with the standard codec registry. +#[derive(Clone)] +pub struct NoteStorageSchema { + wit_text: String, + root: Arc, + codecs: CodecRegistry, +} + +impl NoteStorageSchema { + /// Reads and resolves the note storage schema section from a Miden package. + pub fn from_package(package: &Package) -> Result { + let bytes = crate::section::unique_package_section( + package, + package_note_storage_schema_section_id(), + PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, + )?; + ensure_schema_byte_limit(bytes.len())?; + let text = core::str::from_utf8(trim_trailing_nuls(bytes)).map_err(|err| { + Error::new(format!("note storage schema section is not valid UTF-8: {err}")) + })?; + Self::from_wit_text(text) + } + + /// Resolves a note storage schema from a WIT document. + pub fn from_wit_text(wit_text: &str) -> Result { + ensure_schema_byte_limit(wit_text.len())?; + let wit_text = wit_text.trim_end_matches('\0'); + let mut resolve = Resolve::default(); + let package_id = resolve.push_str("note-storage-schema.wit", wit_text).map_err(|err| { + Error::new(format!("failed to resolve note storage schema WIT: {err:#}")) + })?; + if resolve.types.len() > MAX_NOTE_STORAGE_SCHEMA_TYPES { + return Err(Error::new(format!( + "note storage schema defines {} WIT types; the limit is \ + {MAX_NOTE_STORAGE_SCHEMA_TYPES}", + resolve.types.len() + ))); + } + let package = &resolve.packages[package_id]; + let interface_id = package.interfaces.get("note-storage").copied().ok_or_else(|| { + Error::new(format!( + "schema package `{}` does not define the `note-storage` interface", + package.name + )) + })?; + let interface = &resolve.interfaces[interface_id]; + let storage_id = interface.types.get("storage").copied().ok_or_else(|| { + Error::new("the `note-storage` interface does not define the `storage` type alias") + })?; + validate_resolved_core_types(&resolve)?; + let root = ModelBuilder::new(&resolve).build(Type::Id(storage_id))?; + if !matches!(root.kind(), SchemaTypeKind::Record(_)) { + return Err(Error::new(format!( + "the `note-storage.storage` alias must resolve to a record, found {}", + kind_name(root.kind()) + ))); + } + let schema = Self { + wit_text: wit_text.to_owned(), + root, + codecs: CodecRegistry::default(), + }; + schema.validate_native_leaf_shapes()?; + Ok(schema) + } + + /// Returns the unpadded WIT document. + pub fn wit_text(&self) -> &str { + &self.wit_text + } + + /// Returns the root storage record. + pub fn root(&self) -> &SchemaType { + self.root.as_ref() + } + + /// Verifies native host mappings against the pinned standard type shapes. + pub fn validate_native_leaf_shapes(&self) -> Result<()> { + validate_model_type_shapes(&self.root, &mut HashSet::new()) + } + + /// Returns all custom named types reachable from the storage root. + #[cfg(feature = "codec-component")] + pub(crate) fn custom_type_fqns(&self) -> HashSet { + let mut fqns = HashSet::new(); + collect_custom_type_fqns(&self.root, &mut HashSet::new(), &mut fqns); + fqns + } + + /// Returns the root felt layout. + pub fn layout(&self) -> FeltLayout { + self.root.layout + } + + /// Returns the schema's standard codec registry. + pub const fn codecs(&self) -> &CodecRegistry { + &self.codecs + } + + /// Replaces the codec registry used by `builder` and `decode`. + pub fn with_codec_registry(mut self, codecs: CodecRegistry) -> Self { + self.codecs = codecs; + self + } + + /// Creates a string-value builder with the schema's codec registry. + pub fn builder(&self) -> NoteStorageBuilder<'_> { + self.builder_with_registry(&self.codecs) + } + + /// Creates a string-value builder with a caller-provided codec registry. + pub fn builder_with_registry<'a>( + &'a self, + registry: &'a CodecRegistry, + ) -> NoteStorageBuilder<'a> { + NoteStorageBuilder::new(self, registry) + } + + /// Decodes note storage with the schema's codec registry. + pub fn decode(&self, storage: &NoteStorage) -> Result { + self.decode_with_registry(storage, &self.codecs) + } + + /// Decodes note storage with a caller-provided codec registry. + pub fn decode_with_registry( + &self, + storage: &NoteStorage, + registry: &CodecRegistry, + ) -> Result { + crate::value::decode(&self.root, storage, registry) + } +} + +/// Enforces the parser input budget before WIT resolution or component work begins. +fn ensure_schema_byte_limit(byte_len: usize) -> Result<()> { + if byte_len > MAX_NOTE_STORAGE_SCHEMA_BYTES { + return Err(Error::new(format!( + "note storage schema section is {byte_len} bytes; the limit is \ + {MAX_NOTE_STORAGE_SCHEMA_BYTES}" + ))); + } + Ok(()) +} + +/// Enforces the expanded-tree budget on one resolved schema type. +/// +/// The builder memoizes shared types, so resolution stays linear. Every consumer of the model +/// walks it as a tree, so the budget is applied to the expanded node count of each type as it is +/// resolved. The check therefore protects decoding, builder validation, and code generation. +fn ensure_expanded_node_limit(ty: &SchemaType, expanded_nodes: usize) -> Result<()> { + let limit = MAX_NOTE_STORAGE_SCHEMA_NODES; + if expanded_nodes > limit { + let name = ty.fqn().or_else(|| ty.name()).unwrap_or(""); + return Err(Error::new(format!( + "note storage type `{name}` expands to {expanded_nodes} nodes; the limit is {limit}" + ))); + } + Ok(()) +} + +/// Verifies the raw embedded core-types definitions before the model applies native mappings. +fn validate_resolved_core_types(resolve: &Resolve) -> Result<()> { + let Some((_, package_id)) = resolve.package_names.iter().find(|(name, _)| { + name.namespace == "miden" + && name.name == "base" + && name.version.as_ref().is_some_and(|version| version.to_string() == "1.0.0") + }) else { + return Ok(()); + }; + let package = &resolve.packages[*package_id]; + let Some(interface_id) = package.interfaces.get("core-types").copied() else { + return Ok(()); + }; + let interface = &resolve.interfaces[interface_id]; + + for leaf in StandardLeaf::ALL { + let name = standard_leaf_name(leaf); + let fields = standard_leaf_fields(leaf); + let Some(type_id) = interface.types.get(name).copied() else { + continue; + }; + let type_id = follow_resolved_aliases(resolve, type_id)?; + let TypeDefKind::Record(record) = &resolve.types[type_id].kind else { + return Err(core_shape_error(name, fields)); + }; + if record.fields.len() != fields.len() + || record + .fields + .iter() + .zip(fields) + .any(|(field, expected)| field.name != *expected) + { + return Err(core_shape_error(name, fields)); + } + if leaf == StandardLeaf::Felt { + // Miden's canonical felt uses `f32` as its WIT-level placeholder representation. + if !resolves_to_primitive(resolve, record.fields[0].ty, Type::F32)? { + return Err(core_shape_error(name, fields)); + } + } else { + for field in &record.fields { + if !resolves_to_fqn(resolve, field.ty, crate::FELT_FQN)? { + return Err(core_shape_error(name, fields)); + } + } + } + } + Ok(()) +} + +/// Follows raw WIT aliases to their structural definition. +fn follow_resolved_aliases(resolve: &Resolve, mut id: TypeId) -> Result { + let mut visited = HashSet::new(); + loop { + if !visited.insert(id) { + return Err(Error::new("cyclic WIT type aliases are not supported")); + } + match resolve.types[id].kind { + TypeDefKind::Type(Type::Id(next)) => id = next, + _ => return Ok(id), + } + } +} + +/// Returns true when a raw WIT type resolves to one primitive. +fn resolves_to_primitive(resolve: &Resolve, mut ty: Type, expected: Type) -> Result { + let mut visited = HashSet::new(); + loop { + match ty { + Type::Id(id) => { + if !visited.insert(id) { + return Err(Error::new("cyclic WIT type aliases are not supported")); + } + let TypeDefKind::Type(next) = resolve.types[id].kind else { + return Ok(false); + }; + ty = next; + } + primitive => return Ok(primitive == expected), + } + } +} + +/// Returns true when a raw WIT type resolves to one canonical FQN. +fn resolves_to_fqn(resolve: &Resolve, ty: Type, expected: &str) -> Result { + let Type::Id(id) = ty else { + return Ok(false); + }; + let id = follow_resolved_aliases(resolve, id)?; + Ok(ModelBuilder::new(resolve).type_fqn(id)?.as_deref() == Some(expected)) +} + +/// Verifies mapped type shapes in the owned schema model. +fn validate_model_type_shapes(ty: &SchemaType, seen: &mut HashSet) -> Result<()> { + if let Some(fqn) = ty.fqn() + && !seen.insert(fqn.to_owned()) + { + return Ok(()); + } + + match ty.standard_leaf() { + Some(StandardLeaf::Felt) if !matches!(ty.kind(), SchemaTypeKind::Felt) => { + return Err(core_shape_error( + standard_leaf_name(StandardLeaf::Felt), + standard_leaf_fields(StandardLeaf::Felt), + )); + } + Some(StandardLeaf::Felt) => {} + Some(leaf) => { + validate_model_record( + ty, + standard_leaf_name(leaf), + standard_leaf_fields(leaf), + crate::FELT_FQN, + )?; + } + None => {} + } + + match ty.kind() { + SchemaTypeKind::Record(fields) => { + for field in fields { + validate_model_type_shapes(field.ty(), seen)?; + } + } + SchemaTypeKind::Option(payload) => validate_model_type_shapes(payload, seen)?, + SchemaTypeKind::Variant(cases) => { + for payload in cases.iter().filter_map(SchemaCase::payload) { + validate_model_type_shapes(payload, seen)?; + } + } + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {} + } + Ok(()) +} + +/// Returns the terminal WIT name from a canonical standard-leaf FQN. +fn standard_leaf_name(leaf: StandardLeaf) -> &'static str { + leaf.fqn() + .rsplit_once('.') + .expect("standard-leaf FQNs always contain an interface separator") + .1 +} + +/// Returns the canonical record field order for a standard leaf. +fn standard_leaf_fields(leaf: StandardLeaf) -> &'static [&'static str] { + match leaf { + StandardLeaf::Felt | StandardLeaf::AssetAmount => &["inner"], + StandardLeaf::Word => &["a", "b", "c", "d"], + StandardLeaf::AccountId => &["prefix", "suffix"], + } +} + +/// Verifies one mapped record in the owned schema model. +fn validate_model_record( + ty: &SchemaType, + name: &str, + expected_fields: &[&str], + expected_field_fqn: &str, +) -> Result<()> { + let SchemaTypeKind::Record(fields) = ty.kind() else { + return Err(core_shape_error(name, expected_fields)); + }; + if fields.len() != expected_fields.len() + || fields.iter().zip(expected_fields).any(|(field, expected)| { + field.name() != *expected || field.ty().fqn() != Some(expected_field_fqn) + }) + { + return Err(core_shape_error(name, expected_fields)); + } + Ok(()) +} + +/// Creates the canonical core-type shape diagnostic. +fn core_shape_error(name: &str, fields: &[&str]) -> Error { + let field_shape = if name == "felt" { + "inner: f32".to_owned() + } else { + fields + .iter() + .map(|field| format!("{field}: felt")) + .collect::>() + .join(", ") + }; + Error::new(format!( + "embedded WIT type `miden:base/core-types@1.0.0.{name}` does not match the pinned \ + canonical shape `record {name} {{ {field_shape} }}`" + )) +} + +/// Collects schema-owned types and excludes only the canonical standard leaves. +#[cfg(feature = "codec-component")] +fn collect_custom_type_fqns( + ty: &SchemaType, + seen: &mut HashSet<*const SchemaType>, + fqns: &mut HashSet, +) { + // Pointer identity is sufficient because ModelBuilder memoizes exactly one Arc per TypeId. + if !seen.insert(core::ptr::from_ref(ty)) { + return; + } + if let Some(fqn) = ty.fqn() + && ty.standard_leaf().is_none() + { + fqns.insert(fqn.to_owned()); + } + match ty.kind() { + SchemaTypeKind::Record(fields) => { + for field in fields { + collect_custom_type_fqns(field.ty(), seen, fqns); + } + } + SchemaTypeKind::Option(payload) => collect_custom_type_fqns(payload, seen, fqns), + SchemaTypeKind::Variant(cases) => { + for payload in cases.iter().filter_map(SchemaCase::payload) { + collect_custom_type_fqns(payload, seen, fqns); + } + } + SchemaTypeKind::Felt | SchemaTypeKind::Primitive(_) => {} + } +} + +/// One memoized schema node, its maximum depth, and the size of its expanded subtree. +#[derive(Clone)] +struct MemoizedSchemaType { + /// The resolved node. + ty: Arc, + /// Levels of nesting below this node. + /// + /// A memoized node is reused at a deeper position than the one it was resolved at, so the + /// depth limit is checked again on every reuse with this value added to the new depth. + maximum_subtree_depth: usize, + /// Number of nodes a structural walk visits below and including this node. + expanded_nodes: usize, +} + +/// Builds a memoized schema graph from a resolved WIT graph. +struct ModelBuilder<'a> { + /// The resolved WIT document the schema comes from. + resolve: &'a Resolve, + /// The types the walk is inside, which reports a recursive type. + active: HashSet, + /// The nodes already resolved, keyed by WIT type. + memo: HashMap, +} + +impl<'a> ModelBuilder<'a> { + /// Creates a model builder for one schema package. + fn new(resolve: &'a Resolve) -> Self { + Self { + resolve, + active: HashSet::new(), + memo: HashMap::new(), + } + } + + /// Resolves one WIT type. + fn build(mut self, ty: Type) -> Result> { + self.build_type(ty, 0).map(|memoized| memoized.ty) + } + + /// Resolves a primitive or named type. + fn build_type(&mut self, ty: Type, depth: usize) -> Result { + if depth > MAX_NOTE_STORAGE_SCHEMA_DEPTH { + return Err(Error::new(format!( + "note storage schema nesting depth {depth} exceeds the limit of \ + {MAX_NOTE_STORAGE_SCHEMA_DEPTH}" + ))); + } + match ty { + Type::Id(id) => self.build_type_id(id, depth), + Type::U64 => self.primitive(PrimitiveType::U64, None, None, None), + Type::U32 => self.primitive(PrimitiveType::U32, None, None, None), + Type::U8 => self.primitive(PrimitiveType::U8, None, None, None), + Type::Bool => self.primitive(PrimitiveType::Bool, None, None, None), + unsupported => Err(Error::new(format!( + "WIT primitive `{unsupported:?}` is not supported in note storage schemas" + ))), + } + } + + /// Resolves aliases to the type definition that owns the structural type. + fn build_type_id(&mut self, id: TypeId, depth: usize) -> Result { + let id = self.follow_aliases(id)?; + if let Some(memoized) = self.memo.get(&id) { + let maximum_depth = depth.saturating_add(memoized.maximum_subtree_depth); + if maximum_depth > MAX_NOTE_STORAGE_SCHEMA_DEPTH { + return Err(Error::new(format!( + "note storage schema nesting depth {maximum_depth} exceeds the limit of \ + {MAX_NOTE_STORAGE_SCHEMA_DEPTH}" + ))); + } + return Ok(memoized.clone()); + } + if !self.active.insert(id) { + return Err(Error::new( + "recursive WIT types are not supported in note storage schemas", + )); + } + + let definition = self.resolve.types[id].clone(); + let name = definition.name.clone(); + let docs = definition.docs.contents.clone(); + let fqn = self.type_fqn(id)?; + let result = if fqn.as_deref() == Some(FELT_FQN) { + Ok(MemoizedSchemaType { + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Felt, + layout: FeltLayout::fixed(1), + }), + maximum_subtree_depth: 0, + expanded_nodes: 1, + }) + } else { + match definition.kind { + TypeDefKind::Type(ty) => self.build_named_alias(ty, name, fqn, docs), + TypeDefKind::Record(record) => { + let mut fields = Vec::with_capacity(record.fields.len()); + let mut layout = FeltLayout::fixed(0); + let mut maximum_subtree_depth = 0; + let mut expanded_nodes = 1usize; + for field in record.fields { + let memoized = self.build_type(field.ty, depth + 1)?; + maximum_subtree_depth = + maximum_subtree_depth.max(1 + memoized.maximum_subtree_depth); + expanded_nodes = expanded_nodes.saturating_add(memoized.expanded_nodes); + layout = layout.concatenate(memoized.ty.layout)?; + fields.push(SchemaField { + name: field.name, + docs: field.docs.contents, + ty: memoized.ty, + }); + } + Ok(MemoizedSchemaType { + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Record(fields), + layout, + }), + maximum_subtree_depth, + expanded_nodes, + }) + } + TypeDefKind::Option(payload) => { + let payload = self.build_type(payload, depth + 1)?; + let maximum = 1usize + .checked_add(payload.ty.layout.maximum) + .ok_or_else(|| Error::new("option layout maximum width is too large"))?; + let layout = FeltLayout::bounded(1, maximum)?; + Ok(MemoizedSchemaType { + maximum_subtree_depth: 1 + payload.maximum_subtree_depth, + expanded_nodes: payload.expanded_nodes.saturating_add(1), + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Option(payload.ty), + layout, + }), + }) + } + TypeDefKind::Variant(variant) => { + let mut cases = Vec::with_capacity(variant.cases.len()); + let mut maximum_subtree_depth = 0; + let mut expanded_nodes = 1usize; + for case in variant.cases { + let payload = match case.ty { + Some(ty) => { + let memoized = self.build_type(ty, depth + 1)?; + maximum_subtree_depth = + maximum_subtree_depth.max(1 + memoized.maximum_subtree_depth); + expanded_nodes = + expanded_nodes.saturating_add(memoized.expanded_nodes); + Some(memoized.ty) + } + None => None, + }; + cases.push(SchemaCase { + name: case.name, + docs: case.docs.contents, + payload, + }); + } + let layout = variant_layout(&cases)?; + Ok(MemoizedSchemaType { + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Variant(cases), + layout, + }), + maximum_subtree_depth, + expanded_nodes, + }) + } + TypeDefKind::Enum(enum_) => { + let cases = enum_ + .cases + .into_iter() + .map(|case| SchemaCase { + name: case.name, + docs: case.docs.contents, + payload: None, + }) + .collect::>(); + let layout = variant_layout(&cases)?; + Ok(MemoizedSchemaType { + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Variant(cases), + layout, + }), + maximum_subtree_depth: 0, + expanded_nodes: 1, + }) + } + unsupported => Err(Error::new(format!( + "WIT {} `{}` is not supported in note storage schemas", + unsupported.as_str(), + fqn.as_deref().or(name.as_deref()).unwrap_or("") + ))), + } + }; + self.active.remove(&id); + if let Ok(memoized) = &result { + ensure_expanded_node_limit(&memoized.ty, memoized.expanded_nodes)?; + self.memo.insert(id, memoized.clone()); + } + result + } + + /// Resolves a primitive alias while preserving its name, FQN, and documentation. + /// + /// [`Self::build_type_id`] follows ID aliases before dispatching here. + fn build_named_alias( + &mut self, + ty: Type, + name: Option, + fqn: Option, + docs: Option, + ) -> Result { + match ty { + Type::Id(_) => unreachable!("build_type_id must follow ID aliases first"), + Type::U64 => self.primitive(PrimitiveType::U64, name, fqn, docs), + Type::U32 => self.primitive(PrimitiveType::U32, name, fqn, docs), + Type::U8 => self.primitive(PrimitiveType::U8, name, fqn, docs), + Type::Bool => self.primitive(PrimitiveType::Bool, name, fqn, docs), + unsupported => Err(Error::new(format!( + "WIT primitive alias `{unsupported:?}` is not supported in note storage schemas" + ))), + } + } + + /// Creates a supported primitive type. + fn primitive( + &self, + primitive: PrimitiveType, + name: Option, + fqn: Option, + docs: Option, + ) -> Result { + let width = match primitive { + PrimitiveType::U64 => 2, + PrimitiveType::U32 | PrimitiveType::U8 | PrimitiveType::Bool => 1, + }; + Ok(MemoizedSchemaType { + ty: Arc::new(SchemaType { + name, + fqn, + docs, + kind: SchemaTypeKind::Primitive(primitive), + layout: FeltLayout::fixed(width), + }), + maximum_subtree_depth: 0, + expanded_nodes: 1, + }) + } + + /// Follows `type = id` aliases to their defining type. + fn follow_aliases(&self, mut id: TypeId) -> Result { + let mut visited = HashSet::new(); + loop { + if !visited.insert(id) { + return Err(Error::new("cyclic WIT type aliases are not supported")); + } + match self.resolve.types[id].kind { + TypeDefKind::Type(Type::Id(next)) => id = next, + _ => return Ok(id), + } + } + } + + /// Reconstructs the canonical FQN for a named interface type. + fn type_fqn(&self, id: TypeId) -> Result> { + let definition = &self.resolve.types[id]; + let Some(type_name) = definition.name.as_deref() else { + return Ok(None); + }; + let TypeOwner::Interface(interface_id) = definition.owner else { + return Err(Error::new(format!( + "named WIT type `{type_name}` is not owned by an interface" + ))); + }; + let interface = &self.resolve.interfaces[interface_id]; + let interface_name = interface.name.as_deref().ok_or_else(|| { + Error::new(format!("type `{type_name}` belongs to an unnamed interface")) + })?; + let package_id = interface.package.ok_or_else(|| { + Error::new(format!("interface `{interface_name}` does not belong to a package")) + })?; + let package_name = &self.resolve.packages[package_id].name; + let mut fqn = + format!("{}:{}/{}", package_name.namespace, package_name.name, interface_name); + if let Some(version) = &package_name.version { + fqn.push('@'); + fqn.push_str(&version.to_string()); + } + fqn.push('.'); + fqn.push_str(type_name); + Ok(Some(fqn)) + } +} + +/// Returns a variable layout for declaration-ordinal cases. +fn variant_layout(cases: &[SchemaCase]) -> Result { + if cases.is_empty() { + return Err(Error::new("a note storage variant must define at least one case")); + } + let minimum_payload = cases + .iter() + .map(|case| case.payload.as_ref().map_or(0, |ty| ty.layout.minimum)) + .min() + .unwrap_or(0); + let maximum_payload = cases + .iter() + .map(|case| case.payload.as_ref().map_or(0, |ty| ty.layout.maximum)) + .max() + .unwrap_or(0); + let minimum = 1usize + .checked_add(minimum_payload) + .ok_or_else(|| Error::new("variant layout minimum width is too large"))?; + let maximum = 1usize + .checked_add(maximum_payload) + .ok_or_else(|| Error::new("variant layout maximum width is too large"))?; + FeltLayout::bounded(minimum, maximum) +} + +/// Returns a stable name for a model kind. +fn kind_name(kind: &SchemaTypeKind) -> &'static str { + match kind { + SchemaTypeKind::Felt => "felt", + SchemaTypeKind::Primitive(_) => "primitive", + SchemaTypeKind::Record(_) => "record", + SchemaTypeKind::Option(_) => "option", + SchemaTypeKind::Variant(_) => "variant", + } +} + +/// Normalizes one WIT path segment from snake case to kebab case. +pub(crate) fn normalize_name(name: &str) -> String { + name.trim().replace('_', "-") +} diff --git a/sdk/note-schema/src/section.rs b/sdk/note-schema/src/section.rs new file mode 100644 index 0000000000..6d19177ab7 --- /dev/null +++ b/sdk/note-schema/src/section.rs @@ -0,0 +1,21 @@ +//! Package custom-section access. + +use miden_mast_package::{Package, SectionId}; + +use crate::{Error, Result}; + +/// Returns the only package section with `section_name`. +pub(crate) fn unique_package_section<'a>( + package: &'a Package, + section_id: SectionId, + section_name: &str, +) -> Result<&'a [u8]> { + let mut matches = package.sections.iter().filter(|section| section.id == section_id); + let section = matches.next().ok_or_else(|| { + Error::new(format!("package does not contain the `{section_name}` section")) + })?; + if matches.next().is_some() { + return Err(Error::new(format!("package contains more than one `{section_name}` section"))); + } + Ok(section.data.as_ref()) +} diff --git a/sdk/note-schema/src/tests.rs b/sdk/note-schema/src/tests.rs new file mode 100644 index 0000000000..f56bbab12c --- /dev/null +++ b/sdk/note-schema/src/tests.rs @@ -0,0 +1,586 @@ +//! Unit tests for schema interpretation, building, and decoding. + +use miden_field_repr::ToFeltRepr; +use miden_protocol::{account::AccountId, address::NetworkId}; + +use crate::{ + ACCOUNT_ID_FQN, CodecRegistry, DecodedValueKind, Felt, MAX_NOTE_STORAGE_SCHEMA_BYTES, + MAX_NOTE_STORAGE_SCHEMA_DEPTH, MAX_NOTE_STORAGE_SCHEMA_FELTS, MAX_NOTE_STORAGE_SCHEMA_NODES, + MAX_NOTE_STORAGE_SCHEMA_TYPES, NoteStorage, NoteStorageSchema, SchemaTypeKind, +}; + +const LAYOUT_SCHEMA: &str = r#" +package example:layout-schema@1.2.3; + +use miden:base/core-types@1.0.0; + +interface note-storage { + use core-types.{account-id, felt}; + + record nested-values { + /// A wide counter. + wide-count: u64, + small-count: u8, + } + + variant selection { + empty, + count(u32), + } + + record layout-note { + bedrock: felt, + nested: nested-values, + maybe-enabled: option, + selected: selection, + target-account-id: account-id, + } + + type storage = layout-note; +} + +package miden:base@1.0.0 { + interface core-types { + record felt { + inner: f32, + } + + record account-id { + prefix: felt, + suffix: felt, + } + } +} +"#; + +const NESTED_CONSTRUCTOR_SCHEMA: &str = r#" +package example:nested-constructors@1.0.0; + +interface note-storage { + record payload { + value: u64, + } + + variant selection { + empty, + nested(payload), + } + + record constructor-note { + maybe-payload: option, + selected: selection, + } + + type storage = constructor-note; +} +"#; + +/// Returns a valid account ID and its mainnet bech32 form. +fn account_id() -> (AccountId, String) { + let account_id = AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); + let bech32 = account_id.to_bech32(NetworkId::Mainnet); + (account_id, bech32) +} + +#[test] +fn resolves_alias_fqns_docs_and_structural_widths() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + + assert_eq!( + schema.root().fqn(), + Some("example:layout-schema/note-storage@1.2.3.layout-note") + ); + assert_eq!(schema.layout().minimum(), 8); + assert_eq!(schema.layout().maximum(), 10); + assert_eq!(schema.layout().fixed_width(), None); + + let SchemaTypeKind::Record(root_fields) = schema.root().kind() else { + panic!("storage root must be a record"); + }; + let nested = root_fields.iter().find(|field| field.name() == "nested").unwrap(); + let SchemaTypeKind::Record(nested_fields) = nested.ty().kind() else { + panic!("nested must be a record"); + }; + assert_eq!(nested_fields[0].docs(), Some("A wide counter.")); + + let account_id = root_fields.iter().find(|field| field.name() == "target-account-id").unwrap(); + assert_eq!(account_id.ty().fqn(), Some(ACCOUNT_ID_FQN)); + assert_eq!(account_id.ty().layout().fixed_width(), Some(2)); +} + +#[test] +fn wit_reader_reports_missing_schema_surface() { + let error = NoteStorageSchema::from_wit_text( + "package example:missing@1.0.0; interface other { record value {} }", + ) + .err() + .unwrap() + .to_string(); + + assert!(error.contains("does not define the `note-storage` interface")); +} + +#[test] +fn reader_rejects_noncanonical_native_core_type_shapes() { + for (type_name, definitions) in [ + ("felt", "record felt { inner: u32 }"), + ("word", "record felt { inner: f32 } record word { a: felt, b: felt, c: felt }"), + ( + "account-id", + "record felt { inner: f32 } record account-id { suffix: felt, prefix: felt }", + ), + ("asset-amount", "record felt { inner: f32 } record asset-amount { inner: u64 }"), + ] { + let wit = format!( + r#" +package example:bad-core-shape@1.0.0; + +use miden:base/core-types@1.0.0; + +interface note-storage {{ + use core-types.{{{type_name}}}; + record bad-note {{ value: {type_name} }} + type storage = bad-note; +}} + +package miden:base@1.0.0 {{ + interface core-types {{ {definitions} }} +}} +"# + ); + let error = NoteStorageSchema::from_wit_text(&wit).err().unwrap().to_string(); + assert!( + error.contains(&format!("miden:base/core-types@1.0.0.{type_name}")), + "unexpected error for {type_name}: {error}" + ); + assert!(error.contains("pinned canonical shape")); + } +} + +#[test] +fn builder_normalizes_paths_and_uses_declaration_order() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + let (account_id, bech32) = account_id(); + let storage = schema + .builder() + .set("bedrock", "5") + .unwrap() + .set("nested.wide_count", "4294967298") + .unwrap() + .set("nested.small-count", "7") + .unwrap() + .set("maybe_enabled", "some(true)") + .unwrap() + .set("selected", "count(9)") + .unwrap() + .set("target_account_id", &bech32) + .unwrap() + .build() + .unwrap(); + + let expected = NoteStorage::new(vec![ + Felt::from_u32(5), + Felt::from_u32(2), + Felt::from_u32(1), + Felt::from_u32(7), + Felt::ONE, + Felt::ONE, + Felt::ONE, + Felt::from_u32(9), + account_id.prefix().as_felt(), + account_id.suffix(), + ]) + .unwrap(); + assert_eq!(storage, expected); + + let decoded = schema.decode(&storage).unwrap(); + assert_eq!( + decoded.field("nested").unwrap().field("wide_count").unwrap().to_string(), + "4294967298" + ); + assert_eq!(decoded.field("maybe_enabled").unwrap().to_string(), "some(true)"); + assert_eq!(decoded.field("selected").unwrap().to_string(), "count(9)"); + assert_eq!(decoded.field("target_account_id").unwrap().to_string(), bech32); +} + +#[test] +fn decoder_uses_structural_fallback_without_a_codec() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + let (account_id, bech32) = account_id(); + let storage = schema + .builder() + .set("bedrock", "5") + .unwrap() + .set("nested.wide-count", "0") + .unwrap() + .set("nested.small-count", "0") + .unwrap() + .set("maybe-enabled", "none") + .unwrap() + .set("selected", "empty") + .unwrap() + .set("target-account-id", &bech32) + .unwrap() + .build() + .unwrap(); + + let decoded = schema.decode_with_registry(&storage, &CodecRegistry::empty()).unwrap(); + let account = decoded.field("target-account-id").unwrap(); + let DecodedValueKind::Record(fields) = account.kind() else { + panic!("account-id must use its structural record fallback"); + }; + assert_eq!(fields[0].name(), Some("prefix")); + assert_eq!(fields[0].to_string(), account_id.prefix().as_u64().to_string()); + assert_eq!(fields[1].name(), Some("suffix")); + assert_eq!(fields[1].to_string(), account_id.suffix().as_canonical_u64().to_string()); +} + +#[test] +fn builder_validates_codec_records_assembled_from_child_paths() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + let (account_id, _) = account_id(); + + // Child-path values that form no valid account id must fail the codec, not encode raw. + let error = schema + .builder() + .set("bedrock", "5") + .unwrap() + .set("nested.wide_count", "1") + .unwrap() + .set("nested.small-count", "1") + .unwrap() + .set("maybe_enabled", "none") + .unwrap() + .set("selected", "count(1)") + .unwrap() + .set("target_account_id.prefix", "1") + .unwrap() + .set("target_account_id.suffix", "1") + .unwrap() + .build() + .err() + .unwrap() + .to_string(); + assert!(error.contains("validation failed"), "unexpected error: {error}"); + assert!(error.contains("target-account-id"), "the path is not named: {error}"); + + // Valid child-path values pass the codec and encode like the whole-value assignment. + let storage = schema + .builder() + .set("bedrock", "5") + .unwrap() + .set("nested.wide_count", "1") + .unwrap() + .set("nested.small-count", "1") + .unwrap() + .set("maybe_enabled", "none") + .unwrap() + .set("selected", "count(1)") + .unwrap() + .set( + "target_account_id.prefix", + account_id.prefix().as_felt().as_canonical_u64().to_string(), + ) + .unwrap() + .set("target_account_id.suffix", account_id.suffix().as_canonical_u64().to_string()) + .unwrap() + .build() + .unwrap(); + let felts = storage.to_elements(); + assert_eq!(felts[felts.len() - 2], account_id.prefix().as_felt()); + assert_eq!(felts[felts.len() - 1], account_id.suffix()); +} + +#[test] +fn builder_reports_missing_unknown_conflicting_and_range_errors() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + let (_, bech32) = account_id(); + + let missing = schema.builder().build().unwrap_err().to_string(); + assert!(missing.contains("missing note storage value for `bedrock`")); + + let unknown = schema.builder().set("nested.unknown", "1").err().unwrap().to_string(); + assert!(unknown.contains("has no field named `unknown`")); + + let conflict = schema + .builder() + .set("nested", "value") + .unwrap() + .set("nested.wide-count", "1") + .err() + .unwrap() + .to_string(); + assert!(conflict.contains("conflicts")); + + let range = schema + .builder() + .set("bedrock", "0") + .unwrap() + .set("nested.wide-count", "0") + .unwrap() + .set("nested.small-count", "256") + .unwrap() + .set("maybe-enabled", "none") + .unwrap() + .set("selected", "empty") + .unwrap() + .set("target-account-id", &bech32) + .unwrap() + .build() + .unwrap_err() + .to_string(); + assert!(range.contains("u8 value `256` is out of range")); +} + +#[test] +fn builder_rejects_nested_record_constructor_paths() { + let schema = NoteStorageSchema::from_wit_text(NESTED_CONSTRUCTOR_SCHEMA).unwrap(); + + let option_error = + schema.builder().set("maybe_payload", "some(value)").err().unwrap().to_string(); + assert!(option_error.contains("path `maybe-payload`")); + assert!(option_error.contains("option with record payload")); + assert!(option_error.contains("does not support nested record constructors")); + + let variant_error = + schema.builder().set("selected", "nested(value)").err().unwrap().to_string(); + assert!(variant_error.contains("path `selected`")); + assert!(variant_error.contains("variant case `nested` with a record payload")); + assert!(variant_error.contains("does not support nested record constructors")); +} + +#[test] +fn decoder_rejects_invalid_option_and_variant_tags() { + let schema = NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA).unwrap(); + let (account_id, _) = account_id(); + let suffix = [account_id.prefix().as_felt(), account_id.suffix()]; + + let invalid_option = NoteStorage::new( + [ + vec![Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from_u32(2)], + vec![Felt::ZERO], + suffix.to_vec(), + ] + .concat(), + ) + .unwrap(); + assert!( + schema + .decode(&invalid_option) + .unwrap_err() + .to_string() + .contains("invalid option tag") + ); + + let invalid_variant = NoteStorage::new( + [ + vec![Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO], + vec![Felt::from_u32(2)], + suffix.to_vec(), + ] + .concat(), + ) + .unwrap(); + assert!( + schema + .decode(&invalid_variant) + .unwrap_err() + .to_string() + .contains("invalid variant tag 2") + ); +} + +#[test] +fn u64_layout_uses_shared_low_then_high_limb_encoding() { + let value = 0x1234_5678_90ab_cdefu64; + assert_eq!(value.to_felt_repr(), [Felt::from_u32(0x90ab_cdef), Felt::from_u32(0x1234_5678)]); +} + +#[test] +fn repeated_pair_schema_is_memoized_and_fails_fast_at_width_limit() { + let schema = NoteStorageSchema::from_wit_text(&repeated_pair_schema(8)).unwrap(); + let SchemaTypeKind::Record(fields) = schema.root().kind() else { + panic!("the repeated-pair root must be a record"); + }; + assert!( + core::ptr::eq(fields[0].ty(), fields[1].ty()), + "both references to the completed named type must share one memoized node" + ); + + let error = + NoteStorageSchema::from_wit_text(&repeated_pair_schema(MAX_NOTE_STORAGE_SCHEMA_DEPTH / 2)) + .err() + .expect("an over-wide repeated-pair schema must fail") + .to_string(); + assert!(error.contains("maximum width"), "unexpected repeated-pair error: {error}"); + assert!( + error.contains(&MAX_NOTE_STORAGE_SCHEMA_FELTS.to_string()), + "the protocol width must be present in the diagnostic: {error}" + ); +} + +#[test] +fn deep_schema_chain_fails_fast_at_depth_limit() { + let error = + NoteStorageSchema::from_wit_text(&deep_chain_schema(MAX_NOTE_STORAGE_SCHEMA_DEPTH + 1)) + .err() + .expect("an over-deep schema must fail") + .to_string(); + + assert!(error.contains("nesting depth"), "unexpected deep-chain error: {error}"); + assert!( + error.contains(&MAX_NOTE_STORAGE_SCHEMA_DEPTH.to_string()), + "the nesting limit must be present in the diagnostic: {error}" + ); +} + +#[test] +fn memoized_subtree_reuse_still_enforces_the_depth_limit() { + let error = NoteStorageSchema::from_wit_text(&memoized_reuse_depth_schema()) + .err() + .expect("deep reuse of a memoized subtree must fail") + .to_string(); + + assert!(error.contains("nesting depth"), "unexpected memoized-depth error: {error}"); + assert!( + error.contains(&MAX_NOTE_STORAGE_SCHEMA_DEPTH.to_string()), + "the nesting limit must be present in the diagnostic: {error}" + ); +} + +#[test] +fn zero_width_record_doubling_fails_at_the_expanded_node_limit() { + let error = NoteStorageSchema::from_wit_text(&empty_pair_schema(40)) + .err() + .expect("a doubling chain of empty records must fail") + .to_string(); + + assert!(error.contains("expands to"), "unexpected expanded-node error: {error}"); + assert!( + error.contains(&MAX_NOTE_STORAGE_SCHEMA_NODES.to_string()), + "the node limit must be present in the diagnostic: {error}" + ); + // Level `n` of the chain expands to `2^(n + 1) - 1` nodes, so the first rejected level is the + // first one above the budget. The count in the message pins the counting rule. + let first_rejected = (1..) + .map(|level: u32| (1usize << (level + 1)) - 1) + .find(|nodes| *nodes > MAX_NOTE_STORAGE_SCHEMA_NODES) + .expect("the doubling chain crosses the node limit"); + assert!( + error.contains(&first_rejected.to_string()), + "the expanded node count must be present in the diagnostic: {error}" + ); +} + +#[test] +fn nested_records_below_the_expanded_node_limit_still_resolve() { + let levels = 11; + let schema = NoteStorageSchema::from_wit_text(&empty_pair_schema(levels)) + .expect("a doubling chain below the node limit must resolve"); + assert!( + (1usize << (levels + 1)) - 1 <= MAX_NOTE_STORAGE_SCHEMA_NODES, + "the chain must stay below the node budget" + ); + assert_eq!(schema.layout().maximum(), 0, "empty records have no felts"); + + NoteStorageSchema::from_wit_text(LAYOUT_SCHEMA) + .expect("the layout schema stays below the node limit"); +} + +#[test] +fn schema_reader_enforces_documented_byte_type_and_root_width_limits() { + let oversized = " ".repeat(MAX_NOTE_STORAGE_SCHEMA_BYTES + 1); + let byte_error = NoteStorageSchema::from_wit_text(&oversized) + .err() + .expect("an oversized schema document must fail") + .to_string(); + assert!(byte_error.contains("schema section")); + assert!(byte_error.contains(&MAX_NOTE_STORAGE_SCHEMA_BYTES.to_string())); + + let mut too_many_types = + String::from("package example:many-types@1.0.0; interface note-storage { "); + for index in 0..=MAX_NOTE_STORAGE_SCHEMA_TYPES { + too_many_types.push_str(&format!("type t{index} = u8; ")); + } + too_many_types.push_str("record root { value: u8 } type storage = root; }"); + let type_error = NoteStorageSchema::from_wit_text(&too_many_types) + .err() + .expect("a schema with too many types must fail") + .to_string(); + assert!(type_error.contains("WIT types"), "unexpected type-count error: {type_error}"); + assert!(type_error.contains(&MAX_NOTE_STORAGE_SCHEMA_TYPES.to_string())); + + let mut wide_root = + String::from("package example:wide-root@1.0.0; interface note-storage { record root { "); + for index in 0..=MAX_NOTE_STORAGE_SCHEMA_FELTS { + wide_root.push_str(&format!("field-{index}: u8, ")); + } + wide_root.push_str("} type storage = root; }"); + let width_error = NoteStorageSchema::from_wit_text(&wide_root) + .err() + .expect("an over-wide schema root must fail") + .to_string(); + assert!(width_error.contains("maximum width"), "unexpected width error: {width_error}"); + assert!(width_error.contains(&MAX_NOTE_STORAGE_SCHEMA_FELTS.to_string())); +} + +/// Builds a linear-size WIT DAG of zero-felt records whose expanded tree doubles at each level. +fn empty_pair_schema(levels: usize) -> String { + let mut wit = + String::from("package example:empty-pair@1.0.0; interface note-storage { record t0 { } "); + for level in 1..=levels { + let previous = level - 1; + wit.push_str(&format!("record t{level} {{ left: t{previous}, right: t{previous} }} ")); + } + wit.push_str(&format!("type storage = t{levels}; }}")); + wit +} + +/// Builds a linear-size WIT DAG whose resolved layout doubles at each level. +fn repeated_pair_schema(levels: usize) -> String { + let mut wit = String::from( + "package example:repeated-pair@1.0.0; interface note-storage { record t0 { value: u8 } ", + ); + for level in 1..=levels { + let previous = level - 1; + wit.push_str(&format!("record t{level} {{ left: t{previous}, right: t{previous} }} ")); + } + wit.push_str(&format!("type storage = t{levels}; }}")); + wit +} + +/// Builds a WIT record chain with one nested named type per level. +fn deep_chain_schema(levels: usize) -> String { + let mut wit = String::from( + "package example:deep-chain@1.0.0; interface note-storage { record t0 { value: u8 } ", + ); + for level in 1..=levels { + let previous = level - 1; + wit.push_str(&format!("record t{level} {{ value: t{previous} }} ")); + } + wit.push_str(&format!("type storage = t{levels}; }}")); + wit +} + +/// Builds two individually valid chains whose composition exceeds the depth limit only on reuse. +fn memoized_reuse_depth_schema() -> String { + let levels = MAX_NOTE_STORAGE_SCHEMA_DEPTH / 2; + let mut wit = String::from( + "package example:memoized-depth@1.0.0; interface note-storage { record shared0 { value: \ + u8 } ", + ); + for level in 1..=levels { + let previous = level - 1; + wit.push_str(&format!("record shared{level} {{ value: shared{previous} }} ")); + } + wit.push_str(&format!("record wrapper0 {{ value: shared{levels} }} ")); + for level in 1..=levels { + let previous = level - 1; + wit.push_str(&format!("record wrapper{level} {{ value: wrapper{previous} }} ")); + } + wit.push_str(&format!( + "record root {{ cached: shared{levels}, too-deep: wrapper{levels} }} type storage = root; \ + }}" + )); + wit +} diff --git a/sdk/note-schema/src/value.rs b/sdk/note-schema/src/value.rs new file mode 100644 index 0000000000..5b8b084b2c --- /dev/null +++ b/sdk/note-schema/src/value.rs @@ -0,0 +1,285 @@ +//! Named values decoded from note storage. + +use core::fmt; + +use miden_field_repr::{FeltReader, FromFeltRepr}; + +use crate::{ + CodecRegistry, Error, Felt, NoteStorage, PrimitiveType, Result, SchemaType, SchemaTypeKind, + schema::normalize_name, +}; + +/// A named value decoded from note storage. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DecodedValue { + name: Option, + docs: Option, + fqn: Option, + kind: DecodedValueKind, +} + +impl DecodedValue { + /// Returns the field or root type name. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Returns the field-level or root type documentation. + pub fn docs(&self) -> Option<&str> { + self.docs.as_deref() + } + + /// Returns the canonical WIT FQN when this value has one. + pub fn fqn(&self) -> Option<&str> { + self.fqn.as_deref() + } + + /// Returns the decoded value kind. + pub const fn kind(&self) -> &DecodedValueKind { + &self.kind + } + + /// Finds a direct record field with kebab-case or snake_case spelling. + pub fn field(&self, name: &str) -> Option<&Self> { + let DecodedValueKind::Record(fields) = &self.kind else { + return None; + }; + let name = normalize_name(name); + fields.iter().find(|field| field.name.as_deref() == Some(name.as_str())) + } +} + +/// The structural value stored in a decoded node. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DecodedValueKind { + /// A codec-rendered or primitive leaf with its encoded felts. + Leaf { + /// The structural felt representation. + felts: Vec, + /// The registry-backed or primitive display text. + display: String, + }, + /// Record fields in declaration order. + Record(Vec), + /// An optional value. + Option(Option>), + /// A selected variant case and its optional payload. + Variant { + /// The selected case name. + case: String, + /// The decoded case payload. + value: Option>, + }, +} + +impl fmt::Display for DecodedValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.kind { + DecodedValueKind::Leaf { display, .. } => f.write_str(display), + DecodedValueKind::Record(fields) => { + f.write_str("{")?; + for (index, field) in fields.iter().enumerate() { + if index != 0 { + f.write_str(", ")?; + } + f.write_str(field.name.as_deref().unwrap_or(""))?; + f.write_str(": ")?; + field.fmt(f)?; + } + f.write_str("}") + } + DecodedValueKind::Option(None) => f.write_str("none"), + DecodedValueKind::Option(Some(value)) => write!(f, "some({value})"), + DecodedValueKind::Variant { case, value: None } => f.write_str(case), + DecodedValueKind::Variant { + case, + value: Some(value), + } => write!(f, "{case}({value})"), + } + } +} + +/// Decodes a root storage value and rejects trailing felts. +pub(crate) fn decode( + root: &SchemaType, + storage: &NoteStorage, + registry: &CodecRegistry, +) -> Result { + let (value, consumed) = decode_type( + root, + storage.items(), + Some(registry), + root.name().map(str::to_owned), + root.docs().map(str::to_owned), + )?; + if consumed != storage.items().len() { + return Err(Error::new(format!( + "note storage has {} trailing felt(s) after the schema root", + storage.items().len() - consumed + ))); + } + Ok(value) +} + +/// Validates one complete structural encoding without applying codecs. +pub(crate) fn validate_encoding(ty: &SchemaType, felts: &[Felt]) -> Result<()> { + let (_, consumed) = decode_type(ty, felts, None, None, None)?; + if consumed != felts.len() { + return Err(Error::new(format!( + "value has {} trailing felt(s) after its structural encoding", + felts.len() - consumed + ))); + } + Ok(()) +} + +/// Decodes one value prefix and returns the consumed felt count. +fn decode_type( + ty: &SchemaType, + input: &[Felt], + registry: Option<&CodecRegistry>, + name: Option, + docs: Option, +) -> Result<(DecodedValue, usize)> { + if let (Some(registry), Some(fqn)) = (registry, ty.fqn()) + && let Some(codec) = registry.codec(fqn) + { + let (_, consumed) = decode_type(ty, input, None, None, None)?; + let felts = input[..consumed].to_vec(); + codec + .validate(&felts) + .map_err(|err| err.context(format!("codec `{fqn}` rejected decoded value")))?; + let display = codec + .display(&felts) + .map_err(|err| err.context(format!("codec `{fqn}` failed to display decoded value")))?; + return Ok(( + DecodedValue { + name, + docs, + fqn: Some(fqn.to_owned()), + kind: DecodedValueKind::Leaf { display, felts }, + }, + consumed, + )); + } + + let fqn = ty.fqn().map(str::to_owned); + let (kind, consumed) = match ty.kind() { + SchemaTypeKind::Felt => { + let mut reader = FeltReader::new(input); + let felt = reader + .read() + .map_err(|err| Error::new(format!("invalid felt representation: {err}")))?; + ( + DecodedValueKind::Leaf { + felts: vec![felt], + display: felt.as_canonical_u64().to_string(), + }, + reader.pos(), + ) + } + SchemaTypeKind::Primitive(primitive) => decode_primitive(*primitive, input)?, + SchemaTypeKind::Record(fields) => { + let mut decoded = Vec::with_capacity(fields.len()); + let mut offset = 0usize; + for field in fields { + let (value, consumed) = decode_type( + field.ty(), + &input[offset..], + registry, + Some(field.name().to_owned()), + field.docs().map(str::to_owned), + ) + .map_err(|err| err.context(format!("field `{}`", field.name())))?; + offset = offset + .checked_add(consumed) + .ok_or_else(|| Error::new("decoded record width is too large"))?; + decoded.push(value); + } + (DecodedValueKind::Record(decoded), offset) + } + SchemaTypeKind::Option(payload) => { + let mut reader = FeltReader::new(input); + let tag = reader + .read() + .map_err(|err| Error::new(format!("invalid option representation: {err}")))? + .as_canonical_u64(); + match tag { + 0 => (DecodedValueKind::Option(None), reader.pos()), + 1 => { + let (value, consumed) = + decode_type(payload, &input[reader.pos()..], registry, None, None)?; + (DecodedValueKind::Option(Some(Box::new(value))), reader.pos() + consumed) + } + tag => { + return Err(Error::new(format!("invalid option tag {tag}; expected 0 or 1"))); + } + } + } + SchemaTypeKind::Variant(cases) => { + let mut reader = FeltReader::new(input); + let tag = reader + .read_u32() + .map_err(|err| Error::new(format!("invalid variant representation: {err}")))? + as usize; + let case = cases.get(tag).ok_or_else(|| { + Error::new(format!( + "invalid variant tag {tag}; expected a declaration ordinal below {}", + cases.len() + )) + })?; + let (value, consumed) = match case.payload() { + Some(payload) => { + let (value, consumed) = + decode_type(payload, &input[reader.pos()..], registry, None, None)?; + (Some(Box::new(value)), reader.pos() + consumed) + } + None => (None, reader.pos()), + }; + ( + DecodedValueKind::Variant { + case: case.name().to_owned(), + value, + }, + consumed, + ) + } + }; + + Ok(( + DecodedValue { + name, + docs, + fqn, + kind, + }, + consumed, + )) +} + +/// Decodes a supported primitive through `miden-field-repr`. +fn decode_primitive(primitive: PrimitiveType, input: &[Felt]) -> Result<(DecodedValueKind, usize)> { + match primitive { + PrimitiveType::U64 => read_primitive::(input), + PrimitiveType::U32 => read_primitive::(input), + PrimitiveType::U8 => read_primitive::(input), + PrimitiveType::Bool => read_primitive::(input), + } +} + +/// Decodes and displays one primitive value. +fn read_primitive(input: &[Felt]) -> Result<(DecodedValueKind, usize)> +where + T: FromFeltRepr + fmt::Display, +{ + let mut reader = FeltReader::new(input); + let value = T::from_felt_repr(&mut reader) + .map_err(|err| Error::new(format!("invalid primitive representation: {err}")))?; + Ok(( + DecodedValueKind::Leaf { + felts: input[..reader.pos()].to_vec(), + display: value.to_string(), + }, + reader.pos(), + )) +} diff --git a/sdk/note-schema/tests/p2id_package.rs b/sdk/note-schema/tests/p2id_package.rs new file mode 100644 index 0000000000..b49b2613e0 --- /dev/null +++ b/sdk/note-schema/tests/p2id_package.rs @@ -0,0 +1,28 @@ +//! End-to-end test for a schema embedded in the p2id note package. + +use miden_note_schema::{NoteStorage, NoteStorageSchema}; +use miden_protocol::{account::AccountId, address::NetworkId}; +use midenc_integration_test_support::{compile_project, workspace_root, write_masp_file_atomic}; + +#[test] +fn p2id_schema_builds_and_decodes_account_id_storage() { + let workspace = workspace_root(); + let examples = workspace.join("examples"); + let wallet_dir = examples.join("basic-wallet"); + let wallet = compile_project(&wallet_dir); + write_masp_file_atomic(&wallet, wallet_dir.join("target/miden/release")) + .expect("failed to persist the basic-wallet dependency package"); + + let p2id = compile_project(&examples.join("p2id-note")); + let schema = NoteStorageSchema::from_package(&p2id).expect("p2id schema must resolve"); + let account_id = AccountId::try_from(0xaa00_0000_0000_bc11_0000_bc00_0000_de00u128).unwrap(); + let bech32 = account_id.to_bech32(NetworkId::Mainnet); + + let built = schema.builder().set("target-account-id", &bech32).unwrap().build().unwrap(); + let expected = + NoteStorage::new(vec![account_id.prefix().as_felt(), account_id.suffix()]).unwrap(); + assert_eq!(built, expected); + + let decoded = schema.decode(&built).unwrap(); + assert_eq!(decoded.field("target_account_id").unwrap().to_string(), bech32); +} diff --git a/sdk/note-schema/wit/note-codec.wit b/sdk/note-schema/wit/note-codec.wit new file mode 100644 index 0000000000..950aa77cb9 --- /dev/null +++ b/sdk/note-schema/wit/note-codec.wit @@ -0,0 +1,28 @@ +// Mirror note: the wit-crate file is canonical; sdk/note-schema/wit/note-codec.wit exists because +// Wasmtime bindgen!(path:) needs package-local WIT, and an equality test guards both files. +package miden:note-codec@1.0.0; + +/// Parses, displays, and validates custom note storage types. +interface codec { + /// A canonical Miden field element at the component boundary. + /// + /// This is a `u64`, not the core-types `felt` record, because that record models the + /// compiler's guest felt value and does not expose its canonical integer representation. + type felt = u64; + + /// Returns the fully-qualified WIT names handled by this component. + supported-types: func() -> list; + + /// Parses text into a type's structural felt representation. + parse: func(type-fqn: string, value: string) -> result, string>; + + /// Displays a type's structural felt representation. + display: func(type-fqn: string, value: list) -> result; + + /// Validates a type's structural felt representation. + validate: func(type-fqn: string, value: list) -> result<_, string>; +} + +world note-codec { + export codec; +} diff --git a/sdk/sdk/.cargo/config.toml b/sdk/sdk/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/sdk/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/sdk/Cargo.toml b/sdk/sdk/Cargo.toml index 507c22e273..ad674f3779 100644 --- a/sdk/sdk/Cargo.toml +++ b/sdk/sdk/Cargo.toml @@ -25,7 +25,7 @@ miden-base-sys.workspace = true miden-field-repr.workspace = true miden-field = { workspace = true, default-features = false } miden-tx-script-args = { workspace = true, features = ["miden-vm-guest"] } -wit-bindgen = { version = "0.57", default-features = false, features = ["macros"] } +wit-bindgen = { workspace = true } [features] default = [] diff --git a/sdk/sdk/MIGRATION.md b/sdk/sdk/MIGRATION.md index b0709599a3..5c1734ac69 100644 --- a/sdk/sdk/MIGRATION.md +++ b/sdk/sdk/MIGRATION.md @@ -90,6 +90,19 @@ constructors can commit to the note script's MAST root. Rename any inherent item including methods, `#[note_constructor]` methods, associated constants, and items declared in a separate impl block. +### Keep duplicate `#[export_type]` registrations nominally unique and shape-compatible + +Two different Rust types that map to the same WIT name are now a compile error, even when their +record fields or enum cases are identical. Shape-identical duplicate registrations of the same Rust +type remain allowed, while conflicting shapes do not. Previously, the last registration silently +replaced the first. Rename one Rust type so it maps to a different WIT name. + +### `#[export_type]` reserves `__MIDEN_EXPORT_TYPE_SHAPE` + +The `#[export_type]` macro now emits an inherent public associated constant named +`__MIDEN_EXPORT_TYPE_SHAPE` on the annotated type. Rename any user-defined associated item with +that exact name. + ### Contract crates gain a `build.rs` for IDE and plain-cargo builds New projects created by `cargo miden new` include a small `build.rs` in each contract crate and a @@ -146,6 +159,58 @@ path. Two different projects that contain a contract crate with the same package version and share one `CARGO_TARGET_DIR` reuse each other's build-script output — including the staged package cache. Use per-checkout target directories for such layouts. +### Keep one `#[note]` struct in each crate + +There can now be only one `#[note]` struct per linked artifact. Two note structs compiled before +this change. Now the linker rejects the second struct because both structs define the +`__MIDEN_NOTE_STORAGE_SCHEMA_UNIQUENESS_GUARD` symbol. The guard covers the whole link, so a note +crate cannot depend on another note crate either. + +Keep one note struct in the current crate. Move each extra note struct and its implementation into +a separate note crate, and do not depend on that crate from a note crate. + +### Rewrite tuple-note and `Vec` storage layouts + +`#[note]` now emits a WIT storage schema and therefore requires each stored value to have a stable, +named position. Unit structs remain valid, but tuple structs must become named-field structs. Keep +the fields in the same order to preserve the existing felt layout: + +```rust +// before +#[note] +struct PaymentNote(AccountId, u64); + +// after +#[note] +struct PaymentNote { + target: AccountId, + amount: u64, +} +``` + +Dynamic `Vec` fields have no fixed note-storage layout and are no longer accepted. Replace a vector +with explicit fields. Use `Option` for fixed optional positions, or use a named `#[export_type]` +record when the same fixed group is nested or reused: + +```rust +// before +#[note] +struct ValuesNote { + values: Vec, +} + +// after +#[note] +struct ValuesNote { + first: Felt, + second: Option, +} +``` + +There is no direct replacement for an unbounded vector. Choose a fixed maximum represented by +named and optional fields, or redesign the note so variable-sized data is committed outside its +storage payload. + ### Kernel scalars are typed instead of `Felt` (counts, block heights, nonces, attachments) Binding surfaces whose values are counts now return `u32`: `tx::get_num_input_notes`, diff --git a/sdk/stdlib-sys/.cargo/config.toml b/sdk/stdlib-sys/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/stdlib-sys/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/tx-script-args/.cargo/config.toml b/sdk/tx-script-args/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/tx-script-args/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/wasm-metadata/.cargo/config.toml b/sdk/wasm-metadata/.cargo/config.toml new file mode 100644 index 0000000000..7fe3f77d7f --- /dev/null +++ b/sdk/wasm-metadata/.cargo/config.toml @@ -0,0 +1,3 @@ +# Per-crate copy: per-member check-each builds run in this directory and default to the Miden Wasm target (a no-op for proc-macro roots, which always build for the host). sdk/ has no directory-wide config so host-side crates build natively. +[build] +target = "wasm32-wasip1" diff --git a/sdk/wasm-metadata/src/lib.rs b/sdk/wasm-metadata/src/lib.rs index d002ac66e1..cba8795685 100644 --- a/sdk/wasm-metadata/src/lib.rs +++ b/sdk/wasm-metadata/src/lib.rs @@ -13,6 +13,15 @@ use alloc::{string::String, vec::Vec}; use serde::{Deserialize, Serialize}; +/// Environment variables that must not leak into nested Cargo builds. +pub const NESTED_CARGO_SCRUB_ENV: &[&str] = &[ + "CARGO_BUILD_RUSTFLAGS", + "CARGO_BUILD_TARGET", + "CARGO_ENCODED_RUSTFLAGS", + "CARGO_TARGET_WASM32_WASIP2_RUSTFLAGS", + "RUSTFLAGS", +]; + /// Name of the Wasm custom section used to store frontend metadata bytes. pub const WASM_FRONTEND_METADATA_CUSTOM_SECTION_NAME: &str = "rodata,miden_account_component_frontend"; @@ -35,13 +44,55 @@ pub fn package_wit_section_id() -> miden_mast_package::SectionId { .expect("the WIT section id must be a valid custom section id") } +/// Name of the Wasm custom section that stores a note storage schema. +pub const WASM_NOTE_STORAGE_SCHEMA_CUSTOM_SECTION_NAME: &str = "rodata,miden_note_schema"; + +/// Name of the Miden package section that stores a note storage schema. +pub const PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID: &str = "note_storage_schema"; + +/// Returns the Miden package section id that stores a note storage schema. +/// +/// One accessor keeps every producer and consumer on the same section contract. +pub fn package_note_storage_schema_section_id() -> miden_mast_package::SectionId { + miden_mast_package::SectionId::custom(PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID) + .expect("the note storage schema section id must be a valid custom section id") +} + +/// Name of the Miden package section that stores a note codec. +pub const PACKAGE_NOTE_CODEC_SECTION_ID: &str = "note_codec"; + +/// Returns the Miden package section id that stores a note codec. +/// +/// One accessor keeps every producer and consumer on the same section contract. +pub fn package_note_codec_section_id() -> miden_mast_package::SectionId { + miden_mast_package::SectionId::custom(PACKAGE_NOTE_CODEC_SECTION_ID) + .expect("the note codec section id must be a valid custom section id") +} + +/// Pads metadata bytes with NUL bytes to the 16-byte link-section alignment. +/// +/// Account-component-metadata link sections use this 16-byte padding. Note storage schema link +/// sections use the same contract. +pub fn pad_to_link_section_alignment(mut bytes: Vec) -> Vec { + let padded_len = bytes.len().div_ceil(16) * 16; + bytes.resize(padded_len, 0); + bytes +} + +/// Removes NUL padding from the end of a link-section payload. +pub fn trim_trailing_nuls(bytes: &[u8]) -> &[u8] { + let len = bytes.iter().rposition(|byte| *byte != 0).map_or(0, |index| index + 1); + &bytes[..len] +} + /// The filesystem package-cache exchange contract. /// /// The compiler publishes compiled dependency packages — and its recorded dependency /// resolution — into the directory named by [`package_cache::PACKAGE_CACHE_ENV`]; the SDK /// macros and the build-script support crate consume them. Every spelling of that contract lives -/// here so the producer and the consumers cannot drift apart. (The support crate is the one -/// deliberate exception: it is dependency-free by design and spells the same values inline.) +/// here so the producer and the consumers cannot drift apart. The one exception is +/// `miden-sdk-build-script-support`, which spells the variable name inline because it carries no +/// dependencies. pub mod package_cache { use alloc::{format, string::String}; @@ -250,6 +301,8 @@ pub struct PackageSections { pub account_component_metadata: Option>, /// The component's public WIT source emitted by the `#[component]` macro. pub component_wit: Option>, + /// The note storage schema. + pub note_storage_schema: Option>, } /// Frontend-only metadata emitted by the SDK macros into a dedicated Wasm custom section. @@ -357,6 +410,16 @@ mod tests { use super::*; + /// Ensures shared link-section padding is reversible and aligned. + #[test] + fn link_section_padding_round_trips() { + let source = b"note schema"; + let padded = pad_to_link_section_alignment(source.to_vec()); + + assert_eq!(padded.len() % 16, 0); + assert_eq!(trim_trailing_nuls(&padded), source); + } + /// Ensures a single embedded component WIT source is recognized as one package. #[test] fn component_wit_counts_a_single_package_declaration() { diff --git a/tests/fixtures/components/assert-debug-test/Cargo.lock b/tests/fixtures/components/assert-debug-test/Cargo.lock index 4e0d6b16cd..d43827d596 100644 --- a/tests/fixtures/components/assert-debug-test/Cargo.lock +++ b/tests/fixtures/components/assert-debug-test/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/component-macros-account/Cargo.lock b/tests/fixtures/components/component-macros-account/Cargo.lock index 1f7cbcfa62..06cba60213 100644 --- a/tests/fixtures/components/component-macros-account/Cargo.lock +++ b/tests/fixtures/components/component-macros-account/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-account-word-arg/Cargo.lock b/tests/fixtures/components/cross-ctx-account-word-arg/Cargo.lock index 4a5c46135f..60e31bb8e8 100644 --- a/tests/fixtures/components/cross-ctx-account-word-arg/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-account-word-arg/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-account-word/Cargo.lock b/tests/fixtures/components/cross-ctx-account-word/Cargo.lock index 892f782e6d..09ac7d485b 100644 --- a/tests/fixtures/components/cross-ctx-account-word/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-account-word/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-account/Cargo.lock b/tests/fixtures/components/cross-ctx-account/Cargo.lock index b90f596cc7..6854828b0a 100644 --- a/tests/fixtures/components/cross-ctx-account/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-account/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-note-word-arg/Cargo.lock b/tests/fixtures/components/cross-ctx-note-word-arg/Cargo.lock index a1a9557dae..13dc0117be 100644 --- a/tests/fixtures/components/cross-ctx-note-word-arg/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-note-word-arg/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-note-word/Cargo.lock b/tests/fixtures/components/cross-ctx-note-word/Cargo.lock index d4300213e2..e55b8093d7 100644 --- a/tests/fixtures/components/cross-ctx-note-word/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-note-word/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/cross-ctx-note/Cargo.lock b/tests/fixtures/components/cross-ctx-note/Cargo.lock index aa1c2c6616..b2b96882c9 100644 --- a/tests/fixtures/components/cross-ctx-note/Cargo.lock +++ b/tests/fixtures/components/cross-ctx-note/Cargo.lock @@ -950,7 +950,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1042,7 +1042,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1071,7 +1071,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1520,14 +1520,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/fixtures/components/swapp-note/Cargo.lock b/tests/fixtures/components/swapp-note/Cargo.lock index 4f7e15686f..823b65aa74 100644 --- a/tests/fixtures/components/swapp-note/Cargo.lock +++ b/tests/fixtures/components/swapp-note/Cargo.lock @@ -943,7 +943,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miden" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base", "miden-base-macros", @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "miden-base" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-base-sys", "miden-stdlib-sys", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "miden-base-macros" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "heck", "miden-assembly-syntax", @@ -1064,7 +1064,7 @@ dependencies = [ [[package]] name = "miden-base-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field-repr", "miden-stdlib-sys", @@ -1221,7 +1221,7 @@ dependencies = [ [[package]] name = "miden-field-repr" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr-derive", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "miden-field-repr-derive" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "proc-macro2", "quote", @@ -1476,7 +1476,7 @@ dependencies = [ [[package]] name = "miden-sdk-alloc" -version = "0.14.0-rc.1" +version = "0.14.0" [[package]] name = "miden-serde-utils" @@ -1513,14 +1513,14 @@ dependencies = [ [[package]] name = "miden-stdlib-sys" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", ] [[package]] name = "miden-tx-script-args" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-field", "miden-field-repr", @@ -1592,7 +1592,7 @@ dependencies = [ [[package]] name = "midenc-frontend-wasm-metadata" -version = "0.14.0-rc.1" +version = "0.14.0" dependencies = [ "miden-mast-package", "serde", diff --git a/tests/integration-network/Cargo.toml b/tests/integration-network/Cargo.toml index 3bec4aff8d..01745cc759 100644 --- a/tests/integration-network/Cargo.toml +++ b/tests/integration-network/Cargo.toml @@ -33,3 +33,7 @@ tokio.workspace = true # For accessing shared compiler test builders and helpers midenc-integration-test-support.workspace = true + +[dev-dependencies] +miden-note-schema = { workspace = true, features = ["codec-component"] } +midenc-frontend-wasm-metadata.workspace = true diff --git a/tests/integration-network/src/mockchain/counter/basic_auth.rs b/tests/integration-network/src/mockchain/counter/basic_auth.rs index 989ec3aa75..ce627e3308 100644 --- a/tests/integration-network/src/mockchain/counter/basic_auth.rs +++ b/tests/integration-network/src/mockchain/counter/basic_auth.rs @@ -67,7 +67,7 @@ pub fn counter_note_basic_auth_increments_storage() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); - expect!["9090"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["9097"].assert_eq(single_note_cycles(&tx_measurements)); // The counter contract storage value should be 2 after the note is consumed (incremented by 1). assert_counter_storage( diff --git a/tests/integration-network/src/mockchain/counter/no_auth.rs b/tests/integration-network/src/mockchain/counter/no_auth.rs index dca848961d..dffbb8b212 100644 --- a/tests/integration-network/src/mockchain/counter/no_auth.rs +++ b/tests/integration-network/src/mockchain/counter/no_auth.rs @@ -103,7 +103,7 @@ pub fn counter_note_no_auth_increments_storage_without_signature() { .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); expect!["2125"].assert_eq(auth_procedure_cycles(&tx_measurements)); - expect!["9090"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["9097"].assert_eq(single_note_cycles(&tx_measurements)); // The counter contract storage value should be 2 after the note is consumed assert_counter_storage( diff --git a/tests/integration-network/src/mockchain/notes/basic_wallet.rs b/tests/integration-network/src/mockchain/notes/basic_wallet.rs index 6d8497674b..7ec71cdb28 100644 --- a/tests/integration-network/src/mockchain/notes/basic_wallet.rs +++ b/tests/integration-network/src/mockchain/notes/basic_wallet.rs @@ -111,7 +111,7 @@ pub fn basic_wallet_p2id_transfers_asset_with_custom_tx_script() { .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); expect!["3473"].assert_eq(prologue_cycles(&tx_measurements)); - expect!["5030"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["5040"].assert_eq(single_note_cycles(&tx_measurements)); eprintln!("\n=== Checking Alice's account has the minted asset ==="); let alice_account = chain.committed_account(alice_id).unwrap(); @@ -142,7 +142,7 @@ pub fn basic_wallet_p2id_transfers_asset_with_custom_tx_script() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); - expect!["5030"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["5040"].assert_eq(single_note_cycles(&tx_measurements)); eprintln!("\n=== Checking Bob's account has the transferred asset ==="); let bob_account = chain.committed_account(bob_id).unwrap(); @@ -281,7 +281,7 @@ pub fn basic_wallet_p2ide_allows_recipient_claim() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); - expect!["5467"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["5477"].assert_eq(single_note_cycles(&tx_measurements)); // Step 5: verify balances let bob_account = chain.committed_account(bob_id).unwrap(); @@ -420,7 +420,7 @@ pub fn basic_wallet_p2ide_allows_sender_reclaim() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, mock_tx); - expect!["6042"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["6052"].assert_eq(single_note_cycles(&tx_measurements)); // Step 5: verify Alice has her original amount back let alice_account = chain.committed_account(alice_id).unwrap(); diff --git a/tests/integration-network/src/mockchain/notes/mod.rs b/tests/integration-network/src/mockchain/notes/mod.rs index 6ef1b20e3b..4580940069 100644 --- a/tests/integration-network/src/mockchain/notes/mod.rs +++ b/tests/integration-network/src/mockchain/notes/mod.rs @@ -1,2 +1,3 @@ mod basic_wallet; mod note_constructor; +mod schema; diff --git a/tests/integration-network/src/mockchain/notes/note_constructor.rs b/tests/integration-network/src/mockchain/notes/note_constructor.rs index 2b446a306d..9c994c58b1 100644 --- a/tests/integration-network/src/mockchain/notes/note_constructor.rs +++ b/tests/integration-network/src/mockchain/notes/note_constructor.rs @@ -173,7 +173,7 @@ pub fn tx_script_creates_p2id_note_via_note_constructor() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, create_tx); - expect!["8932"].assert_eq(tx_script_processing_cycles(&tx_measurements)); + expect!["8942"].assert_eq(tx_script_processing_cycles(&tx_measurements)); eprintln!("\n=== Step 4: Bob consumes the note created by the constructor ==="); let faucet_inputs = chain.get_foreign_account_inputs(faucet_id).unwrap(); @@ -184,7 +184,7 @@ pub fn tx_script_creates_p2id_note_via_note_constructor() { .build() .unwrap(); let tx_measurements = execute_tx_measurements(&mut chain, consume_tx); - expect!["5030"].assert_eq(single_note_cycles(&tx_measurements)); + expect!["5040"].assert_eq(single_note_cycles(&tx_measurements)); eprintln!("\n=== Checking Bob's account has the transferred asset ==="); let bob_account = chain.committed_account(bob_id).unwrap(); diff --git a/tests/integration-network/src/mockchain/notes/schema.rs b/tests/integration-network/src/mockchain/notes/schema.rs new file mode 100644 index 0000000000..4618e37071 --- /dev/null +++ b/tests/integration-network/src/mockchain/notes/schema.rs @@ -0,0 +1,169 @@ +//! Schema-driven note storage tests on the mock chain. + +use std::sync::Arc; + +use miden_client::{ + account::{AccountComponent, component::InitStorageData}, + asset::{Asset, FungibleAsset}, + transaction::RawOutputNote, +}; +use miden_mast_package::{Package, SectionId}; +use miden_note_schema::{CodecRegistry, NoteStorage, NoteStorageSchema}; +use miden_protocol::{ + account::{AccountId, auth::AuthScheme}, + address::NetworkId, + crypto::rand::RandomCoin, + note::Note, +}; +use miden_standards::testing::note::NoteBuilder; +use miden_testing::{Auth, MockChain}; +use midenc_frontend_wasm_metadata::{ + PACKAGE_NOTE_CODEC_SECTION_ID, PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, + package_note_codec_section_id, package_note_storage_schema_section_id, +}; +use midenc_integration_test_support::wasm_target_is_installed; + +use super::super::support::{ + assert_account_has_fungible_asset, build_send_notes_script, compile_rust_package, execute_tx, + note_script_root, +}; + +/// Builds and consumes a note, then returns the consumed note and account ID. +fn transfer_with_storage( + note_package: Arc, + build_storage: impl FnOnce(AccountId) -> NoteStorage, +) -> (Note, AccountId) { + let wallet_package = compile_rust_package("../../examples/basic-wallet", true); + let wallet_component = + AccountComponent::from_package(&wallet_package, &InitStorageData::default()).unwrap(); + + let mut builder = MockChain::builder(); + let faucet = builder + .add_existing_basic_faucet( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + "TEST", + 1_000_000, + None, + ) + .unwrap(); + let faucet_id = faucet.id(); + let recipient = builder + .add_existing_account_from_components( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + [wallet_component], + ) + .unwrap(); + let recipient_id = recipient.id(); + + let mut chain = builder.build().unwrap(); + chain.prove_next_block().unwrap(); + chain.prove_next_block().unwrap(); + + let transfer_amount = 100_000; + let asset = FungibleAsset::new(faucet_id, transfer_amount).unwrap(); + let mut note_rng = RandomCoin::new(note_script_root(¬e_package)); + let note = NoteBuilder::new(faucet_id, &mut note_rng) + .package((*note_package).clone()) + .add_assets([Asset::from(asset)]) + .note_storage(build_storage(recipient_id).to_elements()) + .unwrap() + .build() + .unwrap(); + + let faucet = chain.committed_account(faucet_id).unwrap().clone(); + let send_script = build_send_notes_script(&faucet, std::slice::from_ref(¬e)); + let send = chain + .build_transaction(faucet_id) + .send_notes_script(&send_script) + .expected_output_notes(vec![RawOutputNote::Full(note.clone())]) + .build() + .unwrap(); + execute_tx(&mut chain, send); + + let consume = chain + .build_transaction(recipient_id) + .authenticated_input_note(note.id()) + .foreign_accounts(vec![chain.get_foreign_account_inputs(faucet_id).unwrap()]) + .build() + .unwrap(); + execute_tx(&mut chain, consume); + + assert_account_has_fungible_asset( + chain.committed_account(recipient_id).unwrap(), + faucet_id, + transfer_amount, + ); + (note, recipient_id) +} + +#[test] +fn dex_note_uses_embedded_schema_and_component_codec() { + if !wasm_target_is_installed() { + eprintln!("skipping DEX note schema test: wasm32-wasip2 is not installed"); + return; + } + let note_package = compile_rust_package("../../examples/dex-note", true); + assert_package_section( + ¬e_package, + package_note_storage_schema_section_id(), + PACKAGE_NOTE_STORAGE_SCHEMA_SECTION_ID, + ); + assert_package_section( + ¬e_package, + package_note_codec_section_id(), + PACKAGE_NOTE_CODEC_SECTION_ID, + ); + let schema = NoteStorageSchema::from_package(¬e_package).unwrap(); + let codecs = CodecRegistry::load_from_package(¬e_package).unwrap(); + + let (note, recipient_id) = transfer_with_storage(Arc::clone(¬e_package), |recipient_id| { + schema + .builder_with_registry(&codecs) + .set("target", recipient_id.to_bech32(NetworkId::Mainnet)) + .unwrap() + .set("price", "1.5") + .unwrap() + .build() + .unwrap() + }); + + let decoded = schema.decode_with_registry(note.storage(), &codecs).unwrap(); + assert_eq!( + decoded.field("target").unwrap().to_string(), + recipient_id.to_bech32(NetworkId::Mainnet) + ); + assert_eq!(decoded.field("price").unwrap().to_string(), "1.5"); +} + +#[test] +fn p2id_note_builds_storage_without_a_component_codec() { + let note_package = compile_rust_package("../../examples/p2id-note", true); + let schema = NoteStorageSchema::from_package(¬e_package).unwrap(); + + let (note, recipient_id) = transfer_with_storage(note_package, |recipient_id| { + schema + .builder() + .set("target-account-id", recipient_id.to_bech32(NetworkId::Mainnet)) + .unwrap() + .build() + .unwrap() + }); + + let decoded = schema.decode(note.storage()).unwrap(); + assert_eq!( + decoded.field("target_account_id").unwrap().to_string(), + recipient_id.to_bech32(NetworkId::Mainnet) + ); +} + +/// Asserts that a package carries one named custom section. +fn assert_package_section(package: &Package, id: SectionId, name: &str) { + assert!( + package.sections.iter().any(|section| section.id == id), + "package does not contain the `{name}` section" + ); +} diff --git a/tests/integration-network/src/mockchain/support/helpers.rs b/tests/integration-network/src/mockchain/support/helpers.rs index 9d058d6354..187538ddcc 100644 --- a/tests/integration-network/src/mockchain/support/helpers.rs +++ b/tests/integration-network/src/mockchain/support/helpers.rs @@ -17,6 +17,7 @@ use miden_client::{ use miden_core::Felt; use miden_field_repr::{FromFeltRepr, ToFeltRepr}; use miden_mast_package::{Package, TargetType}; +use miden_note_schema::NoteStorageSchema; use miden_protocol::{ account::{ Account, AccountBuilder, AccountComponent, AccountId, AccountStorage, AccountType, @@ -30,7 +31,7 @@ use miden_standards::{testing::note::NoteBuilder, tx_script::SendNotesTransactio use miden_testing::{MockChain, MockTransaction, MockTransactionBuilder}; use miden_tx_script_args::{EncodedScriptArgs, ScriptArgs}; use midenc_frontend_wasm::WasmTranslationConfig; -use midenc_integration_test_support::CompilerTestBuilder; +use midenc_integration_test_support::{CompilerTestBuilder, example_build_lock, workspace_root}; use rand::{SeedableRng, rngs::StdRng}; /// Host-side mirror of the transaction-script arguments declared in @@ -114,6 +115,7 @@ pub(crate) fn block_on(future: F) -> F::Output { /// Compiles a Rust project and returns its Miden package. pub(crate) fn compile_rust_package(project_path: impl AsRef, release: bool) -> Arc { + let _build_lock = example_build_lock(&workspace_root()); let project_path = project_path.as_ref(); let config = WasmTranslationConfig::default(); let mut builder = CompilerTestBuilder::rust_source_cargo_miden(project_path, config, []); @@ -288,10 +290,18 @@ pub(crate) fn build_asset_transfer_tx( let faucet_id = asset.faucet_id(); let asset: Asset = asset.into(); + let schema = NoteStorageSchema::from_package(&p2id_note_package) + .expect("p2id note package should contain a storage schema"); + let note_storage = schema + .builder() + .set("target-account-id", recipient_id.to_hex()) + .expect("recipient account ID should match the p2id schema") + .build() + .expect("p2id schema should produce valid note storage"); let output_note = NoteBuilder::new(sender_id, rng) .serial_number(serial_num) .package((*p2id_note_package).clone()) - .note_storage(to_core_felts(&recipient_id)) + .note_storage(note_storage.to_elements()) .unwrap() .add_assets([asset]) .tag(0) diff --git a/tests/integration-network/src/mockchain/swapp.rs b/tests/integration-network/src/mockchain/swapp.rs index 1d3b26e458..92caefa2b4 100644 --- a/tests/integration-network/src/mockchain/swapp.rs +++ b/tests/integration-network/src/mockchain/swapp.rs @@ -297,7 +297,7 @@ fn assert_no_fungible_asset(account: &Account, faucet_id: AccountId) { #[test] fn swapp_note_package_size() { let packages = compile_swapp_packages(); - expect!["42586"].assert_eq(stripped_mast_size_str(packages.swapp.as_ref()).as_str()); + expect!["42620"].assert_eq(stripped_mast_size_str(packages.swapp.as_ref()).as_str()); } /// Tests a full fill of a SWAPP note. @@ -353,7 +353,7 @@ fn swapp_note_full_fill_transfers_assets() { vec![p2id_note.id()], "full fill must create exactly the P2ID routing note" ); - expect!["12839"].assert_eq(single_note_cycles(executed_tx.measurements())); + expect!["12849"].assert_eq(single_note_cycles(executed_tx.measurements())); let bob_account = chain.committed_account(bob.id()).unwrap(); assert_account_has_fungible_asset(bob_account, usdc_faucet.id(), 50); @@ -440,7 +440,7 @@ fn swapp_note_partial_fill_creates_remainder_and_chains() { vec![first_p2id_note.id(), remainder_note.id()], "partial fill must create the P2ID routing note and the remainder note" ); - expect!["17931"].assert_eq(single_note_cycles(executed_tx.measurements())); + expect!["17941"].assert_eq(single_note_cycles(executed_tx.measurements())); let bob_account = chain.committed_account(bob.id()).unwrap(); assert_account_has_fungible_asset(bob_account, usdc_faucet.id(), 3); @@ -521,7 +521,7 @@ fn swapp_note_creator_reclaims_offered_asset() { output_note_ids(&executed_tx).is_empty(), "reclaiming the swap note must not create any output notes" ); - expect!["5573"].assert_eq(single_note_cycles(executed_tx.measurements())); + expect!["5583"].assert_eq(single_note_cycles(executed_tx.measurements())); let alice_account = chain.committed_account(alice.id()).unwrap(); assert_account_has_fungible_asset(alice_account, usdc_faucet.id(), 50); diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index a3944ce901..cc59df41a2 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -50,3 +50,5 @@ concat-idents = "1.1" libloading = "0.8" wasmi = "1.1.0" wat.workspace = true +wit-bindgen-core.workspace = true +midenc-frontend-wasm-metadata.workspace = true diff --git a/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs b/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs index 27b608044d..5a72d4695d 100644 --- a/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs +++ b/tests/integration/src/end_to_end/examples/basic_wallet_package_sizes.rs @@ -1,41 +1,23 @@ -use midenc_expect_test::expect; -use midenc_frontend_wasm::WasmTranslationConfig; +use std::path::Path; -use crate::{CompilerTest, testing::stripped_mast_size_str}; +use midenc_expect_test::expect; +use midenc_integration_test_support::compile_project; -fn no_debug_flags() -> [String; 2] { - ["--debug".to_string(), "none".to_string()] -} +use crate::testing::stripped_mast_size_str; #[test] fn basic_wallet_and_p2id() { - let config = WasmTranslationConfig::default(); - let mut account_test = CompilerTest::rust_source_cargo_miden( - "../../examples/basic-wallet", - config.clone(), - no_debug_flags(), - ); - let account_package = account_test.compile_package(); + let account_package = compile_project(Path::new("../../examples/basic-wallet")); assert!(account_package.is_library(), "expected library"); expect!["8505"].assert_eq(stripped_mast_size_str(&account_package).as_str()); - let mut tx_script_test = CompilerTest::rust_source_cargo_miden( - "../../examples/basic-wallet-tx-script", - config.clone(), - no_debug_flags(), - ); - let tx_script_package = tx_script_test.compile_package(); + let tx_script_package = compile_project(Path::new("../../examples/basic-wallet-tx-script")); assert!(tx_script_package.is_library(), "expected library"); expect!["13784"].assert_eq(stripped_mast_size_str(&tx_script_package).as_str()); - let mut p2id_test = CompilerTest::rust_source_cargo_miden( - "../../examples/p2id-note", - config.clone(), - no_debug_flags(), - ); - let note_package = p2id_test.compile_package(); + let note_package = compile_project(Path::new("../../examples/p2id-note")); assert!(note_package.is_library(), "expected library"); - expect!["21763"].assert_eq(stripped_mast_size_str(¬e_package).as_str()); + expect!["21797"].assert_eq(stripped_mast_size_str(¬e_package).as_str()); // The note package exports both the note script and the `build-recipient` constructor; the // constructor must not interfere with the `@note_script`-attributed export selection. assert!( @@ -45,12 +27,7 @@ fn basic_wallet_and_p2id() { miden_protocol::note::NoteScript::from_package(¬e_package) .expect("expected the p2id note package to contain exactly one note script export"); - let mut p2ide_test = CompilerTest::rust_source_cargo_miden( - "../../examples/p2ide-note", - config, - no_debug_flags(), - ); - let p2ide_package = p2ide_test.compile_package(); + let p2ide_package = compile_project(Path::new("../../examples/p2ide-note")); assert!(p2ide_package.is_library(), "expected library"); - expect!["16402"].assert_eq(stripped_mast_size_str(&p2ide_package).as_str()); + expect!["16436"].assert_eq(stripped_mast_size_str(&p2ide_package).as_str()); } diff --git a/tests/integration/src/end_to_end/examples/mod.rs b/tests/integration/src/end_to_end/examples/mod.rs index 2ff4a3adce..a0ea501de6 100644 --- a/tests/integration/src/end_to_end/examples/mod.rs +++ b/tests/integration/src/end_to_end/examples/mod.rs @@ -7,4 +7,5 @@ mod counter_metadata; mod counter_note; mod fibonacci; mod is_prime; +mod note_schema_metadata; mod storage_metadata; diff --git a/tests/integration/src/end_to_end/examples/note_schema_metadata.rs b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs new file mode 100644 index 0000000000..948cdbd415 --- /dev/null +++ b/tests/integration/src/end_to_end/examples/note_schema_metadata.rs @@ -0,0 +1,250 @@ +//! Integration tests for note storage schemas stored in Miden packages. + +use miden_mast_package::Package; +use midenc_expect_test::{Expect, expect}; +use midenc_frontend_wasm_metadata::{package_note_storage_schema_section_id, trim_trailing_nuls}; +use midenc_integration_test_support::{compile_project, workspace_root}; +use wit_bindgen_core::wit_parser::{Resolve, Type as WitType, TypeDefKind}; + +/// Shared SDK core-types package appended to every generated note schema. +const EXPECTED_CORE_TYPES: &str = concat!( + r#"package miden:base@1.0.0 { + interface core-types { + /// Represents an on-chain felt. + /// + /// Field modulus M = 2^64 - 2^32 + 1. + record felt { + /// The backing type is `f32` which will be treated as a felt by the compiler. +"#, + " \t/// We're basically hijacking the Wasm `f32` type and treat as felt.\n", + r#" inner: f32, + } + + + /// A group of four field elements in the Miden base field. + record word { + a: felt, +"#, + " \tb: felt,\n", + " \tc: felt,\n", + " \td: felt,\n", + r#" } + + /// A cryptographic digest representing a 256-bit hash value. + /// This is a wrapper around `word` which contains 4 field elements. + record digest { + inner: word + } + + /// Unique identifier of an account. + /// + /// # Layout + /// + /// An `AccountId` consists of two field elements, where the first is called the prefix and the + /// second is called the suffix. It is laid out as follows: + /// + /// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)] + /// suffix: [zero bit | hash (55 bits) | 8 zero bits] + record account-id { +"#, + " \tprefix: felt,\n", + " \tsuffix: felt\n", + r#" } + + /// Recipient of the note, i.e., hash(hash(hash(serial_num, [0; 4]), note_script_hash), input_hash) + record recipient { + inner: word + } + + record tag { + inner: felt + } + + /// A fungible or a non-fungible asset. + /// + /// In protocol v0.14 assets are encoded as two words: an asset key and an asset value. + /// + /// The methodology for constructing fungible and non-fungible assets is described below. + /// + /// # Fungible assets + /// - `key`: `[0, 0, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `[amount, 0, 0, 0]` + /// + /// # Non-fungible assets + /// - `key`: `[hash0, hash1, faucet_id_suffix, faucet_id_prefix]` + /// - `value`: `DATA_HASH` + record asset { + key: word, + value: word, + } + + /// A validated fungible asset amount, at most 2^63 - 2^31. + record asset-amount { + inner: felt + } + + /// Account nonce + record nonce { + inner: felt + } + + /// A block height in the chain + record block-number { + inner: felt + } + + /// Account hash + record account-hash { + inner: word + } + + /// Block hash + record block-hash { + inner: word + } + + /// Storage value + record storage-value { + inner: word + } + + /// Account storage root + record storage-root { + inner: word + } + + /// Account code root + record account-code-root { + inner: word + } + + /// Commitment to the account vault + record vault-commitment { + inner: word + } + + /// An index of the created note + record note-idx { + inner: felt + } + + record note-type { + inner: felt + } + + record note-execution-hint { + inner: felt + } + + } +} +"#, +); + +/// Returns the unpadded note storage schema text from a package. +fn note_storage_schema(package: &Package) -> &str { + let section_id = package_note_storage_schema_section_id(); + let bytes = package + .sections + .iter() + .find(|section| section.id == section_id) + .expect("package must contain a note storage schema") + .data + .as_ref(); + assert_eq!(bytes.len() % 16, 0, "schema payload must use 16-byte padding"); + str::from_utf8(trim_trailing_nuls(bytes)).expect("note storage schema must be UTF-8") +} + +/// Checks a schema golden and resolves its root storage alias with wit-parser. +fn assert_note_storage_schema(package: &Package, expected_root: &str, expected: Expect) { + let source = note_storage_schema(package); + let core_types_start = source + .find("package miden:base@1.0.0 {") + .expect("schema must contain the SDK core-types package"); + let (schema, core_types) = source.split_at(core_types_start); + expected.assert_eq(schema); + assert_eq!(core_types, EXPECTED_CORE_TYPES); + + let mut resolve = Resolve::default(); + let package_id = resolve + .push_str("note-schema.wit", source) + .expect("note storage schema must resolve"); + let schema_package = &resolve.packages[package_id]; + let interface_id = schema_package.interfaces["note-storage"]; + let interface = &resolve.interfaces[interface_id]; + let storage_id = interface.types["storage"]; + let TypeDefKind::Type(WitType::Id(root_id)) = resolve.types[storage_id].kind else { + panic!("storage must be a named type alias"); + }; + assert_eq!(resolve.types[root_id].name.as_deref(), Some(expected_root)); +} + +#[test] +fn note_packages_carry_resolvable_storage_schema_metadata() { + let workspace = workspace_root(); + let p2id = compile_project(&workspace.join("examples/p2id-note")); + assert_note_storage_schema( + &p2id, + "p2id-note", + expect![[r#" + // This file is auto-generated by the `#[note]` macro. + // Do not edit this file manually. + + package miden:p2id-schema@0.1.0; + + use miden:base/core-types@1.0.0; + + interface note-storage { + use core-types.{account-id}; + + record p2id-note { + target-account-id: account-id, + } + + type storage = p2id-note; + } + + "#]], + ); + + let swapp = compile_project(&workspace.join("tests/fixtures/components/swapp-note")); + assert_note_storage_schema( + &swapp, + "swapp-note", + expect![[r#" + // This file is auto-generated by the `#[note]` macro. + // Do not edit this file manually. + + package miden:swapp-note-schema@0.1.0; + + use miden:base/core-types@1.0.0; + + interface note-storage { + use core-types.{account-id, felt, word}; + + /// SWAPP note storage. + /// + /// The note creator stores the swap terms in the note storage; the fields below are decoded + /// from the storage elements in declaration order. + record swapp-note { + /// Vault key identifying the requested asset (faucet id, composition, callback flags). + requested-asset-key: word, + /// Total requested asset amount for the full offer. + requested-total: felt, + /// The account that created the swap offer and receives the requested asset. + creator: account-id, + /// Note type used for the notes created by this script (P2ID routing note and remainder + /// SWAPP note). + output-note-type: felt, + /// Tag routing the P2ID note to the creator. + p2id-tag: felt, + /// Script root of the P2ID note script used for the routing note. + p2id-script-root: word, + } + + type storage = swapp-note; + } + + "#]], + ); +} diff --git a/tests/integration/src/sdk/build_script.rs b/tests/integration/src/sdk/build_script.rs index 85409ac0d2..f4d251d268 100644 --- a/tests/integration/src/sdk/build_script.rs +++ b/tests/integration/src/sdk/build_script.rs @@ -19,6 +19,8 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use midenc_integration_test_support::scrub_nested_cargo_env; + use super::basic_wallet_swapp_note_project; use crate::cargo_proj::project; @@ -179,21 +181,23 @@ fn main() { /// concurrent tests reuse the fixture package names, so a shared cache would /// let one test observe another's generations. fn plain_cargo_check(consumer: &Path) -> Output { - Command::new("cargo") + let mut command = Command::new("cargo"); + scrub_nested_cargo_env(&mut command); + command .arg("check") .env("CARGO_MIDEN", cargo_miden_binary()) .env_remove("MIDENC_PACKAGE_CACHE") .env_remove("CARGO_TARGET_DIR") .env_remove("CARGO_BUILD_BUILD_DIR") - .env_remove("RUSTFLAGS") - .env_remove("CARGO_ENCODED_RUSTFLAGS") .current_dir(consumer) .output() .expect("failed to spawn cargo check") } fn counted_plain_cargo_check(consumer: &Path, counter: &Path) -> Output { - Command::new("cargo") + let mut command = Command::new("cargo"); + scrub_nested_cargo_env(&mut command); + command .arg("check") .env("CARGO_MIDEN", counting_cargo_miden_binary()) .env("MIDENC_TEST_REAL_CARGO_MIDEN", cargo_miden_binary()) @@ -201,8 +205,6 @@ fn counted_plain_cargo_check(consumer: &Path, counter: &Path) -> Output { .env_remove("MIDENC_PACKAGE_CACHE") .env_remove("CARGO_TARGET_DIR") .env_remove("CARGO_BUILD_BUILD_DIR") - .env_remove("RUSTFLAGS") - .env_remove("CARGO_ENCODED_RUSTFLAGS") .current_dir(consumer) .output() .expect("failed to spawn counted cargo check") @@ -686,7 +688,9 @@ path = "src/lib.rs" .file("src/lib.rs", "") .build(); - let output = std::process::Command::new("cargo") + let mut command = Command::new("cargo"); + scrub_nested_cargo_env(&mut command); + let output = command .arg("check") .env("CARGO_MIDEN", project.root().join("definitely-missing-cargo-miden")) .env_remove("MIDENC_PACKAGE_CACHE") diff --git a/tests/integration/src/sdk/macros.rs b/tests/integration/src/sdk/macros.rs index 55d990ea31..d6ebbf9816 100644 --- a/tests/integration/src/sdk/macros.rs +++ b/tests/integration/src/sdk/macros.rs @@ -1,16 +1,19 @@ use std::panic::{self, AssertUnwindSafe}; +use midenc_integration_test_support::scrub_nested_cargo_env; + use super::*; fn cargo_check_miden_target(project: &crate::cargo_proj::Project) -> std::process::Output { - std::process::Command::new("cargo") + let mut command = std::process::Command::new("cargo"); + // Scrub first: the helper also clears `RUSTFLAGS`, which this build sets below. Cargo prefers + // the encoded variable, so an inherited value would silently replace those flags. + scrub_nested_cargo_env(&mut command); + command .arg("check") .arg("--target") .arg("wasm32-wasip2") .env("RUSTFLAGS", "--cfg miden -C target-feature=+bulk-memory,+wide-arithmetic") - // Cargo prefers the encoded variable; an inherited value would silently replace the - // `RUSTFLAGS` set above. - .env_remove("CARGO_ENCODED_RUSTFLAGS") // The macros read dependency packages only from this directory, the way a driven build // or the contract build script exposes it. .env( diff --git a/tests/integration/src/sdk/mod.rs b/tests/integration/src/sdk/mod.rs index b099b11df1..5975a89f3d 100644 --- a/tests/integration/src/sdk/mod.rs +++ b/tests/integration/src/sdk/mod.rs @@ -5,6 +5,7 @@ use miden_core::serde::Serializable; use miden_mast_package::{Package, PackageExport, ProcedureExport, QualifiedProcedureName}; use miden_protocol::note::NoteScript; use midenc_frontend_wasm::WasmTranslationConfig; +use midenc_integration_test_support::{scrub_nested_cargo_env, write_masp_file_atomic}; use crate::{ CompilerTest, CompilerTestBuilder, @@ -354,7 +355,11 @@ fn build_consumer_wat_with_package_cache( cargo_target_dir: &Path, package_cache_dir: &Path, ) -> String { - let output = std::process::Command::new("cargo") + let mut command = std::process::Command::new("cargo"); + // Scrub first: the helper also clears `RUSTFLAGS`, which this build sets below. Cargo prefers + // the encoded variable, so an inherited value would silently replace those flags. + scrub_nested_cargo_env(&mut command); + let output = command .args(["build", "--release", "--locked", "--manifest-path"]) .arg(consumer.join("Cargo.toml")) .env("CARGO_TARGET_DIR", cargo_target_dir) @@ -363,7 +368,6 @@ fn build_consumer_wat_with_package_cache( package_cache_dir, ) .env("RUSTFLAGS", "--cfg miden -C target-feature=+bulk-memory,+wide-arithmetic") - .env_remove("CARGO_ENCODED_RUSTFLAGS") .current_dir(consumer) .output() .expect("failed to spawn Cargo for the option_env isolation fixture"); @@ -908,11 +912,9 @@ fn rust_sdk_fpi_reexpands_after_only_package_cache_env_changes() { for cache in [&first_cache, &second_cache] { fs::create_dir_all(cache).unwrap(); } - first_package - .write_masp_file(&first_cache) + write_masp_file_atomic(&first_package, &first_cache) .expect("failed to prepopulate the first package cache"); - second_package - .write_masp_file(&second_cache) + write_masp_file_atomic(&second_package, &second_cache) .expect("failed to prepopulate the second package cache"); let cargo_target_dir = project.root().join("option-env-cargo-target"); diff --git a/tests/support/src/lib.rs b/tests/support/src/lib.rs index 4ae60063d0..1e3fe26c4d 100644 --- a/tests/support/src/lib.rs +++ b/tests/support/src/lib.rs @@ -2,6 +2,17 @@ #![deny(warnings)] #![deny(missing_docs)] +use std::{ + fs::{self, File}, + path::{Path, PathBuf}, + process::Command, + sync::Arc, +}; + +use miden_mast_package::Package; +use midenc_frontend_wasm::WasmTranslationConfig; +use midenc_frontend_wasm_metadata::NESTED_CARGO_SCRUB_ENV; + /// Utilities for generating on-disk Cargo projects for tests. pub mod cargo_proj; /// Compiler test builders and pipeline assertions. @@ -20,3 +31,73 @@ pub use self::{ compiler_test::{CargoTest, CompilerTest, CompilerTestBuilder, RustcTest, WasmTest}, testing::setup::default_session, }; + +/// Compiles one Cargo Miden project without debug output. +pub fn compile_project(project_path: &Path) -> Arc { + let _build_lock = example_build_lock(&workspace_root()); + let mut test = CompilerTest::rust_source_cargo_miden( + project_path, + WasmTranslationConfig::default(), + ["--debug".to_owned(), "none".to_owned()], + ); + test.compile_package() +} + +/// Returns the compiler workspace root. +pub fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap() +} + +/// Locks shared example outputs while one build is running. +pub fn example_build_lock(workspace: &Path) -> File { + let target_dir = workspace.join("target"); + fs::create_dir_all(&target_dir).expect("failed to create the workspace target directory"); + let lock = File::options() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(target_dir.join("example-build.lock")) + .expect("failed to open the example build lock"); + lock.lock().expect("failed to lock example builds"); + lock +} + +/// Writes a Miden package through the production atomic package publisher. +pub fn write_masp_file_atomic( + package: &Package, + output_dir: impl AsRef, +) -> std::io::Result<()> { + midenc_session::registry::write_package_atomically(package, output_dir.as_ref()) + .map(|_| ()) + .map_err(std::io::Error::other) +} + +/// Removes outer build settings that would poison a nested Cargo invocation. +pub fn scrub_nested_cargo_env(cmd: &mut Command) { + for &variable in NESTED_CARGO_SCRUB_ENV { + cmd.env_remove(variable); + } +} + +/// Returns true when the active Rust sysroot contains the codec component target. +pub fn wasm_target_is_installed() -> bool { + const WASM_TARGET: &str = "wasm32-wasip2"; + + let output = match Command::new("rustc").args(["--print", "sysroot"]).output() { + Ok(output) if output.status.success() => output, + Ok(output) => { + eprintln!( + "`rustc --print sysroot` failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + return false; + } + Err(error) => { + eprintln!("could not run `rustc --print sysroot` (rustup may be unavailable): {error}"); + return false; + } + }; + let sysroot = Path::new(String::from_utf8_lossy(&output.stdout).trim()).to_path_buf(); + sysroot.join("lib").join("rustlib").join(WASM_TARGET).exists() +} diff --git a/tools/cargo-miden/Cargo.toml b/tools/cargo-miden/Cargo.toml index da30d8303b..fcb3bc4e19 100644 --- a/tools/cargo-miden/Cargo.toml +++ b/tools/cargo-miden/Cargo.toml @@ -57,3 +57,10 @@ walkdir = "2.5" serde_json.workspace = true # Verifies a downloaded template bundle against the digest GitHub reports. sha2.workspace = true + +[dev-dependencies] +miden-mast-package = { workspace = true, features = ["std"] } +midenc-frontend-wasm-metadata.workspace = true +midenc-integration-test-support.workspace = true +wit-component.workspace = true +wit-parser.workspace = true diff --git a/tools/cargo-miden/templates.tar.gz b/tools/cargo-miden/templates.tar.gz index 3c60cb91b1..ca921b5fa5 100644 Binary files a/tools/cargo-miden/templates.tar.gz and b/tools/cargo-miden/templates.tar.gz differ diff --git a/tools/cargo-miden/tests/dex_note_codec_build.rs b/tools/cargo-miden/tests/dex_note_codec_build.rs new file mode 100644 index 0000000000..92cd0036f8 --- /dev/null +++ b/tools/cargo-miden/tests/dex_note_codec_build.rs @@ -0,0 +1,112 @@ +//! Integration test for DEX note schema and codec package sections. + +use std::env; + +use cargo_miden::run; +use miden_mast_package::Package; +use midenc_frontend_wasm_metadata::{ + package_note_codec_section_id, package_note_storage_schema_section_id, +}; +use midenc_integration_test_support::{ + example_build_lock, wasm_target_is_installed, workspace_root, +}; +use wit_component::DecodedWasm; +use wit_parser::WorldItem; + +use crate::utils::{RestoreEnvironment, current_dir_lock}; + +#[test] +fn dex_note_build_embeds_schema_and_wasi_only_codec_component() { + if !wasm_target_is_installed() { + eprintln!("skipping DEX note codec build test: wasm32-wasip2 is not installed"); + return; + } + // The command reads the process working directory, so serialize cwd changes. + let _cwd_lock = current_dir_lock(); + let _ = midenc_log::Builder::from_env("MIDENC_TRACE") + .is_test(true) + .format_timestamp(None) + .try_init(); + + // Clear the outer override. The example package then uses its own target layout. + let _restore_environment = RestoreEnvironment::new(["CARGO_TARGET_DIR"]); + unsafe { + env::remove_var("CARGO_TARGET_DIR"); + } + + let workspace = workspace_root(); + let note_dir = workspace.join("examples/dex-note"); + env::set_current_dir(¬e_dir).unwrap(); + let result = { + let _build_lock = example_build_lock(&workspace); + run(["cargo", "miden", "build", "--release"].into_iter().map(str::to_owned)) + }; + + let output = result + .expect("cargo miden build for dex-note failed") + .expect("expected BuildCommandOutput") + .unwrap_build_output(); + let codec_work_dir = workspace.join("examples/dex-note-codec/target/midenc.note-codec"); + assert!( + codec_work_dir.join("package-cache").is_dir(), + "the codec package cache is outside the codec crate" + ); + assert!( + codec_work_dir.join("cargo-target").is_dir(), + "the codec Cargo target directory is outside the codec crate" + ); + assert_eq!(output.len(), 1, "expected one dex-note package artifact, got {output:?}"); + let package = Package::deserialize_from_file(&output[0]) + .expect("failed to read the built dex-note package"); + + let schema_id = package_note_storage_schema_section_id(); + assert!( + package.sections.iter().any(|section| section.id == schema_id), + "dex-note package has no note storage schema section" + ); + + let codec_id = package_note_codec_section_id(); + let codec = package + .sections + .iter() + .find(|section| section.id == codec_id) + .expect("dex-note package has no note codec section"); + assert_note_codec_component(codec.data.as_ref()); +} + +/// Verifies the sandbox and versioned interface exported by a note codec component. +fn assert_note_codec_component(component: &[u8]) { + let DecodedWasm::Component(resolve, world_id) = + wit_component::decode(component).expect("note codec section is not valid component bytes") + else { + panic!("note codec section is not a component"); + }; + let world = &resolve.worlds[world_id]; + for (key, _) in world.imports.iter() { + let name = resolve.name_world_key(key); + assert!(name.starts_with("wasi:"), "unexpected non-WASI import `{name}`"); + } + assert_eq!(world.exports.len(), 1, "unexpected note codec exports: {:#?}", world.exports); + + let interface = world + .exports + .values() + .find_map(|item| { + let WorldItem::Interface { id, .. } = item else { + return None; + }; + let interface = &resolve.interfaces[*id]; + let package_id = interface.package?; + let package = &resolve.packages[package_id].name; + (interface.name.as_deref() == Some("codec") + && package.namespace == "miden" + && package.name == "note-codec" + && package.version.as_ref().is_some_and(|version| version.to_string() == "1.0.0")) + .then_some(interface) + }) + .expect("component does not export `miden:note-codec/codec@1.0.0`"); + assert_eq!( + interface.functions.keys().map(String::as_str).collect::>(), + ["supported-types", "parse", "display", "validate"] + ); +} diff --git a/tools/cargo-miden/tests/dex_note_dependency_build.rs b/tools/cargo-miden/tests/dex_note_dependency_build.rs new file mode 100644 index 0000000000..bc99e5d880 --- /dev/null +++ b/tools/cargo-miden/tests/dex_note_dependency_build.rs @@ -0,0 +1,169 @@ +//! Integration test for a note that is built as a dependency of another project. +//! +//! A note carries its author codec only when it is the root target of the build. A note that +//! another project pulls in as a source dependency gets the note storage schema, but no codec +//! section: the codec crate's inputs are outside the build provenance, so a stored dependency +//! copy could keep a stale codec, and every dependency assembly would otherwise run a nested +//! cargo build. + +use std::{env, fs, path::Path}; + +use cargo_miden::run; +use miden_mast_package::Package; +use midenc_frontend_wasm_metadata::{ + package_note_codec_section_id, package_note_storage_schema_section_id, +}; +use midenc_integration_test_support::{ + example_build_lock, wasm_target_is_installed, workspace_root, +}; + +use crate::utils::{RestoreEnvironment, current_dir_lock, with_package_cache_env}; + +#[test] +fn dex_note_built_as_a_dependency_has_the_schema_but_no_codec() { + if !wasm_target_is_installed() { + eprintln!("skipping DEX note dependency build test: wasm32-wasip2 is not installed"); + return; + } + // The command reads the process working directory, so serialize cwd changes. + let _cwd_lock = current_dir_lock(); + let _ = midenc_log::Builder::from_env("MIDENC_TRACE") + .is_test(true) + .format_timestamp(None) + .try_init(); + + // Clear the outer override. The scratch project then uses its own target layout. + let _restore_environment = RestoreEnvironment::new(["CARGO_TARGET_DIR"]); + unsafe { + env::remove_var("CARGO_TARGET_DIR"); + } + + let workspace = workspace_root(); + let project_dir = tempfile::tempdir().expect("failed to create the dependent project dir"); + let dependent = project_dir.path().join("counter-contract"); + scaffold_dependent_project(&workspace, &dependent); + + // A caller-provided package cache is adopted and left in place, so the dependency packages + // this build publishes can be read after the compiler exits. + let cache_dir = tempfile::tempdir().expect("failed to create the package cache dir"); + let export_dir = cache_dir.path().to_path_buf(); + + env::set_current_dir(&dependent).unwrap(); + let result = { + let _build_lock = example_build_lock(&workspace); + with_package_cache_env(&export_dir, || { + run(["cargo", "miden", "build", "--release"].into_iter().map(str::to_owned)) + }) + }; + + let output = result + .expect("cargo miden build for the dex-note dependent failed") + .expect("expected BuildCommandOutput") + .unwrap_build_output(); + assert_eq!(output.len(), 1, "expected one dependent package artifact, got {output:?}"); + assert!(output[0].exists(), "the dependent package was not written to {:?}", output[0]); + + let dependency_package = export_dir.join("dex-note.masp"); + assert!( + dependency_package.exists(), + "expected the dex-note dependency package at {}; the cache holds {:?}", + dependency_package.display(), + exported_file_names(&export_dir) + ); + let package = Package::deserialize_from_file(&dependency_package) + .expect("failed to read the dex-note dependency package"); + + let schema_id = package_note_storage_schema_section_id(); + assert!( + package.sections.iter().any(|section| section.id == schema_id), + "the dex-note dependency package has no note storage schema section" + ); + let codec_id = package_note_codec_section_id(); + assert!( + !package.sections.iter().any(|section| section.id == codec_id), + "the dex-note dependency package must carry no note codec section" + ); +} + +/// Copies the counter-contract example to `destination` and makes it depend on `dex-note`. +/// +/// The copy is built outside the compiler workspace, so every relative path in its manifests is +/// rewritten to the workspace copy it names. +fn scaffold_dependent_project(workspace: &Path, destination: &Path) { + let source = workspace.join("examples/counter-contract"); + copy_project(&source, destination); + + for manifest in ["Cargo.toml", "miden-project.toml"] { + let path = destination.join(manifest); + let rewritten = absolutize_manifest_paths(&fs::read_to_string(&path).unwrap(), &source); + fs::write(&path, rewritten).unwrap(); + } + + let manifest = destination.join("miden-project.toml"); + let contents = fs::read_to_string(&manifest).unwrap(); + let dex_note = workspace.join("examples/dex-note"); + let with_dependency = contents.replace( + "[dependencies]\n", + &format!("[dependencies]\ndex-note = {{ path = \"{}\" }}\n", dex_note.display()), + ); + assert_ne!(with_dependency, contents, "the example manifest has no `[dependencies]` table"); + fs::write(&manifest, with_dependency).unwrap(); +} + +/// Copies a project directory, skipping build outputs. +fn copy_project(source: &Path, destination: &Path) { + fs::create_dir_all(destination).unwrap(); + for entry in fs::read_dir(source).unwrap() { + let entry = entry.unwrap(); + let name = entry.file_name(); + if name == "target" { + continue; + } + let target = destination.join(&name); + if entry.file_type().unwrap().is_dir() { + copy_project(&entry.path(), &target); + } else { + fs::copy(entry.path(), &target).unwrap(); + } + } +} + +/// Rewrites every relative `path = "..."` value to the location it names under `manifest_dir`. +fn absolutize_manifest_paths(manifest: &str, manifest_dir: &Path) -> String { + const KEY: &str = "path = \""; + + let mut out = String::with_capacity(manifest.len()); + let mut rest = manifest; + while let Some(start) = rest.find(KEY) { + let (head, tail) = rest.split_at(start + KEY.len()); + out.push_str(head); + let end = tail.find('"').expect("unterminated path value in the manifest"); + let (value, tail) = tail.split_at(end); + let path = Path::new(value); + if path.is_absolute() { + out.push_str(value); + } else { + let resolved = manifest_dir.join(path); + // `src/lib.rs` and the like must stay relative to the copy, not point back to it. + match resolved.canonicalize() { + Ok(resolved) if !resolved.starts_with(manifest_dir) => { + out.push_str(&resolved.display().to_string()) + } + _ => out.push_str(value), + } + } + rest = tail; + } + out.push_str(rest); + out +} + +/// The file names the build left in the package cache, for assertion messages. +fn exported_file_names(export_dir: &Path) -> Vec { + let Ok(entries) = fs::read_dir(export_dir) else { + return Vec::new(); + }; + entries + .filter_map(|entry| Some(entry.ok()?.file_name().to_string_lossy().into_owned())) + .collect() +} diff --git a/tools/cargo-miden/tests/mod.rs b/tools/cargo-miden/tests/mod.rs index 7f555af08a..4b0760f623 100755 --- a/tools/cargo-miden/tests/mod.rs +++ b/tools/cargo-miden/tests/mod.rs @@ -1,3 +1,5 @@ +mod dex_note_codec_build; +mod dex_note_dependency_build; mod masm_dependency; mod p2id_cargo_miden_build; mod target_dir; diff --git a/tools/cargo-miden/tests/p2id_cargo_miden_build.rs b/tools/cargo-miden/tests/p2id_cargo_miden_build.rs index c488f14f67..8176109d41 100644 --- a/tools/cargo-miden/tests/p2id_cargo_miden_build.rs +++ b/tools/cargo-miden/tests/p2id_cargo_miden_build.rs @@ -1,8 +1,9 @@ use std::{env, time::SystemTime}; use cargo_miden::run; +use midenc_integration_test_support::{example_build_lock, workspace_root}; -use crate::utils::{current_dir_lock, workspace_root}; +use crate::utils::current_dir_lock; /// A caller-provided `MIDENC_PACKAGE_CACHE` materializes a build's Miden dependencies on disk. /// @@ -27,7 +28,8 @@ fn p2id_build_materializes_basic_wallet_dependency() { env::remove_var("CARGO_TARGET_DIR"); } - let examples = workspace_root().join("examples"); + let workspace = workspace_root(); + let examples = workspace.join("examples"); let p2id_note_dir = examples.join("p2id-note"); // Build the p2id-note project, which pulls in basic-wallet as a Miden dependency. @@ -35,9 +37,12 @@ fn p2id_build_materializes_basic_wallet_dependency() { env::set_current_dir(&p2id_note_dir).unwrap(); let build_started_at = SystemTime::now(); let export_dir = crate::utils::exported_packages_dir(&p2id_note_dir); - let result = crate::utils::with_package_cache_env(&export_dir, || { - run(["cargo", "miden", "build", "--release"].into_iter().map(|s| s.to_string())) - }); + let result = { + let _build_lock = example_build_lock(&workspace); + crate::utils::with_package_cache_env(&export_dir, || { + run(["cargo", "miden", "build", "--release"].into_iter().map(|s| s.to_string())) + }) + }; env::set_current_dir(&restore_dir).unwrap(); // Restore `CARGO_TARGET_DIR` before asserting, so a build failure doesn't leak the unset state. diff --git a/tools/cargo-miden/tests/target_dir.rs b/tools/cargo-miden/tests/target_dir.rs index b2e34ac847..25e88c68f3 100644 --- a/tools/cargo-miden/tests/target_dir.rs +++ b/tools/cargo-miden/tests/target_dir.rs @@ -2,7 +2,7 @@ use std::{env, fs}; use cargo_miden::run; -use crate::utils::{current_dir_lock, project_template_arg}; +use crate::utils::{RestoreEnvironment, current_dir_lock, project_template_arg}; /// A custom Midenc target is an umbrella for every artifact the compiler-owned build writes. /// @@ -11,35 +11,13 @@ use crate::utils::{current_dir_lock, project_template_arg}; /// manifest frontend redirects its nested Cargo invocation beneath the custom target. #[test] fn a_custom_midenc_target_contains_nested_cargo_artifacts() { - struct RestoreEnvironment { - cargo_target_dir: Option, - cargo_build_target_dir: Option, - midenc_target_dir: Option, - test: Option, - } - impl Drop for RestoreEnvironment { - fn drop(&mut self) { - for (name, value) in [ - ("CARGO_TARGET_DIR", self.cargo_target_dir.take()), - ("CARGO_BUILD_TARGET_DIR", self.cargo_build_target_dir.take()), - ("MIDENC_TARGET_DIR", self.midenc_target_dir.take()), - ("TEST", self.test.take()), - ] { - match value { - Some(value) => unsafe { env::set_var(name, value) }, - None => unsafe { env::remove_var(name) }, - } - } - } - } - let _cwd = current_dir_lock(); - let _restore_environment = RestoreEnvironment { - cargo_target_dir: env::var_os("CARGO_TARGET_DIR"), - cargo_build_target_dir: env::var_os("CARGO_BUILD_TARGET_DIR"), - midenc_target_dir: env::var_os("MIDENC_TARGET_DIR"), - test: env::var_os("TEST"), - }; + let _restore_environment = RestoreEnvironment::new([ + "CARGO_TARGET_DIR", + "CARGO_BUILD_TARGET_DIR", + "MIDENC_TARGET_DIR", + "TEST", + ]); unsafe { env::remove_var("CARGO_TARGET_DIR"); env::remove_var("CARGO_BUILD_TARGET_DIR"); diff --git a/tools/cargo-miden/tests/utils.rs b/tools/cargo-miden/tests/utils.rs index 1c194b017f..0f7da08f52 100644 --- a/tools/cargo-miden/tests/utils.rs +++ b/tools/cargo-miden/tests/utils.rs @@ -63,6 +63,31 @@ pub(crate) fn current_dir_lock() -> CurrentDirGuard { } } +/// Restores selected process environment variables when dropped. +pub(crate) struct RestoreEnvironment { + values: Vec<(&'static str, Option)>, +} + +impl RestoreEnvironment { + /// Captures the current values of the selected environment variables. + pub(crate) fn new(names: [&'static str; N]) -> Self { + Self { + values: names.into_iter().map(|name| (name, env::var_os(name))).collect(), + } + } +} + +impl Drop for RestoreEnvironment { + fn drop(&mut self) { + for (name, value) in self.values.drain(..) { + match value { + Some(value) => unsafe { env::set_var(name, value) }, + None => unsafe { env::remove_var(name) }, + } + } + } +} + /// The directory the post-build package tests hand to the compiler as its package cache. /// /// A lease the compiler mints itself is deleted when the compiler finishes, so a test that @@ -298,11 +323,3 @@ fn write_template( fs::write(template_root.join("src/lib.rs"), lib_rs)?; Ok(()) } - -pub(crate) fn workspace_root() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .and_then(Path::parent) - .expect("cargo-miden should live under tools/cargo-miden") - .to_path_buf() -}