diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e757b42019..bf5d127501 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,5 +21,6 @@ jobs: - name: Build for no-std run: | rustup update --no-self-update ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} rustup target add wasm32-unknown-unknown make build-no-std diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3b6f274248..6135a2a3ce 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,6 +28,7 @@ jobs: - name: Rustfmt run: | rustup update --no-self-update nightly + rustup default ${{ matrix.toolchain }} rustup +nightly component add rustfmt make format-check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b258c94860..e466b0c4fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: matrix: toolchain: [stable, nightly] os: [ubuntu] - args: [default, smt-hashmaps, no-std] + args: [default, hashmaps, no-std] timeout-minutes: 30 steps: - uses: actions/checkout@main @@ -25,6 +25,7 @@ jobs: - name: Perform tests run: | rustup update --no-self-update ${{matrix.toolchain}} + rustup default ${{ matrix.toolchain }} make test-${{matrix.args}} test-smt-concurrent: @@ -42,6 +43,7 @@ jobs: - name: Perform concurrent SMT tests run: | rustup update --no-self-update ${{matrix.toolchain}} + rustup default ${{ matrix.toolchain }} make test-smt-concurrent doc-tests: @@ -56,5 +58,5 @@ jobs: - name: Run doc-tests run: | rustup update --no-self-update + rustup default ${{ matrix.toolchain }} make test-docs - diff --git a/CHANGELOG.md b/CHANGELOG.md index 6909b5f437..e27b3ede18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## 0.16.0 (TBD) + +- [BREAKING] Incremented MSRV to 1.88. +- Added implementation of Poseidon2 hash function ([#429](https://github.com/0xMiden/crypto/issues/429)). +- [BREAKING] Make Falcon DSA deterministic ([#436](https://github.com/0xMiden/crypto/pull/436). +- [BREAKING] Remove generics from `MerkleStore` and remove `KvMap` and `RecordingMap` ([#442](https://github.com/0xMiden/crypto/issues/442)). +- [BREAKING] Rename `smt_hashmaps` feature to `hashmaps` ([#442](https://github.com/0xMiden/crypto/issues/442)). +- [BREAKING] Refactor `parse_hex_string_as_word()` to `Word::parse()` ([#450](https://github.com/0xMiden/crypto/issues/450)). +- `Smt.insert_inner_nodes` does not store empty subtrees ([#452](https://github.com/0xMiden/crypto/pull/452)). +- Optimized `Smt::num_entries()` ([#455](https://github.com/0xMiden/crypto/pull/455)). +- [BREAKING] Disallow leaves with more than 2^16 entries ([#455](https://github.com/0xMiden/crypto/pull/455), [#462](https://github.com/0xMiden/crypto/pull/462)). +- [BREAKING] Modified the public key in Falcon DSA to be the polynomial instead of the commitment ([#460](https://github.com/0xMiden/crypto/pull/460)). +- [BREAKING] Use `SparseMerklePath` in SMT proofs for better memory efficiency ([#477](https://github.com/0xMiden/crypto/pull/477)). +- [BREAKING] Rename `SparseValuePath` to `SimpleSmtProof` ([#477](https://github.com/0xMiden/crypto/pull/477)). +- Validate `NodeIndex` depth ([#482](https://github.com/0xMiden/crypto/pull/482)). +- [BREAKING] Rename `ValuePath` to `MerkleProof` ([#483](https://github.com/0xMiden/crypto/pull/483)). +- Added an implementation of Keccak256 hash function ([#487](https://github.com/0xMiden/crypto/pull/487)). + # 0.15.9 (2025-07-24) - Added serialization for `Mmr` and `Forest` ([#466](https://github.com/0xMiden/crypto/pull/466)). diff --git a/Cargo.lock b/Cargo.lock index 859c069546..1ff8413d57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -55,22 +55,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] @@ -139,9 +139,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.29" +version = "1.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" dependencies = [ "jobserver", "libc", @@ -183,9 +183,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -193,9 +193,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -205,9 +205,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", @@ -244,9 +244,9 @@ dependencies = [ [[package]] name = "criterion" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" +checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" dependencies = [ "anes", "cast", @@ -267,12 +267,12 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" dependencies = [ "cast", - "itertools 0.10.5", + "itertools 0.13.0", ] [[package]] @@ -411,9 +411,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "half" @@ -427,9 +427,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -466,15 +466,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[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" @@ -530,9 +521,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libm" @@ -554,7 +545,7 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "miden-crypto" -version = "0.15.9" +version = "0.16.0" dependencies = [ "assert_matches", "blake3", @@ -736,9 +727,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" dependencies = [ "unicode-ident", ] @@ -774,9 +765,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", @@ -812,9 +803,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -822,9 +813,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -867,21 +858,20 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rstest" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" dependencies = [ "futures-timer", "futures-util", "rstest_macros", - "rustc_version", ] [[package]] name = "rstest_macros" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" dependencies = [ "cfg-if", "glob", @@ -906,9 +896,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -959,9 +949,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -987,9 +977,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "strsim" @@ -999,9 +989,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1010,18 +1000,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", @@ -1178,16 +1168,31 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", ] [[package]] @@ -1196,14 +1201,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -1212,62 +1234,110 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] [[package]] name = "winter-crypto" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef01ae983f420aee0943c3bd6a413df43a151c12d1851daf384e0a4c6e0563ec" +checksum = "1cdb247bc142438798edb04067ab72a22cf815f57abbd7b78a6fa986fc101db8" dependencies = [ "blake3", "sha3", @@ -1277,9 +1347,9 @@ dependencies = [ [[package]] name = "winter-math" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147699a1350037b39c3fbbb993d944532e461fe3e46537ef7c36742ffc1a3498" +checksum = "7aecfb48ee6a8b4746392c8ff31e33e62df8528a3b5628c5af27b92b14aef1ea" dependencies = [ "serde", "winter-utils", @@ -1287,9 +1357,9 @@ dependencies = [ [[package]] name = "winter-rand-utils" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ff50bd9ffaf905154d96e967f863246378dd5600997db07575bd1e1fa22260" +checksum = "a4ff3b651754a7bd216f959764d0a5ab6f4b551c9a3a08fb9ccecbed594b614a" dependencies = [ "rand", "winter-utils", @@ -1297,9 +1367,9 @@ dependencies = [ [[package]] name = "winter-utils" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c14bedf485c77b8a6ce76b0afbd86038d12bf441c2ede64ea9e573e3ff9658f" +checksum = "9951263ef5317740cd0f49e618db00c72fabb70b75756ea26c4d5efe462c04dd" [[package]] name = "wit-bindgen-rt" diff --git a/Cargo.toml b/Cargo.toml index b213968ed2..8312f57f82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,4 @@ repository = "https://github.com/0xMiden/crypto" categories = ["cryptography", "no-std"] keywords = ["miden", "crypto", "hash", "merkle"] edition = "2024" -rust-version = "1.87" +rust-version = "1.88" diff --git a/Makefile b/Makefile index 73eaca7d98..b6ae0903d7 100644 --- a/Makefile +++ b/Makefile @@ -46,9 +46,9 @@ doc: ## Generate and check documentation test-default: ## Run tests with default features $(DEBUG_OVERFLOW_INFO) cargo nextest run --profile default --release --all-features -.PHONY: test-smt-hashmaps -test-smt-hashmaps: ## Run tests with `smt_hashmaps` feature enabled - $(DEBUG_OVERFLOW_INFO) cargo nextest run --profile default --release --features smt_hashmaps +.PHONY: test-hashmaps +test-hashmaps: ## Run tests with `hashmaps` feature enabled + $(DEBUG_OVERFLOW_INFO) cargo nextest run --profile default --release --features hashmaps .PHONY: test-no-std test-no-std: ## Run tests with `no-default-features` (std) diff --git a/README.md b/README.md index c3802ec144..fef49d3eda 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/0xMiden/crypto/blob/main/LICENSE) [![test](https://github.com/0xMiden/crypto/actions/workflows/test.yml/badge.svg)](https://github.com/0xMiden/crypto/actions/workflows/test.yml) [![build](https://github.com/0xMiden/crypto/actions/workflows/build.yml/badge.svg)](https://github.com/0xMiden/crypto/actions/workflows/build.yml) -[![RUST_VERSION](https://img.shields.io/badge/rustc-1.87+-lightgray.svg)](https://www.rust-lang.org/tools/install) +[![RUST_VERSION](https://img.shields.io/badge/rustc-1.88+-lightgray.svg)](https://www.rust-lang.org/tools/install) [![CRATE](https://img.shields.io/crates/v/miden-crypto)](https://crates.io/crates/miden-crypto) This crate contains cryptographic primitives used in Miden. @@ -13,8 +13,10 @@ This crate contains cryptographic primitives used in Miden. [Hash module](./miden-crypto/src/hash) provides a set of cryptographic hash functions which are used by the Miden protocol. Currently, these functions are: - [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) hash function with 256-bit, 192-bit, or 160-bit output. The 192-bit and 160-bit outputs are obtained by truncating the 256-bit output of the standard BLAKE3. +- [Keccak256](https://keccak.team/specifications.html) hash function with 256-bit. - [RPO](https://eprint.iacr.org/2022/1577) hash function with 256-bit output. This hash function is an algebraic hash function suitable for recursive STARKs. - [RPX](https://eprint.iacr.org/2023/1045) hash function with 256-bit output. Similar to RPO, this hash function is suitable for recursive STARKs but it is about 2x faster as compared to RPO. +- [Poseidon2](https://eprint.iacr.org/2023/323) hash function with 256-bit output. Similar to RPO and RPX, this hash function is suitable for recursive STARKs but it is about 2x faster as compared to RPX. For performance benchmarks of these hash functions and their comparison to other popular hash functions please see [here](./miden-crypto/benches/). @@ -22,7 +24,7 @@ For performance benchmarks of these hash functions and their comparison to other [Merkle module](./miden-crypto/src/merkle/) provides a set of data structures related to Merkle trees. All these data structures are implemented using the RPO hash function described above. The data structures are: -- `MerkleStore`: a collection of Merkle trees of different heights designed to efficiently store trees with common subtrees. When instantiated with `RecordingMap`, a Merkle store records all accesses to the original data. +- `MerkleStore`: a collection of Merkle trees of different heights designed to efficiently store trees with common subtrees. - `MerkleTree`: a regular fully-balanced binary Merkle tree. The depth of this tree can be at most 64. - `Mmr`: a Merkle mountain range structure designed to function as an append-only log. - `PartialMerkleTree`: a partial view of a Merkle tree where some sub-trees may not be known. This is similar to a collection of Merkle paths all resolving to the same root. The length of the paths can be at most 64. @@ -30,13 +32,13 @@ For performance benchmarks of these hash functions and their comparison to other - `SimpleSmt`: a Sparse Merkle Tree (with no compaction), mapping 64-bit keys to 4-element values. - `Smt`: a Sparse Merkle tree (with compaction at depth 64), mapping 4-element keys to 4-element values. -The module also contains additional supporting components such as `NodeIndex`, `MerklePath`, and `MerkleError` to assist with tree indexation, opening proofs, and reporting inconsistent arguments/state. +The module also contains additional supporting components such as `NodeIndex`, `MerklePath`, `SparseMerklePath`, and `MerkleError` to assist with tree indexation, opening proofs, and reporting inconsistent arguments/state. `SparseMerklePath` provides a memory-efficient representation for Merkle paths with nodes representing empty subtrees. ## Signatures [DSA module](./miden-crypto/src/dsa) provides a set of digital signature schemes supported by default in the Miden VM. Currently, these schemes are: -- `RPO Falcon512`: a variant of the [Falcon](https://falcon-sign.info/) signature scheme. This variant differs from the standard in that instead of using SHAKE256 hash function in the _hash-to-point_ algorithm we use RPO256. This makes the signature more efficient to verify in Miden VM. +- `RPO Falcon512`: a variant of the [Falcon](https://falcon-sign.info/) signature scheme. This variant differs from the standard in that instead of using SHAKE256 hash function in the _hash-to-point_ algorithm we use RPO256. This makes the signature more efficient to verify in Miden VM. Another point of difference is with respect to the signing process, which is deterministic in our case. For the above signatures, key generation, signing, and signature verification are available for both `std` and `no_std` contexts (see [crate features](#crate-features) below). However, in `no_std` context, the user is responsible for supplying the key generation and signing procedures with a random number generator. @@ -63,7 +65,7 @@ This crate can be compiled with the following features: - `concurrent`- enabled by default; enables multi-threaded implementation of `Smt::with_entries()` which significantly improves performance on multi-core CPUs. - `std` - enabled by default and relies on the Rust standard library. - `no_std` does not rely on the Rust standard library and enables compilation to WebAssembly. -- `smt_hashmaps` - uses hashbrown hashmaps in SMT implementation which significantly improves performance of SMT updating. Keys ordering in SMT iterators is not guaranteed when this feature is enabled. +- `hashmaps` - uses hashbrown hashmaps in SMT and Merkle Store implementation which significantly improves performance of updates. Keys ordering in iterators is not guaranteed when this feature is enabled. All of these features imply the use of [alloc](https://doc.rust-lang.org/alloc/) to support heap-allocated collections. diff --git a/miden-crypto-fuzz/Cargo.lock b/miden-crypto-fuzz/Cargo.lock index 965dc96901..1bf9b5dde1 100644 --- a/miden-crypto-fuzz/Cargo.lock +++ b/miden-crypto-fuzz/Cargo.lock @@ -10,9 +10,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arrayref" @@ -28,21 +28,21 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "blake3" -version = "1.6.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1230237285e3e10cde447185e8975408ae24deaa67205ce684805c25bc0c7937" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" dependencies = [ "arrayref", "arrayvec", @@ -60,17 +60,11 @@ dependencies = [ "generic-array", ] -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "cc" -version = "1.2.15" +version = "1.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" dependencies = [ "jobserver", "libc", @@ -79,9 +73,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "constant_time_eq" @@ -145,9 +139,9 @@ dependencies = [ [[package]] name = "either" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" @@ -157,9 +151,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "generic-array" @@ -173,27 +167,27 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", + "r-efi", "wasi", - "windows-targets", ] [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -204,10 +198,11 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom", "libc", ] @@ -222,15 +217,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.170" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libfuzzer-sys" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", @@ -238,13 +233,13 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "miden-crypto" -version = "0.14.0" +version = "0.16.0" dependencies = [ "blake3", "cc", @@ -253,6 +248,7 @@ dependencies = [ "num", "num-complex", "rand", + "rand_chacha", "rand_core", "rayon", "sha3", @@ -347,40 +343,45 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", ] [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 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 = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", - "zerocopy 0.8.23", ] [[package]] @@ -404,9 +405,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -414,9 +415,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -424,18 +425,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -460,9 +461,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "syn" -version = "2.0.98" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -471,18 +472,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", @@ -497,9 +498,9 @@ checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "version_check" @@ -509,82 +510,18 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "winter-crypto" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32247cde9f43e5bbd05362caa7274608790ea69b14f7c81cd509aae7127c5ff2" +checksum = "1cdb247bc142438798edb04067ab72a22cf815f57abbd7b78a6fa986fc101db8" dependencies = [ "blake3", "sha3", @@ -594,63 +531,42 @@ dependencies = [ [[package]] name = "winter-math" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "326dfe4bfa4072b7c909133a88f8807820d3e49e5dfd246f67981771f74a0ed3" +checksum = "7aecfb48ee6a8b4746392c8ff31e33e62df8528a3b5628c5af27b92b14aef1ea" dependencies = [ "winter-utils", ] [[package]] name = "winter-utils" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d47518e6931955dcac73a584cacb04550b82ab2f45c72880cbbbdbe13adb63c" +checksum = "9951263ef5317740cd0f49e618db00c72fabb70b75756ea26c4d5efe462c04dd" [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" -dependencies = [ - "zerocopy-derive 0.8.23", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/miden-crypto/Cargo.toml b/miden-crypto/Cargo.toml index f8e8dee65f..8ffac1dc22 100644 --- a/miden-crypto/Cargo.toml +++ b/miden-crypto/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "miden-crypto" -version = "0.15.9" +version = "0.16.0" description = "Miden Cryptographic primitives" authors.workspace = true readme = "../README.md" license.workspace = true repository.workspace = true -documentation = "https://docs.rs/miden-crypto/0.15.9" +documentation = "https://docs.rs/miden-crypto/0.16.0" categories.workspace = true keywords.workspace = true edition.workspace = true @@ -48,10 +48,18 @@ harness = false concurrent = ["dep:rayon", "hashbrown?/rayon"] default = ["std", "concurrent"] executable = ["dep:clap", "dep:rand-utils", "std"] -smt_hashmaps = ["dep:hashbrown"] +hashmaps = ["dep:hashbrown"] internal = [] serde = ["dep:serde", "serde?/alloc", "winter-math/serde"] -std = ["blake3/std", "dep:cc", "rand/std", "rand/thread_rng", "winter-crypto/std", "winter-math/std", "winter-utils/std"] +std = [ + "blake3/std", + "dep:cc", + "rand/std", + "rand/thread_rng", + "winter-crypto/std", + "winter-math/std", + "winter-utils/std", +] [dependencies] blake3 = { version = "1.8", default-features = false } @@ -60,6 +68,7 @@ hashbrown = { version = "0.15", optional = true, features = ["serde"] } num = { version = "0.4", default-features = false, features = ["alloc", "libm"] } num-complex = { version = "0.4", default-features = false } rand = { version = "0.9", default-features = false } +rand_chacha = { version = "0.9", default-features = false } rand_core = { version = "0.9", default-features = false } rand-utils = { version = "0.13", package = "winter-rand-utils", optional = true } rayon = { version = "1.10", optional = true } @@ -72,14 +81,14 @@ winter-utils = { version = "0.13", default-features = false } [dev-dependencies] assert_matches = { version = "1.5", default-features = false } -criterion = { version = "0.6", features = ["html_reports"] } +criterion = { version = "0.7", features = ["html_reports"] } getrandom = { version = "0.3", default-features = false } hex = { version = "0.4", default-features = false, features = ["alloc"] } itertools = { version = "0.14" } -proptest = { version = "1.7", default-features = false, features = ["alloc"]} +proptest = { version = "1.7", default-features = false, features = ["alloc"] } rand_chacha = { version = "0.9", default-features = false } rand-utils = { version = "0.13", package = "winter-rand-utils" } -rstest = { version = "0.25" } +rstest = { version = "0.26" } seq-macro = { version = "0.3" } [build-dependencies] diff --git a/miden-crypto/benches/README.md b/miden-crypto/benches/README.md index 689d50123c..375c57fb40 100644 --- a/miden-crypto/benches/README.md +++ b/miden-crypto/benches/README.md @@ -1,67 +1,68 @@ # Benchmarks ## Hash Functions + In the Miden VM, we make use of different hash functions. Some of these are "traditional" hash functions, like `BLAKE3`, which are optimized for out-of-STARK performance, while others are algebraic hash functions, like `Rescue Prime`, and are more optimized for a better performance inside the STARK. In what follows, we benchmark several such hash functions and compare against other constructions that are used by other proving systems. More precisely, we benchmark: * **BLAKE3** as specified [here](https://github.com/BLAKE3-team/BLAKE3-specs/blob/master/blake3.pdf) and implemented [here](https://github.com/BLAKE3-team/BLAKE3) (with a wrapper exposed via this crate). * **SHA3** as specified [here](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf) and implemented [here](https://github.com/novifinancial/winterfell/blob/46dce1adf0/crypto/src/hash/sha/mod.rs). -* **Poseidon** as specified [here](https://eprint.iacr.org/2019/458.pdf) and implemented [here](https://github.com/mir-protocol/plonky2/blob/806b88d7d6e69a30dc0b4775f7ba275c45e8b63b/plonky2/src/hash/poseidon_goldilocks.rs) (but in pure Rust, without vectorized instructions). -* **Rescue Prime (RP)** as specified [here](https://eprint.iacr.org/2020/1143) and implemented [here](https://github.com/novifinancial/winterfell/blob/46dce1adf0/crypto/src/hash/rescue/rp64_256/mod.rs). +* **Keccak256** as specified [here](https://keccak.team/specifications.html) and implemented [here](https://github.com/RustCrypto/hashes/tree/master/sha3) (with a wrapper exposed via this crate). * **Rescue Prime Optimized (RPO)** as specified [here](https://eprint.iacr.org/2022/1577) and implemented in this crate. * **Rescue Prime Extended (RPX)** a variant of the [xHash](https://eprint.iacr.org/2023/1045) hash function as implemented in this crate. +* **Poseidon2** as specified [here](https://eprint.iacr.org/2023/323) and implemented in this crate. -We benchmark the above hash functions using two scenarios. The first is a 2-to-1 $(a,b)\mapsto h(a,b)$ hashing where both $a$, $b$ and $h(a,b)$ are the digests corresponding to each of the hash functions. -The second scenario is that of sequential hashing where we take a sequence of length $100$ field elements and hash these to produce a single digest. The digests are $4$ field elements in a prime field with modulus $2^{64} - 2^{32} + 1$ (i.e., 32 bytes) for Poseidon, Rescue Prime and RPO, and an array `[u8; 32]` for SHA3 and BLAKE3. +We benchmark the above hash functions using two scenarios. The first is a 2-to-1 $(a,b)\mapsto h(a,b)$ hashing where both $a$, $b$ and $h(a,b)$ are the digests corresponding to each of the hash functions. The second scenario is that of sequential hashing where we take a sequence of length $100$ field elements and hash these to produce a single digest. The digests are $4$ field elements in a prime field with modulus $2^{64} - 2^{32} + 1$ (i.e., 32 bytes) for Poseidon2, RPO, and RPX, and an array `[u8; 32]` for SHA3, BLAKE3, and Keccak256. ### Scenario 1: 2-to-1 hashing `h(a,b)` -| Function | BLAKE3 | SHA3 | Poseidon | Rp64_256 | RPO_256 | RPX_256 | -| ------------------- | ------ | ------- | --------- | --------- | ------- | ------- | -| Apple M1 Pro | 76 ns | 245 ns | 1.5 µs | 9.1 µs | 5.2 µs | 2.7 µs | -| Apple M2 Max | 71 ns | 233 ns | 1.3 µs | 7.9 µs | 4.6 µs | 2.4 µs | -| Amazon Graviton 3 | 108 ns | | | | 5.3 µs | 3.1 µs | -| Amazon Graviton 4 | 96 ns | | | | 5.1 µs | 2.8 µs | -| AMD Ryzen 9 5950X | 64 ns | 273 ns | 1.2 µs | 9.1 µs | 5.5 µs | | -| AMD EPYC 9R14 | 83 ns | | | | 4.3 µs | 2.4 µs | -| Intel Core i5-8279U | 68 ns | 536 ns | 2.0 µs | 13.6 µs | 8.5 µs | 4.4 µs | -| Intel Xeon 8375C | 67 ns | | | | 8.2 µs | | +| Function | BLAKE3 | SHA3 | Keccak256 | Poseidon2 | RPO_256 | RPX_256 | +| ------------------- | :----: | :----: | :-------: | :-------: | :-----: | :-----: | +| Apple M1 Pro | 76 ns | 245 ns | | | 5.2 µs | 2.7 µs | +| Apple M2 Max | 71 ns | 233 ns | | | 4.6 µs | 2.4 µs | +| Apple M4 Max | 48 ns | | 155 ns | 0.7 µs | 2.9 µs | 1.5 µs | +| Amazon Graviton 3 | 108 ns | | | | 5.3 µs | 3.1 µs | +| Amazon Graviton 4 | 96 ns | | | | 5.1 µs | 2.8 µs | +| AMD Ryzen 9 5950X | 64 ns | 273 ns | | | 5.5 µs | | +| AMD EPYC 9R14 | 83 ns | | | | 4.3 µs | 2.4 µs | +| Intel Core i5-8279U | 68 ns | 536 ns | 514 ns | 1.7 µs | 8.5 µs | 4.4 µs | +| Intel Xeon 8375C | 67 ns | | | | 8.2 µs | | ### Scenario 2: Sequential hashing of 100 elements `h([a_0,...,a_99])` -| Function | BLAKE3 | SHA3 | Poseidon | Rp64_256 | RPO_256 | RPX_256 | -| ------------------- | -------| ------- | --------- | --------- | ------- | ------- | -| Apple M1 Pro | 1.0 µs | 1.5 µs | 19.4 µs | 118 µs | 69 µs | 35 µs | -| Apple M2 Max | 0.9 µs | 1.5 µs | 17.4 µs | 103 µs | 60 µs | 31 µs | -| Amazon Graviton 3 | 1.4 µs | | | | 69 µs | 41 µs | -| Amazon Graviton 4 | 1.2 µs | | | | 67 µs | 36 µs | -| AMD Ryzen 9 5950X | 0.8 µs | 1.7 µs | 15.7 µs | 120 µs | 72 µs | | -| AMD EPYC 9R14 | 0.9 µs | | | | 56 µs | 32 µs | -| Intel Core i5-8279U | 0.9 µs | | | | 107 µs | 56 µs | -| Intel Xeon 8375C | 0.8 µs | | | | 110 µs | | +| Function | BLAKE3 | SHA3 | Keccak256 | Poseidon2 | RPO_256 | RPX_256 | +| ------------------- | :----: | :----: | :-------: | :-------: | :-----: | :-----: | +| Apple M1 Pro | 1.0 µs | 1.5 µs | | | 69 µs | 35 µs | +| Apple M2 Max | 0.9 µs | 1.5 µs | | | 60 µs | 31 µs | +| Apple M4 Max | 0.7 µs | | 0.9 µs | 9.7 µs | 37.5 µs | 19.4 µs | +| Amazon Graviton 3 | 1.4 µs | | | | 69 µs | 41 µs | +| Amazon Graviton 4 | 1.2 µs | | | | 67 µs | 36 µs | +| AMD Ryzen 9 5950X | 0.8 µs | 1.7 µs | | | 72 µs | | +| AMD EPYC 9R14 | 0.9 µs | | | | 56 µs | 32 µs | +| Intel Core i5-8279U | 0.9 µs | | 3.4 µs | 27 µs | 107 µs | 56 µs | +| Intel Xeon 8375C | 0.8 µs | | | | 110 µs | | Notes: - On Graviton 3 and 4, RPO256 and RPX256 are run with SVE acceleration enabled. - On AMD EPYC 9R14, RPO256 and RPX256 are run with AVX2 acceleration enabled. ## Sparse Merkle Tree -We build cryptographic data structures incorporating these hash functions. -What follows are benchmarks of operations on sparse Merkle trees (SMTs) which use the above `RPO_256` hash function. -We perform a batched modification of 1,000 values in a tree with 1,000,000 leaves (with the `smt_hashmaps` feature to use the `hashbrown` crate). + +We build cryptographic data structures incorporating these hash functions. What follows are benchmarks of operations on sparse Merkle trees (SMTs) which use the above `RPO_256` hash function. We perform a batched modification of 1,000 values in a tree with 1,000,000 leaves (with the `hashmaps` feature to use the `hashbrown` crate). ### Scenario 1: SMT Construction (1M pairs) | Hardware | Sequential | Concurrent | Improvement | | ----------------- | ---------- | ---------- | ----------- | -| AMD Ryzen 9 7950X | 196 sec | 15 sec | 13x | +| AMD Ryzen 9 7950X | 196 sec | 15 sec | 13x | | Apple M1 Air | 352 sec | 57 sec | 6.2x | | Apple M1 Pro | 351 sec | 37 sec | 9.5x | -| Apple M4 Max | 195 sec | 15 sec | 13x | +| Apple M4 Max | 195 sec | 15 sec | 13x | ### Scenario 2: SMT Batched Insertion (1k pairs, 1M leaves) | Function | Sequential | Concurrent | Improvement | | ----------------- | ---------- | ---------- | ----------- | -| AMD Ryzen 9 7950X | 201 ms | 19 ms | 11x | +| AMD Ryzen 9 7950X | 201 ms | 19 ms | 11x | | Apple M1 Air | 729 ms | 406 ms | 1.8x | | Apple M1 Pro | 623 ms | 86 ms | 7.2x | | Apple M4 Max | 212 ms | 28 ms | 7.6x | @@ -70,7 +71,7 @@ We perform a batched modification of 1,000 values in a tree with 1,000,000 leave | Function | Sequential | Concurrent | Improvement | | ----------------- | ---------- | ---------- | ----------- | -| AMD Ryzen 9 7950X | 202 ms | 19 ms | 11x | +| AMD Ryzen 9 7950X | 202 ms | 19 ms | 11x | | Apple M1 Air | 691 ms | 307 ms | 2.3x | | Apple M1 Pro | 419 ms | 56 ms | 7.5x | | Apple M4 Max | 218 ms | 24 ms | 9.1x | @@ -79,13 +80,13 @@ Notes: - On AMD Ryzen 9 7950X, benchmarks are run with AVX2 acceleration enabled. ## Instructions -Before you can run the benchmarks, you'll need to make sure you have Rust [installed](https://www.rust-lang.org/tools/install). After that, to run the benchmarks for RPO and BLAKE3, clone the current repository, and from the root directory of the repo run the following: +Before you can run the benchmarks, you'll need to make sure you have Rust [installed](https://www.rust-lang.org/tools/install). After that, to run the benchmarks for RPO, Poseidon2, BLAKE3 and Keccak256, clone the current repository, and from the root directory of the repo run the following: ``` cargo bench hash ``` -To run the benchmarks for Rescue Prime, Poseidon and SHA3, clone the following [repository](https://github.com/Dominik1999/winterfell.git) as above, then checkout the `hash-functions-benches` branch, and from the root directory of the repo run the following: +To run the benchmarks for SHA3, clone the following [repository](https://github.com/Dominik1999/winterfell.git) as above, then checkout the `hash-functions-benches` branch, and from the root directory of the repo run the following: ``` cargo bench hash @@ -97,11 +98,10 @@ To run the benchmarks for SMT operations, run the binary target with the `execut cargo run --features=executable ``` -The `concurrent` feature enables the concurrent benchmark, and is enabled by default. To run a sequential benchmark, -disable the crate's default features: +The `concurrent` feature enables the concurrent benchmark, and is enabled by default. To run a sequential benchmark, disable the crate's default features: ``` -cargo run --no-default-features --features=executable,smt_hashmaps +cargo run --no-default-features --features=executable,hashmaps ``` The benchmark parameters may also be customized with the `-s`/`--size`, `-i`/`--insertions`, and `-u`/`--updates` options. diff --git a/miden-crypto/benches/hash.rs b/miden-crypto/benches/hash.rs index 464a3d51c9..0428076e09 100644 --- a/miden-crypto/benches/hash.rs +++ b/miden-crypto/benches/hash.rs @@ -3,7 +3,7 @@ use std::hint::black_box; use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; use miden_crypto::{ Felt, Word, - hash::{blake::Blake3_256, rpo::Rpo256, rpx::Rpx256}, + hash::{blake::Blake3_256, keccak::Keccak256, poseidon2::Poseidon2, rpo::Rpo256, rpx::Rpx256}, }; use rand_utils::rand_value; use winter_crypto::Hasher; @@ -100,6 +100,52 @@ fn rpx256_sequential(c: &mut Criterion) { }); } +fn poseidon2_2to1(c: &mut Criterion) { + let v: [Word; 2] = [Poseidon2::hash(&[1_u8]), Poseidon2::hash(&[2_u8])]; + c.bench_function("Poseidon2 2-to-1 hashing (cached)", |bench| { + bench.iter(|| Poseidon2::merge(black_box(&v))) + }); + + c.bench_function("Poseidon2 2-to-1 hashing (random)", |bench| { + bench.iter_batched( + || { + [ + Poseidon2::hash(&rand_value::().to_le_bytes()), + Poseidon2::hash(&rand_value::().to_le_bytes()), + ] + }, + |state| Poseidon2::merge(&state), + BatchSize::SmallInput, + ) + }); +} + +fn poseidon2_sequential(c: &mut Criterion) { + let v: [Felt; 100] = (0..100) + .map(Felt::new) + .collect::>() + .try_into() + .expect("should not fail"); + c.bench_function("Poseidon2 sequential hashing (cached)", |bench| { + bench.iter(|| Poseidon2::hash_elements(black_box(&v))) + }); + + c.bench_function("Poseidon2 sequential hashing (random)", |bench| { + bench.iter_batched( + || { + let v: [Felt; 100] = (0..100) + .map(|_| Felt::new(rand_value())) + .collect::>() + .try_into() + .expect("should not fail"); + v + }, + |state| Poseidon2::hash_elements(&state), + BatchSize::SmallInput, + ) + }); +} + fn blake3_2to1(c: &mut Criterion) { let v: [::Digest; 2] = [Blake3_256::hash(&[1_u8]), Blake3_256::hash(&[2_u8])]; @@ -147,13 +193,64 @@ fn blake3_sequential(c: &mut Criterion) { }); } +fn keccak256_2to1(c: &mut Criterion) { + let v: [::Digest; 2] = + [Keccak256::hash(&[1_u8]), Keccak256::hash(&[2_u8])]; + c.bench_function("Keccak256 2-to-1 hashing (cached)", |bench| { + bench.iter(|| Keccak256::merge(black_box(&v))) + }); + + c.bench_function("Keccak256 2-to-1 hashing (random)", |bench| { + bench.iter_batched( + || { + [ + Keccak256::hash(&rand_value::().to_le_bytes()), + Keccak256::hash(&rand_value::().to_le_bytes()), + ] + }, + |state| Keccak256::merge(&state), + BatchSize::SmallInput, + ) + }); +} + +fn keccak256_sequential(c: &mut Criterion) { + let v: [Felt; 100] = (0..100) + .map(Felt::new) + .collect::>() + .try_into() + .expect("should not fail"); + c.bench_function("Keccak256 sequential hashing (cached)", |bench| { + bench.iter(|| Keccak256::hash_elements(black_box(&v))) + }); + + c.bench_function("Keccak256 sequential hashing (random)", |bench| { + bench.iter_batched( + || { + let v: [Felt; 100] = (0..100) + .map(|_| Felt::new(rand_value())) + .collect::>() + .try_into() + .expect("should not fail"); + v + }, + |state| Keccak256::hash_elements(&state), + BatchSize::SmallInput, + ) + }); +} + criterion_group!( hash_group, rpx256_2to1, rpx256_sequential, rpo256_2to1, rpo256_sequential, + poseidon2_2to1, + poseidon2_sequential, blake3_2to1, - blake3_sequential + blake3_sequential, + keccak256_2to1, + keccak256_sequential ); criterion_main!(hash_group); diff --git a/miden-crypto/benches/store.rs b/miden-crypto/benches/store.rs index 0bbd0dc873..50fb693ea5 100644 --- a/miden-crypto/benches/store.rs +++ b/miden-crypto/benches/store.rs @@ -3,10 +3,7 @@ use std::hint::black_box; use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; use miden_crypto::{ Felt, Word, - merkle::{ - DefaultMerkleStore as MerkleStore, LeafIndex, MerkleTree, NodeIndex, SMT_MAX_DEPTH, - SimpleSmt, - }, + merkle::{LeafIndex, MerkleStore, MerkleTree, NodeIndex, SMT_MAX_DEPTH, SimpleSmt}, }; use rand_utils::{rand_array, rand_value}; diff --git a/miden-crypto/src/dsa/rpo_falcon512/hash_to_point.rs b/miden-crypto/src/dsa/rpo_falcon512/hash_to_point.rs index 37b13f9518..045af7d216 100644 --- a/miden-crypto/src/dsa/rpo_falcon512/hash_to_point.rs +++ b/miden-crypto/src/dsa/rpo_falcon512/hash_to_point.rs @@ -54,7 +54,7 @@ pub fn hash_to_point_shake256(message: &[u8], nonce: &Nonce) -> Polynomial); impl PublicKey { - /// Returns a new [PublicKey] which is a commitment to the provided expanded public key. - pub fn new(pub_key: Word) -> Self { - Self(pub_key) - } - /// Verifies the provided signature against provided message and this public key. pub fn verify(&self, message: Word, signature: &Signature) -> bool { - signature.verify(message, self.0) + signature.verify(message, self) } -} -impl From for PublicKey { - fn from(pk_poly: PubKeyPoly) -> Self { - let pk_felts: Polynomial = pk_poly.0.into(); - let pk_digest = Rpo256::hash_elements(&pk_felts.coefficients); - Self(pk_digest) + /// Recovers from the signature the public key associated to the secret key used to sign + /// a message. + pub fn recover_from(_message: Word, signature: &Signature) -> Self { + signature.public_key().clone() } -} -impl From for Word { - fn from(key: PublicKey) -> Self { - key.0 + /// Returns a commitment to the public key using the RPO256 hash function. + pub fn to_commitment(&self) -> Word { + ::to_commitment(self) } } -// PUBLIC KEY POLYNOMIAL -// ================================================================================================ +impl SequentialCommit for PublicKey { + type Commitment = Word; -/// Public key represented as a polynomial with coefficients over the Falcon prime field. -/// Used in the RPO Falcon 512 signature scheme. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PubKeyPoly(pub Polynomial); + fn to_elements(&self) -> Vec { + Into::>::into(self.0.clone()).coefficients + } +} -impl Deref for PubKeyPoly { +impl Deref for PublicKey { type Target = Polynomial; fn deref(&self) -> &Self::Target { @@ -73,13 +53,13 @@ impl Deref for PubKeyPoly { } } -impl From> for PubKeyPoly { +impl From> for PublicKey { fn from(pk_poly: Polynomial) -> Self { Self(pk_poly) } } -impl Serializable for &PubKeyPoly { +impl Serializable for &PublicKey { fn write_into(&self, target: &mut W) { let mut buf = [0_u8; PK_LEN]; buf[0] = LOG_N; @@ -106,7 +86,7 @@ impl Serializable for &PubKeyPoly { } } -impl Deserializable for PubKeyPoly { +impl Deserializable for PublicKey { fn read_from(source: &mut R) -> Result { let buf = source.read_array::()?; diff --git a/miden-crypto/src/dsa/rpo_falcon512/keys/secret_key.rs b/miden-crypto/src/dsa/rpo_falcon512/keys/secret_key.rs index 89ccaf8b58..3d2dcd27ee 100644 --- a/miden-crypto/src/dsa/rpo_falcon512/keys/secret_key.rs +++ b/miden-crypto/src/dsa/rpo_falcon512/keys/secret_key.rs @@ -13,11 +13,12 @@ use super::{ math::{FalconFelt, FastFft, LdlTree, Polynomial, ffldl, ffsampling, gram, normalize_tree}, signature::SignaturePoly, }, - PubKeyPoly, PublicKey, + PublicKey, }; use crate::{ Word, - dsa::rpo_falcon512::{SK_LEN, hash_to_point::hash_to_point_rpo256, math::ntru_gen}, + dsa::rpo_falcon512::{LOG_N, SK_LEN, hash_to_point::hash_to_point_rpo256, math::ntru_gen}, + hash::blake::Blake3_256, }; // CONSTANTS @@ -102,7 +103,7 @@ impl SecretKey { /// Returns the public key corresponding to this secret key. pub fn public_key(&self) -> PublicKey { - self.compute_pub_key_poly().into() + self.compute_pub_key_poly() } /// Returns the LDL tree associated to this secret key. @@ -114,17 +115,18 @@ impl SecretKey { // -------------------------------------------------------------------------------------------- /// Signs a message with this secret key. - #[cfg(feature = "std")] pub fn sign(&self, message: crate::Word) -> Signature { - use rand::{SeedableRng, rngs::StdRng}; + use rand::SeedableRng; + use rand_chacha::ChaCha20Rng; - let mut rng = StdRng::from_os_rng(); + let seed = self.generate_seed(&message); + let mut rng = ChaCha20Rng::from_seed(seed); self.sign_with_rng(message, &mut rng) } /// Signs a message with the secret key relying on the provided randomness generator. pub fn sign_with_rng(&self, message: Word, rng: &mut R) -> Signature { - let nonce = Nonce::random(rng); + let nonce = Nonce::deterministic(); let h = self.compute_pub_key_poly(); let c = hash_to_point_rpo256(message, &nonce); @@ -145,7 +147,7 @@ impl SecretKey { /// number generators for generating the nonce and in `Self::sign_helper`. /// /// These changes make the signature algorithm compliant with the reference implementation. - #[cfg(test)] + #[cfg(all(test, feature = "std"))] pub fn sign_with_rng_testing(&self, message: &[u8], rng: &mut R) -> Signature { use crate::dsa::rpo_falcon512::{hash_to_point::hash_to_point_shake256, tests::ChaCha}; @@ -164,7 +166,7 @@ impl SecretKey { // -------------------------------------------------------------------------------------------- /// Derives the public key corresponding to this secret key using h = g /f [mod ϕ][mod p]. - pub fn compute_pub_key_poly(&self) -> PubKeyPoly { + fn compute_pub_key_poly(&self) -> PublicKey { let g: Polynomial = self.secret_key[0].clone().into(); let g_fft = g.fft(); let minus_f: Polynomial = self.secret_key[1].clone().into(); @@ -225,6 +227,29 @@ impl SecretKey { } } } + + /// Deterministically generates a seed for seeding the PRNG used in the trapdoor sampling + /// algorithm used during signature generation. + /// + /// This uses the argument described in [RFC 6979](https://datatracker.ietf.org/doc/html/rfc6979#section-3.5) + /// § 3.5 where the concatenation of the private key and the hashed message, i.e., sk || H(m), + /// is used in order to construct the initial seed of a PRNG. See also [1]. + /// + /// + /// Note that we hash in also a `log_2(N)` where `N = 512` in order to domain separate between + /// different versions of the Falcon DSA, see [1] Section 3.4.1. + /// + /// [1]: https://github.com/algorand/falcon/blob/main/falcon-det.pdf + fn generate_seed(&self, message: &Word) -> [u8; 32] { + let mut buffer = Vec::with_capacity(1 + SK_LEN + Word::SERIALIZED_SIZE); + buffer.push(LOG_N); + buffer.extend_from_slice(&self.to_bytes()); + buffer.extend_from_slice(&message.to_bytes()); + + let digest = Blake3_256::hash(&buffer); + + digest.into() + } } // SERIALIZATION / DESERIALIZATION diff --git a/miden-crypto/src/dsa/rpo_falcon512/math/mod.rs b/miden-crypto/src/dsa/rpo_falcon512/math/mod.rs index 1b8221f966..48bc20f26e 100644 --- a/miden-crypto/src/dsa/rpo_falcon512/math/mod.rs +++ b/miden-crypto/src/dsa/rpo_falcon512/math/mod.rs @@ -34,8 +34,8 @@ use self::samplerz::sampler_z; mod polynomial; pub use polynomial::Polynomial; -const MAX_SMALL_POLY_COEFFICENT_SIZE: i16 = (1 << (WIDTH_SMALL_POLY_COEFFICIENT - 1)) - 1; -const MAX_BIG_POLY_COEFFICENT_SIZE: i16 = (1 << (WIDTH_BIG_POLY_COEFFICIENT - 1)) - 1; +const MAX_SMALL_POLY_COEFFICIENT_SIZE: i16 = (1 << (WIDTH_SMALL_POLY_COEFFICIENT - 1)) - 1; +const MAX_BIG_POLY_COEFFICIENT_SIZE: i16 = (1 << (WIDTH_BIG_POLY_COEFFICIENT - 1)) - 1; pub trait Inverse: Copy + Zero + MulAssign + One { /// Gets the inverse of a, or zero if it is zero. @@ -94,8 +94,8 @@ pub(crate) fn ntru_gen(n: usize, rng: &mut R) -> [Polynomial; 4] { // we do bound checks on the coefficients of the sampled polynomials in order to make sure // that they will be encodable/decodable - if !(check_coefficients_bound(&f, MAX_SMALL_POLY_COEFFICENT_SIZE) - && check_coefficients_bound(&g, MAX_SMALL_POLY_COEFFICENT_SIZE)) + if !(check_coefficients_bound(&f, MAX_SMALL_POLY_COEFFICIENT_SIZE) + && check_coefficients_bound(&g, MAX_SMALL_POLY_COEFFICIENT_SIZE)) { continue; } @@ -116,8 +116,8 @@ pub(crate) fn ntru_gen(n: usize, rng: &mut R) -> [Polynomial; 4] { // sure that they will be encodable/decodable let capital_f = capital_f.map(|i| i.try_into().unwrap()); let capital_g = capital_g.map(|i| i.try_into().unwrap()); - if !(check_coefficients_bound(&capital_f, MAX_BIG_POLY_COEFFICENT_SIZE) - && check_coefficients_bound(&capital_g, MAX_BIG_POLY_COEFFICENT_SIZE)) + if !(check_coefficients_bound(&capital_f, MAX_BIG_POLY_COEFFICIENT_SIZE) + && check_coefficients_bound(&capital_g, MAX_BIG_POLY_COEFFICIENT_SIZE)) { continue; } diff --git a/miden-crypto/src/dsa/rpo_falcon512/mod.rs b/miden-crypto/src/dsa/rpo_falcon512/mod.rs index c80f13361c..31127d17ef 100644 --- a/miden-crypto/src/dsa/rpo_falcon512/mod.rs +++ b/miden-crypto/src/dsa/rpo_falcon512/mod.rs @@ -1,3 +1,37 @@ +//! A deterministic RPO Falcon512 signature over a message. +//! +//! This version differs from the reference implementation in its use of the RPO algebraic hash +//! function in its hash-to-point algorithm. +//! +//! Another point of difference is the determinism in the signing process. The approach used to +//! achieve this is the one proposed in [1]. +//! The main challenge in making the signing procedure deterministic is ensuring that the same +//! secret key is never used to produce two inequivalent signatures for the same `c`. +//! For a precise definition of equivalence of signatures see [1]. +//! The reference implementation uses a random nonce per signature in order to make sure that, +//! with overwhelming probability, no two c-s will ever repeat and this non-repetition turns out +//! to be enough to make the security proof of the underlying construction go through in +//! the random-oracle model. +//! +//! Making the signing process deterministic means that we cannot rely on the above use of nonce +//! in the hash-to-point algorithm, i.e., the hash-to-point algorithm is deterministic. It also +//! means that we have to derandomize the trapdoor sampling process and use the entropy in +//! the secret key, together with the message, as the seed of a CPRNG. This is exactly the approach +//! taken in [2] but, as explained at length in [1], this is not enough. The reason for this +//! is that the sampling process during signature generation must be ensured to be consistent +//! across the entire computing stack i.e., hardware, compiler, OS, sampler implementations ... +//! +//! This is made even more difficult by the extensive use of floating-point arithmetic by +//! the sampler. In relation to this point, the current implementation does not use any platform +//! specific optimizations (e.g., AVX2, NEON, FMA ...) and relies solely on the builtin `f64` type. +//! Moreover, as per the time of this writing, the implementation does not use any methods or +//! functions from `std::f64` that have non-deterministic precision mentioned in their +//! documentation. +//! +//! [1]: https://github.com/algorand/falcon/blob/main/falcon-det.pdf +//! [2]: https://datatracker.ietf.org/doc/html/rfc6979#section-3.5 + +#[cfg(test)] use rand::Rng; use crate::{ @@ -11,11 +45,11 @@ mod keys; mod math; mod signature; -#[cfg(test)] +#[cfg(all(test, feature = "std"))] mod tests; pub use self::{ - keys::{PubKeyPoly, PublicKey, SecretKey}, + keys::{PublicKey, SecretKey}, math::Polynomial, signature::{Signature, SignatureHeader, SignaturePoly}, }; @@ -34,9 +68,29 @@ const FALCON_ENCODING_BITS: u32 = 14; const N: usize = 512; const LOG_N: u8 = 9; -/// Length of nonce used for key-pair generation. +/// Length of nonce used for signature generation. const SIG_NONCE_LEN: usize = 40; +/// Length of the preversioned portion of the fixed nonce. +/// +/// Since we use one byte to encode the version of the nonce, this is equal to `SIG_NONCE_LEN - 1`. +const PREVERSIONED_NONCE_LEN: usize = 39; + +/// Current version of the fixed nonce. +/// +/// The usefulness of the notion of versioned fixed nonce is discussed in Section 2.1 in [1]. +/// +/// [1]: https://github.com/algorand/falcon/blob/main/falcon-det.pdf +const NONCE_VERSION_BYTE: u8 = 1; + +/// The preversioned portion of the fixed nonce constructed following [1]. +/// +/// Note that reference [1] uses the term salt instead of nonce. +const PREVERSIONED_NONCE: [u8; PREVERSIONED_NONCE_LEN] = [ + 9, 82, 80, 79, 45, 70, 65, 76, 67, 79, 78, 45, 68, 69, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + /// Number of filed elements used to encode a nonce. const NONCE_ELEMENTS: usize = 8; @@ -49,6 +103,10 @@ pub const SK_LEN: usize = 1281; /// Signature length as a u8 vector. const SIG_POLY_BYTE_LEN: usize = 625; +/// Signature size when serialized as a u8 vector. +#[cfg(test)] +const SIG_SERIALIZED_LEN: usize = 1524; + /// Bound on the squared-norm of the signature. const SIG_L2_BOUND: u64 = 34034726; @@ -68,21 +126,43 @@ type ShortLatticeBasis = [Polynomial; 4]; pub struct Nonce([u8; SIG_NONCE_LEN]); impl Nonce { - /// Returns a new [Nonce] instantiated from the provided bytes. - pub fn new(bytes: [u8; SIG_NONCE_LEN]) -> Self { - Self(bytes) + /// Returns a new deterministic [Nonce]. + /// + /// This is used in deterministic signing following [1] and is composed of two parts: + /// + /// 1. a byte serving as a version byte, + /// 2. a pre-versioned fixed nonce which is the UTF8 encoding of the domain separator + /// "RPO-FALCON-DET" padded with enough zeros to make it of size 39 bytes. + /// + /// The usefulness of the notion of versioned fixed nonce is discussed in Section 2.1 in [1]. + /// + /// [1]: https://github.com/algorand/falcon/blob/main/falcon-det.pdf + pub fn deterministic() -> Self { + let mut nonce_bytes = [0u8; SIG_NONCE_LEN]; + nonce_bytes[0] = NONCE_VERSION_BYTE; + nonce_bytes[1..].copy_from_slice(&PREVERSIONED_NONCE); + Self(nonce_bytes) } /// Returns a new [Nonce] drawn from the provided RNG. + /// + /// This is used only in testing against the test vectors of the reference (non-deterministic) + /// Falcon DSA implementation. + #[cfg(test)] pub fn random(rng: &mut R) -> Self { let mut nonce_bytes = [0u8; SIG_NONCE_LEN]; rng.fill_bytes(&mut nonce_bytes); - Self::new(nonce_bytes) + Self::from_bytes(nonce_bytes) + } + + /// Returns the underlying concatenated bytes of this nonce. + pub fn as_bytes(&self) -> [u8; SIG_NONCE_LEN] { + self.0 } - /// Returns the underlying bytes of this nonce. - pub fn as_bytes(&self) -> &[u8; SIG_NONCE_LEN] { - &self.0 + /// Returns a `Nonce` given an array of bytes. + pub fn from_bytes(nonce_bytes: [u8; SIG_NONCE_LEN]) -> Self { + Self(nonce_bytes) } /// Converts byte representation of the nonce into field element representation. @@ -92,7 +172,7 @@ impl Nonce { pub fn to_elements(&self) -> [Felt; NONCE_ELEMENTS] { let mut buffer = [0_u8; 8]; let mut result = [ZERO; 8]; - for (i, bytes) in self.0.chunks(5).enumerate() { + for (i, bytes) in self.as_bytes().chunks(5).enumerate() { buffer[..5].copy_from_slice(bytes); // we can safely (without overflow) create a new Felt from u64 value here since this // value contains at most 5 bytes @@ -105,13 +185,18 @@ impl Nonce { impl Serializable for &Nonce { fn write_into(&self, target: &mut W) { - target.write_bytes(&self.0) + target.write_u8(self.0[0]) } } impl Deserializable for Nonce { fn read_from(source: &mut R) -> Result { - let bytes = source.read()?; - Ok(Self(bytes)) + let nonce_version: u8 = source.read()?; + + let mut nonce_bytes = [0u8; SIG_NONCE_LEN]; + nonce_bytes[0] = nonce_version; + nonce_bytes[1..].copy_from_slice(&PREVERSIONED_NONCE); + + Ok(Self(nonce_bytes)) } } diff --git a/miden-crypto/src/dsa/rpo_falcon512/signature.rs b/miden-crypto/src/dsa/rpo_falcon512/signature.rs index aaa5b892d8..c69c7c5c51 100644 --- a/miden-crypto/src/dsa/rpo_falcon512/signature.rs +++ b/miden-crypto/src/dsa/rpo_falcon512/signature.rs @@ -4,10 +4,10 @@ use core::ops::Deref; use num::Zero; use super::{ - ByteReader, ByteWriter, Deserializable, DeserializationError, Felt, LOG_N, MODULUS, N, Nonce, - Rpo256, SIG_L2_BOUND, SIG_POLY_BYTE_LEN, Serializable, + ByteReader, ByteWriter, Deserializable, DeserializationError, LOG_N, MODULUS, N, Nonce, + SIG_L2_BOUND, SIG_POLY_BYTE_LEN, Serializable, hash_to_point::hash_to_point_rpo256, - keys::PubKeyPoly, + keys::PublicKey, math::{FalconFelt, FastFft, Polynomial}, }; use crate::Word; @@ -15,7 +15,7 @@ use crate::Word; // FALCON SIGNATURE // ================================================================================================ -/// An RPO Falcon512 signature over a message. +/// A deterministic RPO Falcon512 signature over a message. /// /// The signature is a pair of polynomials (s1, s2) in (Z_p\[x\]/(phi))^2 a nonce `r`, and a public /// key polynomial `h` where: @@ -32,26 +32,44 @@ use crate::Word; /// /// Here h is a polynomial representing the public key and pk is its digest using the Rpo256 hash /// function. c is a polynomial that is the hash-to-point of the message being signed. +/// +/// To summarize the main points of differences with the reference implementation, we have that: /// -/// The polynomial h is serialized as: -/// 1. 1 byte representing the log2(512) i.e., 9. -/// 2. 896 bytes for the public key itself. +/// 1. the hash-to-point algorithm is made deterministic by using a fixed nonce `r`. This fixed +/// nonce is formed as `nonce_version_byte || preversioned_nonce` where `preversioned_nonce` is a +/// 39-byte string that is defined as: i. a byte representing `log_2(512)`, followed by ii. the +/// UTF8 representation of the string "RPO-FALCON-DET", followed by iii. the required number of +/// 0_u8 padding to make the total length equal 39 bytes. Note that the above means in particular +/// that only the `nonce_version_byte` needs to be serialized when serializing the signature. +/// This reduces the deterministic signature compared to the reference implementation by 39 +/// bytes. +/// 2. the RNG used in the trapdoor sampler (i.e., the ffSampling algorithm) is ChaCha20Rng seeded +/// with the `Blake3` hash of `log_2(512) || sk || message`. /// /// The signature is serialized as: +/// /// 1. A header byte specifying the algorithm used to encode the coefficients of the `s2` polynomial /// together with the degree of the irreducible polynomial phi. For RPO Falcon512, the header -/// byte is set to `10111001` which differentiates it from the standardized instantiation of the +/// byte is set to `10111001` to differentiate it from the standardized instantiation of the /// Falcon signature. -/// 2. 40 bytes for the nonce. +/// 2. 1 byte for the nonce version. /// 4. 625 bytes encoding the `s2` polynomial above. /// -/// The total size of the signature (including the extended public key) is 1563 bytes. +/// In addition to the signature itself, the polynomial h is also serialized with the signature as: +/// +/// 1. 1 byte representing the log2(512) i.e., 9. +/// 2. 896 bytes for the public key itself. +/// +/// The total size of the signature (including the extended public key) is 1524 bytes. +/// +/// [1]: https://github.com/algorand/falcon/blob/main/falcon-det.pdf +/// [2]: https://datatracker.ietf.org/doc/html/rfc6979#section-3.5 #[derive(Debug, Clone, PartialEq, Eq)] pub struct Signature { header: SignatureHeader, nonce: Nonce, s2: SignaturePoly, - h: PubKeyPoly, + h: PublicKey, } impl Signature { @@ -60,7 +78,7 @@ impl Signature { /// Creates a new signature from the given nonce, public key polynomial, and signature /// polynomial. - pub fn new(nonce: Nonce, h: PubKeyPoly, s2: SignaturePoly) -> Signature { + pub fn new(nonce: Nonce, h: PublicKey, s2: SignaturePoly) -> Signature { Self { header: SignatureHeader::default(), nonce, @@ -73,7 +91,7 @@ impl Signature { // -------------------------------------------------------------------------------------------- /// Returns the public key polynomial h. - pub fn pk_poly(&self) -> &PubKeyPoly { + pub fn public_key(&self) -> &PublicKey { &self.h } @@ -92,16 +110,12 @@ impl Signature { /// Returns true if this signature is a valid signature for the specified message generated /// against the secret key matching the specified public key commitment. - pub fn verify(&self, message: Word, pubkey_com: Word) -> bool { - // compute the hash of the public key polynomial - let h_felt: Polynomial = (&**self.pk_poly()).into(); - let h_digest: Word = Rpo256::hash_elements(&h_felt.coefficients); - if h_digest != pubkey_com { + pub fn verify(&self, message: Word, pub_key: &PublicKey) -> bool { + if self.h != *pub_key { return false; } - let c = hash_to_point_rpo256(message, &self.nonce); - verify_helper(&c, &self.s2, self.pk_poly()) + verify_helper(&c, &self.s2, pub_key) } } @@ -325,7 +339,7 @@ impl Deserializable for SignaturePoly { /// Takes the hash-to-point polynomial `c` of a message, the signature polynomial over /// the message `s2` and a public key polynomial and returns `true` is the signature is a valid /// signature for the given parameters, otherwise it returns `false`. -fn verify_helper(c: &Polynomial, s2: &SignaturePoly, h: &PubKeyPoly) -> bool { +fn verify_helper(c: &Polynomial, s2: &SignaturePoly, h: &PublicKey) -> bool { let h_fft = h.fft(); let s2_fft = s2.fft(); let c_fft = c.fft(); @@ -365,7 +379,10 @@ mod tests { use rand::SeedableRng; use rand_chacha::ChaCha20Rng; - use super::{super::SecretKey, *}; + use super::{ + super::{SIG_SERIALIZED_LEN, SecretKey}, + *, + }; #[test] fn test_serialization_round_trip() { @@ -375,6 +392,7 @@ mod tests { let sk = SecretKey::with_rng(&mut rng); let signature = sk.sign_with_rng(Word::default(), &mut rng); let serialized = signature.to_bytes(); + assert_eq!(serialized.len(), SIG_SERIALIZED_LEN); let deserialized = Signature::read_from_bytes(&serialized).unwrap(); assert_eq!(signature.sig_poly(), deserialized.sig_poly()); } diff --git a/miden-crypto/src/dsa/rpo_falcon512/tests/data.rs b/miden-crypto/src/dsa/rpo_falcon512/tests/data.rs index 3b5c5624ac..bc5c69a705 100644 --- a/miden-crypto/src/dsa/rpo_falcon512/tests/data.rs +++ b/miden-crypto/src/dsa/rpo_falcon512/tests/data.rs @@ -1,3 +1,5 @@ +use crate::dsa::rpo_falcon512::SIG_SERIALIZED_LEN; + pub(crate) const NUM_TEST_VECTORS: usize = 12; /// Helper data to indicate the number of bytes the should be draw from the SHAKE256-based PRNG @@ -1749,3 +1751,85 @@ pub(crate) static SK_POLYS: [[[i16; 512]; 4]; NUM_TEST_VECTORS] = [ ], ], ]; + +/// Serialized deterministic RPO-Falcon-512 signature intended for use as a test vector +/// for the determinism in the signing procedure accros platforms. +/// +/// This was generated on an `Intel Core i5-8279U` running on Linux kernel `5.4.0-144-generic` and +/// built with Rust `1.88.0`. +pub(crate) const DETERMINISTIC_SIGNATURE: [u8; SIG_SERIALIZED_LEN] = [ + 185, 1, 22, 144, 158, 211, 85, 196, 199, 194, 123, 220, 135, 121, 154, 120, 141, 154, 66, 32, + 56, 47, 239, 41, 38, 121, 124, 172, 190, 21, 238, 237, 69, 36, 36, 245, 63, 146, 222, 205, 107, + 153, 60, 69, 60, 10, 91, 243, 160, 222, 120, 21, 132, 54, 134, 200, 184, 209, 102, 174, 244, + 236, 77, 155, 224, 162, 181, 104, 251, 8, 40, 30, 9, 14, 184, 153, 181, 189, 100, 74, 238, 146, + 99, 20, 84, 157, 181, 82, 118, 220, 172, 2, 233, 176, 72, 241, 169, 13, 245, 117, 157, 112, 30, + 76, 218, 217, 199, 73, 94, 220, 50, 113, 143, 125, 218, 52, 196, 133, 90, 209, 27, 230, 125, + 153, 181, 235, 98, 178, 151, 63, 190, 194, 43, 186, 139, 54, 38, 8, 203, 17, 91, 137, 246, 187, + 114, 179, 210, 11, 61, 177, 55, 129, 172, 18, 200, 29, 53, 203, 254, 78, 168, 251, 249, 209, 1, + 103, 177, 6, 28, 222, 220, 220, 55, 158, 166, 228, 43, 183, 31, 38, 32, 174, 174, 113, 247, + 108, 148, 225, 245, 15, 228, 225, 234, 160, 25, 161, 201, 189, 147, 158, 12, 249, 57, 71, 113, + 17, 104, 43, 187, 53, 240, 35, 244, 54, 198, 79, 88, 154, 133, 242, 85, 168, 180, 233, 161, + 103, 77, 75, 161, 81, 33, 75, 155, 10, 247, 73, 46, 24, 55, 237, 87, 219, 83, 17, 138, 226, 41, + 250, 159, 229, 73, 94, 89, 161, 70, 82, 45, 13, 193, 6, 33, 70, 127, 181, 120, 203, 81, 171, + 39, 166, 31, 201, 41, 65, 240, 178, 93, 136, 58, 71, 147, 38, 27, 204, 158, 63, 123, 120, 81, + 136, 101, 47, 63, 22, 238, 79, 226, 137, 126, 71, 217, 53, 217, 204, 96, 108, 222, 34, 161, 31, + 162, 42, 186, 101, 139, 61, 37, 97, 145, 133, 179, 65, 163, 79, 87, 19, 49, 80, 126, 112, 246, + 92, 214, 184, 153, 247, 246, 187, 199, 133, 116, 184, 45, 223, 6, 33, 101, 117, 101, 227, 207, + 127, 238, 91, 114, 134, 53, 127, 98, 204, 219, 219, 168, 136, 63, 210, 153, 218, 186, 138, 170, + 76, 215, 67, 34, 132, 146, 12, 38, 42, 149, 76, 172, 209, 231, 24, 77, 212, 205, 171, 235, 236, + 159, 220, 92, 62, 9, 164, 54, 49, 51, 192, 47, 238, 3, 229, 98, 26, 100, 47, 101, 132, 194, 8, + 142, 141, 173, 107, 191, 102, 19, 181, 209, 71, 168, 61, 175, 33, 37, 125, 37, 203, 19, 116, + 144, 176, 55, 4, 165, 47, 238, 101, 20, 131, 197, 146, 167, 222, 185, 140, 132, 80, 128, 226, + 150, 93, 203, 160, 196, 162, 141, 105, 190, 50, 92, 98, 31, 136, 102, 46, 24, 153, 6, 55, 78, + 135, 146, 24, 147, 221, 31, 74, 189, 115, 157, 83, 74, 147, 64, 255, 204, 79, 255, 31, 74, 65, + 143, 115, 35, 72, 59, 244, 26, 130, 173, 69, 96, 26, 215, 61, 97, 41, 69, 236, 230, 105, 119, + 30, 220, 90, 128, 250, 48, 134, 130, 205, 142, 196, 49, 184, 190, 101, 220, 199, 168, 217, 105, + 242, 157, 100, 135, 163, 156, 205, 172, 241, 35, 148, 124, 244, 45, 97, 213, 114, 55, 10, 126, + 117, 173, 135, 77, 239, 135, 58, 68, 243, 200, 222, 100, 52, 219, 26, 19, 217, 109, 32, 39, + 118, 130, 139, 38, 101, 231, 38, 126, 228, 20, 197, 91, 211, 248, 253, 74, 27, 201, 4, 52, 158, + 38, 116, 79, 62, 17, 107, 99, 75, 166, 247, 119, 31, 140, 97, 229, 48, 73, 179, 23, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 9, 155, 125, 185, 64, 84, 225, 93, 95, 10, 178, 100, 198, 160, 180, 110, 66, + 53, 41, 212, 204, 170, 160, 237, 167, 160, 122, 168, 168, 68, 93, 180, 2, 255, 84, 191, 48, + 157, 91, 57, 228, 201, 192, 75, 145, 62, 104, 175, 135, 156, 9, 128, 122, 1, 210, 73, 150, 34, + 200, 59, 228, 99, 89, 40, 54, 25, 217, 86, 245, 170, 187, 28, 224, 144, 102, 208, 225, 180, + 106, 60, 184, 144, 29, 213, 222, 166, 45, 212, 135, 24, 164, 201, 83, 26, 181, 36, 130, 38, + 173, 109, 97, 235, 192, 26, 43, 248, 157, 36, 62, 182, 118, 28, 234, 32, 6, 171, 179, 224, 30, + 170, 75, 80, 101, 228, 221, 195, 238, 144, 170, 6, 57, 109, 171, 41, 157, 234, 0, 103, 243, + 207, 105, 76, 164, 83, 35, 27, 73, 134, 177, 241, 38, 98, 86, 16, 200, 61, 190, 53, 115, 49, + 157, 169, 143, 109, 119, 196, 109, 151, 31, 54, 94, 22, 246, 174, 164, 162, 173, 60, 74, 18, + 220, 166, 122, 176, 32, 166, 107, 100, 229, 32, 161, 185, 210, 8, 49, 230, 61, 184, 212, 197, + 41, 114, 239, 214, 114, 177, 9, 39, 254, 197, 24, 151, 86, 141, 25, 206, 200, 146, 167, 36, 29, + 224, 66, 141, 123, 73, 246, 49, 80, 207, 109, 160, 72, 249, 70, 164, 10, 211, 190, 15, 104, + 147, 186, 216, 202, 251, 27, 246, 250, 104, 57, 91, 119, 19, 98, 173, 247, 70, 85, 8, 13, 70, + 69, 120, 52, 21, 87, 112, 50, 11, 75, 213, 167, 79, 42, 106, 58, 250, 77, 12, 133, 174, 108, + 113, 82, 17, 17, 98, 126, 97, 172, 87, 218, 221, 79, 84, 113, 33, 148, 62, 105, 150, 66, 152, + 153, 39, 237, 96, 75, 81, 1, 56, 6, 98, 92, 138, 114, 242, 189, 40, 38, 197, 118, 96, 130, 145, + 229, 138, 153, 44, 49, 89, 120, 209, 167, 205, 202, 28, 65, 174, 219, 125, 99, 31, 88, 48, 254, + 227, 34, 88, 138, 138, 60, 144, 106, 148, 158, 248, 154, 181, 53, 3, 45, 233, 164, 68, 80, 207, + 42, 209, 157, 159, 128, 94, 241, 55, 166, 231, 115, 130, 41, 132, 19, 135, 225, 120, 36, 101, + 204, 210, 161, 84, 197, 63, 5, 36, 178, 4, 229, 237, 43, 49, 212, 80, 219, 20, 172, 182, 189, + 9, 193, 112, 73, 63, 37, 148, 148, 184, 201, 96, 83, 62, 32, 186, 249, 54, 103, 208, 112, 216, + 216, 217, 97, 70, 4, 18, 42, 182, 117, 21, 222, 204, 168, 164, 123, 1, 189, 145, 70, 80, 218, + 192, 136, 81, 22, 159, 137, 194, 70, 246, 187, 150, 50, 54, 154, 203, 214, 73, 174, 205, 44, + 192, 105, 138, 192, 109, 238, 21, 64, 232, 181, 218, 129, 125, 92, 145, 87, 64, 222, 169, 183, + 57, 25, 220, 56, 92, 95, 107, 128, 117, 34, 88, 177, 235, 247, 115, 248, 208, 139, 215, 206, + 133, 153, 146, 133, 18, 219, 194, 85, 118, 162, 148, 33, 70, 46, 21, 175, 86, 230, 182, 233, + 31, 127, 188, 200, 2, 21, 203, 4, 82, 19, 185, 79, 195, 149, 207, 234, 145, 240, 155, 74, 21, + 94, 207, 38, 138, 136, 118, 45, 85, 239, 35, 210, 120, 55, 183, 88, 79, 26, 98, 215, 198, 93, + 79, 137, 27, 0, 253, 131, 54, 234, 89, 147, 30, 0, 161, 87, 69, 116, 171, 156, 11, 53, 205, + 148, 66, 106, 202, 224, 234, 155, 225, 149, 13, 40, 254, 217, 69, 232, 212, 152, 253, 119, 80, + 137, 205, 196, 98, 0, 163, 57, 144, 228, 31, 112, 141, 133, 54, 204, 155, 16, 178, 246, 44, + 138, 228, 218, 203, 49, 70, 10, 181, 7, 221, 30, 137, 14, 176, 25, 133, 174, 24, 146, 184, 191, + 54, 35, 140, 154, 45, 158, 230, 243, 4, 26, 23, 41, 210, 21, 117, 35, 136, 74, 97, 229, 76, + 217, 243, 214, 32, 2, 64, 109, 52, 243, 94, 80, 12, 238, 20, 37, 157, 159, 48, 110, 21, 243, + 154, 239, 42, 91, 34, 234, 158, 56, 194, 218, 242, 105, 244, 125, 64, 241, 13, 102, 44, 78, 23, + 56, 23, 193, 108, 97, 217, 44, 186, 3, 98, 94, 110, 94, 129, 128, 19, 3, 198, 13, 129, 46, 117, + 100, 92, 82, 50, 25, 195, 83, 39, 231, 87, 134, 238, 108, 22, 165, 101, 102, 21, 200, 31, 247, + 189, 47, 246, 128, 101, 4, 137, 74, 11, 17, 65, 192, 116, 16, 165, 47, 245, 148, 122, 75, 251, + 193, 47, 172, 28, 229, 144, 129, 39, 207, 156, 36, 180, 73, 102, 92, 109, 65, 150, 86, 22, 144, + 69, 4, 128, 184, 225, 97, 202, 86, 89, 103, 144, 165, 29, 197, 28, 139, 86, 191, 161, 122, 137, + 235, 224, 184, 130, 157, 242, 225, 96, 194, 119, 25, 222, 66, 98, 11, 34, 26, 147, 208, 199, + 195, 58, 230, 93, 49, 52, 221, 131, 2, 96, 7, 64, 242, 230, 87, 178, 141, 154, 39, 56, 108, 54, + 156, 109, 10, 91, 88, 43, 247, 88, 217, 84, 106, 36, 114, 241, 199, 15, 208, 122, 160, 66, 248, + 4, 8, 189, 5, 189, 25, 169, 107, 175, 228, +]; diff --git a/miden-crypto/src/dsa/rpo_falcon512/tests/mod.rs b/miden-crypto/src/dsa/rpo_falcon512/tests/mod.rs index a885d3c1e5..1c1c559be8 100644 --- a/miden-crypto/src/dsa/rpo_falcon512/tests/mod.rs +++ b/miden-crypto/src/dsa/rpo_falcon512/tests/mod.rs @@ -4,10 +4,14 @@ use data::{ EXPECTED_SIG, EXPECTED_SIG_POLYS, NUM_TEST_VECTORS, SK_POLYS, SYNC_DATA_FOR_TEST_VECTOR, }; use prng::Shake256Testing; -use rand::RngCore; +use rand::{RngCore, SeedableRng}; +use rand_chacha::ChaCha20Rng; use super::{Serializable, math::Polynomial}; -use crate::dsa::rpo_falcon512::SecretKey; +use crate::dsa::rpo_falcon512::{ + PREVERSIONED_NONCE, PREVERSIONED_NONCE_LEN, SIG_NONCE_LEN, SIG_POLY_BYTE_LEN, SecretKey, + tests::data::DETERMINISTIC_SIGNATURE, +}; mod data; mod prng; @@ -63,12 +67,60 @@ fn test_signature_gen_reference_impl() { assert_eq!(sig_coef, EXPECTED_SIG_POLYS[i]); // 4. compare the encoded signatures including the nonce - let sig_bytes = signature.to_bytes(); + let sig_bytes = &signature.to_bytes(); let expected_sig_bytes = EXPECTED_SIG[i]; let hex_expected_sig_bytes = hex::decode(expected_sig_bytes).unwrap(); - // we remove the headers when comparing as RPO_FALCON512 uses a different header format. - // we also remove the public key from the RPO_FALCON512 signature as this is not part of - // the signature in the reference implementation - assert_eq!(&hex_expected_sig_bytes[2..], &sig_bytes[2..2 + 664]); + // to compare against the test vectors we: + // 1. remove the headers when comparing as RPO_FALCON512 uses a different header format, + // 2. compare the nonce part separately as the deterministic version we use omits the + // inclusion of the preversioned portion of the nonce by in its serialized format, + // 3. we remove the public key from the RPO_FALCON512 signature as this is not part of the + // signature in the reference implementation, + // 4. remove the nonce version byte, in addition to the header, from `sig_bytes`. + let nonce = signature.nonce(); + assert_eq!(hex_expected_sig_bytes[1..1 + SIG_NONCE_LEN], nonce.as_bytes()); + assert_eq!( + &hex_expected_sig_bytes[1 + SIG_NONCE_LEN..], + &sig_bytes[2..2 + SIG_POLY_BYTE_LEN] + ); } } + +#[test] +fn test_signature_determinism() { + let seed = [0_u8; 32]; + let mut rng = ChaCha20Rng::from_seed(seed); + + let sk = SecretKey::with_rng(&mut rng); + let message = b"data"; + let signature = sk.sign(message.into()); + let serialized_signature = signature.to_bytes(); + + assert_eq!(serialized_signature, DETERMINISTIC_SIGNATURE); +} + +#[test] +fn check_preversioned_fixed_nonce() { + assert_eq!(build_preversioned_fixed_nonce(), PREVERSIONED_NONCE) +} + +/// Builds the preversioned portion of the fixed nonce following [1]. +/// +/// Note that [1] uses the term salt instead of nonce. +/// +/// [1]: https://github.com/algorand/falcon/blob/main/falcon-det.pdf +fn build_preversioned_fixed_nonce() -> [u8; PREVERSIONED_NONCE_LEN] { + use crate::dsa::rpo_falcon512::LOG_N; + + let mut result = [0_u8; 39]; + result[0] = LOG_N; + let domain_separator = "RPO-FALCON-DET".as_bytes(); + + result + .iter_mut() + .skip(1) + .zip(domain_separator.iter()) + .for_each(|(dst, src)| *dst = *src); + + result +} diff --git a/miden-crypto/src/hash/algebraic_sponge/mod.rs b/miden-crypto/src/hash/algebraic_sponge/mod.rs new file mode 100644 index 0000000000..84d89de650 --- /dev/null +++ b/miden-crypto/src/hash/algebraic_sponge/mod.rs @@ -0,0 +1,243 @@ +//! Algebraic sponge-based hash functions. +//! +//! These are hash functions based on the sponge construction, which itself is defined from +//! a cryptographic permutation function and a padding rule. +//! +//! Throughout the module, the padding rule used is the one in . +//! The core of the definition of an algebraic sponge-based hash function is then the definition +//! of its cryptographic permutation function. This can be done by implementing the trait +//! `[AlgebraicSponge]` which boils down to implementing the `apply_permutation` method. +//! +//! There are currently three algebraic sponge-based hash functions implemented in the module, RPO +//! and RPX hash functions, both of which belong to the Rescue familly of hash functions, and +//! Poseidon2 hash function. + +use core::ops::Range; + +use super::{CubeExtension, ElementHasher, Felt, FieldElement, Hasher, StarkField, Word, ZERO}; + +pub(crate) mod poseidon2; +pub(crate) mod rescue; + +// CONSTANTS +// ================================================================================================ + +/// Sponge state is set to 12 field elements or 96 bytes; 8 elements are reserved for rate and +/// the remaining 4 elements are reserved for capacity. +pub(crate) const STATE_WIDTH: usize = 12; + +/// The rate portion of the state is located in elements 4 through 11. +pub(crate) const RATE_RANGE: Range = 4..12; +pub(crate) const RATE_WIDTH: usize = RATE_RANGE.end - RATE_RANGE.start; + +pub(crate) const INPUT1_RANGE: Range = 4..8; +pub(crate) const INPUT2_RANGE: Range = 8..12; + +/// The capacity portion of the state is located in elements 0, 1, 2, and 3. +pub(crate) const CAPACITY_RANGE: Range = 0..4; + +/// The output of the hash function is a digest which consists of 4 field elements or 32 bytes. +/// +/// The digest is returned from state elements 4, 5, 6, and 7 (the first four elements of the +/// rate portion). +pub(crate) const DIGEST_RANGE: Range = 4..8; + +/// The number of byte chunks defining a field element when hashing a sequence of bytes +const BINARY_CHUNK_SIZE: usize = 7; + +/// S-Box and Inverse S-Box powers; +/// +/// The constants are defined for tests only because the exponentiations in the code are unrolled +/// for efficiency reasons. +#[cfg(test)] +const ALPHA: u64 = 7; +#[cfg(test)] +const INV_ALPHA: u64 = 10540996611094048183; + +// ALGEBRAIC SPONGE +// ================================================================================================ + +pub(crate) trait AlgebraicSponge { + fn apply_permutation(state: &mut [Felt; STATE_WIDTH]); + + /// Returns a hash of the provided field elements. + fn hash_elements(elements: &[E]) -> Word + where + E: FieldElement, + { + // convert the elements into a list of base field elements + let elements = E::slice_as_base_elements(elements); + + // initialize state to all zeros, except for the first element of the capacity part, which + // is set to `elements.len() % RATE_WIDTH`. + let mut state = [ZERO; STATE_WIDTH]; + state[CAPACITY_RANGE.start] = Felt::from((elements.len() % RATE_WIDTH) as u8); + + // absorb elements into the state one by one until the rate portion of the state is filled + // up; then apply the permutation and start absorbing again; repeat until all + // elements have been absorbed + let mut i = 0; + for &element in elements.iter() { + state[RATE_RANGE.start + i] = element; + i += 1; + if i.is_multiple_of(RATE_WIDTH) { + Self::apply_permutation(&mut state); + i = 0; + } + } + + // if we absorbed some elements but didn't apply a permutation to them (would happen when + // the number of elements is not a multiple of RATE_WIDTH), apply the permutation after + // padding by as many 0 as necessary to make the input length a multiple of the RATE_WIDTH. + if i > 0 { + while i != RATE_WIDTH { + state[RATE_RANGE.start + i] = ZERO; + i += 1; + } + Self::apply_permutation(&mut state); + } + + // return the first 4 elements of the state as hash result + Word::new(state[DIGEST_RANGE].try_into().unwrap()) + } + + /// Returns a hash of the provided sequence of bytes. + fn hash(bytes: &[u8]) -> Word { + // initialize the state with zeroes + let mut state = [ZERO; STATE_WIDTH]; + + // determine the number of field elements needed to encode `bytes` when each field element + // represents at most 7 bytes. + let num_field_elem = bytes.len().div_ceil(BINARY_CHUNK_SIZE); + + // set the first capacity element to `RATE_WIDTH + (num_field_elem % RATE_WIDTH)`. We do + // this to achieve: + // 1. Domain separating hashing of `[u8]` from hashing of `[Felt]`. + // 2. Avoiding collisions at the `[Felt]` representation of the encoded bytes. + state[CAPACITY_RANGE.start] = + Felt::from((RATE_WIDTH + (num_field_elem % RATE_WIDTH)) as u8); + + // initialize a buffer to receive the little-endian elements. + let mut buf = [0_u8; 8]; + + // iterate the chunks of bytes, creating a field element from each chunk and copying it + // into the state. + // + // every time the rate range is filled, a permutation is performed. if the final value of + // `rate_pos` is not zero, then the chunks count wasn't enough to fill the state range, + // and an additional permutation must be performed. + let mut current_chunk_idx = 0_usize; + // handle the case of an empty `bytes` + let last_chunk_idx = if num_field_elem == 0 { + current_chunk_idx + } else { + num_field_elem - 1 + }; + let rate_pos = bytes.chunks(BINARY_CHUNK_SIZE).fold(0, |rate_pos, chunk| { + // copy the chunk into the buffer + if current_chunk_idx != last_chunk_idx { + buf[..BINARY_CHUNK_SIZE].copy_from_slice(chunk); + } else { + // on the last iteration, we pad `buf` with a 1 followed by as many 0's as are + // needed to fill it + buf.fill(0); + buf[..chunk.len()].copy_from_slice(chunk); + buf[chunk.len()] = 1; + } + current_chunk_idx += 1; + + // set the current rate element to the input. since we take at most 7 bytes, we are + // guaranteed that the inputs data will fit into a single field element. + state[RATE_RANGE.start + rate_pos] = Felt::new(u64::from_le_bytes(buf)); + + // proceed filling the range. if it's full, then we apply a permutation and reset the + // counter to the beginning of the range. + if rate_pos == RATE_WIDTH - 1 { + Self::apply_permutation(&mut state); + 0 + } else { + rate_pos + 1 + } + }); + + // if we absorbed some elements but didn't apply a permutation to them (would happen when + // the number of elements is not a multiple of RATE_WIDTH), apply the permutation. we + // don't need to apply any extra padding because the first capacity element contains a + // flag indicating the number of field elements constituting the last block when the latter + // is not divisible by `RATE_WIDTH`. + if rate_pos != 0 { + state[RATE_RANGE.start + rate_pos..RATE_RANGE.end].fill(ZERO); + Self::apply_permutation(&mut state); + } + + // return the first 4 elements of the rate as hash result. + Word::new(state[DIGEST_RANGE].try_into().unwrap()) + } + + /// Returns a hash of two digests. This method is intended for use in construction of + /// Merkle trees and verification of Merkle paths. + fn merge(values: &[Word; 2]) -> Word { + // initialize the state by copying the digest elements into the rate portion of the state + // (8 total elements), and set the capacity elements to 0. + let mut state = [ZERO; STATE_WIDTH]; + let it = Word::words_as_elements_iter(values.iter()); + for (i, v) in it.enumerate() { + state[RATE_RANGE.start + i] = *v; + } + + // apply the permutation and return the digest portion of the state + Self::apply_permutation(&mut state); + Word::new(state[DIGEST_RANGE].try_into().unwrap()) + } + + /// Returns a hash of many digests. + fn merge_many(values: &[Word]) -> Word { + let elements = Word::words_as_elements(values); + Self::hash_elements(elements) + } + + /// Returns hash(`seed` || `value`). This method is intended for use in PRNG and PoW contexts. + fn merge_with_int(seed: Word, value: u64) -> Word { + // initialize the state as follows: + // - seed is copied into the first 4 elements of the rate portion of the state. + // - if the value fits into a single field element, copy it into the fifth rate element and + // set the first capacity element to 5. + // - if the value doesn't fit into a single field element, split it into two field elements, + // copy them into rate elements 5 and 6 and set the first capacity element to 6. + let mut state = [ZERO; STATE_WIDTH]; + state[INPUT1_RANGE].copy_from_slice(seed.as_elements()); + state[INPUT2_RANGE.start] = Felt::new(value); + if value < Felt::MODULUS { + state[CAPACITY_RANGE.start] = Felt::from(5_u8); + } else { + state[INPUT2_RANGE.start + 1] = Felt::new(value / Felt::MODULUS); + state[CAPACITY_RANGE.start] = Felt::from(6_u8); + } + + // apply the permutation and return the digest portion of the rate + Self::apply_permutation(&mut state); + Word::new(state[DIGEST_RANGE].try_into().unwrap()) + } + + // DOMAIN IDENTIFIER HASHING + // -------------------------------------------------------------------------------------------- + + /// Returns a hash of two digests and a domain identifier. + fn merge_in_domain(values: &[Word; 2], domain: Felt) -> Word { + // initialize the state by copying the digest elements into the rate portion of the state + // (8 total elements), and set the capacity elements to 0. + let mut state = [ZERO; STATE_WIDTH]; + let it = Word::words_as_elements_iter(values.iter()); + for (i, v) in it.enumerate() { + state[RATE_RANGE.start + i] = *v; + } + + // set the second capacity element to the domain value. The first capacity element is used + // for padding purposes. + state[CAPACITY_RANGE.start + 1] = domain; + + // apply the permutation and return the first four elements of the state + Self::apply_permutation(&mut state); + Word::new(state[DIGEST_RANGE].try_into().unwrap()) + } +} diff --git a/miden-crypto/src/hash/algebraic_sponge/poseidon2/constants.rs b/miden-crypto/src/hash/algebraic_sponge/poseidon2/constants.rs new file mode 100644 index 0000000000..247a1a2a1f --- /dev/null +++ b/miden-crypto/src/hash/algebraic_sponge/poseidon2/constants.rs @@ -0,0 +1,175 @@ +use super::{Felt, STATE_WIDTH}; + +// HASH FUNCTION DEFINING CONSTANTS +// ================================================================================================ + +/// Number of external rounds. +pub(crate) const NUM_EXTERNAL_ROUNDS: usize = 8; +/// Number of either initial or terminal external rounds. +pub(crate) const NUM_EXTERNAL_ROUNDS_HALF: usize = NUM_EXTERNAL_ROUNDS / 2; +/// Number of internal rounds. +pub(crate) const NUM_INTERNAL_ROUNDS: usize = 22; + +// DIAGONAL MATRIX USED IN INTERNAL ROUNDS +// ================================================================================================ + +pub(crate) const MAT_DIAG: [Felt; STATE_WIDTH] = [ + Felt::new(0xc3b6c08e23ba9300), + Felt::new(0xd84b5de94a324fb6), + Felt::new(0x0d0c371c5b35b84f), + Felt::new(0x7964f570e7188037), + Felt::new(0x5daf18bbd996604b), + Felt::new(0x6743bc47b9595257), + Felt::new(0x5528b9362c59bb70), + Felt::new(0xac45e25b7127b68b), + Felt::new(0xa2077d7dfbb606b5), + Felt::new(0xf3faac6faee378ae), + Felt::new(0x0c6388b51545e883), + Felt::new(0xd27dbb6944917b60), +]; + +// ROUND CONSTANTS +// ================================================================================================ + +pub(crate) const ARK_EXT_INITIAL: [[Felt; 12]; 4] = [ + [ + Felt::new(0x13dcf33aba214f46), + Felt::new(0x30b3b654a1da6d83), + Felt::new(0x1fc634ada6159b56), + Felt::new(0x937459964dc03466), + Felt::new(0xedd2ef2ca7949924), + Felt::new(0xede9affde0e22f68), + Felt::new(0x8515b9d6bac9282d), + Felt::new(0x6b5c07b4e9e900d8), + Felt::new(0x1ec66368838c8a08), + Felt::new(0x9042367d80d1fbab), + Felt::new(0x400283564a3c3799), + Felt::new(0x4a00be0466bca75e), + ], + [ + Felt::new(0x7913beee58e3817f), + Felt::new(0xf545e88532237d90), + Felt::new(0x22f8cb8736042005), + Felt::new(0x6f04990e247a2623), + Felt::new(0xfe22e87ba37c38cd), + Felt::new(0xd20e32c85ffe2815), + Felt::new(0x117227674048fe73), + Felt::new(0x4e9fb7ea98a6b145), + Felt::new(0xe0866c232b8af08b), + Felt::new(0x00bbc77916884964), + Felt::new(0x7031c0fb990d7116), + Felt::new(0x240a9e87cf35108f), + ], + [ + Felt::new(0x2e6363a5a12244b3), + Felt::new(0x5e1c3787d1b5011c), + Felt::new(0x4132660e2a196e8b), + Felt::new(0x3a013b648d3d4327), + Felt::new(0xf79839f49888ea43), + Felt::new(0xfe85658ebafe1439), + Felt::new(0xb6889825a14240bd), + Felt::new(0x578453605541382b), + Felt::new(0x4508cda8f6b63ce9), + Felt::new(0x9c3ef35848684c91), + Felt::new(0x0812bde23c87178c), + Felt::new(0xfe49638f7f722c14), + ], + [ + Felt::new(0x8e3f688ce885cbf5), + Felt::new(0xb8e110acf746a87d), + Felt::new(0xb4b2e8973a6dabef), + Felt::new(0x9e714c5da3d462ec), + Felt::new(0x6438f9033d3d0c15), + Felt::new(0x24312f7cf1a27199), + Felt::new(0x23f843bb47acbf71), + Felt::new(0x9183f11a34be9f01), + Felt::new(0x839062fbb9d45dbf), + Felt::new(0x24b56e7e6c2e43fa), + Felt::new(0xe1683da61c962a72), + Felt::new(0xa95c63971a19bfa7), + ], +]; + +pub(crate) const ARK_INT: [Felt; 22] = [ + Felt::new(0x4adf842aa75d4316), + Felt::new(0xf8fbb871aa4ab4eb), + Felt::new(0x68e85b6eb2dd6aeb), + Felt::new(0x07a0b06b2d270380), + Felt::new(0xd94e0228bd282de4), + Felt::new(0x8bdd91d3250c5278), + Felt::new(0x209c68b88bba778f), + Felt::new(0xb5e18cdab77f3877), + Felt::new(0xb296a3e808da93fa), + Felt::new(0x8370ecbda11a327e), + Felt::new(0x3f9075283775dad8), + Felt::new(0xb78095bb23c6aa84), + Felt::new(0x3f36b9fe72ad4e5f), + Felt::new(0x69bc96780b10b553), + Felt::new(0x3f1d341f2eb7b881), + Felt::new(0x4e939e9815838818), + Felt::new(0xda366b3ae2a31604), + Felt::new(0xbc89db1e7287d509), + Felt::new(0x6102f411f9ef5659), + Felt::new(0x58725c5e7ac1f0ab), + Felt::new(0x0df5856c798883e7), + Felt::new(0xf7bb62a8da4c961b), +]; + +pub(crate) const ARK_EXT_TERMINAL: [[Felt; STATE_WIDTH]; 4] = [ + [ + Felt::new(0xc68be7c94882a24d), + Felt::new(0xaf996d5d5cdaedd9), + Felt::new(0x9717f025e7daf6a5), + Felt::new(0x6436679e6e7216f4), + Felt::new(0x8a223d99047af267), + Felt::new(0xbb512e35a133ba9a), + Felt::new(0xfbbf44097671aa03), + Felt::new(0xf04058ebf6811e61), + Felt::new(0x5cca84703fac7ffb), + Felt::new(0x9b55c7945de6469f), + Felt::new(0x8e05bf09808e934f), + Felt::new(0x2ea900de876307d7), + ], + [ + Felt::new(0x7748fff2b38dfb89), + Felt::new(0x6b99a676dd3b5d81), + Felt::new(0xac4bb7c627cf7c13), + Felt::new(0xadb6ebe5e9e2f5ba), + Felt::new(0x2d33378cafa24ae3), + Felt::new(0x1e5b73807543f8c2), + Felt::new(0x09208814bfebb10f), + Felt::new(0x782e64b6bb5b93dd), + Felt::new(0xadd5a48eac90b50f), + Felt::new(0xadd4c54c736ea4b1), + Felt::new(0xd58dbb86ed817fd8), + Felt::new(0x6d5ed1a533f34ddd), + ], + [ + Felt::new(0x28686aa3e36b7cb9), + Felt::new(0x591abd3476689f36), + Felt::new(0x047d766678f13875), + Felt::new(0xa2a11112625f5b49), + Felt::new(0x21fd10a3f8304958), + Felt::new(0xf9b40711443b0280), + Felt::new(0xd2697eb8b2bde88e), + Felt::new(0x3493790b51731b3f), + Felt::new(0x11caf9dd73764023), + Felt::new(0x7acfb8f72878164e), + Felt::new(0x744ec4db23cefc26), + Felt::new(0x1e00e58f422c6340), + ], + [ + Felt::new(0x21dd28d906a62dda), + Felt::new(0xf32a46ab5f465b5f), + Felt::new(0xbfce13201f3f7e6b), + Felt::new(0xf30d2e7adb5304e2), + Felt::new(0xecdf4ee4abad48e9), + Felt::new(0xf94e82182d395019), + Felt::new(0x4ee52e3744d887c5), + Felt::new(0xa1341c7cac0083b2), + Felt::new(0x2302fb26c30c834a), + Felt::new(0xaea3c587273bf7d3), + Felt::new(0xf798e24961823ec7), + Felt::new(0x962deba3e9a2cd94), + ], +]; diff --git a/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs b/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs new file mode 100644 index 0000000000..f239b7d04f --- /dev/null +++ b/miden-crypto/src/hash/algebraic_sponge/poseidon2/mod.rs @@ -0,0 +1,315 @@ +use super::{ + AlgebraicSponge, CAPACITY_RANGE, DIGEST_RANGE, ElementHasher, Felt, FieldElement, Hasher, + RATE_RANGE, Range, STATE_WIDTH, Word, ZERO, +}; + +mod constants; +use constants::*; + +#[cfg(test)] +mod test; + +/// Implementation of the Poseidon2 hash function with 256-bit output. +/// +/// The implementation follows the orignal [specification](https://eprint.iacr.org/2023/323) and +/// its accompanying reference [implementation](https://github.com/HorizenLabs/poseidon2). +/// +/// The parameters used to instantiate the function are: +/// * Field: 64-bit prime field with modulus 2^64 - 2^32 + 1. +/// * State width: 12 field elements. +/// * Capacity size: 4 field elements. +/// * S-Box degree: 7. +/// * Rounds: There are 2 different types of rounds, called internal and external, and are +/// structured as follows: +/// - Initial External rounds (IE): `add_constants` → `apply_sbox` → `apply_matmul_external`. +/// - Internal rounds: `add_constants` → `apply_sbox` → `apply_matmul_internal`, where the constant +/// addition and sbox application apply only to the first entry of the state. +/// - Terminal External rounds (TE): `add_constants` → `apply_sbox` → `apply_matmul_external`. +/// - An additional `apply_matmul_external` is inserted at the beginning in order to protect against +/// some recent attacks. +/// +/// The above parameters target a 128-bit security level. The digest consists of four field elements +/// and it can be serialized into 32 bytes (256 bits). +/// +/// ## Hash output consistency +/// Functions [hash_elements()](Poseidon2::hash_elements), [merge()](Poseidon2::merge), and +/// [merge_with_int()](Poseidon2::merge_with_int) are internally consistent. That is, computing +/// a hash for the same set of elements using these functions will always produce the same +/// result. For example, merging two digests using [merge()](Poseidon2::merge) will produce the +/// same result as hashing 8 elements which make up these digests using +/// [hash_elements()](Poseidon2::hash_elements) function. +/// +/// However, [hash()](Poseidon2::hash) function is not consistent with functions mentioned above. +/// For example, if we take two field elements, serialize them to bytes and hash them using +/// [hash()](Poseidon2::hash), the result will differ from the result obtained by hashing these +/// elements directly using [hash_elements()](Poseidon2::hash_elements) function. The reason for +/// this difference is that [hash()](Poseidon2::hash) function needs to be able to handle +/// arbitrary binary strings, which may or may not encode valid field elements - and thus, +/// deserialization procedure used by this function is different from the procedure used to +/// deserialize valid field elements. +/// +/// Thus, if the underlying data consists of valid field elements, it might make more sense +/// to deserialize them into field elements and then hash them using +/// [hash_elements()](Poseidon2::hash_elements) function rather than hashing the serialized bytes +/// using [hash()](Poseidon2::hash) function. +/// +/// ## Domain separation +/// [merge_in_domain()](Poseidon2::merge_in_domain) hashes two digests into one digest with some +/// domain identifier and the current implementation sets the second capacity element to the value +/// of this domain identifier. Using a similar argument to the one formulated for domain separation +/// in Appendix C of the [specifications](https://eprint.iacr.org/2023/1045), one sees that doing +/// so degrades only pre-image resistance, from its initial bound of c.log_2(p), by as much as +/// the log_2 of the size of the domain identifier space. Since pre-image resistance becomes +/// the bottleneck for the security bound of the sponge in overwrite-mode only when it is +/// lower than 2^128, we see that the target 128-bit security level is maintained as long as +/// the size of the domain identifier space, including for padding, is less than 2^128. +/// +/// ## Hashing of empty input +/// The current implementation hashes empty input to the zero digest [0, 0, 0, 0]. This has +/// the benefit of requiring no calls to the Poseidon2 permutation when hashing empty input. +#[allow(rustdoc::private_intra_doc_links)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Poseidon2(); + +impl AlgebraicSponge for Poseidon2 { + fn apply_permutation(state: &mut [Felt; STATE_WIDTH]) { + // 1. Apply (external) linear layer to the input + Self::apply_matmul_external(state); + + // 2. Apply initial external rounds to the state + Self::initial_external_rounds(state); + + // 3. Apply internal rounds to the state + Self::internal_rounds(state); + + // 4. Apply terminal external rounds to the state + Self::terminal_external_rounds(state); + } +} + +impl Hasher for Poseidon2 { + const COLLISION_RESISTANCE: u32 = 128; + + type Digest = Word; + + fn hash(bytes: &[u8]) -> Self::Digest { + ::hash(bytes) + } + + fn merge(values: &[Self::Digest; 2]) -> Self::Digest { + ::merge(values) + } + + fn merge_many(values: &[Self::Digest]) -> Self::Digest { + ::merge_many(values) + } + + fn merge_with_int(seed: Self::Digest, value: u64) -> Self::Digest { + ::merge_with_int(seed, value) + } +} + +impl ElementHasher for Poseidon2 { + type BaseField = Felt; + + fn hash_elements>(elements: &[E]) -> Self::Digest { + ::hash_elements(elements) + } +} + +impl Poseidon2 { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// Number of initial or terminal external rounds. + pub const NUM_EXTERNAL_ROUNDS_HALF: usize = NUM_EXTERNAL_ROUNDS_HALF; + /// Number of internal rounds. + pub const NUM_INTERNAL_ROUNDS: usize = NUM_INTERNAL_ROUNDS; + + /// Sponge state is set to 12 field elements or 768 bytes; 8 elements are reserved for rate and + /// the remaining 4 elements are reserved for capacity. + pub const STATE_WIDTH: usize = STATE_WIDTH; + + /// The rate portion of the state is located in elements 4 through 11 (inclusive). + pub const RATE_RANGE: Range = RATE_RANGE; + + /// The capacity portion of the state is located in elements 0, 1, 2, and 3. + pub const CAPACITY_RANGE: Range = CAPACITY_RANGE; + + /// The output of the hash function can be read from state elements 4, 5, 6, and 7. + pub const DIGEST_RANGE: Range = DIGEST_RANGE; + + /// Matrix used for computing the linear layers of internal rounds. + pub const MAT_DIAG: [Felt; STATE_WIDTH] = MAT_DIAG; + + /// Round constants added to the hasher state. + pub const ARK_EXT_INITIAL: [[Felt; STATE_WIDTH]; NUM_EXTERNAL_ROUNDS_HALF] = ARK_EXT_INITIAL; + pub const ARK_EXT_TERMINAL: [[Felt; STATE_WIDTH]; NUM_EXTERNAL_ROUNDS_HALF] = ARK_EXT_TERMINAL; + pub const ARK_INT: [Felt; NUM_INTERNAL_ROUNDS] = ARK_INT; + + // TRAIT PASS-THROUGH FUNCTIONS + // -------------------------------------------------------------------------------------------- + + /// Returns a hash of the provided sequence of bytes. + #[inline(always)] + pub fn hash(bytes: &[u8]) -> Word { + ::hash(bytes) + } + + /// Returns a hash of two digests. This method is intended for use in construction of + /// Merkle trees and verification of Merkle paths. + #[inline(always)] + pub fn merge(values: &[Word; 2]) -> Word { + ::merge(values) + } + + /// Returns a hash of the provided field elements. + #[inline(always)] + pub fn hash_elements>(elements: &[E]) -> Word { + ::hash_elements(elements) + } + + /// Returns a hash of two digests and a domain identifier. + #[inline(always)] + pub fn merge_in_domain(values: &[Word; 2], domain: Felt) -> Word { + ::merge_in_domain(values, domain) + } + + // POSEIDON2 PERMUTATION + // -------------------------------------------------------------------------------------------- + + /// Applies the initial external rounds of the permutation. + #[allow(clippy::needless_range_loop)] + #[inline(always)] + fn initial_external_rounds(state: &mut [Felt; STATE_WIDTH]) { + for r in 0..NUM_EXTERNAL_ROUNDS_HALF { + Self::add_rc(state, &ARK_EXT_INITIAL[r]); + Self::apply_sbox(state); + Self::apply_matmul_external(state); + } + } + + /// Applies the internal rounds of the permutation. + #[allow(clippy::needless_range_loop)] + #[inline(always)] + fn internal_rounds(state: &mut [Felt; STATE_WIDTH]) { + for r in 0..NUM_INTERNAL_ROUNDS { + state[0] += ARK_INT[r]; + state[0] = state[0].exp7(); + Self::matmul_internal(state, MAT_DIAG); + } + } + + /// Applies the terminal external rounds of the permutation. + #[inline(always)] + #[allow(clippy::needless_range_loop)] + fn terminal_external_rounds(state: &mut [Felt; STATE_WIDTH]) { + for r in 0..NUM_EXTERNAL_ROUNDS_HALF { + Self::add_rc(state, &ARK_EXT_TERMINAL[r]); + Self::apply_sbox(state); + Self::apply_matmul_external(state); + } + } + + /// Applies the M_E linear layer to the state. + /// + /// This basically takes any 4 x 4 MDS matrix M and computes the matrix-vector product with + /// the matrix defined by `[[2M, M, ..., M], [M, 2M, ..., M], ..., [M, M, ..., 2M]]`. + /// + /// Given the structure of the above matrix, we can compute the product of the state with + /// matrix `[M, M, ..., M]` and compute the final result using a few addition. + #[inline(always)] + fn apply_matmul_external(state: &mut [Felt; STATE_WIDTH]) { + // multiply the state by `[M, M, ..., M]` block-wise + Self::matmul_m4(state); + + // accumulate column-wise sums + let number_blocks = STATE_WIDTH / 4; + let mut stored = [ZERO; 4]; + for j in 0..number_blocks { + let base = j * 4; + for l in 0..4 { + stored[l] += state[base + l]; + } + } + + // add stored column-sums to each element + for (i, val) in state.iter_mut().enumerate() { + *val += stored[i % 4]; + } + } + + /// Multiplies the state block-wise with a 4 x 4 MDS matrix. + #[inline(always)] + fn matmul_m4(state: &mut [Felt; STATE_WIDTH]) { + let t4 = STATE_WIDTH / 4; + + for i in 0..t4 { + let idx = i * 4; + + let a = state[idx]; + let b = state[idx + 1]; + let c = state[idx + 2]; + let d = state[idx + 3]; + + let t0 = a + b; + let t1 = c + d; + let two_b = b.double(); + let two_d = d.double(); + + let t2 = two_b + t1; + let t3 = two_d + t0; + + let t4 = t1.mul_small(4) + t3; + let t5 = t0.mul_small(4) + t2; + + let t6 = t3 + t5; + let t7 = t2 + t4; + + state[idx] = t6; + state[idx + 1] = t5; + state[idx + 2] = t7; + state[idx + 3] = t4; + } + } + + /// Applies the M_I linear layer to the state. + /// + /// The matrix is given by its diagonal entries with the remaining entries set equal to 1. + /// Hence, given the sum of the state entries, the matrix-vector product is computed using + /// a multiply-and-add per state entry. + #[inline(always)] + fn matmul_internal(state: &mut [Felt; STATE_WIDTH], mat_diag: [Felt; 12]) { + let mut sum = ZERO; + for s in state.iter().take(STATE_WIDTH) { + sum += *s + } + + for i in 0..state.len() { + state[i] = state[i] * mat_diag[i] + sum; + } + } + + /// Adds the round-constants to the state during external rounds. + #[inline(always)] + fn add_rc(state: &mut [Felt; STATE_WIDTH], ark: &[Felt; 12]) { + state.iter_mut().zip(ark).for_each(|(s, &k)| *s += k); + } + + /// Applies the sbox entry-wise to the state. + #[inline(always)] + fn apply_sbox(state: &mut [Felt; STATE_WIDTH]) { + state[0] = state[0].exp7(); + state[1] = state[1].exp7(); + state[2] = state[2].exp7(); + state[3] = state[3].exp7(); + state[4] = state[4].exp7(); + state[5] = state[5].exp7(); + state[6] = state[6].exp7(); + state[7] = state[7].exp7(); + state[8] = state[8].exp7(); + state[9] = state[9].exp7(); + state[10] = state[10].exp7(); + state[11] = state[11].exp7(); + } +} diff --git a/miden-crypto/src/hash/algebraic_sponge/poseidon2/test.rs b/miden-crypto/src/hash/algebraic_sponge/poseidon2/test.rs new file mode 100644 index 0000000000..0d2cdf193b --- /dev/null +++ b/miden-crypto/src/hash/algebraic_sponge/poseidon2/test.rs @@ -0,0 +1,38 @@ +use super::{Felt, ZERO}; +use crate::hash::{algebraic_sponge::AlgebraicSponge, poseidon2::Poseidon2}; + +#[test] +fn permutation_test_vector() { + // tests that the current implementation is consistent with + // the reference [implementation](https://github.com/HorizenLabs/poseidon2) and uses + // the test vectors provided therein + let mut elements = [ + ZERO, + Felt::new(1), + Felt::new(2), + Felt::new(3), + Felt::new(4), + Felt::new(5), + Felt::new(6), + Felt::new(7), + Felt::new(8), + Felt::new(9), + Felt::new(10), + Felt::new(11), + ]; + + Poseidon2::apply_permutation(&mut elements); + let perm = elements; + assert_eq!(perm[0], Felt::new(0x01eaef96bdf1c0c1)); + assert_eq!(perm[1], Felt::new(0x1f0d2cc525b2540c)); + assert_eq!(perm[2], Felt::new(0x6282c1dfe1e0358d)); + assert_eq!(perm[3], Felt::new(0xe780d721f698e1e6)); + assert_eq!(perm[4], Felt::new(0x280c0b6f753d833b)); + assert_eq!(perm[5], Felt::new(0x1b942dd5023156ab)); + assert_eq!(perm[6], Felt::new(0x43f0df3fcccb8398)); + assert_eq!(perm[7], Felt::new(0xe8e8190585489025)); + assert_eq!(perm[8], Felt::new(0x56bdbf72f77ada22)); + assert_eq!(perm[9], Felt::new(0x7911c32bf9dcd705)); + assert_eq!(perm[10], Felt::new(0xec467926508fbe67)); + assert_eq!(perm[11], Felt::new(0x6a50450ddf85a6ed)); +} diff --git a/miden-crypto/src/hash/rescue/arch/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/arch/mod.rs similarity index 97% rename from miden-crypto/src/hash/rescue/arch/mod.rs rename to miden-crypto/src/hash/algebraic_sponge/rescue/arch/mod.rs index 2ac3937891..f8aa228c99 100644 --- a/miden-crypto/src/hash/rescue/arch/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/arch/mod.rs @@ -81,7 +81,7 @@ pub mod optimized { #[cfg(not(any(target_feature = "avx2", target_feature = "sve")))] pub mod optimized { - use crate::{Felt, hash::rescue::STATE_WIDTH}; + use crate::{Felt, hash::algebraic_sponge::rescue::STATE_WIDTH}; #[inline(always)] pub fn add_constants_and_apply_sbox( diff --git a/miden-crypto/src/hash/rescue/arch/x86_64_avx2.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/arch/x86_64_avx2.rs similarity index 100% rename from miden-crypto/src/hash/rescue/arch/x86_64_avx2.rs rename to miden-crypto/src/hash/algebraic_sponge/rescue/arch/x86_64_avx2.rs diff --git a/miden-crypto/src/hash/rescue/mds/freq.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/mds/freq.rs similarity index 100% rename from miden-crypto/src/hash/rescue/mds/freq.rs rename to miden-crypto/src/hash/algebraic_sponge/rescue/mds/freq.rs diff --git a/miden-crypto/src/hash/rescue/mds/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/mds/mod.rs similarity index 100% rename from miden-crypto/src/hash/rescue/mds/mod.rs rename to miden-crypto/src/hash/algebraic_sponge/rescue/mds/mod.rs diff --git a/miden-crypto/src/hash/rescue/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/mod.rs similarity index 88% rename from miden-crypto/src/hash/rescue/mod.rs rename to miden-crypto/src/hash/algebraic_sponge/rescue/mod.rs index f4f03b4800..f05136d30c 100644 --- a/miden-crypto/src/hash/rescue/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/mod.rs @@ -1,6 +1,9 @@ -use core::ops::Range; - -use super::{CubeExtension, ElementHasher, Felt, FieldElement, Hasher, StarkField, ZERO}; +#[cfg(test)] +use super::{ALPHA, INV_ALPHA}; +use super::{ + AlgebraicSponge, CAPACITY_RANGE, CubeExtension, DIGEST_RANGE, ElementHasher, Felt, + FieldElement, Hasher, RATE_RANGE, Range, STATE_WIDTH, StarkField, Word, ZERO, +}; mod arch; pub use arch::optimized::{add_constants_and_apply_inv_sbox, add_constants_and_apply_sbox}; @@ -20,42 +23,10 @@ mod tests; // CONSTANTS // ================================================================================================ -/// The number of rounds is set to 7. For the RPO hash functions all rounds are uniform. For the +/// The number of rounds is set to 7. For the RPO hash function all rounds are uniform. For the /// RPX hash function, there are 3 different types of rounds. const NUM_ROUNDS: usize = 7; -/// Sponge state is set to 12 field elements or 96 bytes; 8 elements are reserved for rate and -/// the remaining 4 elements are reserved for capacity. -const STATE_WIDTH: usize = 12; - -/// The rate portion of the state is located in elements 4 through 11. -const RATE_RANGE: Range = 4..12; -const RATE_WIDTH: usize = RATE_RANGE.end - RATE_RANGE.start; - -const INPUT1_RANGE: Range = 4..8; -const INPUT2_RANGE: Range = 8..12; - -/// The capacity portion of the state is located in elements 0, 1, 2, and 3. -const CAPACITY_RANGE: Range = 0..4; - -/// The output of the hash function is a digest which consists of 4 field elements or 32 bytes. -/// -/// The digest is returned from state elements 4, 5, 6, and 7 (the first four elements of the -/// rate portion). -const DIGEST_RANGE: Range = 4..8; - -/// The number of byte chunks defining a field element when hashing a sequence of bytes -const BINARY_CHUNK_SIZE: usize = 7; - -/// S-Box and Inverse S-Box powers; -/// -/// The constants are defined for tests only because the exponentiations in the code are unrolled -/// for efficiency reasons. -#[cfg(test)] -const ALPHA: u64 = 7; -#[cfg(test)] -const INV_ALPHA: u64 = 10540996611094048183; - // SBOX FUNCTION // ================================================================================================ diff --git a/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs new file mode 100644 index 0000000000..07bba9533d --- /dev/null +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/mod.rs @@ -0,0 +1,199 @@ +use super::{ + ARK1, ARK2, AlgebraicSponge, CAPACITY_RANGE, DIGEST_RANGE, ElementHasher, Felt, FieldElement, + Hasher, MDS, NUM_ROUNDS, RATE_RANGE, Range, STATE_WIDTH, Word, add_constants, + add_constants_and_apply_inv_sbox, add_constants_and_apply_sbox, apply_inv_sbox, apply_mds, + apply_sbox, +}; + +#[cfg(test)] +mod tests; + +// HASHER IMPLEMENTATION +// ================================================================================================ + +/// Implementation of the Rescue Prime Optimized hash function with 256-bit output. +/// +/// The hash function is implemented according to the Rescue Prime Optimized +/// [specifications](https://eprint.iacr.org/2022/1577) while the padding rule follows the one +/// described [here](https://eprint.iacr.org/2023/1045). +/// +/// The parameters used to instantiate the function are: +/// * Field: 64-bit prime field with modulus p = 2^64 - 2^32 + 1. +/// * State width: 12 field elements. +/// * Rate size: r = 8 field elements. +/// * Capacity size: c = 4 field elements. +/// * Number of founds: 7. +/// * S-Box degree: 7. +/// +/// The above parameters target a 128-bit security level. The digest consists of four field elements +/// and it can be serialized into 32 bytes (256 bits). +/// +/// ## Hash output consistency +/// Functions [hash_elements()](Rpo256::hash_elements), [merge()](Rpo256::merge), and +/// [merge_with_int()](Rpo256::merge_with_int) are internally consistent. That is, computing +/// a hash for the same set of elements using these functions will always produce the same +/// result. For example, merging two digests using [merge()](Rpo256::merge) will produce the +/// same result as hashing 8 elements which make up these digests using +/// [hash_elements()](Rpo256::hash_elements) function. +/// +/// However, [hash()](Rpo256::hash) function is not consistent with functions mentioned above. +/// For example, if we take two field elements, serialize them to bytes and hash them using +/// [hash()](Rpo256::hash), the result will differ from the result obtained by hashing these +/// elements directly using [hash_elements()](Rpo256::hash_elements) function. The reason for +/// this difference is that [hash()](Rpo256::hash) function needs to be able to handle +/// arbitrary binary strings, which may or may not encode valid field elements - and thus, +/// deserialization procedure used by this function is different from the procedure used to +/// deserialize valid field elements. +/// +/// Thus, if the underlying data consists of valid field elements, it might make more sense +/// to deserialize them into field elements and then hash them using +/// [hash_elements()](Rpo256::hash_elements) function rather than hashing the serialized bytes +/// using [hash()](Rpo256::hash) function. +/// +/// ## Domain separation +/// [merge_in_domain()](Rpo256::merge_in_domain) hashes two digests into one digest with some domain +/// identifier and the current implementation sets the second capacity element to the value of +/// this domain identifier. Using a similar argument to the one formulated for domain separation of +/// the RPX hash function in Appendix C of its [specification](https://eprint.iacr.org/2023/1045), +/// one sees that doing so degrades only pre-image resistance, from its initial bound of c.log_2(p), +/// by as much as the log_2 of the size of the domain identifier space. Since pre-image resistance +/// becomes the bottleneck for the security bound of the sponge in overwrite-mode only when it is +/// lower than 2^128, we see that the target 128-bit security level is maintained as long as +/// the size of the domain identifier space, including for padding, is less than 2^128. +/// +/// ## Hashing of empty input +/// The current implementation hashes empty input to the zero digest [0, 0, 0, 0]. This has +/// the benefit of requiring no calls to the RPO permutation when hashing empty input. +#[allow(rustdoc::private_intra_doc_links)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Rpo256(); + +impl AlgebraicSponge for Rpo256 { + // RESCUE PERMUTATION + // -------------------------------------------------------------------------------------------- + + /// Applies RPO permutation to the provided state. + #[inline(always)] + fn apply_permutation(state: &mut [Felt; STATE_WIDTH]) { + for i in 0..NUM_ROUNDS { + Self::apply_round(state, i); + } + } +} + +impl Rpo256 { + // CONSTANTS + // -------------------------------------------------------------------------------------------- + + /// The number of rounds is set to 7 to target 128-bit security level. + pub const NUM_ROUNDS: usize = NUM_ROUNDS; + + /// Sponge state is set to 12 field elements or 768 bytes; 8 elements are reserved for rate and + /// the remaining 4 elements are reserved for capacity. + pub const STATE_WIDTH: usize = STATE_WIDTH; + + /// The rate portion of the state is located in elements 4 through 11 (inclusive). + pub const RATE_RANGE: Range = RATE_RANGE; + + /// The capacity portion of the state is located in elements 0, 1, 2, and 3. + pub const CAPACITY_RANGE: Range = CAPACITY_RANGE; + + /// The output of the hash function can be read from state elements 4, 5, 6, and 7. + pub const DIGEST_RANGE: Range = DIGEST_RANGE; + + /// MDS matrix used for computing the linear layer in a RPO round. + pub const MDS: [[Felt; STATE_WIDTH]; STATE_WIDTH] = MDS; + + /// Round constants added to the hasher state in the first half of the RPO round. + pub const ARK1: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = ARK1; + + /// Round constants added to the hasher state in the second half of the RPO round. + pub const ARK2: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = ARK2; + + // TRAIT PASS-THROUGH FUNCTIONS + // -------------------------------------------------------------------------------------------- + + /// Returns a hash of the provided sequence of bytes. + #[inline(always)] + pub fn hash(bytes: &[u8]) -> Word { + ::hash(bytes) + } + + /// Returns a hash of two digests. This method is intended for use in construction of + /// Merkle trees and verification of Merkle paths. + #[inline(always)] + pub fn merge(values: &[Word; 2]) -> Word { + ::merge(values) + } + + /// Returns a hash of the provided field elements. + #[inline(always)] + pub fn hash_elements>(elements: &[E]) -> Word { + ::hash_elements(elements) + } + + /// Returns a hash of two digests and a domain identifier. + #[inline(always)] + pub fn merge_in_domain(values: &[Word; 2], domain: Felt) -> Word { + ::merge_in_domain(values, domain) + } + + // RESCUE PERMUTATION + // -------------------------------------------------------------------------------------------- + + /// Applies RPO permutation to the provided state. + #[inline(always)] + pub fn apply_permutation(state: &mut [Felt; STATE_WIDTH]) { + for i in 0..NUM_ROUNDS { + Self::apply_round(state, i); + } + } + + /// RPO round function. + #[inline(always)] + pub fn apply_round(state: &mut [Felt; STATE_WIDTH], round: usize) { + // apply first half of RPO round + apply_mds(state); + if !add_constants_and_apply_sbox(state, &ARK1[round]) { + add_constants(state, &ARK1[round]); + apply_sbox(state); + } + + // apply second half of RPO round + apply_mds(state); + if !add_constants_and_apply_inv_sbox(state, &ARK2[round]) { + add_constants(state, &ARK2[round]); + apply_inv_sbox(state); + } + } +} + +impl Hasher for Rpo256 { + const COLLISION_RESISTANCE: u32 = 128; + + type Digest = Word; + + fn hash(bytes: &[u8]) -> Self::Digest { + ::hash(bytes) + } + + fn merge(values: &[Self::Digest; 2]) -> Self::Digest { + ::merge(values) + } + + fn merge_many(values: &[Self::Digest]) -> Self::Digest { + ::merge_many(values) + } + + fn merge_with_int(seed: Self::Digest, value: u64) -> Self::Digest { + ::merge_with_int(seed, value) + } +} + +impl ElementHasher for Rpo256 { + type BaseField = Felt; + + fn hash_elements>(elements: &[E]) -> Self::Digest { + ::hash_elements(elements) + } +} diff --git a/miden-crypto/src/hash/rescue/rpo/tests.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs similarity index 98% rename from miden-crypto/src/hash/rescue/rpo/tests.rs rename to miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs index eba0104747..bb177eaa91 100644 --- a/miden-crypto/src/hash/rescue/rpo/tests.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpo/tests.rs @@ -5,11 +5,11 @@ use rand_utils::rand_value; use super::{ super::{ALPHA, INV_ALPHA, apply_inv_sbox, apply_sbox}, - Felt, FieldElement, Hasher, Rpo256, STATE_WIDTH, StarkField, Word, ZERO, + Felt, Hasher, Rpo256, STATE_WIDTH, }; use crate::{ - ONE, - hash::rescue::{BINARY_CHUNK_SIZE, CAPACITY_RANGE, RATE_WIDTH}, + FieldElement, ONE, StarkField, Word, ZERO, + hash::algebraic_sponge::{BINARY_CHUNK_SIZE, CAPACITY_RANGE, RATE_WIDTH}, }; #[test] @@ -131,7 +131,7 @@ fn hash_padding() { #[test] fn hash_padding_no_extra_permutation_call() { - use crate::hash::rescue::DIGEST_RANGE; + use crate::hash::algebraic_sponge::DIGEST_RANGE; // Implementation let num_bytes = BINARY_CHUNK_SIZE * RATE_WIDTH; diff --git a/miden-crypto/src/hash/rescue/rpx/mod.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs similarity index 53% rename from miden-crypto/src/hash/rescue/rpx/mod.rs rename to miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs index 7050234e5a..55040f5d89 100644 --- a/miden-crypto/src/hash/rescue/rpx/mod.rs +++ b/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/mod.rs @@ -1,12 +1,12 @@ -use core::ops::Range; - use super::{ - ARK1, ARK2, BINARY_CHUNK_SIZE, CAPACITY_RANGE, CubeExtension, DIGEST_RANGE, ElementHasher, - Felt, FieldElement, Hasher, INPUT1_RANGE, INPUT2_RANGE, MDS, NUM_ROUNDS, RATE_RANGE, - RATE_WIDTH, STATE_WIDTH, StarkField, ZERO, add_constants, add_constants_and_apply_inv_sbox, - add_constants_and_apply_sbox, apply_inv_sbox, apply_mds, apply_sbox, + ARK1, ARK2, CAPACITY_RANGE, CubeExtension, DIGEST_RANGE, ElementHasher, Felt, FieldElement, + Hasher, MDS, NUM_ROUNDS, RATE_RANGE, Range, STATE_WIDTH, Word, add_constants, + add_constants_and_apply_inv_sbox, add_constants_and_apply_sbox, apply_inv_sbox, apply_mds, + apply_sbox, }; -use crate::Word; +#[cfg(test)] +use super::{StarkField, ZERO}; +use crate::hash::algebraic_sponge::AlgebraicSponge; #[cfg(test)] mod tests; @@ -72,172 +72,24 @@ pub type CubicExtElement = CubeExtension; /// ## Hashing of empty input /// The current implementation hashes empty input to the zero digest [0, 0, 0, 0]. This has /// the benefit of requiring no calls to the RPX permutation when hashing empty input. +#[allow(rustdoc::private_intra_doc_links)] #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Rpx256(); -impl Hasher for Rpx256 { - /// Rpx256 collision resistance is 128-bits. - const COLLISION_RESISTANCE: u32 = 128; - - type Digest = Word; - - fn hash(bytes: &[u8]) -> Self::Digest { - // initialize the state with zeroes - let mut state = [ZERO; STATE_WIDTH]; - - // determine the number of field elements needed to encode `bytes` when each field element - // represents at most 7 bytes. - let num_field_elem = bytes.len().div_ceil(BINARY_CHUNK_SIZE); - - // set the first capacity element to `RATE_WIDTH + (num_field_elem % RATE_WIDTH)`. We do - // this to achieve: - // 1. Domain separating hashing of `[u8]` from hashing of `[Felt]`. - // 2. Avoiding collisions at the `[Felt]` representation of the encoded bytes. - state[CAPACITY_RANGE.start] = - Felt::from((RATE_WIDTH + (num_field_elem % RATE_WIDTH)) as u8); - - // initialize a buffer to receive the little-endian elements. - let mut buf = [0_u8; 8]; - - // iterate the chunks of bytes, creating a field element from each chunk and copying it - // into the state. - // - // every time the rate range is filled, a permutation is performed. if the final value of - // `rate_pos` is not zero, then the chunks count wasn't enough to fill the state range, - // and an additional permutation must be performed. - let mut current_chunk_idx = 0_usize; - // handle the case of an empty `bytes` - let last_chunk_idx = if num_field_elem == 0 { - current_chunk_idx - } else { - num_field_elem - 1 - }; - let rate_pos = bytes.chunks(BINARY_CHUNK_SIZE).fold(0, |rate_pos, chunk| { - // copy the chunk into the buffer - if current_chunk_idx != last_chunk_idx { - buf[..BINARY_CHUNK_SIZE].copy_from_slice(chunk); - } else { - // on the last iteration, we pad `buf` with a 1 followed by as many 0's as are - // needed to fill it - buf.fill(0); - buf[..chunk.len()].copy_from_slice(chunk); - buf[chunk.len()] = 1; - } - current_chunk_idx += 1; - - // set the current rate element to the input. since we take at most 7 bytes, we are - // guaranteed that the inputs data will fit into a single field element. - state[RATE_RANGE.start + rate_pos] = Felt::new(u64::from_le_bytes(buf)); - - // proceed filling the range. if it's full, then we apply a permutation and reset the - // counter to the beginning of the range. - if rate_pos == RATE_WIDTH - 1 { - Self::apply_permutation(&mut state); - 0 - } else { - rate_pos + 1 - } - }); - - // if we absorbed some elements but didn't apply a permutation to them (would happen when - // the number of elements is not a multiple of RATE_WIDTH), apply the RPX permutation. we - // don't need to apply any extra padding because the first capacity element contains a - // flag indicating the number of field elements constituting the last block when the latter - // is not divisible by `RATE_WIDTH`. - if rate_pos != 0 { - state[RATE_RANGE.start + rate_pos..RATE_RANGE.end].fill(ZERO); - Self::apply_permutation(&mut state); - } - - // return the first 4 elements of the rate as hash result. - Word::new(state[DIGEST_RANGE].try_into().unwrap()) - } - - fn merge(values: &[Self::Digest; 2]) -> Self::Digest { - // initialize the state by copying the digest elements into the rate portion of the state - // (8 total elements), and set the capacity elements to 0. - let mut state = [ZERO; STATE_WIDTH]; - let it = Self::Digest::words_as_elements_iter(values.iter()); - for (i, v) in it.enumerate() { - state[RATE_RANGE.start + i] = *v; - } - - // apply the RPX permutation and return the first four elements of the state - Self::apply_permutation(&mut state); - Word::new(state[DIGEST_RANGE].try_into().unwrap()) - } - - fn merge_many(values: &[Self::Digest]) -> Self::Digest { - Self::hash_elements(Self::Digest::words_as_elements(values)) - } - - fn merge_with_int(seed: Self::Digest, value: u64) -> Self::Digest { - // initialize the state as follows: - // - seed is copied into the first 4 elements of the rate portion of the state. - // - if the value fits into a single field element, copy it into the fifth rate element and - // set the first capacity element to 5. - // - if the value doesn't fit into a single field element, split it into two field elements, - // copy them into rate elements 5 and 6 and set the first capacity element to 6. - let mut state = [ZERO; STATE_WIDTH]; - state[INPUT1_RANGE].copy_from_slice(seed.as_elements()); - state[INPUT2_RANGE.start] = Felt::new(value); - if value < Felt::MODULUS { - state[CAPACITY_RANGE.start] = Felt::from(5_u8); - } else { - state[INPUT2_RANGE.start + 1] = Felt::new(value / Felt::MODULUS); - state[CAPACITY_RANGE.start] = Felt::from(6_u8); - } - - // apply the RPX permutation and return the first four elements of the rate - Self::apply_permutation(&mut state); - Word::new(state[DIGEST_RANGE].try_into().unwrap()) - } -} - -impl ElementHasher for Rpx256 { - type BaseField = Felt; - - fn hash_elements>(elements: &[E]) -> Self::Digest { - // convert the elements into a list of base field elements - let elements = E::slice_as_base_elements(elements); - - // initialize state to all zeros, except for the first element of the capacity part, which - // is set to `elements.len() % RATE_WIDTH`. - let mut state = [ZERO; STATE_WIDTH]; - state[CAPACITY_RANGE.start] = Self::BaseField::from((elements.len() % RATE_WIDTH) as u8); - - // absorb elements into the state one by one until the rate portion of the state is filled - // up; then apply the Rescue permutation and start absorbing again; repeat until all - // elements have been absorbed - let mut i = 0; - for &element in elements.iter() { - state[RATE_RANGE.start + i] = element; - i += 1; - if i.is_multiple_of(RATE_WIDTH) { - Self::apply_permutation(&mut state); - i = 0; - } - } - - // if we absorbed some elements but didn't apply a permutation to them (would happen when - // the number of elements is not a multiple of RATE_WIDTH), apply the RPX permutation after - // padding by as many 0 as necessary to make the input length a multiple of the RATE_WIDTH. - if i > 0 { - while i != RATE_WIDTH { - state[RATE_RANGE.start + i] = ZERO; - i += 1; - } - Self::apply_permutation(&mut state); - } - - // return the first 4 elements of the state as hash result - Word::new(state[DIGEST_RANGE].try_into().unwrap()) +impl AlgebraicSponge for Rpx256 { + /// Applies RPX permutation to the provided state. + #[inline(always)] + fn apply_permutation(state: &mut [Felt; STATE_WIDTH]) { + Self::apply_fb_round(state, 0); + Self::apply_ext_round(state, 1); + Self::apply_fb_round(state, 2); + Self::apply_ext_round(state, 3); + Self::apply_fb_round(state, 4); + Self::apply_ext_round(state, 5); + Self::apply_final_round(state, 6); } } -// HASH FUNCTION IMPLEMENTATION -// ================================================================================================ - impl Rpx256 { // CONSTANTS // -------------------------------------------------------------------------------------------- @@ -286,26 +138,10 @@ impl Rpx256 { ::hash_elements(elements) } - // DOMAIN IDENTIFIER - // -------------------------------------------------------------------------------------------- - /// Returns a hash of two digests and a domain identifier. + #[inline(always)] pub fn merge_in_domain(values: &[Word; 2], domain: Felt) -> Word { - // initialize the state by copying the digest elements into the rate portion of the state - // (8 total elements), and set the capacity elements to 0. - let mut state = [ZERO; STATE_WIDTH]; - let it = Word::words_as_elements_iter(values.iter()); - for (i, v) in it.enumerate() { - state[RATE_RANGE.start + i] = *v; - } - - // set the second capacity element to the domain value. The first capacity element is used - // for padding purposes. - state[CAPACITY_RANGE.start + 1] = domain; - - // apply the RPX permutation and return the first four elements of the state - Self::apply_permutation(&mut state); - Word::new(state[DIGEST_RANGE].try_into().unwrap()) + ::merge_in_domain(values, domain) } // RPX PERMUTATION @@ -380,3 +216,33 @@ impl Rpx256 { x3 * x4 } } + +impl Hasher for Rpx256 { + const COLLISION_RESISTANCE: u32 = 128; + + type Digest = Word; + + fn hash(bytes: &[u8]) -> Self::Digest { + ::hash(bytes) + } + + fn merge(values: &[Self::Digest; 2]) -> Self::Digest { + ::merge(values) + } + + fn merge_many(values: &[Self::Digest]) -> Self::Digest { + ::merge_many(values) + } + + fn merge_with_int(seed: Self::Digest, value: u64) -> Self::Digest { + ::merge_with_int(seed, value) + } +} + +impl ElementHasher for Rpx256 { + type BaseField = Felt; + + fn hash_elements>(elements: &[E]) -> Self::Digest { + ::hash_elements(elements) + } +} diff --git a/miden-crypto/src/hash/rescue/rpx/tests.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/rpx/tests.rs similarity index 100% rename from miden-crypto/src/hash/rescue/rpx/tests.rs rename to miden-crypto/src/hash/algebraic_sponge/rescue/rpx/tests.rs diff --git a/miden-crypto/src/hash/rescue/tests.rs b/miden-crypto/src/hash/algebraic_sponge/rescue/tests.rs similarity index 100% rename from miden-crypto/src/hash/rescue/tests.rs rename to miden-crypto/src/hash/algebraic_sponge/rescue/tests.rs diff --git a/miden-crypto/src/hash/blake/mod.rs b/miden-crypto/src/hash/blake/mod.rs index c6c6eda1fa..a756221592 100644 --- a/miden-crypto/src/hash/blake/mod.rs +++ b/miden-crypto/src/hash/blake/mod.rs @@ -330,7 +330,7 @@ where } else { let mut hasher = blake3::Hasher::new(); - // BLAKE3 state is 64 bytes - so, we can absorb 64 bytes into the state in a single + // BLAKE3 rate is 64 bytes - so, we can absorb 64 bytes into the state in a single // permutation. we move the elements into the hasher via the buffer to give the CPU // a chance to process multiple element-to-byte conversions in parallel let mut buf = [0_u8; 64]; diff --git a/miden-crypto/src/hash/keccak/mod.rs b/miden-crypto/src/hash/keccak/mod.rs new file mode 100644 index 0000000000..2f384e12a4 --- /dev/null +++ b/miden-crypto/src/hash/keccak/mod.rs @@ -0,0 +1,226 @@ +use alloc::string::String; +use core::{ + mem::size_of, + ops::Deref, + slice::{self, from_raw_parts}, +}; + +use sha3::Digest as Sha3Digest; + +use super::{Digest, ElementHasher, Felt, FieldElement, Hasher}; +use crate::utils::{ + ByteReader, ByteWriter, Deserializable, DeserializationError, HexParseError, Serializable, + bytes_to_hex_string, hex_to_bytes, +}; + +#[cfg(test)] +mod tests; + +// CONSTANTS +// ================================================================================================ + +const DIGEST_BYTES: usize = 32; + +// DIGEST +// ================================================================================================ + +/// Keccak digest +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub struct Keccak256Digest([u8; DIGEST_BYTES]); + +impl Keccak256Digest { + pub fn digests_as_bytes(digests: &[Keccak256Digest]) -> &[u8] { + let p = digests.as_ptr(); + let len = digests.len() * DIGEST_BYTES; + unsafe { slice::from_raw_parts(p as *const u8, len) } + } +} + +impl Default for Keccak256Digest { + fn default() -> Self { + Self([0; DIGEST_BYTES]) + } +} + +impl Deref for Keccak256Digest { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for [u8; DIGEST_BYTES] { + fn from(value: Keccak256Digest) -> Self { + value.0 + } +} + +impl From<[u8; DIGEST_BYTES]> for Keccak256Digest { + fn from(value: [u8; DIGEST_BYTES]) -> Self { + Self(value) + } +} + +impl From for String { + fn from(value: Keccak256Digest) -> Self { + bytes_to_hex_string(value.as_bytes()) + } +} + +impl TryFrom<&str> for Keccak256Digest { + type Error = HexParseError; + + fn try_from(value: &str) -> Result { + hex_to_bytes(value).map(|v| v.into()) + } +} + +impl Serializable for Keccak256Digest { + fn write_into(&self, target: &mut W) { + target.write_bytes(&self.0); + } +} + +impl Deserializable for Keccak256Digest { + fn read_from(source: &mut R) -> Result { + source.read_array().map(Self) + } +} + +impl Digest for Keccak256Digest { + fn as_bytes(&self) -> [u8; 32] { + self.0 + } +} + +// KECCAK256 HASHER +// ================================================================================================ + +/// Keccak256 hash function +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Keccak256; + +impl Hasher for Keccak256 { + /// Keccak256 collision resistance is 128-bits for 32-bytes output. + const COLLISION_RESISTANCE: u32 = 128; + + type Digest = Keccak256Digest; + + fn hash(bytes: &[u8]) -> Self::Digest { + let mut hasher = sha3::Keccak256::new(); + hasher.update(bytes); + + Keccak256Digest(hasher.finalize().into()) + } + + fn merge(values: &[Self::Digest; 2]) -> Self::Digest { + Self::hash(prepare_merge(values)) + } + + fn merge_many(values: &[Self::Digest]) -> Self::Digest { + let data = Keccak256Digest::digests_as_bytes(values); + let mut hasher = sha3::Keccak256::new(); + hasher.update(data); + + Keccak256Digest(hasher.finalize().into()) + } + + fn merge_with_int(seed: Self::Digest, value: u64) -> Self::Digest { + let mut hasher = sha3::Keccak256::new(); + hasher.update(seed.0); + hasher.update(value.to_le_bytes()); + + Keccak256Digest(hasher.finalize().into()) + } +} + +impl ElementHasher for Keccak256 { + type BaseField = Felt; + + fn hash_elements(elements: &[E]) -> Self::Digest + where + E: FieldElement, + { + Keccak256Digest(hash_elements(elements)) + } +} + +impl Keccak256 { + /// Returns a hash of the provided sequence of bytes. + #[inline(always)] + pub fn hash(bytes: &[u8]) -> Keccak256Digest { + ::hash(bytes) + } + + /// Returns a hash of two digests. This method is intended for use in construction of + /// Merkle trees and verification of Merkle paths. + #[inline(always)] + pub fn merge(values: &[Keccak256Digest; 2]) -> Keccak256Digest { + ::merge(values) + } + + /// Returns a hash of the provided field elements. + #[inline(always)] + pub fn hash_elements(elements: &[E]) -> Keccak256Digest + where + E: FieldElement, + { + ::hash_elements(elements) + } +} + +// HELPER FUNCTIONS +// ================================================================================================ + +/// Hash the elements into bytes and shrink the output. +fn hash_elements(elements: &[E]) -> [u8; DIGEST_BYTES] +where + E: FieldElement, +{ + // don't leak assumptions from felt and check its actual implementation. + // this is a compile-time branch so it is for free + let digest = if Felt::IS_CANONICAL { + let mut hasher = sha3::Keccak256::new(); + hasher.update(E::elements_as_bytes(elements)); + hasher.finalize() + } else { + let mut hasher = sha3::Keccak256::new(); + // The Keccak-p permutation has a state of size 1600 bits. For Keccak256, the capacity + // is set to 512 bits and the rate is thus of size 1088 bits. + // This means that we can absorb 136 bytes into the rate portion of the state per invocation + // of the permutation function. + // we move the elements into the hasher via the buffer to give the CPU a chance to process + // multiple element-to-byte conversions in parallel + let mut buf = [0_u8; 136]; + let mut chunk_iter = E::slice_as_base_elements(elements).chunks_exact(17); + for chunk in chunk_iter.by_ref() { + for i in 0..17 { + buf[i * 8..(i + 1) * 8].copy_from_slice(&chunk[i].as_int().to_le_bytes()); + } + hasher.update(buf); + } + + for element in chunk_iter.remainder() { + hasher.update(element.as_int().to_le_bytes()); + } + + hasher.finalize() + }; + digest.into() +} + +// Cast the slice into contiguous bytes. +fn prepare_merge(args: &[D; N]) -> &[u8] +where + D: Deref, +{ + // compile-time assertion + assert!(N > 0, "N shouldn't represent an empty slice!"); + let values = args.as_ptr() as *const u8; + let len = size_of::() * N; + // safety: the values are tested to be contiguous + let bytes = unsafe { from_raw_parts(values, len) }; + debug_assert_eq!(args[0].deref(), &bytes[..len / N]); + bytes +} diff --git a/miden-crypto/src/hash/keccak/tests.rs b/miden-crypto/src/hash/keccak/tests.rs new file mode 100644 index 0000000000..3cae63e82f --- /dev/null +++ b/miden-crypto/src/hash/keccak/tests.rs @@ -0,0 +1,119 @@ +use alloc::vec::Vec; + +use proptest::prelude::*; +use rand_utils::rand_vector; + +use super::*; + +#[test] +fn keccak256_hash_elements() { + // test multiple of 8 + let elements = rand_vector::(16); + let expected = compute_expected_element_hash(&elements); + let actual: [u8; 32] = hash_elements(&elements); + assert_eq!(&expected, &actual); + + // test not multiple of 8 + let elements = rand_vector::(17); + let expected = compute_expected_element_hash(&elements); + let actual: [u8; 32] = hash_elements(&elements); + assert_eq!(&expected, &actual); +} + +proptest! { + #[test] + fn keccak256_wont_panic_with_arbitrary_input(ref vec in any::>()) { + Keccak256::hash(vec); + } +} + +#[test] +fn test_nist_test_vectors() { + for (i, vector) in NIST_TEST_VECTORS.iter().enumerate() { + let result = Keccak256::hash(vector.input); + let expected = hex::decode(vector.expected).unwrap(); + assert_eq!( + result.to_vec(), + expected, + "NIST test vector {} failed: {}", + i, + vector.description + ); + } +} + +#[test] +fn test_ethereum_test_vectors() { + for (i, vector) in ETHEREUM_TEST_VECTORS.iter().enumerate() { + let result = Keccak256::hash(vector.input); + let expected = hex::decode(vector.expected).unwrap(); + assert_eq!( + result.to_vec(), + expected, + "Ethereum test vector {} failed: {}", + i, + vector.description + ); + } +} + +// HELPER FUNCTION AND STRUCT +// ================================================================================================ + +fn compute_expected_element_hash(elements: &[Felt]) -> [u8; DIGEST_BYTES] { + let mut bytes = Vec::new(); + for element in elements.iter() { + bytes.extend_from_slice(&element.as_int().to_le_bytes()); + } + let mut hasher = sha3::Keccak256::new(); + hasher.update(&bytes); + + hasher.finalize().into() +} + +struct TestVector { + input: &'static [u8], + expected: &'static str, + description: &'static str, +} + +// TEST VECTORS +// ================================================================================================ + +// Derived from the wrapped implementation +const NIST_TEST_VECTORS: &[TestVector] = &[ + TestVector { + input: b"", + expected: "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + description: "Empty input", + }, + TestVector { + input: b"a", + expected: "3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb", + description: "Single byte 'a'", + }, + TestVector { + input: b"abc", + expected: "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45", + description: "String 'abc'", + }, +]; + +// Fetched from https://docs.ethers.org/v5/api/utils/hashing/ +const ETHEREUM_TEST_VECTORS: &[TestVector] = &[ + TestVector { + input: b"\x19Ethereum Signed Message:\n11Hello World", + expected: "a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2", + description: "Ethereum signed message prefix: Hello World", + }, + TestVector { + input: b"\x19Ethereum Signed Message:\n40x42", + expected: "f0d544d6e4a96e1c08adc3efabe2fcb9ec5e28db1ad6c33ace880ba354ab0fce", + description: "Ethereum signed message prefix: `[0, x, 4, 2]` sequence of characters ", + }, + TestVector { + input: b"\x19Ethereum Signed Message:\n1B", + expected: "d18c12b87124f9ceb7e1d3a5d06a5ac92ecab15931417e8d1558d9a263f99d63", + description: "Ethereum signed message prefix: `0x42` byte in UTF-8", + }, +]; diff --git a/miden-crypto/src/hash/mod.rs b/miden-crypto/src/hash/mod.rs index 9a097fd570..c24b4ef2c9 100644 --- a/miden-crypto/src/hash/mod.rs +++ b/miden-crypto/src/hash/mod.rs @@ -1,22 +1,30 @@ //! Cryptographic hash functions used by the Miden protocol. -use super::{CubeExtension, Felt, FieldElement, StarkField, ZERO}; +use super::{CubeExtension, Felt, FieldElement, StarkField, Word, ZERO}; -/// Blake2s hash function. +/// Blake3 hash function. pub mod blake; -mod rescue; +/// Keccak hash function. +pub mod keccak; + +/// Poseidon2 hash function. +pub mod poseidon2 { + pub use super::algebraic_sponge::poseidon2::Poseidon2; +} /// Rescue Prime Optimized (RPO) hash function. pub mod rpo { - pub use super::rescue::Rpo256; + pub use super::algebraic_sponge::rescue::Rpo256; } /// Rescue Prime Extended (RPX) hash function. pub mod rpx { - pub use super::rescue::Rpx256; + pub use super::algebraic_sponge::rescue::Rpx256; } +mod algebraic_sponge; + // RE-EXPORTS // ================================================================================================ diff --git a/miden-crypto/src/hash/rescue/rpo/mod.rs b/miden-crypto/src/hash/rescue/rpo/mod.rs deleted file mode 100644 index c26c80d002..0000000000 --- a/miden-crypto/src/hash/rescue/rpo/mod.rs +++ /dev/null @@ -1,337 +0,0 @@ -use core::ops::Range; - -use super::{ - ARK1, ARK2, BINARY_CHUNK_SIZE, CAPACITY_RANGE, DIGEST_RANGE, ElementHasher, Felt, FieldElement, - Hasher, INPUT1_RANGE, INPUT2_RANGE, MDS, NUM_ROUNDS, RATE_RANGE, RATE_WIDTH, STATE_WIDTH, - StarkField, ZERO, add_constants, add_constants_and_apply_inv_sbox, - add_constants_and_apply_sbox, apply_inv_sbox, apply_mds, apply_sbox, -}; -use crate::Word; - -#[cfg(test)] -mod tests; - -// HASHER IMPLEMENTATION -// ================================================================================================ - -/// Implementation of the Rescue Prime Optimized hash function with 256-bit output. -/// -/// The hash function is implemented according to the Rescue Prime Optimized -/// [specifications](https://eprint.iacr.org/2022/1577) while the padding rule follows the one -/// described [here](https://eprint.iacr.org/2023/1045). -/// -/// The parameters used to instantiate the function are: -/// * Field: 64-bit prime field with modulus p = 2^64 - 2^32 + 1. -/// * State width: 12 field elements. -/// * Rate size: r = 8 field elements. -/// * Capacity size: c = 4 field elements. -/// * Number of founds: 7. -/// * S-Box degree: 7. -/// -/// The above parameters target a 128-bit security level. The digest consists of four field elements -/// and it can be serialized into 32 bytes (256 bits). -/// -/// ## Hash output consistency -/// Functions [hash_elements()](Rpo256::hash_elements), [merge()](Rpo256::merge), and -/// [merge_with_int()](Rpo256::merge_with_int) are internally consistent. That is, computing -/// a hash for the same set of elements using these functions will always produce the same -/// result. For example, merging two digests using [merge()](Rpo256::merge) will produce the -/// same result as hashing 8 elements which make up these digests using -/// [hash_elements()](Rpo256::hash_elements) function. -/// -/// However, [hash()](Rpo256::hash) function is not consistent with functions mentioned above. -/// For example, if we take two field elements, serialize them to bytes and hash them using -/// [hash()](Rpo256::hash), the result will differ from the result obtained by hashing these -/// elements directly using [hash_elements()](Rpo256::hash_elements) function. The reason for -/// this difference is that [hash()](Rpo256::hash) function needs to be able to handle -/// arbitrary binary strings, which may or may not encode valid field elements - and thus, -/// deserialization procedure used by this function is different from the procedure used to -/// deserialize valid field elements. -/// -/// Thus, if the underlying data consists of valid field elements, it might make more sense -/// to deserialize them into field elements and then hash them using -/// [hash_elements()](Rpo256::hash_elements) function rather than hashing the serialized bytes -/// using [hash()](Rpo256::hash) function. -/// -/// ## Domain separation -/// [merge_in_domain()](Rpo256::merge_in_domain) hashes two digests into one digest with some domain -/// identifier and the current implementation sets the second capacity element to the value of -/// this domain identifier. Using a similar argument to the one formulated for domain separation of -/// the RPX hash function in Appendix C of its [specification](https://eprint.iacr.org/2023/1045), -/// one sees that doing so degrades only pre-image resistance, from its initial bound of c.log_2(p), -/// by as much as the log_2 of the size of the domain identifier space. Since pre-image resistance -/// becomes the bottleneck for the security bound of the sponge in overwrite-mode only when it is -/// lower than 2^128, we see that the target 128-bit security level is maintained as long as -/// the size of the domain identifier space, including for padding, is less than 2^128. -/// -/// ## Hashing of empty input -/// The current implementation hashes empty input to the zero digest [0, 0, 0, 0]. This has -/// the benefit of requiring no calls to the RPO permutation when hashing empty input. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct Rpo256(); - -impl Hasher for Rpo256 { - /// Rpo256 collision resistance is 128-bits. - const COLLISION_RESISTANCE: u32 = 128; - - type Digest = Word; - - fn hash(bytes: &[u8]) -> Self::Digest { - // initialize the state with zeroes - let mut state = [ZERO; STATE_WIDTH]; - - // determine the number of field elements needed to encode `bytes` when each field element - // represents at most 7 bytes. - let num_field_elem = bytes.len().div_ceil(BINARY_CHUNK_SIZE); - - // set the first capacity element to `RATE_WIDTH + (num_field_elem % RATE_WIDTH)`. We do - // this to achieve: - // 1. Domain separating hashing of `[u8]` from hashing of `[Felt]`. - // 2. Avoiding collisions at the `[Felt]` representation of the encoded bytes. - state[CAPACITY_RANGE.start] = - Felt::from((RATE_WIDTH + (num_field_elem % RATE_WIDTH)) as u8); - - // initialize a buffer to receive the little-endian elements. - let mut buf = [0_u8; 8]; - - // iterate the chunks of bytes, creating a field element from each chunk and copying it - // into the state. - // - // every time the rate range is filled, a permutation is performed. if the final value of - // `rate_pos` is not zero, then the chunks count wasn't enough to fill the state range, - // and an additional permutation must be performed. - let mut current_chunk_idx = 0_usize; - // handle the case of an empty `bytes` - let last_chunk_idx = if num_field_elem == 0 { - current_chunk_idx - } else { - num_field_elem - 1 - }; - let rate_pos = bytes.chunks(BINARY_CHUNK_SIZE).fold(0, |rate_pos, chunk| { - // copy the chunk into the buffer - if current_chunk_idx != last_chunk_idx { - buf[..BINARY_CHUNK_SIZE].copy_from_slice(chunk); - } else { - // on the last iteration, we pad `buf` with a 1 followed by as many 0's as are - // needed to fill it - buf.fill(0); - buf[..chunk.len()].copy_from_slice(chunk); - buf[chunk.len()] = 1; - } - current_chunk_idx += 1; - - // set the current rate element to the input. since we take at most 7 bytes, we are - // guaranteed that the inputs data will fit into a single field element. - state[RATE_RANGE.start + rate_pos] = Felt::new(u64::from_le_bytes(buf)); - - // proceed filling the range. if it's full, then we apply a permutation and reset the - // counter to the beginning of the range. - if rate_pos == RATE_WIDTH - 1 { - Self::apply_permutation(&mut state); - 0 - } else { - rate_pos + 1 - } - }); - - // if we absorbed some elements but didn't apply a permutation to them (would happen when - // the number of elements is not a multiple of RATE_WIDTH), apply the RPO permutation. we - // don't need to apply any extra padding because the first capacity element contains a - // flag indicating the number of field elements constituting the last block when the latter - // is not divisible by `RATE_WIDTH`. - if rate_pos != 0 { - state[RATE_RANGE.start + rate_pos..RATE_RANGE.end].fill(ZERO); - Self::apply_permutation(&mut state); - } - - // return the first 4 elements of the rate as hash result. - Word::new(state[DIGEST_RANGE].try_into().unwrap()) - } - - fn merge(values: &[Self::Digest; 2]) -> Self::Digest { - // initialize the state by copying the digest elements into the rate portion of the state - // (8 total elements), and set the capacity elements to 0. - let mut state = [ZERO; STATE_WIDTH]; - let it = Self::Digest::words_as_elements_iter(values.iter()); - for (i, v) in it.enumerate() { - state[RATE_RANGE.start + i] = *v; - } - - // apply the RPO permutation and return the first four elements of the state - Self::apply_permutation(&mut state); - Word::new(state[DIGEST_RANGE].try_into().unwrap()) - } - - fn merge_many(values: &[Self::Digest]) -> Self::Digest { - Self::hash_elements(Self::Digest::words_as_elements(values)) - } - - fn merge_with_int(seed: Self::Digest, value: u64) -> Self::Digest { - // initialize the state as follows: - // - seed is copied into the first 4 elements of the rate portion of the state. - // - if the value fits into a single field element, copy it into the fifth rate element and - // set the first capacity element to 5. - // - if the value doesn't fit into a single field element, split it into two field elements, - // copy them into rate elements 5 and 6 and set the first capacity element to 6. - let mut state = [ZERO; STATE_WIDTH]; - state[INPUT1_RANGE].copy_from_slice(seed.as_elements()); - state[INPUT2_RANGE.start] = Felt::new(value); - if value < Felt::MODULUS { - state[CAPACITY_RANGE.start] = Felt::from(5_u8); - } else { - state[INPUT2_RANGE.start + 1] = Felt::new(value / Felt::MODULUS); - state[CAPACITY_RANGE.start] = Felt::from(6_u8); - } - - // apply the RPO permutation and return the first four elements of the rate - Self::apply_permutation(&mut state); - Word::new(state[DIGEST_RANGE].try_into().unwrap()) - } -} - -impl ElementHasher for Rpo256 { - type BaseField = Felt; - - fn hash_elements>(elements: &[E]) -> Self::Digest { - // convert the elements into a list of base field elements - let elements = E::slice_as_base_elements(elements); - - // initialize state to all zeros, except for the first element of the capacity part, which - // is set to `elements.len() % RATE_WIDTH`. - let mut state = [ZERO; STATE_WIDTH]; - state[CAPACITY_RANGE.start] = Self::BaseField::from((elements.len() % RATE_WIDTH) as u8); - - // absorb elements into the state one by one until the rate portion of the state is filled - // up; then apply the Rescue permutation and start absorbing again; repeat until all - // elements have been absorbed - let mut i = 0; - for &element in elements.iter() { - state[RATE_RANGE.start + i] = element; - i += 1; - if i.is_multiple_of(RATE_WIDTH) { - Self::apply_permutation(&mut state); - i = 0; - } - } - - // if we absorbed some elements but didn't apply a permutation to them (would happen when - // the number of elements is not a multiple of RATE_WIDTH), apply the RPO permutation after - // padding by as many 0 as necessary to make the input length a multiple of the RATE_WIDTH. - if i > 0 { - while i != RATE_WIDTH { - state[RATE_RANGE.start + i] = ZERO; - i += 1; - } - Self::apply_permutation(&mut state); - } - - // return the first 4 elements of the state as hash result - Word::new(state[DIGEST_RANGE].try_into().unwrap()) - } -} - -// HASH FUNCTION IMPLEMENTATION -// ================================================================================================ - -impl Rpo256 { - // CONSTANTS - // -------------------------------------------------------------------------------------------- - - /// The number of rounds is set to 7 to target 128-bit security level. - pub const NUM_ROUNDS: usize = NUM_ROUNDS; - - /// Sponge state is set to 12 field elements or 768 bytes; 8 elements are reserved for rate and - /// the remaining 4 elements are reserved for capacity. - pub const STATE_WIDTH: usize = STATE_WIDTH; - - /// The rate portion of the state is located in elements 4 through 11 (inclusive). - pub const RATE_RANGE: Range = RATE_RANGE; - - /// The capacity portion of the state is located in elements 0, 1, 2, and 3. - pub const CAPACITY_RANGE: Range = CAPACITY_RANGE; - - /// The output of the hash function can be read from state elements 4, 5, 6, and 7. - pub const DIGEST_RANGE: Range = DIGEST_RANGE; - - /// MDS matrix used for computing the linear layer in a RPO round. - pub const MDS: [[Felt; STATE_WIDTH]; STATE_WIDTH] = MDS; - - /// Round constants added to the hasher state in the first half of the RPO round. - pub const ARK1: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = ARK1; - - /// Round constants added to the hasher state in the second half of the RPO round. - pub const ARK2: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = ARK2; - - // TRAIT PASS-THROUGH FUNCTIONS - // -------------------------------------------------------------------------------------------- - - /// Returns a hash of the provided sequence of bytes. - #[inline(always)] - pub fn hash(bytes: &[u8]) -> Word { - ::hash(bytes) - } - - /// Returns a hash of two digests. This method is intended for use in construction of - /// Merkle trees and verification of Merkle paths. - #[inline(always)] - pub fn merge(values: &[Word; 2]) -> Word { - ::merge(values) - } - - /// Returns a hash of the provided field elements. - #[inline(always)] - pub fn hash_elements>(elements: &[E]) -> Word { - ::hash_elements(elements) - } - - // DOMAIN IDENTIFIER - // -------------------------------------------------------------------------------------------- - - /// Returns a hash of two digests and a domain identifier. - pub fn merge_in_domain(values: &[Word; 2], domain: Felt) -> Word { - // initialize the state by copying the digest elements into the rate portion of the state - // (8 total elements), and set the capacity elements to 0. - let mut state = [ZERO; STATE_WIDTH]; - let it = Word::words_as_elements_iter(values.iter()); - for (i, v) in it.enumerate() { - state[RATE_RANGE.start + i] = *v; - } - - // set the second capacity element to the domain value. The first capacity element is used - // for padding purposes. - state[CAPACITY_RANGE.start + 1] = domain; - - // apply the RPO permutation and return the first four elements of the state - Self::apply_permutation(&mut state); - Word::new(state[DIGEST_RANGE].try_into().unwrap()) - } - - // RESCUE PERMUTATION - // -------------------------------------------------------------------------------------------- - - /// Applies RPO permutation to the provided state. - #[inline(always)] - pub fn apply_permutation(state: &mut [Felt; STATE_WIDTH]) { - for i in 0..NUM_ROUNDS { - Self::apply_round(state, i); - } - } - - /// RPO round function. - #[inline(always)] - pub fn apply_round(state: &mut [Felt; STATE_WIDTH], round: usize) { - // apply first half of RPO round - apply_mds(state); - if !add_constants_and_apply_sbox(state, &ARK1[round]) { - add_constants(state, &ARK1[round]); - apply_sbox(state); - } - - // apply second half of RPO round - apply_mds(state); - if !add_constants_and_apply_inv_sbox(state, &ARK2[round]) { - add_constants(state, &ARK2[round]); - apply_inv_sbox(state); - } - } -} diff --git a/miden-crypto/src/lib.rs b/miden-crypto/src/lib.rs index 6b2d3236f2..154db1a078 100644 --- a/miden-crypto/src/lib.rs +++ b/miden-crypto/src/lib.rs @@ -22,6 +22,23 @@ pub use winter_math::{ }; pub use word::{Word, WordError}; +// TYPE ALIASES +// ================================================================================================ + +/// An alias for a key-value map. +/// +/// By default, this is an alias for the [`alloc::collections::BTreeMap`], however, when the +/// `hashmaps` feature is enabled, this is an alias for the `hashbrown`'s `HashMap`. +#[cfg(feature = "hashmaps")] +pub type Map = hashbrown::HashMap; + +/// An alias for a key-value map. +/// +/// By default, this is an alias for the [`alloc::collections::BTreeMap`], however, when the +/// `hashmaps` feature is enabled, this is an alias for the `hashbrown`'s `HashMap`. +#[cfg(not(feature = "hashmaps"))] +pub type Map = alloc::collections::BTreeMap; + // CONSTANTS // ================================================================================================ diff --git a/miden-crypto/src/main.rs b/miden-crypto/src/main.rs index eb4d90e469..17983d4ae6 100644 --- a/miden-crypto/src/main.rs +++ b/miden-crypto/src/main.rs @@ -74,7 +74,7 @@ pub fn insertion(tree: &mut Smt, insertions: usize) -> Result<(), MerkleError> { let test_value = Word::new([ONE, ONE, ONE, Felt::new((size + i) as u64)]); let now = Instant::now(); - tree.insert(test_key, test_value); + tree.insert(test_key, test_value)?; let elapsed = now.elapsed(); insertion_times.push(elapsed.as_micros()); } @@ -102,7 +102,7 @@ pub fn batched_insertion(tree: &mut Smt, insertions: usize) -> Result<(), Merkle .collect(); let now = Instant::now(); - let mutations = tree.compute_mutations(new_pairs); + let mutations = tree.compute_mutations(new_pairs)?; let compute_elapsed = now.elapsed().as_secs_f64() * 1000_f64; // time in ms println!( @@ -161,7 +161,7 @@ pub fn batched_update( assert_eq!(new_pairs.len(), updates); let now = Instant::now(); - let mutations = tree.compute_mutations(new_pairs); + let mutations = tree.compute_mutations(new_pairs)?; let compute_elapsed = now.elapsed().as_secs_f64() * 1000_f64; // time in ms let now = Instant::now(); @@ -202,7 +202,7 @@ pub fn proof_generation(tree: &mut Smt) -> Result<(), MerkleError> { for i in 0..NUM_PROOFS { let test_key = Rpo256::hash(&rand_value::().to_be_bytes()); let test_value = Word::new([ONE, ONE, ONE, Felt::new((size + i) as u64)]); - tree.insert(test_key, test_value); + tree.insert(test_key, test_value)?; let now = Instant::now(); let _proof = tree.open(&test_key); diff --git a/miden-crypto/src/merkle/error.rs b/miden-crypto/src/merkle/error.rs index c8c2e57678..bc090e10c4 100644 --- a/miden-crypto/src/merkle/error.rs +++ b/miden-crypto/src/merkle/error.rs @@ -1,6 +1,6 @@ use thiserror::Error; -use super::{NodeIndex, Word}; +use super::{MAX_LEAF_ENTRIES, NodeIndex, Word}; #[derive(Debug, Error)] pub enum MerkleError { @@ -22,6 +22,8 @@ pub enum MerkleError { SubtreeDepthExceedsDepth { subtree_depth: u8, tree_depth: u8 }, #[error("number of entries in the merkle tree exceeds the maximum of {0}")] TooManyEntries(usize), + #[error("number of entries in a leaf ({actual}) exceeds the maximum of ({MAX_LEAF_ENTRIES})")] + TooManyLeafEntries { actual: usize }, #[error("node index `{0}` not found in the tree")] NodeIndexNotFoundInTree(NodeIndex), #[error("node {0:?} with index `{1}` not found in the store")] diff --git a/miden-crypto/src/merkle/index.rs b/miden-crypto/src/merkle/index.rs index 72031b3736..ca1412b2ab 100644 --- a/miden-crypto/src/merkle/index.rs +++ b/miden-crypto/src/merkle/index.rs @@ -35,9 +35,13 @@ impl NodeIndex { /// Creates a new node index. /// /// # Errors - /// Returns an error if the `value` is greater than or equal to 2^{depth}. + /// Returns an error if: + /// - `depth` is greater than 64. + /// - `value` is greater than or equal to 2^{depth}. pub const fn new(depth: u8, value: u64) -> Result { - if (64 - value.leading_zeros()) > depth as u32 { + if depth > 64 { + Err(MerkleError::DepthTooBig(depth as u64)) + } else if (64 - value.leading_zeros()) > depth as u32 { Err(MerkleError::InvalidNodeIndex { depth, value }) } else { Ok(Self { depth, value }) @@ -46,6 +50,7 @@ impl NodeIndex { /// Creates a new node index without checking its validity. pub const fn new_unchecked(depth: u8, value: u64) -> Self { + debug_assert!(depth <= 64); debug_assert!((64 - value.leading_zeros()) <= depth as u32); Self { depth, value } } @@ -63,7 +68,7 @@ impl NodeIndex { /// /// # Errors /// Returns an error if: - /// - `depth` doesn't fit in a `u8`. + /// - `depth` is greater than 64. /// - `value` is greater than or equal to 2^{depth}. pub fn from_elements(depth: &Felt, value: &Felt) -> Result { let depth = depth.as_int(); diff --git a/miden-crypto/src/merkle/mmr/partial.rs b/miden-crypto/src/merkle/mmr/partial.rs index f6a05000e8..0ede64f517 100644 --- a/miden-crypto/src/merkle/mmr/partial.rs +++ b/miden-crypto/src/merkle/mmr/partial.rs @@ -569,19 +569,17 @@ impl> Iterator for InnerNodeIterator<'_, I> { // if we haven't seen this node's parent before, and the node has a sibling, return // the inner node defined by the parent of this node, and move up the branch - if new_node { - if let Some(sibling) = self.nodes.get(&idx.sibling()) { - let (left, right) = if parent_idx.left_child() == idx { - (node, *sibling) - } else { - (*sibling, node) - }; - let parent = Rpo256::merge(&[left, right]); - let inner_node = InnerNodeInfo { value: parent, left, right }; + if new_node && let Some(sibling) = self.nodes.get(&idx.sibling()) { + let (left, right) = if parent_idx.left_child() == idx { + (node, *sibling) + } else { + (*sibling, node) + }; + let parent = Rpo256::merge(&[left, right]); + let inner_node = InnerNodeInfo { value: parent, left, right }; - self.stack.push((parent_idx, parent)); - return Some(inner_node); - } + self.stack.push((parent_idx, parent)); + return Some(inner_node); } // the previous leaf has been processed, try to process the next leaf @@ -687,7 +685,7 @@ mod tests { let tracked_leaves = partial .nodes .iter() - .filter_map(|(index, _)| if index.is_leaf() { Some(index.sibling()) } else { None }) + .filter_map(|(index, _)| (index.is_leaf()).then(|| index.sibling())) .collect::>(); let nodes_before = partial.nodes.clone(); diff --git a/miden-crypto/src/merkle/mmr/tests.rs b/miden-crypto/src/merkle/mmr/tests.rs index 79425a54a4..35677f60ae 100644 --- a/miden-crypto/src/merkle/mmr/tests.rs +++ b/miden-crypto/src/merkle/mmr/tests.rs @@ -1027,7 +1027,7 @@ mod property_tests { proptest! { #[test] - fn test_last_position_is_always_contained_in_the_last_tree(leaves in any::().prop_filter("cant have an empty tree", |v| *v != 0)) { + fn test_last_position_is_always_contained_in_the_last_tree(leaves in any::().prop_filter("can't have an empty tree", |v| *v != 0)) { let last_pos = leaves - 1; let lowest_bit = leaves.trailing_zeros(); diff --git a/miden-crypto/src/merkle/mod.rs b/miden-crypto/src/merkle/mod.rs index cf7d2d84c7..7f08bb3d0b 100644 --- a/miden-crypto/src/merkle/mod.rs +++ b/miden-crypto/src/merkle/mod.rs @@ -16,15 +16,16 @@ mod merkle_tree; pub use merkle_tree::{MerkleTree, path_to_text, tree_to_text}; mod path; -pub use path::{MerklePath, RootPath, ValuePath}; +pub use path::{MerklePath, MerkleProof, RootPath}; mod sparse_path; -pub use sparse_path::{SparseMerklePath, SparseValuePath}; +pub use sparse_path::SparseMerklePath; mod smt; pub use smt::{ - InnerNode, LeafIndex, MutationSet, NodeMutation, PartialSmt, SMT_DEPTH, SMT_MAX_DEPTH, - SMT_MIN_DEPTH, SimpleSmt, Smt, SmtLeaf, SmtLeafError, SmtProof, SmtProofError, + InnerNode, LeafIndex, MAX_LEAF_ENTRIES, MutationSet, NodeMutation, PartialSmt, SMT_DEPTH, + SMT_MAX_DEPTH, SMT_MIN_DEPTH, SimpleSmt, SimpleSmtProof, Smt, SmtLeaf, SmtLeafError, SmtProof, + SmtProofError, }; #[cfg(feature = "internal")] pub use smt::{SubtreeLeaf, build_subtree_for_bench}; @@ -33,7 +34,7 @@ mod mmr; pub use mmr::{Forest, InOrderIndex, Mmr, MmrDelta, MmrError, MmrPeaks, MmrProof, PartialMmr}; mod store; -pub use store::{DefaultMerkleStore, MerkleStore, RecordingMerkleStore, StoreNode}; +pub use store::{MerkleStore, StoreNode}; mod node; pub use node::InnerNodeInfo; diff --git a/miden-crypto/src/merkle/partial_mt/mod.rs b/miden-crypto/src/merkle/partial_mt/mod.rs index 1678ed8767..4da1590345 100644 --- a/miden-crypto/src/merkle/partial_mt/mod.rs +++ b/miden-crypto/src/merkle/partial_mt/mod.rs @@ -6,7 +6,7 @@ use alloc::{ use core::fmt; use super::{ - EMPTY_WORD, InnerNodeInfo, MerkleError, MerklePath, NodeIndex, Rpo256, ValuePath, Word, + EMPTY_WORD, InnerNodeInfo, MerkleError, MerklePath, MerkleProof, NodeIndex, Rpo256, Word, }; use crate::utils::{ ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, word_to_hex, @@ -196,12 +196,12 @@ impl PartialMerkleTree { } /// Returns a vector of paths from every leaf to the root. - pub fn to_paths(&self) -> Vec<(NodeIndex, ValuePath)> { + pub fn to_paths(&self) -> Vec<(NodeIndex, MerkleProof)> { let mut paths = Vec::new(); self.leaves.iter().for_each(|&leaf| { paths.push(( leaf, - ValuePath { + MerkleProof { value: self.get_node(leaf).expect("Failed to get leaf node"), path: self.get_path(leaf).expect("Failed to get path"), }, diff --git a/miden-crypto/src/merkle/partial_mt/tests.rs b/miden-crypto/src/merkle/partial_mt/tests.rs index ebcccaa730..e0523ab14d 100644 --- a/miden-crypto/src/merkle/partial_mt/tests.rs +++ b/miden-crypto/src/merkle/partial_mt/tests.rs @@ -1,10 +1,8 @@ use alloc::{collections::BTreeMap, vec::Vec}; use super::{ - super::{ - DefaultMerkleStore as MerkleStore, MerkleTree, NodeIndex, PartialMerkleTree, int_to_node, - }, - Deserializable, InnerNodeInfo, Serializable, ValuePath, Word, + super::{MerkleStore, MerkleTree, NodeIndex, PartialMerkleTree, int_to_node}, + Deserializable, InnerNodeInfo, MerkleProof, Serializable, Word, }; // TEST DATA @@ -211,12 +209,12 @@ fn get_paths() { // for each leaf. let leaves = [NODE20, NODE22, NODE23, NODE32, NODE33]; - let expected_paths: Vec<(NodeIndex, ValuePath)> = leaves + let expected_paths: Vec<(NodeIndex, MerkleProof)> = leaves .iter() .map(|&leaf| { ( leaf, - ValuePath { + MerkleProof { value: mt.get_node(leaf).unwrap(), path: mt.get_path(leaf).unwrap(), }, diff --git a/miden-crypto/src/merkle/path.rs b/miden-crypto/src/merkle/path.rs index 0064d59215..0c4f3e47da 100644 --- a/miden-crypto/src/merkle/path.rs +++ b/miden-crypto/src/merkle/path.rs @@ -203,23 +203,23 @@ impl Iterator for InnerNodeIterator<'_> { /// A container for a [crate::Word] value and its [MerklePath] opening. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct ValuePath { +pub struct MerkleProof { /// The node value opening for `path`. pub value: Word, /// The path from `value` to `root` (exclusive). pub path: MerklePath, } -impl ValuePath { - /// Returns a new [ValuePath] instantiated from the specified value and path. +impl MerkleProof { + /// Returns a new [MerkleProof] instantiated from the specified value and path. pub fn new(value: Word, path: MerklePath) -> Self { Self { value, path } } } -impl From<(MerklePath, Word)> for ValuePath { +impl From<(MerklePath, Word)> for MerkleProof { fn from((path, value): (MerklePath, Word)) -> Self { - ValuePath::new(value, path) + MerkleProof::new(value, path) } } @@ -254,14 +254,14 @@ impl Deserializable for MerklePath { } } -impl Serializable for ValuePath { +impl Serializable for MerkleProof { fn write_into(&self, target: &mut W) { self.value.write_into(target); self.path.write_into(target); } } -impl Deserializable for ValuePath { +impl Deserializable for MerkleProof { fn read_from(source: &mut R) -> Result { let value = Word::read_from(source)?; let path = MerklePath::read_from(source)?; diff --git a/miden-crypto/src/merkle/smt/full/concurrent/mod.rs b/miden-crypto/src/merkle/smt/full/concurrent/mod.rs index 6f883e9659..befa2239db 100644 --- a/miden-crypto/src/merkle/smt/full/concurrent/mod.rs +++ b/miden-crypto/src/merkle/smt/full/concurrent/mod.rs @@ -8,7 +8,10 @@ use super::{ EmptySubtreeRoots, InnerNode, InnerNodes, Leaves, MerkleError, MutationSet, NodeIndex, SMT_DEPTH, Smt, SmtLeaf, SparseMerkleTree, Word, leaf, }; -use crate::merkle::smt::{NodeMutation, NodeMutations, UnorderedMap}; +use crate::merkle::{ + SmtLeafError, + smt::{Map, NodeMutation, NodeMutations}, +}; #[cfg(test)] mod tests; @@ -92,10 +95,13 @@ impl Smt { /// /// 3. These subtree roots become the "leaves" for the next iteration, which processes the next /// 8 levels up. This continues until reaching the tree's root at depth 0. + /// + /// # Errors + /// Returns an error if mutations would exceed [`MAX_LEAF_ENTRIES`] (1024 entries) in a leaf. pub(crate) fn compute_mutations_concurrent( &self, kv_pairs: impl IntoIterator, - ) -> MutationSet + ) -> Result, MerkleError> where Self: Sized + Sync, { @@ -105,16 +111,16 @@ impl Smt { // Convert sorted pairs into mutated leaves and capture any new pairs let (mut subtree_leaves, new_pairs) = - self.sorted_pairs_to_mutated_subtree_leaves(sorted_kv_pairs); + self.sorted_pairs_to_mutated_subtree_leaves(sorted_kv_pairs)?; // If no mutations, return an empty mutation set if subtree_leaves.is_empty() { - return MutationSet { + return Ok(MutationSet { old_root: self.root(), new_root: self.root(), node_mutations: NodeMutations::default(), new_pairs, - }; + }); } let mut node_mutations = NodeMutations::default(); @@ -154,7 +160,7 @@ impl Smt { !mutation_set.node_mutations().is_empty() && !mutation_set.new_pairs().is_empty() ); - mutation_set + Ok(mutation_set) } // SUBTREE MUTATION @@ -258,17 +264,15 @@ impl Smt { } for current_depth in (SUBTREE_DEPTH..=SMT_DEPTH).step_by(SUBTREE_DEPTH as usize).rev() { - let (nodes, mut subtree_roots): (Vec>, Vec) = - leaf_subtrees - .into_par_iter() - .map(|subtree| { - debug_assert!(subtree.is_sorted()); - debug_assert!(!subtree.is_empty()); - let (nodes, subtree_root) = - build_subtree(subtree, SMT_DEPTH, current_depth); - (nodes, subtree_root) - }) - .unzip(); + let (nodes, mut subtree_roots): (Vec>, Vec) = leaf_subtrees + .into_par_iter() + .map(|subtree| { + debug_assert!(subtree.is_sorted()); + debug_assert!(!subtree.is_empty()); + let (nodes, subtree_root) = build_subtree(subtree, SMT_DEPTH, current_depth); + (nodes, subtree_root) + }) + .unzip(); leaf_subtrees = SubtreeLeavesIter::from_leaves(&mut subtree_roots).collect(); accumulated_nodes.extend(nodes.into_iter().flatten()); @@ -337,9 +341,9 @@ impl Smt { fn sorted_pairs_to_mutated_subtree_leaves( &self, pairs: Vec<(Word, Word)>, - ) -> (MutatedSubtreeLeaves, UnorderedMap) { + ) -> Result<(MutatedSubtreeLeaves, Map), MerkleError> { // Map to track new key-value pairs for mutated leaves - let mut new_pairs = UnorderedMap::new(); + let mut new_pairs = Map::new(); let accumulator = Self::process_sorted_pairs_to_leaves(pairs, |leaf_pairs| { let mut leaf = self.get_leaf(&leaf_pairs[0].0); @@ -357,7 +361,14 @@ impl Smt { if value != old_value { // Update the leaf and track the new key-value pair - leaf = self.construct_prospective_leaf(leaf, &key, &value); + leaf = self.construct_prospective_leaf(leaf, &key, &value).map_err( + |e| match e { + SmtLeafError::TooManyLeafEntries { actual } => { + MerkleError::TooManyLeafEntries { actual } + }, + other => panic!("unexpected SmtLeaf::insert error: {:?}", other), + }, + )?; new_pairs.insert(key, value); leaf_changed = true; } @@ -374,10 +385,7 @@ impl Smt { // The closure is the only possible source of errors. // Since it never returns an error - only `Ok(Some(_))` or `Ok(None)` - we can safely assume // `accumulator` is always `Ok(_)`. - ( - accumulator.expect("process_sorted_pairs_to_leaves never fails").leaves, - new_pairs, - ) + Ok((accumulator?.leaves, new_pairs)) } /// Processes sorted key-value pairs to compute leaves for a subtree. @@ -503,7 +511,7 @@ pub struct SubtreeLeaf { #[derive(Debug, Clone)] pub(crate) struct PairComputations { /// Literal leaves to be added to the sparse Merkle tree's internal mapping. - pub nodes: UnorderedMap, + pub nodes: Map, /// "Conceptual" leaves that will be used for computations. pub leaves: Vec>, } @@ -590,7 +598,7 @@ fn build_subtree( mut leaves: Vec, tree_depth: u8, bottom_depth: u8, -) -> (UnorderedMap, SubtreeLeaf) { +) -> (Map, SubtreeLeaf) { #[cfg(debug_assertions)] { // Ensure that all leaves have unique column indices within this subtree. @@ -608,7 +616,7 @@ fn build_subtree( debug_assert!(Integer::is_multiple_of(&bottom_depth, &SUBTREE_DEPTH)); debug_assert!(leaves.len() <= usize::pow(2, SUBTREE_DEPTH as u32)); let subtree_root = bottom_depth - SUBTREE_DEPTH; - let mut inner_nodes: UnorderedMap = Default::default(); + let mut inner_nodes: Map = Default::default(); let mut next_leaves: Vec = Vec::with_capacity(leaves.len() / 2); for next_depth in (subtree_root..bottom_depth).rev() { debug_assert!(next_depth <= bottom_depth); @@ -714,6 +722,6 @@ pub fn build_subtree_for_bench( leaves: Vec, tree_depth: u8, bottom_depth: u8, -) -> (UnorderedMap, SubtreeLeaf) { +) -> (Map, SubtreeLeaf) { build_subtree(leaves, tree_depth, bottom_depth) } diff --git a/miden-crypto/src/merkle/smt/full/concurrent/tests.rs b/miden-crypto/src/merkle/smt/full/concurrent/tests.rs index b32877679b..159e4392e8 100644 --- a/miden-crypto/src/merkle/smt/full/concurrent/tests.rs +++ b/miden-crypto/src/merkle/smt/full/concurrent/tests.rs @@ -8,9 +8,9 @@ use proptest::prelude::*; use rand::{Rng, prelude::IteratorRandom, rng}; use super::{ - COLS_PER_SUBTREE, InnerNode, NodeIndex, NodeMutations, PairComputations, SMT_DEPTH, - SUBTREE_DEPTH, Smt, SmtLeaf, SparseMerkleTree, SubtreeLeaf, SubtreeLeavesIter, UnorderedMap, - Word, build_subtree, + COLS_PER_SUBTREE, InnerNode, Map, NodeIndex, NodeMutations, PairComputations, SMT_DEPTH, + SUBTREE_DEPTH, Smt, SmtLeaf, SparseMerkleTree, SubtreeLeaf, SubtreeLeavesIter, Word, + build_subtree, }; use crate::{ EMPTY_WORD, ONE, ZERO, @@ -227,7 +227,7 @@ fn test_singlethreaded_subtrees() { } = Smt::sorted_pairs_to_leaves(entries).unwrap(); for current_depth in (SUBTREE_DEPTH..=SMT_DEPTH).step_by(SUBTREE_DEPTH as usize).rev() { // There's no flat_map_unzip(), so this is the best we can do. - let (nodes, mut subtree_roots): (Vec>, Vec) = leaf_subtrees + let (nodes, mut subtree_roots): (Vec>, Vec) = leaf_subtrees .into_iter() .enumerate() .map(|(i, subtree)| { @@ -309,7 +309,7 @@ fn test_multithreaded_subtrees() { nodes: test_leaves, } = Smt::sorted_pairs_to_leaves(entries).unwrap(); for current_depth in (SUBTREE_DEPTH..=SMT_DEPTH).step_by(SUBTREE_DEPTH as usize).rev() { - let (nodes, mut subtree_roots): (Vec>, Vec) = leaf_subtrees + let (nodes, mut subtree_roots): (Vec>, Vec) = leaf_subtrees .into_par_iter() .enumerate() .map(|(i, subtree)| { @@ -400,9 +400,10 @@ fn test_singlethreaded_subtree_mutations() { let entries = generate_entries(PAIR_COUNT); let updates = generate_updates(entries.clone(), 1000); let tree = Smt::with_entries_sequential(entries.clone()).unwrap(); - let control = tree.compute_mutations_sequential(updates.clone()); + let control = tree.compute_mutations_sequential(updates.clone()).unwrap(); let mut node_mutations = NodeMutations::default(); - let (mut subtree_leaves, new_pairs) = tree.sorted_pairs_to_mutated_subtree_leaves(updates); + let (mut subtree_leaves, new_pairs) = + tree.sorted_pairs_to_mutated_subtree_leaves(updates).unwrap(); for current_depth in (SUBTREE_DEPTH..=SMT_DEPTH).step_by(SUBTREE_DEPTH as usize).rev() { // There's no flat_map_unzip(), so this is the best we can do. let (mutations_per_subtree, mut subtree_roots): (Vec<_>, Vec<_>) = subtree_leaves @@ -460,8 +461,8 @@ fn test_compute_mutations_parallel() { let entries = generate_entries(PAIR_COUNT); let tree = Smt::with_entries(entries.clone()).unwrap(); let updates = generate_updates(entries, 1000); - let control = tree.compute_mutations_sequential(updates.clone()); - let mutations = tree.compute_mutations(updates); + let control = tree.compute_mutations_sequential(updates.clone()).unwrap(); + let mutations = tree.compute_mutations(updates).unwrap(); assert_eq!(mutations.root(), control.root()); assert_eq!(mutations.old_root(), control.old_root()); assert_eq!(mutations.node_mutations(), control.node_mutations()); @@ -667,8 +668,8 @@ proptest! { } }); - let sequential = tree.compute_mutations_sequential(update_entries.clone()); - let concurrent = tree.compute_mutations(update_entries.clone()); + let sequential = tree.compute_mutations_sequential(update_entries.clone()).unwrap(); + let concurrent = tree.compute_mutations(update_entries.clone()).unwrap(); // If there are real changes, the root should change if has_real_changes { diff --git a/miden-crypto/src/merkle/smt/full/error.rs b/miden-crypto/src/merkle/smt/full/error.rs index b1864b3bb6..1e66ffd4a9 100644 --- a/miden-crypto/src/merkle/smt/full/error.rs +++ b/miden-crypto/src/merkle/smt/full/error.rs @@ -2,7 +2,7 @@ use thiserror::Error; use crate::{ Word, - merkle::{LeafIndex, SMT_DEPTH}, + merkle::{LeafIndex, MAX_LEAF_ENTRIES, SMT_DEPTH}, }; // SMT LEAF ERROR @@ -38,6 +38,12 @@ pub enum SmtLeafError { /// Multiple leaf requires at least two entries, but fewer were provided. #[error("multiple leaf requires at least two entries but only {0} were given")] MultipleLeafRequiresTwoEntries(usize), + + /// Multiple leaf contains more entries than the maximum allowed. + #[error( + "multiple leaf contains {actual} entries but the maximum allowed is {MAX_LEAF_ENTRIES}" + )] + TooManyLeafEntries { actual: usize }, } // SMT PROOF ERROR diff --git a/miden-crypto/src/merkle/smt/full/leaf.rs b/miden-crypto/src/merkle/smt/full/leaf.rs index 38f3e1c5e7..0c3037bdac 100644 --- a/miden-crypto/src/merkle/smt/full/leaf.rs +++ b/miden-crypto/src/merkle/smt/full/leaf.rs @@ -1,7 +1,7 @@ use alloc::{string::ToString, vec::Vec}; use core::cmp::Ordering; -use super::{EMPTY_WORD, Felt, LeafIndex, Rpo256, SMT_DEPTH, SmtLeafError, Word}; +use super::{EMPTY_WORD, Felt, LeafIndex, MAX_LEAF_ENTRIES, Rpo256, SMT_DEPTH, SmtLeafError, Word}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; /// Represents a leaf node in the Sparse Merkle Tree. @@ -81,11 +81,16 @@ impl SmtLeaf { /// /// # Errors /// - Returns an error if 2 keys in `entries` map to a different leaf index + /// - Returns an error if the number of entries exceeds [`MAX_LEAF_ENTRIES`] pub fn new_multiple(entries: Vec<(Word, Word)>) -> Result { if entries.len() < 2 { return Err(SmtLeafError::MultipleLeafRequiresTwoEntries(entries.len())); } + if entries.len() > MAX_LEAF_ENTRIES { + return Err(SmtLeafError::TooManyLeafEntries { actual: entries.len() }); + } + // Check that all keys map to the same leaf index { let mut keys = entries.iter().map(|(key, _)| key); @@ -130,13 +135,11 @@ impl SmtLeaf { } /// Returns the number of entries stored in the leaf - pub fn num_entries(&self) -> u64 { + pub fn num_entries(&self) -> usize { match self { SmtLeaf::Empty(_) => 0, SmtLeaf::Single(_) => 1, - SmtLeaf::Multiple(entries) => { - entries.len().try_into().expect("shouldn't have more than 2^64 entries") - }, + SmtLeaf::Multiple(entries) => entries.len(), } } @@ -222,11 +225,15 @@ impl SmtLeaf { /// any. /// /// The caller needs to ensure that `key` has the same leaf index as all other keys in the leaf - pub(super) fn insert(&mut self, key: Word, value: Word) -> Option { + /// + /// # Errors + /// Returns an error if inserting the key-value pair would exceed [`MAX_LEAF_ENTRIES`] (1024 + /// entries) in the leaf. + pub(super) fn insert(&mut self, key: Word, value: Word) -> Result, SmtLeafError> { match self { SmtLeaf::Empty(_) => { *self = SmtLeaf::new_single(key, value); - None + Ok(None) }, SmtLeaf::Single(kv_pair) => { if kv_pair.0 == key { @@ -234,16 +241,16 @@ impl SmtLeaf { // value let old_value = kv_pair.1; kv_pair.1 = value; - Some(old_value) + Ok(Some(old_value)) } else { // Another entry is present in this leaf. Transform the entry into a list // entry, and make sure the key-value pairs are sorted by key + // This stays within MAX_LEAF_ENTRIES limit. We're only adding one entry to a + // single leaf let mut pairs = vec![*kv_pair, (key, value)]; pairs.sort_by(|(key_1, _), (key_2, _)| cmp_keys(*key_1, *key_2)); - *self = SmtLeaf::Multiple(pairs); - - None + Ok(None) } }, SmtLeaf::Multiple(kv_pairs) => { @@ -251,13 +258,16 @@ impl SmtLeaf { Ok(pos) => { let old_value = kv_pairs[pos].1; kv_pairs[pos].1 = value; - - Some(old_value) + Ok(Some(old_value)) }, Err(pos) => { + if kv_pairs.len() >= MAX_LEAF_ENTRIES { + return Err(SmtLeafError::TooManyLeafEntries { + actual: kv_pairs.len() + 1, + }); + } kv_pairs.insert(pos, (key, value)); - - None + Ok(None) }, } }, @@ -331,7 +341,7 @@ impl Serializable for SmtLeaf { impl Deserializable for SmtLeaf { fn read_from(source: &mut R) -> Result { // Read: num entries - let num_entries = source.read_u64()?; + let num_entries = source.read_usize()?; // Read: leaf index let leaf_index: LeafIndex = { diff --git a/miden-crypto/src/merkle/smt/full/mod.rs b/miden-crypto/src/merkle/smt/full/mod.rs index b06e0596c0..c38441edb0 100644 --- a/miden-crypto/src/merkle/smt/full/mod.rs +++ b/miden-crypto/src/merkle/smt/full/mod.rs @@ -2,7 +2,7 @@ use alloc::{string::ToString, vec::Vec}; use super::{ EMPTY_WORD, EmptySubtreeRoots, Felt, InnerNode, InnerNodeInfo, InnerNodes, LeafIndex, - MerkleError, MerklePath, MutationSet, NodeIndex, Rpo256, SparseMerkleTree, Word, + MerkleError, MutationSet, NodeIndex, Rpo256, SparseMerklePath, SparseMerkleTree, Word, }; mod error; @@ -32,6 +32,9 @@ mod tests; /// All leaves in this SMT are located at depth 64. pub const SMT_DEPTH: u8 = 64; +/// The maximum number of entries allowed in a multiple leaf. +pub const MAX_LEAF_ENTRIES: usize = 1024; + // SMT // ================================================================================================ @@ -51,6 +54,7 @@ type Leaves = super::Leaves; pub struct Smt { root: Word, // pub(super) for use in PartialSmt. + pub(super) num_entries: usize, pub(super) leaves: Leaves, pub(super) inner_nodes: InnerNodes, } @@ -72,6 +76,7 @@ impl Smt { Self { root, + num_entries: 0, inner_nodes: Default::default(), leaves: Default::default(), } @@ -85,7 +90,9 @@ impl Smt { /// All leaves omitted from the entries list are set to [Self::EMPTY_VALUE]. /// /// # Errors - /// Returns an error if the provided entries contain multiple values for the same key. + /// Returns an error if: + /// - the provided entries contain multiple values for the same key. + /// - inserting a key-value pair would exceed [`MAX_LEAF_ENTRIES`] (1024 entries) in a leaf. pub fn with_entries( entries: impl IntoIterator, ) -> Result { @@ -104,6 +111,10 @@ impl Smt { /// /// This only applies if the "concurrent" feature is enabled. Without the feature, the behavior /// is equivalent to `with_entiries`. + /// + /// # Errors + /// Returns an error if inserting a key-value pair would exceed [`MAX_LEAF_ENTRIES`] (1024 + /// entries) in a leaf. pub fn with_sorted_entries( entries: impl IntoIterator, ) -> Result { @@ -123,7 +134,9 @@ impl Smt { /// All leaves omitted from the entries list are set to [Self::EMPTY_VALUE]. /// /// # Errors - /// Returns an error if the provided entries contain multiple values for the same key. + /// Returns an error if: + /// - the provided entries contain multiple values for the same key. + /// - inserting a key-value pair would exceed [`MAX_LEAF_ENTRIES`] (1024 entries) in a leaf. #[cfg(any(not(feature = "concurrent"), fuzzing, test))] fn with_entries_sequential( entries: impl IntoIterator, @@ -138,7 +151,7 @@ impl Smt { let mut key_set_to_zero = BTreeSet::new(); for (key, value) in entries { - let old_value = tree.insert(key, value); + let old_value = tree.insert(key, value)?; if old_value != EMPTY_WORD || key_set_to_zero.contains(&key) { return Err(MerkleError::DuplicateValuesForIndex( @@ -191,11 +204,8 @@ impl Smt { /// /// Note that this may return a different value from [Self::num_leaves()] as a single leaf may /// contain more than one key-value pair. - /// - /// Also note that this is currently an expensive operation as counting the number of - /// entries requires iterating over all leaves of the tree. pub fn num_entries(&self) -> usize { - self.entries().count() + self.num_entries } /// Returns the leaf to which `key` maps @@ -253,7 +263,11 @@ impl Smt { /// /// This also recomputes all hashes between the leaf (associated with the key) and the root, /// updating the root itself. - pub fn insert(&mut self, key: Word, value: Word) -> Word { + /// + /// # Errors + /// Returns an error if inserting the key-value pair would exceed [`MAX_LEAF_ENTRIES`] (1024 + /// entries) in the leaf. + pub fn insert(&mut self, key: Word, value: Word) -> Result { >::insert(self, key, value) } @@ -272,15 +286,15 @@ impl Smt { /// # use miden_crypto::merkle::{Smt, EmptySubtreeRoots, SMT_DEPTH}; /// let mut smt = Smt::new(); /// let pair = (Word::default(), Word::default()); - /// let mutations = smt.compute_mutations(vec![pair]); + /// let mutations = smt.compute_mutations(vec![pair]).unwrap(); /// assert_eq!(mutations.root(), *EmptySubtreeRoots::entry(SMT_DEPTH, 0)); - /// smt.apply_mutations(mutations); + /// smt.apply_mutations(mutations).unwrap(); /// assert_eq!(smt.root(), *EmptySubtreeRoots::entry(SMT_DEPTH, 0)); /// ``` pub fn compute_mutations( &self, kv_pairs: impl IntoIterator, - ) -> MutationSet { + ) -> Result, MerkleError> { #[cfg(feature = "concurrent")] { self.compute_mutations_concurrent(kv_pairs) @@ -327,17 +341,32 @@ impl Smt { /// Inserts `value` at leaf index pointed to by `key`. `value` is guaranteed to not be the empty /// value, such that this is indeed an insertion. - fn perform_insert(&mut self, key: Word, value: Word) -> Option { + /// + /// # Errors + /// Returns an error if inserting the key-value pair would exceed [`MAX_LEAF_ENTRIES`] (1024 + /// entries) in the leaf. + fn perform_insert(&mut self, key: Word, value: Word) -> Result, MerkleError> { debug_assert_ne!(value, Self::EMPTY_VALUE); let leaf_index: LeafIndex = Self::key_to_leaf_index(&key); match self.leaves.get_mut(&leaf_index.value()) { - Some(leaf) => leaf.insert(key, value), + Some(leaf) => { + let prev_entries = leaf.num_entries(); + let result = leaf.insert(key, value).map_err(|e| match e { + SmtLeafError::TooManyLeafEntries { actual } => { + MerkleError::TooManyLeafEntries { actual } + }, + other => panic!("unexpected SmtLeaf::insert error: {:?}", other), + })?; + let current_entries = leaf.num_entries(); + self.num_entries += current_entries - prev_entries; + Ok(result) + }, None => { self.leaves.insert(leaf_index.value(), SmtLeaf::Single((key, value))); - - None + self.num_entries += 1; + Ok(None) }, } } @@ -347,7 +376,10 @@ impl Smt { let leaf_index: LeafIndex = Self::key_to_leaf_index(&key); if let Some(leaf) = self.leaves.get_mut(&leaf_index.value()) { + let prev_entries = leaf.num_entries(); let (old_value, is_empty) = leaf.remove(key); + let current_entries = leaf.num_entries(); + self.num_entries -= prev_entries - current_entries; if is_empty { self.leaves.remove(&leaf_index.value()); } @@ -381,8 +413,8 @@ impl SparseMerkleTree for Smt { assert_eq!(root_node_hash, root); } - - Ok(Self { root, inner_nodes, leaves }) + let num_entries = leaves.values().map(|leaf| leaf.num_entries()).sum(); + Ok(Self { root, inner_nodes, leaves, num_entries }) } fn root(&self) -> Word { @@ -401,19 +433,27 @@ impl SparseMerkleTree for Smt { } fn insert_inner_node(&mut self, index: NodeIndex, inner_node: InnerNode) -> Option { - self.inner_nodes.insert(index, inner_node) + if inner_node == EmptySubtreeRoots::get_inner_node(SMT_DEPTH, index.depth()) { + self.remove_inner_node(index) + } else { + self.inner_nodes.insert(index, inner_node) + } } fn remove_inner_node(&mut self, index: NodeIndex) -> Option { self.inner_nodes.remove(&index) } - fn insert_value(&mut self, key: Self::Key, value: Self::Value) -> Option { + fn insert_value( + &mut self, + key: Self::Key, + value: Self::Value, + ) -> Result, MerkleError> { // inserting an `EMPTY_VALUE` is equivalent to removing any value associated with `key` if value != Self::EMPTY_VALUE { self.perform_insert(key, value) } else { - self.perform_remove(key) + Ok(self.perform_remove(key)) } } @@ -444,19 +484,19 @@ impl SparseMerkleTree for Smt { mut existing_leaf: SmtLeaf, key: &Word, value: &Word, - ) -> SmtLeaf { + ) -> Result { debug_assert_eq!(existing_leaf.index(), Self::key_to_leaf_index(key)); match existing_leaf { - SmtLeaf::Empty(_) => SmtLeaf::new_single(*key, *value), + SmtLeaf::Empty(_) => Ok(SmtLeaf::new_single(*key, *value)), _ => { if *value != EMPTY_WORD { - existing_leaf.insert(*key, *value); + existing_leaf.insert(*key, *value)?; } else { existing_leaf.remove(*key); } - existing_leaf + Ok(existing_leaf) }, } } @@ -466,7 +506,7 @@ impl SparseMerkleTree for Smt { LeafIndex::new_max_depth(most_significant_felt.as_int()) } - fn path_and_leaf_to_opening(path: MerklePath, leaf: SmtLeaf) -> SmtProof { + fn path_and_leaf_to_opening(path: SparseMerklePath, leaf: SmtLeaf) -> SmtProof { SmtProof::new_unchecked(path, leaf) } } diff --git a/miden-crypto/src/merkle/smt/full/proof.rs b/miden-crypto/src/merkle/smt/full/proof.rs index 055e6f3e8c..d9a55dbb3f 100644 --- a/miden-crypto/src/merkle/smt/full/proof.rs +++ b/miden-crypto/src/merkle/smt/full/proof.rs @@ -1,17 +1,17 @@ use alloc::string::ToString; -use super::{MerklePath, SMT_DEPTH, SmtLeaf, SmtProofError, Word}; +use super::{SMT_DEPTH, SmtLeaf, SmtProofError, SparseMerklePath, Word}; use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; /// A proof which can be used to assert membership (or non-membership) of key-value pairs /// in a [`super::Smt`] (Sparse Merkle Tree). /// -/// The proof consists of a Merkle path and a leaf, which describes the node located at +/// The proof consists of a sparse Merkle path and a leaf, which describes the node located at /// the base of the path. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SmtProof { - /// The Merkle path from the leaf to the root. - path: MerklePath, + /// The sparse Merkle path from the leaf to the root. + path: SparseMerklePath, /// The leaf node containing one or more key-value pairs. leaf: SmtLeaf, } @@ -25,10 +25,10 @@ impl SmtProof { /// # Errors /// Returns an error if the path length does not match the expected [`SMT_DEPTH`], /// which would make the proof invalid. - pub fn new(path: MerklePath, leaf: SmtLeaf) -> Result { - let depth: usize = SMT_DEPTH.into(); - if path.len() != depth { - return Err(SmtProofError::InvalidMerklePathLength(path.len())); + pub fn new(path: SparseMerklePath, leaf: SmtLeaf) -> Result { + let depth = path.depth(); + if depth != SMT_DEPTH { + return Err(SmtProofError::InvalidMerklePathLength(depth as usize)); } Ok(Self { path, leaf }) @@ -37,7 +37,7 @@ impl SmtProof { /// Returns a new instance of [`SmtProof`] instantiated from the specified path and leaf. /// /// The length of the path is not checked. Reserved for internal use. - pub(super) fn new_unchecked(path: MerklePath, leaf: SmtLeaf) -> Self { + pub(super) fn new_unchecked(path: SparseMerklePath, leaf: SmtLeaf) -> Self { Self { path, leaf } } @@ -85,8 +85,8 @@ impl SmtProof { .expect("failed to compute Merkle path root") } - /// Returns the proof's Merkle path. - pub fn path(&self) -> &MerklePath { + /// Returns the proof's sparse Merkle path. + pub fn path(&self) -> &SparseMerklePath { &self.path } @@ -96,7 +96,7 @@ impl SmtProof { } /// Consume the proof and returns its parts. - pub fn into_parts(self) -> (MerklePath, SmtLeaf) { + pub fn into_parts(self) -> (SparseMerklePath, SmtLeaf) { (self.path, self.leaf) } } @@ -110,7 +110,7 @@ impl Serializable for SmtProof { impl Deserializable for SmtProof { fn read_from(source: &mut R) -> Result { - let path = MerklePath::read_from(source)?; + let path = SparseMerklePath::read_from(source)?; let leaf = SmtLeaf::read_from(source)?; Self::new(path, leaf).map_err(|err| DeserializationError::InvalidValue(err.to_string())) diff --git a/miden-crypto/src/merkle/smt/full/tests.rs b/miden-crypto/src/merkle/smt/full/tests.rs index c89afeeaab..db8fdc426b 100644 --- a/miden-crypto/src/merkle/smt/full/tests.rs +++ b/miden-crypto/src/merkle/smt/full/tests.rs @@ -1,14 +1,17 @@ use alloc::vec::Vec; +use assert_matches::assert_matches; + use super::{EMPTY_WORD, Felt, LeafIndex, NodeIndex, Rpo256, SMT_DEPTH, Smt, SmtLeaf, Word}; use crate::{ ONE, WORD_SIZE, merkle::{ - EmptySubtreeRoots, MerkleStore, MutationSet, - smt::{NodeMutation, SparseMerkleTree, UnorderedMap}, + EmptySubtreeRoots, MerkleStore, MutationSet, SmtLeafError, + smt::{Map, NodeMutation, SparseMerkleTree, full::MAX_LEAF_ENTRIES}, }, utils::{Deserializable, Serializable}, }; + // SMT // -------------------------------------------------------------------------------------------- @@ -36,7 +39,7 @@ fn test_smt_insert_at_same_key() { let leaf_node = build_empty_or_single_leaf_node(key_1, value_1); let tree_root = store.set_node(smt.root(), key_1_index, leaf_node).unwrap().root; - let old_value_1 = smt.insert(key_1, value_1); + let old_value_1 = smt.insert(key_1, value_1).unwrap(); assert_eq!(old_value_1, EMPTY_WORD); assert_eq!(smt.root(), tree_root); @@ -47,7 +50,7 @@ fn test_smt_insert_at_same_key() { let leaf_node = build_empty_or_single_leaf_node(key_1, value_2); let tree_root = store.set_node(smt.root(), key_1_index, leaf_node).unwrap().root; - let old_value_2 = smt.insert(key_1, value_2); + let old_value_2 = smt.insert(key_1, value_2).unwrap(); assert_eq!(old_value_2, value_1); assert_eq!(smt.root(), tree_root); @@ -96,7 +99,7 @@ fn test_smt_insert_at_same_key_2() { ]); let tree_root = store.set_node(smt.root(), key_1_index, leaf_node).unwrap().root; - let old_value_1 = smt.insert(key_1, value_1); + let old_value_1 = smt.insert(key_1, value_1).unwrap(); assert_eq!(old_value_1, EMPTY_WORD); assert_eq!(smt.root(), tree_root); @@ -110,7 +113,7 @@ fn test_smt_insert_at_same_key_2() { ]); let tree_root = store.set_node(smt.root(), key_1_index, leaf_node).unwrap().root; - let old_value_2 = smt.insert(key_1, value_2); + let old_value_2 = smt.insert(key_1, value_2).unwrap(); assert_eq!(old_value_2, value_1); assert_eq!(smt.root(), tree_root); @@ -132,7 +135,7 @@ fn test_smt_insert_and_remove_multiple_values() { let leaf_node = build_empty_or_single_leaf_node(key, value); let tree_root = store.set_node(smt.root(), key_index, leaf_node).unwrap().root; - let _ = smt.insert(key, value); + smt.insert(key, value).unwrap(); assert_eq!(smt.root(), tree_root); @@ -183,6 +186,39 @@ fn test_smt_insert_and_remove_multiple_values() { assert!(smt.inner_nodes.is_empty()); } +/// Verify that the `insert_inner_node` doesn't store empty subtrees. +#[test] +fn test_smt_dont_store_empty_subtrees() { + use crate::merkle::smt::InnerNode; + + let mut smt = Smt::default(); + + let node_index = NodeIndex::new(10, 42).unwrap(); + let depth = node_index.depth(); + let empty_subtree_node = EmptySubtreeRoots::get_inner_node(SMT_DEPTH, depth); + + // Empty subtrees are not stored + assert!(!smt.inner_nodes.contains_key(&node_index)); + let old_node = smt.insert_inner_node(node_index, empty_subtree_node.clone()); + assert_eq!(old_node, None); + assert!(!smt.inner_nodes.contains_key(&node_index)); + + // Insert a non-empty node, then insert the empty subtree node again. This should remove the + // inner node. + let non_empty_node = InnerNode { + left: Word::new([ONE; 4]), + right: Word::new([ONE + ONE; 4]), + }; + smt.insert_inner_node(node_index, non_empty_node.clone()); + let old_node = smt.insert_inner_node(node_index, empty_subtree_node.clone()); + assert_eq!(old_node, Some(non_empty_node)); + assert!(!smt.inner_nodes.contains_key(&node_index)); + + // Verify that get_inner_node returns the correct empty subtree node + let retrieved_node = smt.get_inner_node(node_index); + assert_eq!(retrieved_node, empty_subtree_node); +} + /// This tests that inserting the empty value does indeed remove the key-value contained at the /// leaf. We insert & remove 3 values at the same leaf to ensure that all cases are covered (empty, /// single, multiple). @@ -202,7 +238,7 @@ fn test_smt_removal() { // insert key-value 1 { - let old_value_1 = smt.insert(key_1, value_1); + let old_value_1 = smt.insert(key_1, value_1).unwrap(); assert_eq!(old_value_1, EMPTY_WORD); assert_eq!(smt.get_leaf(&key_1), SmtLeaf::Single((key_1, value_1))); @@ -210,7 +246,7 @@ fn test_smt_removal() { // insert key-value 2 { - let old_value_2 = smt.insert(key_2, value_2); + let old_value_2 = smt.insert(key_2, value_2).unwrap(); assert_eq!(old_value_2, EMPTY_WORD); assert_eq!( @@ -221,7 +257,7 @@ fn test_smt_removal() { // insert key-value 3 { - let old_value_3 = smt.insert(key_3, value_3); + let old_value_3 = smt.insert(key_3, value_3).unwrap(); assert_eq!(old_value_3, EMPTY_WORD); assert_eq!( @@ -232,7 +268,7 @@ fn test_smt_removal() { // remove key 3 { - let old_value_3 = smt.insert(key_3, EMPTY_WORD); + let old_value_3 = smt.insert(key_3, EMPTY_WORD).unwrap(); assert_eq!(old_value_3, value_3); assert_eq!( @@ -243,7 +279,7 @@ fn test_smt_removal() { // remove key 2 { - let old_value_2 = smt.insert(key_2, EMPTY_WORD); + let old_value_2 = smt.insert(key_2, EMPTY_WORD).unwrap(); assert_eq!(old_value_2, value_2); assert_eq!(smt.get_leaf(&key_2), SmtLeaf::Single((key_1, value_1))); @@ -251,7 +287,7 @@ fn test_smt_removal() { // remove key 1 { - let old_value_1 = smt.insert(key_1, EMPTY_WORD); + let old_value_1 = smt.insert(key_1, EMPTY_WORD).unwrap(); assert_eq!(old_value_1, value_1); assert_eq!(smt.get_leaf(&key_1), SmtLeaf::new_empty(key_1.into())); @@ -278,9 +314,11 @@ fn test_prospective_hash() { // insert key-value 1 { - let prospective = - smt.construct_prospective_leaf(smt.get_leaf(&key_1), &key_1, &value_1).hash(); - smt.insert(key_1, value_1); + let prospective = smt + .construct_prospective_leaf(smt.get_leaf(&key_1), &key_1, &value_1) + .unwrap() + .hash(); + smt.insert(key_1, value_1).unwrap(); let leaf = smt.get_leaf(&key_1); assert_eq!( @@ -292,9 +330,11 @@ fn test_prospective_hash() { // insert key-value 2 { - let prospective = - smt.construct_prospective_leaf(smt.get_leaf(&key_2), &key_2, &value_2).hash(); - smt.insert(key_2, value_2); + let prospective = smt + .construct_prospective_leaf(smt.get_leaf(&key_2), &key_2, &value_2) + .unwrap() + .hash(); + smt.insert(key_2, value_2).unwrap(); let leaf = smt.get_leaf(&key_2); assert_eq!( @@ -306,9 +346,11 @@ fn test_prospective_hash() { // insert key-value 3 { - let prospective = - smt.construct_prospective_leaf(smt.get_leaf(&key_3), &key_3, &value_3).hash(); - smt.insert(key_3, value_3); + let prospective = smt + .construct_prospective_leaf(smt.get_leaf(&key_3), &key_3, &value_3) + .unwrap() + .hash(); + smt.insert(key_3, value_3).unwrap(); let leaf = smt.get_leaf(&key_3); assert_eq!( @@ -321,10 +363,11 @@ fn test_prospective_hash() { // remove key 3 { let old_leaf = smt.get_leaf(&key_3); - let old_value_3 = smt.insert(key_3, EMPTY_WORD); + let old_value_3 = smt.insert(key_3, EMPTY_WORD).unwrap(); assert_eq!(old_value_3, value_3); - let prospective_leaf = - smt.construct_prospective_leaf(smt.get_leaf(&key_3), &key_3, &old_value_3); + let prospective_leaf = smt + .construct_prospective_leaf(smt.get_leaf(&key_3), &key_3, &old_value_3) + .unwrap(); assert_eq!( old_leaf.hash(), @@ -338,10 +381,11 @@ fn test_prospective_hash() { // remove key 2 { let old_leaf = smt.get_leaf(&key_2); - let old_value_2 = smt.insert(key_2, EMPTY_WORD); + let old_value_2 = smt.insert(key_2, EMPTY_WORD).unwrap(); assert_eq!(old_value_2, value_2); - let prospective_leaf = - smt.construct_prospective_leaf(smt.get_leaf(&key_2), &key_2, &old_value_2); + let prospective_leaf = smt + .construct_prospective_leaf(smt.get_leaf(&key_2), &key_2, &old_value_2) + .unwrap(); assert_eq!( old_leaf.hash(), @@ -355,10 +399,11 @@ fn test_prospective_hash() { // remove key 1 { let old_leaf = smt.get_leaf(&key_1); - let old_value_1 = smt.insert(key_1, EMPTY_WORD); + let old_value_1 = smt.insert(key_1, EMPTY_WORD).unwrap(); assert_eq!(old_value_1, value_1); - let prospective_leaf = - smt.construct_prospective_leaf(smt.get_leaf(&key_1), &key_1, &old_value_1); + let prospective_leaf = smt + .construct_prospective_leaf(smt.get_leaf(&key_1), &key_1, &old_value_1) + .unwrap(); assert_eq!( old_leaf.hash(), prospective_leaf.hash(), @@ -388,17 +433,17 @@ fn test_prospective_insertion() { let root_empty = smt.root(); let root_1 = { - smt.insert(key_1, value_1); + smt.insert(key_1, value_1).unwrap(); smt.root() }; let root_2 = { - smt.insert(key_2, value_2); + smt.insert(key_2, value_2).unwrap(); smt.root() }; let root_3 = { - smt.insert(key_3, value_3); + smt.insert(key_3, value_3).unwrap(); smt.root() }; @@ -406,7 +451,7 @@ fn test_prospective_insertion() { let mut smt = Smt::default(); - let mutations = smt.compute_mutations(vec![(key_1, value_1)]); + let mutations = smt.compute_mutations(vec![(key_1, value_1)]).unwrap(); assert_eq!(mutations.root(), root_1, "prospective root 1 did not match actual root 1"); let revert = apply_mutations(&mut smt, mutations); assert_eq!(smt.root(), root_1, "mutations before and after apply did not match"); @@ -414,7 +459,7 @@ fn test_prospective_insertion() { assert_eq!(revert.root(), root_empty, "reverse mutations new root did not match"); assert_eq!( revert.new_pairs, - UnorderedMap::from_iter([(key_1, EMPTY_WORD)]), + Map::from_iter([(key_1, EMPTY_WORD)]), "reverse mutations pairs did not match" ); assert_eq!( @@ -423,10 +468,11 @@ fn test_prospective_insertion() { "reverse mutations inner nodes did not match" ); - let mutations = smt.compute_mutations(vec![(key_2, value_2)]); + let mutations = smt.compute_mutations(vec![(key_2, value_2)]).unwrap(); assert_eq!(mutations.root(), root_2, "prospective root 2 did not match actual root 2"); - let mutations = - smt.compute_mutations(vec![(key_3, EMPTY_WORD), (key_2, value_2), (key_3, value_3)]); + let mutations = smt + .compute_mutations(vec![(key_3, EMPTY_WORD), (key_2, value_2), (key_3, value_3)]) + .unwrap(); assert_eq!(mutations.root(), root_3, "mutations before and after apply did not match"); let old_root = smt.root(); let revert = apply_mutations(&mut smt, mutations); @@ -434,12 +480,12 @@ fn test_prospective_insertion() { assert_eq!(revert.root(), old_root, "reverse mutations new root did not match"); assert_eq!( revert.new_pairs, - UnorderedMap::from_iter([(key_2, EMPTY_WORD), (key_3, EMPTY_WORD)]), + Map::from_iter([(key_2, EMPTY_WORD), (key_3, EMPTY_WORD)]), "reverse mutations pairs did not match" ); // Edge case: multiple values at the same key, where a later pair restores the original value. - let mutations = smt.compute_mutations(vec![(key_3, EMPTY_WORD), (key_3, value_3)]); + let mutations = smt.compute_mutations(vec![(key_3, EMPTY_WORD), (key_3, value_3)]).unwrap(); assert_eq!(mutations.root(), root_3); let old_root = smt.root(); let revert = apply_mutations(&mut smt, mutations); @@ -448,14 +494,14 @@ fn test_prospective_insertion() { assert_eq!(revert.root(), old_root, "reverse mutations new root did not match"); assert_eq!( revert.new_pairs, - UnorderedMap::from_iter([(key_3, value_3)]), + Map::from_iter([(key_3, value_3)]), "reverse mutations pairs did not match" ); // Test batch updates, and that the order doesn't matter. let pairs = vec![(key_3, value_2), (key_2, EMPTY_WORD), (key_1, EMPTY_WORD), (key_3, EMPTY_WORD)]; - let mutations = smt.compute_mutations(pairs); + let mutations = smt.compute_mutations(pairs).unwrap(); assert_eq!( mutations.root(), root_empty, @@ -468,12 +514,12 @@ fn test_prospective_insertion() { assert_eq!(revert.root(), old_root, "reverse mutations new root did not match"); assert_eq!( revert.new_pairs, - UnorderedMap::from_iter([(key_1, value_1), (key_2, value_2), (key_3, value_3)]), + Map::from_iter([(key_1, value_1), (key_2, value_2), (key_3, value_3)]), "reverse mutations pairs did not match" ); let pairs = vec![(key_3, value_3), (key_1, value_1), (key_2, value_2)]; - let mutations = smt.compute_mutations(pairs); + let mutations = smt.compute_mutations(pairs).unwrap(); assert_eq!(mutations.root(), root_3); smt.apply_mutations(mutations).unwrap(); assert_eq!(smt.root(), root_3); @@ -486,7 +532,7 @@ fn test_mutations_no_mutations() { let entries = [(key, value)]; let tree = Smt::with_entries(entries).unwrap(); - let mutations = tree.compute_mutations(entries); + let mutations = tree.compute_mutations(entries).unwrap(); assert_eq!(mutations.root(), mutations.old_root(), "Root should not change"); assert!(mutations.node_mutations().is_empty(), "Node mutations should be empty"); @@ -505,11 +551,12 @@ fn test_mutations_revert() { let value_2 = Word::new([2_u32.into(); WORD_SIZE]); let value_3 = Word::new([3_u32.into(); WORD_SIZE]); - smt.insert(key_1, value_1); - smt.insert(key_2, value_2); + smt.insert(key_1, value_1).unwrap(); + smt.insert(key_2, value_2).unwrap(); - let mutations = - smt.compute_mutations(vec![(key_1, EMPTY_WORD), (key_2, value_1), (key_3, value_3)]); + let mutations = smt + .compute_mutations(vec![(key_1, EMPTY_WORD), (key_2, value_1), (key_3, value_3)]) + .unwrap(); let original = smt.clone(); @@ -534,11 +581,12 @@ fn test_mutation_set_serialization() { let value_2 = Word::new([2_u32.into(); WORD_SIZE]); let value_3 = Word::new([3_u32.into(); WORD_SIZE]); - smt.insert(key_1, value_1); - smt.insert(key_2, value_2); + smt.insert(key_1, value_1).unwrap(); + smt.insert(key_2, value_2).unwrap(); - let mutations = - smt.compute_mutations(vec![(key_1, EMPTY_WORD), (key_2, value_1), (key_3, value_3)]); + let mutations = smt + .compute_mutations(vec![(key_1, EMPTY_WORD), (key_2, value_1), (key_3, value_3)]) + .unwrap(); let serialized = mutations.to_bytes(); let deserialized = MutationSet::::read_from_bytes(&serialized).unwrap(); @@ -683,6 +731,34 @@ fn test_multiple_smt_leaf_serialization_success() { assert_eq!(multiple_leaf, deserialized); } +/// Test that creating a multiple leaf with exactly MAX_LEAF_ENTRIES works +/// and that constructing a leaf with MAX_LEAF_ENTRIES + 1 returns an error. +#[test] +fn test_max_leaf_entries_validation() { + let mut entries = Vec::new(); + + for i in 0..MAX_LEAF_ENTRIES { + let key = Word::new([ONE, ONE, Felt::new(i as u64), ONE]); + let value = Word::new([ONE, ONE, ONE, Felt::new(i as u64)]); + entries.push((key, value)); + } + + let result = SmtLeaf::new_multiple(entries.clone()); + assert!(result.is_ok(), "Should allow exactly MAX_LEAF_ENTRIES entries"); + + // Test that creating a multiple leaf with more than MAX_LEAF_ENTRIES fails + let key = Word::new([ONE, ONE, Felt::new(MAX_LEAF_ENTRIES as u64), ONE]); + let value = Word::new([ONE, ONE, ONE, Felt::new(MAX_LEAF_ENTRIES as u64)]); + entries.push((key, value)); + + let error = SmtLeaf::new_multiple(entries).unwrap_err(); + assert_matches!( + error, + SmtLeafError::TooManyLeafEntries { .. }, + "should reject more than MAX_LEAF_ENTRIES entries" + ); +} + // HELPERS // -------------------------------------------------------------------------------------------- diff --git a/miden-crypto/src/merkle/smt/mod.rs b/miden-crypto/src/merkle/smt/mod.rs index 8120cf0a12..f26c5f70d1 100644 --- a/miden-crypto/src/merkle/smt/mod.rs +++ b/miden-crypto/src/merkle/smt/mod.rs @@ -3,16 +3,16 @@ use core::hash::Hash; use winter_utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; -use super::{EmptySubtreeRoots, InnerNodeInfo, MerkleError, MerklePath, NodeIndex}; -use crate::{EMPTY_WORD, Felt, Word, hash::rpo::Rpo256}; +use super::{EmptySubtreeRoots, InnerNodeInfo, MerkleError, NodeIndex, SparseMerklePath}; +use crate::{EMPTY_WORD, Felt, Map, Word, hash::rpo::Rpo256}; mod full; -pub use full::{SMT_DEPTH, Smt, SmtLeaf, SmtLeafError, SmtProof, SmtProofError}; +pub use full::{MAX_LEAF_ENTRIES, SMT_DEPTH, Smt, SmtLeaf, SmtLeafError, SmtProof, SmtProofError}; #[cfg(feature = "internal")] pub use full::{SubtreeLeaf, build_subtree_for_bench}; mod simple; -pub use simple::SimpleSmt; +pub use simple::{SimpleSmt, SimpleSmtProof}; mod partial; pub use partial::PartialSmt; @@ -29,14 +29,9 @@ pub const SMT_MAX_DEPTH: u8 = 64; // SPARSE MERKLE TREE // ================================================================================================ -/// A map whose keys are not guaranteed to be ordered. -#[cfg(feature = "smt_hashmaps")] -type UnorderedMap = hashbrown::HashMap; -#[cfg(not(feature = "smt_hashmaps"))] -type UnorderedMap = alloc::collections::BTreeMap; -type InnerNodes = UnorderedMap; -type Leaves = UnorderedMap; -type NodeMutations = UnorderedMap; +type InnerNodes = Map; +type Leaves = Map; +type NodeMutations = Map; /// An abstract description of a sparse Merkle tree. /// @@ -76,12 +71,17 @@ pub(crate) trait SparseMerkleTree { // PROVIDED METHODS // --------------------------------------------------------------------------------------------- - /// Returns a [MerklePath] to the specified key. + /// Returns a [SparseMerklePath] to the specified key. /// /// Mostly this is an implementation detail of [`Self::open()`]. - fn get_path(&self, key: &Self::Key) -> MerklePath { + fn get_path(&self, key: &Self::Key) -> SparseMerklePath { let index = NodeIndex::from(Self::key_to_leaf_index(key)); - index.proof_indices().map(|index| self.get_node_hash(index)).collect() + + // SAFETY: this is guaranteed to have depth <= SMT_MAX_DEPTH + SparseMerklePath::from_sized_iter( + index.proof_indices().map(|index| self.get_node_hash(index)), + ) + .expect("failed to convert to SparseMerklePath") } /// Get the hash of a node at an arbitrary index, including the root or leaf hashes. @@ -99,8 +99,8 @@ pub(crate) trait SparseMerkleTree { if index_is_right { right } else { left } } - /// Returns an opening of the leaf associated with `key`. Conceptually, an opening is a Merkle - /// path to the leaf, as well as the leaf itself. + /// Returns an opening of the leaf associated with `key`. Conceptually, an opening is a sparse + /// Merkle path to the leaf, as well as the leaf itself. fn open(&self, key: &Self::Key) -> Self::Opening { let leaf = self.get_leaf(key); let merkle_path = self.get_path(key); @@ -114,12 +114,12 @@ pub(crate) trait SparseMerkleTree { /// /// This also recomputes all hashes between the leaf (associated with the key) and the root, /// updating the root itself. - fn insert(&mut self, key: Self::Key, value: Self::Value) -> Self::Value { - let old_value = self.insert_value(key.clone(), value.clone()).unwrap_or(Self::EMPTY_VALUE); + fn insert(&mut self, key: Self::Key, value: Self::Value) -> Result { + let old_value = self.insert_value(key.clone(), value.clone())?.unwrap_or(Self::EMPTY_VALUE); // if the old value and new value are the same, there is nothing to update if value == old_value { - return value; + return Ok(value); } let leaf = self.get_leaf(&key); @@ -130,7 +130,7 @@ pub(crate) trait SparseMerkleTree { self.recompute_nodes_from_index_to_root(node_index, Self::hash_leaf(&leaf)); - old_value + Ok(old_value) } /// Recomputes the branch nodes (including the root) from `index` all the way to the root. @@ -171,10 +171,14 @@ pub(crate) trait SparseMerkleTree { /// be queried with [`MutationSet::root()`]. Once a mutation set is returned, /// [`SparseMerkleTree::apply_mutations()`] can be called in order to commit these changes to /// the Merkle tree, or [`drop()`] to discard them. + /// + /// # Errors + /// If mutations would exceed [`MAX_LEAF_ENTRIES`] (1024 entries) in a leaf, returns + /// [`MerkleError::TooManyLeafEntries`]. fn compute_mutations( &self, kv_pairs: impl IntoIterator, - ) -> MutationSet { + ) -> Result, MerkleError> { self.compute_mutations_sequential(kv_pairs) } @@ -183,11 +187,11 @@ pub(crate) trait SparseMerkleTree { fn compute_mutations_sequential( &self, kv_pairs: impl IntoIterator, - ) -> MutationSet { + ) -> Result, MerkleError> { use NodeMutation::*; let mut new_root = self.root(); - let mut new_pairs: UnorderedMap = Default::default(); + let mut new_pairs: Map = Default::default(); let mut node_mutations: NodeMutations = Default::default(); for (key, value) in kv_pairs { @@ -215,10 +219,17 @@ pub(crate) trait SparseMerkleTree { // none at all), as multi-leaves should be really rare. let existing_leaf = acc.clone(); self.construct_prospective_leaf(existing_leaf, k, v) + .expect("current leaf should be valid") }) }; - let new_leaf = self.construct_prospective_leaf(old_leaf, &key, &value); + let new_leaf = + self.construct_prospective_leaf(old_leaf, &key, &value).map_err(|e| match e { + SmtLeafError::TooManyLeafEntries { actual } => { + MerkleError::TooManyLeafEntries { actual } + }, + other => panic!("unexpected SmtLeaf::insert error: {:?}", other), + })?; let mut new_child_hash = Self::hash_leaf(&new_leaf); @@ -262,12 +273,12 @@ pub(crate) trait SparseMerkleTree { new_pairs.insert(key, value); } - MutationSet { + Ok(MutationSet { old_root: self.root(), new_root, node_mutations, new_pairs, - } + }) } /// Applies the prospective mutations computed with [`SparseMerkleTree::compute_mutations()`] to @@ -278,6 +289,8 @@ pub(crate) trait SparseMerkleTree { /// [`MerkleError::ConflictingRoots`] with a two-item [`Vec`]. The first item is the root hash /// the `mutations` were computed against, and the second item is the actual current root of /// this tree. + /// If mutations would exceed [`MAX_LEAF_ENTRIES`] (1024 entries) in a leaf, returns + /// [`MerkleError::TooManyLeafEntries`]. fn apply_mutations( &mut self, mutations: MutationSet, @@ -314,7 +327,7 @@ pub(crate) trait SparseMerkleTree { } for (key, value) in new_pairs { - self.insert_value(key, value); + self.insert_value(key, value)?; } self.set_root(new_root); @@ -373,12 +386,15 @@ pub(crate) trait SparseMerkleTree { } } - let mut reverse_pairs = UnorderedMap::new(); + let mut reverse_pairs = Map::new(); for (key, value) in new_pairs { - if let Some(old_value) = self.insert_value(key.clone(), value) { - reverse_pairs.insert(key, old_value); - } else { - reverse_pairs.insert(key, Self::EMPTY_VALUE); + match self.insert_value(key.clone(), value)? { + Some(old_value) => { + reverse_pairs.insert(key, old_value); + }, + None => { + reverse_pairs.insert(key, Self::EMPTY_VALUE); + }, } } @@ -421,7 +437,11 @@ pub(crate) trait SparseMerkleTree { fn remove_inner_node(&mut self, index: NodeIndex) -> Option; /// Inserts a leaf node, and returns the value at the key if already exists - fn insert_value(&mut self, key: Self::Key, value: Self::Value) -> Option; + fn insert_value( + &mut self, + key: Self::Key, + value: Self::Value, + ) -> Result, MerkleError>; /// Returns the value at the specified key. Recall that by definition, any key that hasn't been /// updated is associated with [`Self::EMPTY_VALUE`]. @@ -444,20 +464,24 @@ pub(crate) trait SparseMerkleTree { /// Because this method is for a prospective key-value insertion into a specific leaf, /// `existing_leaf` must have the same leaf index as `key` (as determined by /// [`SparseMerkleTree::key_to_leaf_index()`]), or the result will be meaningless. + /// + /// # Errors + /// If inserting the key-value pair would exceed [`MAX_LEAF_ENTRIES`] (1024 entries) in a leaf, + /// returns [`SmtLeafError::TooManyLeafEntries`]. fn construct_prospective_leaf( &self, existing_leaf: Self::Leaf, key: &Self::Key, value: &Self::Value, - ) -> Self::Leaf; + ) -> Result; /// Maps a key to a leaf index fn key_to_leaf_index(key: &Self::Key) -> LeafIndex; - /// Maps a (MerklePath, Self::Leaf) to an opening. + /// Maps a (SparseMerklePath, Self::Leaf) to an opening. /// /// The length `path` is guaranteed to be equal to `DEPTH` - fn path_and_leaf_to_opening(path: MerklePath, leaf: Self::Leaf) -> Self::Opening; + fn path_and_leaf_to_opening(path: SparseMerklePath, leaf: Self::Leaf) -> Self::Opening; } // INNER NODE @@ -581,10 +605,10 @@ pub struct MutationSet { /// corresponds to a [`SparseMerkleTree::remove_inner_node()`] call. node_mutations: NodeMutations, /// The set of top-level key-value pairs we're prospectively adding to the tree, including - /// adding empty values. The "effective" value for a key is the value in this BTreeMap, falling + /// adding empty values. The "effective" value for a key is the value in this Map, falling /// back to the existing value in the Merkle tree. Each entry corresponds to a /// [`SparseMerkleTree::insert_value()`] call. - new_pairs: UnorderedMap, + new_pairs: Map, /// The calculated root for the Merkle tree, given these mutations. Publicly retrievable with /// [`MutationSet::root()`]. Corresponds to a [`SparseMerkleTree::set_root()`]. call. new_root: Word, @@ -609,7 +633,7 @@ impl MutationSet { /// Returns the set of top-level key-value pairs that need to be added, updated or deleted /// (i.e. set to `EMPTY_WORD`). - pub fn new_pairs(&self) -> &UnorderedMap { + pub fn new_pairs(&self) -> &Map { &self.new_pairs } } @@ -706,7 +730,7 @@ impl De let num_new_pairs = source.read_usize()?; let new_pairs = source.read_many(num_new_pairs)?; - let new_pairs = UnorderedMap::from_iter(new_pairs); + let new_pairs = Map::from_iter(new_pairs); Ok(Self { old_root, diff --git a/miden-crypto/src/merkle/smt/partial.rs b/miden-crypto/src/merkle/smt/partial.rs index 4400f468ea..92af80daa5 100644 --- a/miden-crypto/src/merkle/smt/partial.rs +++ b/miden-crypto/src/merkle/smt/partial.rs @@ -4,7 +4,7 @@ use super::{LeafIndex, SMT_DEPTH}; use crate::{ EMPTY_WORD, Word, merkle::{ - InnerNode, InnerNodeInfo, MerkleError, MerklePath, NodeIndex, Smt, SmtLeaf, SmtProof, + InnerNode, InnerNodeInfo, MerkleError, NodeIndex, Smt, SmtLeaf, SmtProof, SparseMerklePath, smt::{InnerNodes, Leaves, SparseMerkleTree}, }, }; @@ -125,12 +125,14 @@ impl PartialSmt { /// - the key and its merkle path were not previously added (using [`PartialSmt::add_path`]) to /// this [`PartialSmt`], which means it is almost certainly incorrect to update its value. If /// an error is returned the tree is in the same state as before. + /// - inserting the key-value pair would exceed [`super::MAX_LEAF_ENTRIES`] (1024 entries) in + /// the leaf. pub fn insert(&mut self, key: Word, value: Word) -> Result { if !self.is_leaf_tracked(&key) { return Err(MerkleError::UntrackedKey(key)); } - let previous_value = self.0.insert(key, value); + let previous_value = self.0.insert(key, value)?; // If the value was removed the SmtLeaf was removed as well by the underlying Smt // implementation. However, we still want to consider that leaf tracked so it can be @@ -152,7 +154,7 @@ impl PartialSmt { self.add_path(leaf, path) } - /// Adds a leaf and its merkle path to this [`PartialSmt`]. + /// Adds a leaf and its sparse merkle path to this [`PartialSmt`]. /// /// If this function was called, any key that is part of the `leaf` can subsequently be updated /// to a new value and produce a correct new tree root. @@ -163,7 +165,7 @@ impl PartialSmt { /// - the new root after the insertion of the leaf and the path does not match the existing root /// (except when the first leaf is added). If an error is returned, the tree is left in an /// inconsistent state. - pub fn add_path(&mut self, leaf: SmtLeaf, path: MerklePath) -> Result<(), MerkleError> { + pub fn add_path(&mut self, leaf: SmtLeaf, path: SparseMerklePath) -> Result<(), MerkleError> { let mut current_index = leaf.index().index; let mut node_hash_at_current_index = leaf.hash(); @@ -176,8 +178,18 @@ impl PartialSmt { // PartialSmt::insert, this will not error for such empty leaves whose merkle path was // added, but will error for otherwise non-existent leaves whose paths were not added, // which is what we want. + let prev_entries = self + .0 + .leaves + .get(¤t_index.value()) + .map(|leaf| leaf.num_entries()) + .unwrap_or(0); + let current_entries = leaf.num_entries(); self.0.leaves.insert(current_index.value(), leaf); + // Guaranteed not to over/underflow. All variables are <= MAX_LEAF_ENTRIES and result > 0. + self.0.num_entries = self.0.num_entries + current_entries - prev_entries; + for sibling_hash in path { // Find the index of the sibling node and compute whether it is a left or right child. let is_sibling_right = current_index.sibling().is_value_odd(); @@ -270,9 +282,6 @@ impl PartialSmt { /// /// Note that this may return a different value from [Self::num_leaves()] as a single leaf may /// contain more than one key-value pair. - /// - /// Also note that this is currently an expensive operation as counting the number of - /// entries requires iterating over all leaves of the tree. pub fn num_entries(&self) -> usize { self.0.num_entries() } @@ -353,7 +362,7 @@ mod tests { use winter_math::fields::f64::BaseElement as Felt; use super::*; - use crate::{EMPTY_WORD, ONE, ZERO}; + use crate::{EMPTY_WORD, ONE, ZERO, merkle::EmptySubtreeRoots}; /// Tests that a basic PartialSmt can be built from a full one and that inserting or removing /// values whose merkle path were added to the partial SMT results in the same root as the @@ -407,9 +416,9 @@ mod tests { // A non-empty value for the key that was previously empty. let new_value_empty_key = Word::from(rand_array::()); - full.insert(key0, new_value0); - full.insert(key2, new_value2); - full.insert(key_empty, new_value_empty_key); + full.insert(key0, new_value0).unwrap(); + full.insert(key2, new_value2).unwrap(); + full.insert(key_empty, new_value_empty_key).unwrap(); partial.insert(key0, new_value0).unwrap(); partial.insert(key2, new_value2).unwrap(); @@ -424,7 +433,7 @@ mod tests { // Remove an added key. // ---------------------------------------------------------------------------------------- - full.insert(key0, EMPTY_WORD); + full.insert(key0, EMPTY_WORD).unwrap(); partial.insert(key0, EMPTY_WORD).unwrap(); assert_eq!(full.root(), partial.root()); @@ -501,8 +510,8 @@ mod tests { let stale_proof0 = full.open(&key0); // Insert a non-empty value so the root actually changes. - full.insert(key1, value1); - full.insert(key2, value2); + full.insert(key1, value1).unwrap(); + full.insert(key2, value2).unwrap(); let proof2 = full.open(&key2); @@ -533,7 +542,7 @@ mod tests { // This proof will be stale after we insert another value. let stale_proof0 = full.open(&key0); - full.insert(key2, value2); + full.insert(key2, value2).unwrap(); let proof2 = full.open(&key2); @@ -627,10 +636,16 @@ mod tests { let partial_inner_nodes: BTreeSet<_> = partial.inner_nodes().flat_map(|node| [node.left, node.right]).collect(); + let empty_subtree_roots: BTreeSet<_> = (0..SMT_DEPTH) + .map(|depth| *EmptySubtreeRoots::entry(SMT_DEPTH, depth)) + .collect(); for merkle_path in proofs.into_iter().map(|proof| proof.into_parts().0) { for (idx, digest) in merkle_path.into_iter().enumerate() { - assert!(partial_inner_nodes.contains(&digest), "failed at idx {idx}"); + assert!( + partial_inner_nodes.contains(&digest) || empty_subtree_roots.contains(&digest), + "failed at idx {idx}" + ); } } } @@ -667,12 +682,28 @@ mod tests { assert_eq!(partial_smt, decoded); } + /// Tests that add_path correctly updates num_entries for both increasing and decreasing entry + /// counts. #[test] - fn partial_smt_serialization_rounfdtrip() { - let partial_smt = PartialSmt::new(); - let bytes = partial_smt.to_bytes(); - let decoded = PartialSmt::read_from_bytes(&bytes).unwrap(); + fn partial_smt_add_path_num_entries() { + // key0 and key1 have the same felt at index 3 so they will be placed in the same leaf. + let key0 = Word::from([ZERO, ZERO, ZERO, ONE]); + let key1 = Word::from([ONE, ONE, ONE, ONE]); + let value0 = Word::from(rand_array::()); + let value1 = Word::from(rand_array::()); - assert_eq!(partial_smt, decoded); + let full = Smt::with_entries([(key0, value0), (key1, value1)]).unwrap(); + let mut partial = PartialSmt::new(); + + // Add the multi-entry leaf via add_path + let proof0 = full.open(&key0); + let (path0, leaf0) = proof0.into_parts(); + partial.add_path(leaf0.clone(), path0.clone()).unwrap(); + assert_eq!(partial.num_entries(), 2); + + // Now, replace the multi-entry leaf with a single-entry leaf (simulate removing one entry) + let single_leaf = SmtLeaf::new_single(key0, value0); + partial.add_path(single_leaf.clone(), path0.clone()).unwrap(); + assert_eq!(partial.num_entries(), 1); } } diff --git a/miden-crypto/src/merkle/smt/simple/mod.rs b/miden-crypto/src/merkle/smt/simple/mod.rs index e04cb24599..d831b24a43 100644 --- a/miden-crypto/src/merkle/smt/simple/mod.rs +++ b/miden-crypto/src/merkle/smt/simple/mod.rs @@ -1,11 +1,13 @@ use alloc::collections::BTreeSet; use super::{ - super::ValuePath, EMPTY_WORD, EmptySubtreeRoots, InnerNode, InnerNodeInfo, InnerNodes, - LeafIndex, MerkleError, MerklePath, MutationSet, NodeIndex, SMT_MAX_DEPTH, SMT_MIN_DEPTH, - SparseMerkleTree, Word, + EMPTY_WORD, EmptySubtreeRoots, InnerNode, InnerNodeInfo, InnerNodes, LeafIndex, MerkleError, + MutationSet, NodeIndex, SMT_MAX_DEPTH, SMT_MIN_DEPTH, SparseMerkleTree, Word, }; -use crate::merkle::{SparseMerklePath, SparseValuePath}; +use crate::merkle::{SmtLeafError, SparseMerklePath}; + +mod proof; +pub use proof::SimpleSmtProof; #[cfg(test)] mod tests; @@ -170,7 +172,7 @@ impl SimpleSmt { /// Returns an opening of the leaf associated with `key`. Conceptually, an opening is a Merkle /// path to the leaf, as well as the leaf itself. - pub fn open(&self, key: &LeafIndex) -> SparseValuePath { + pub fn open(&self, key: &LeafIndex) -> SimpleSmtProof { let value = self.get_value(key); let nodes = key.index.proof_indices().map(|index| self.get_node_hash(index)); // `from_sized_iter()` returns an error if there are more nodes than `SMT_MAX_DEPTH`, but @@ -178,7 +180,7 @@ impl SimpleSmt { // guarded against in `SimpleSmt::new()`. let path = SparseMerklePath::from_sized_iter(nodes).unwrap(); - SparseValuePath { value, path } + SimpleSmtProof { value, path } } /// Returns a boolean value indicating whether the SMT is empty. @@ -214,7 +216,10 @@ impl SimpleSmt { /// This also recomputes all hashes between the leaf (associated with the key) and the root, /// updating the root itself. pub fn insert(&mut self, key: LeafIndex, value: Word) -> Word { + // SAFETY: a SimpleSmt does not contain multi-value leaves. The underlaying + // SimpleSmt::insert_value does not return any errors so it's safe to unwrap here. >::insert(self, key, value) + .expect("inserting a value into a simple smt never returns an error") } /// Computes what changes are necessary to insert the specified key-value pairs into this @@ -234,14 +239,18 @@ impl SimpleSmt { /// let pair = (LeafIndex::default(), Word::default()); /// let mutations = smt.compute_mutations(vec![pair]); /// assert_eq!(mutations.root(), *EmptySubtreeRoots::entry(3, 0)); - /// smt.apply_mutations(mutations); + /// smt.apply_mutations(mutations).unwrap(); /// assert_eq!(smt.root(), *EmptySubtreeRoots::entry(3, 0)); /// ``` pub fn compute_mutations( &self, kv_pairs: impl IntoIterator, Word)>, ) -> MutationSet, Word> { + // SAFETY: a SimpleSmt does not contain multi-value leaves. The underlaying + // SimpleSmt::construct_prospective_leaf does not return any errors so it's safe to unwrap + // here. >::compute_mutations(self, kv_pairs) + .expect("computing mutations on a simple smt never returns an error") } /// Applies the prospective mutations computed with [`SimpleSmt::compute_mutations()`] to this @@ -341,7 +350,7 @@ impl SparseMerkleTree for SimpleSmt { type Key = LeafIndex; type Value = Word; type Leaf = Word; - type Opening = ValuePath; + type Opening = SimpleSmtProof; const EMPTY_VALUE: Self::Value = EMPTY_WORD; const EMPTY_ROOT: Word = *EmptySubtreeRoots::entry(DEPTH, 0); @@ -386,12 +395,17 @@ impl SparseMerkleTree for SimpleSmt { self.inner_nodes.remove(&index) } - fn insert_value(&mut self, key: LeafIndex, value: Word) -> Option { - if value == Self::EMPTY_VALUE { + fn insert_value( + &mut self, + key: LeafIndex, + value: Word, + ) -> Result, MerkleError> { + let result = if value == Self::EMPTY_VALUE { self.leaves.remove(&key.value()) } else { self.leaves.insert(key.value(), value) - } + }; + Ok(result) } fn get_value(&self, key: &LeafIndex) -> Word { @@ -416,15 +430,15 @@ impl SparseMerkleTree for SimpleSmt { _existing_leaf: Word, _key: &LeafIndex, value: &Word, - ) -> Word { - *value + ) -> Result { + Ok(*value) } fn key_to_leaf_index(key: &LeafIndex) -> LeafIndex { *key } - fn path_and_leaf_to_opening(path: MerklePath, leaf: Word) -> ValuePath { + fn path_and_leaf_to_opening(path: SparseMerklePath, leaf: Word) -> SimpleSmtProof { (path, leaf).into() } } diff --git a/miden-crypto/src/merkle/smt/simple/proof.rs b/miden-crypto/src/merkle/smt/simple/proof.rs new file mode 100644 index 0000000000..9b88bd0bdd --- /dev/null +++ b/miden-crypto/src/merkle/smt/simple/proof.rs @@ -0,0 +1,62 @@ +use crate::{ + Word, + merkle::{MerkleError, MerkleProof, SparseMerklePath}, +}; + +/// A container for a [crate::Word] value and its [SparseMerklePath] opening. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SimpleSmtProof { + /// The node value opening for `path`. + pub value: Word, + /// The path from `value` to `root` (exclusive), using an efficient memory representation for + /// empty nodes. + pub path: SparseMerklePath, +} + +impl SimpleSmtProof { + /// Convenience function to construct a [SimpleSmtProof]. + /// + /// `value` is the value `path` leads to, in the tree. + pub fn new(value: Word, path: SparseMerklePath) -> Self { + Self { value, path } + } +} + +impl From<(SparseMerklePath, Word)> for SimpleSmtProof { + fn from((path, value): (SparseMerklePath, Word)) -> Self { + SimpleSmtProof::new(value, path) + } +} + +impl TryFrom for SimpleSmtProof { + type Error = MerkleError; + + /// # Errors + /// + /// This conversion returns [MerkleError::DepthTooBig] if the path length is greater than + /// [`super::SMT_MAX_DEPTH`]. + fn try_from(other: MerkleProof) -> Result { + let MerkleProof { value, path } = other; + let path = SparseMerklePath::try_from(path)?; + Ok(SimpleSmtProof { value, path }) + } +} + +impl From for MerkleProof { + fn from(other: SimpleSmtProof) -> Self { + let SimpleSmtProof { value, path } = other; + MerkleProof { value, path: path.into() } + } +} + +impl PartialEq for SimpleSmtProof { + fn eq(&self, rhs: &MerkleProof) -> bool { + self.value == rhs.value && self.path == rhs.path + } +} + +impl PartialEq for MerkleProof { + fn eq(&self, rhs: &SimpleSmtProof) -> bool { + rhs == self + } +} diff --git a/miden-crypto/src/merkle/sparse_path.rs b/miden-crypto/src/merkle/sparse_path.rs index 0100cfeef8..6c6b1f6f94 100644 --- a/miden-crypto/src/merkle/sparse_path.rs +++ b/miden-crypto/src/merkle/sparse_path.rs @@ -7,8 +7,7 @@ use core::{ use winter_utils::{Deserializable, DeserializationError, Serializable}; use super::{ - EmptySubtreeRoots, InnerNodeInfo, MerkleError, MerklePath, NodeIndex, SMT_MAX_DEPTH, ValuePath, - Word, + EmptySubtreeRoots, InnerNodeInfo, MerkleError, MerklePath, NodeIndex, SMT_MAX_DEPTH, Word, }; use crate::hash::rpo::Rpo256; @@ -435,66 +434,6 @@ impl PartialEq for MerklePath { } } -// SPARSE VALUE PATH -// ================================================================================================ -/// A container for a [crate::Word] value and its [SparseMerklePath] opening. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct SparseValuePath { - /// The node value opening for `path`. - pub value: Word, - /// The path from `value` to `root` (exclusive), using an efficient memory representation for - /// empty nodes. - pub path: SparseMerklePath, -} - -impl SparseValuePath { - /// Convenience function to construct a [SparseValuePath]. - /// - /// `value` is the value `path` leads to, in the tree. - pub fn new(value: Word, path: SparseMerklePath) -> Self { - Self { value, path } - } -} - -impl From<(SparseMerklePath, Word)> for SparseValuePath { - fn from((path, value): (SparseMerklePath, Word)) -> Self { - SparseValuePath::new(value, path) - } -} - -impl TryFrom for SparseValuePath { - type Error = MerkleError; - - /// # Errors - /// - /// This conversion returns [MerkleError::DepthTooBig] if the path length is greater than - /// [`SMT_MAX_DEPTH`]. - fn try_from(other: ValuePath) -> Result { - let ValuePath { value, path } = other; - let path = SparseMerklePath::try_from(path)?; - Ok(SparseValuePath { value, path }) - } -} - -impl From for ValuePath { - fn from(other: SparseValuePath) -> Self { - let SparseValuePath { value, path } = other; - ValuePath { value, path: path.into() } - } -} - -impl PartialEq for SparseValuePath { - fn eq(&self, rhs: &ValuePath) -> bool { - self.value == rhs.value && self.path == rhs.path - } -} - -impl PartialEq for ValuePath { - fn eq(&self, rhs: &SparseValuePath) -> bool { - rhs == self - } -} - // HELPERS // ================================================================================================ @@ -520,13 +459,14 @@ mod tests { use core::num::NonZero; use assert_matches::assert_matches; + use winter_math::FieldElement; use super::SparseMerklePath; use crate::{ Felt, ONE, Word, merkle::{ - EmptySubtreeRoots, MerkleError, MerklePath, NodeIndex, SMT_DEPTH, Smt, - smt::SparseMerkleTree, sparse_path::path_depth_iter, + EmptySubtreeRoots, LeafIndex, MerkleError, MerklePath, MerkleTree, NodeIndex, + SMT_MAX_DEPTH, SimpleSmt, Smt, smt::SparseMerkleTree, sparse_path::path_depth_iter, }, }; @@ -543,23 +483,6 @@ mod tests { Smt::with_entries(entries).unwrap() } - #[test] - fn test_roundtrip() { - let tree = make_smt(8192); - - for (key, _value) in tree.entries() { - let (control_path, _) = tree.open(key).into_parts(); - assert_eq!(control_path.len(), tree.depth() as usize); - - let sparse_path = SparseMerklePath::try_from(control_path.clone()).unwrap(); - assert_eq!(control_path.depth(), sparse_path.depth()); - assert_eq!(sparse_path.depth(), SMT_DEPTH); - let test_path = MerklePath::from_iter(sparse_path.clone().into_iter()); - - assert_eq!(control_path, test_path); - } - } - /// Manually test the exact bit patterns for a sample path of 8 nodes, including both empty and /// non-empty nodes. /// @@ -687,134 +610,486 @@ mod tests { for (key, _value) in tree.entries() { let index = NodeIndex::from(Smt::key_to_leaf_index(key)); - - let control_path = tree.get_path(key); - for (&control_node, proof_index) in - itertools::zip_eq(&*control_path, index.proof_indices()) - { - let proof_node = tree.get_node_hash(proof_index); - assert_eq!(control_node, proof_node); - } - - let sparse_path = - SparseMerklePath::from_sized_iter(control_path.clone().into_iter()).unwrap(); + let sparse_path = tree.get_path(key); for (sparse_node, proof_idx) in itertools::zip_eq(sparse_path.clone(), index.proof_indices()) { let proof_node = tree.get_node_hash(proof_idx); assert_eq!(sparse_node, proof_node); } - - assert_eq!(control_path.depth(), sparse_path.depth()); - for (control, sparse) in itertools::zip_eq(control_path, sparse_path) { - assert_eq!(control, sparse); - } } } #[test] - fn test_random_access() { - let tree = make_smt(8192); + fn test_zero_sized() { + let nodes: Vec = Default::default(); - for (i, (key, _value)) in tree.entries().enumerate() { - let control_path = tree.get_path(key); - let sparse_path = SparseMerklePath::try_from(control_path.clone()).unwrap(); - assert_eq!(control_path.depth(), sparse_path.depth()); - assert_eq!(sparse_path.depth(), SMT_DEPTH); - - // Test random access by depth. - for depth in path_depth_iter(control_path.depth()) { - let control_node = control_path.at_depth(depth).unwrap(); - let sparse_node = sparse_path.at_depth(depth).unwrap(); - assert_eq!(control_node, sparse_node, "at depth {depth} for entry {i}"); + // Sparse paths that don't actually contain any nodes should still be well behaved. + let sparse_path = SparseMerklePath::from_sized_iter(nodes).unwrap(); + assert_eq!(sparse_path.depth(), 0); + assert_matches!( + sparse_path.at_depth(NonZero::new(1).unwrap()), + Err(MerkleError::DepthTooBig(1)) + ); + assert_eq!(sparse_path.iter().next(), None); + assert_eq!(sparse_path.into_iter().next(), None); + } + + use proptest::prelude::*; + + // Arbitrary instance for Word + impl Arbitrary for Word { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop::collection::vec(any::(), 4) + .prop_map(|vals| { + Word::new([ + Felt::new(vals[0]), + Felt::new(vals[1]), + Felt::new(vals[2]), + Felt::new(vals[3]), + ]) + }) + .no_shrink() + .boxed() + } + } + + // Arbitrary instance for MerklePath + impl Arbitrary for MerklePath { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop::collection::vec(any::(), 0..=SMT_MAX_DEPTH as usize) + .prop_map(MerklePath::new) + .boxed() + } + } + + // Arbitrary instance for SparseMerklePath + impl Arbitrary for SparseMerklePath { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (0..=SMT_MAX_DEPTH as usize) + .prop_flat_map(|depth| { + // Generate a bitmask for empty nodes - avoid overflow + let max_mask = if depth > 0 && depth < 64 { + (1u64 << depth) - 1 + } else if depth == 64 { + u64::MAX + } else { + 0 + }; + let empty_nodes_mask = + prop::num::u64::ANY.prop_map(move |mask| mask & max_mask); + + // Generate non-empty nodes based on the mask + empty_nodes_mask.prop_flat_map(move |mask| { + let empty_count = mask.count_ones() as usize; + let non_empty_count = depth.saturating_sub(empty_count); + + prop::collection::vec(any::(), non_empty_count).prop_map( + move |nodes| SparseMerklePath::from_parts(mask, nodes).unwrap(), + ) + }) + }) + .boxed() + } + } + + proptest! { + #[test] + fn sparse_merkle_path_roundtrip_equivalence(path in any::()) { + // Convert MerklePath to SparseMerklePath and back + let sparse_result = SparseMerklePath::try_from(path.clone()); + if path.depth() <= SMT_MAX_DEPTH { + let sparse = sparse_result.unwrap(); + let reconstructed = MerklePath::from(sparse); + prop_assert_eq!(path, reconstructed); + } else { + prop_assert!(sparse_result.is_err()); } } } + proptest! { + + #[test] + fn merkle_path_roundtrip_equivalence(sparse in any::()) { + // Convert SparseMerklePath to MerklePath and back + let merkle = MerklePath::from(sparse.clone()); + let reconstructed = SparseMerklePath::try_from(merkle.clone()).unwrap(); + prop_assert_eq!(sparse, reconstructed); + } + } + proptest! { - #[test] - fn test_borrowing_iterator() { - let tree = make_smt(8192); + #[test] + fn path_equivalence_tests(path in any::(), path2 in any::()) { + if path.depth() > SMT_MAX_DEPTH { + return Ok(()); + } - for (key, _value) in tree.entries() { - let control_path = tree.get_path(key); - let sparse_path = SparseMerklePath::try_from(control_path.clone()).unwrap(); - assert_eq!(control_path.depth(), sparse_path.depth()); - assert_eq!(sparse_path.depth(), SMT_DEPTH); - - // Test that both iterators yield the same amount of the same values. - let mut count: u64 = 0; - for (&control_node, sparse_node) in - itertools::zip_eq(control_path.iter(), sparse_path.iter()) - { - count += 1; - assert_eq!(control_node, sparse_node); + let sparse = SparseMerklePath::try_from(path.clone()).unwrap(); + + // Depth consistency + prop_assert_eq!(path.depth(), sparse.depth()); + + // Node access consistency including path_depth_iter + if path.depth() > 0 { + for depth in path_depth_iter(path.depth()) { + let merkle_node = path.at_depth(depth); + let sparse_node = sparse.at_depth(depth); + + match (merkle_node, sparse_node) { + (Some(m), Ok(s)) => prop_assert_eq!(m, s), + (None, Err(_)) => {}, + _ => prop_assert!(false, "Inconsistent node access at depth {}", depth.get()), + } + } + } + + // Iterator consistency + if path.depth() > 0 { + let merkle_nodes: Vec<_> = path.iter().collect(); + let sparse_nodes: Vec<_> = sparse.iter().collect(); + + prop_assert_eq!(merkle_nodes.len(), sparse_nodes.len()); + for (m, s) in merkle_nodes.iter().zip(sparse_nodes.iter()) { + prop_assert_eq!(*m, s); + } + } + + // Test equality between different representations + if path2.depth() <= SMT_MAX_DEPTH { + let sparse2 = SparseMerklePath::try_from(path2.clone()).unwrap(); + prop_assert_eq!(path == path2, sparse == sparse2); + prop_assert_eq!(path == sparse2, sparse == path2); } - assert_eq!(count, control_path.depth() as u64); } } + // rather heavy tests + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + #[test] + fn compute_root_consistency( + tree_data in any::(), + node in any::() + ) { + let RandomMerkleTree { tree, leaves: _, indices } = tree_data; + + for &leaf_index in indices.iter() { + let path = tree.get_path(NodeIndex::new(tree.depth(), leaf_index).unwrap()).unwrap(); + let sparse = SparseMerklePath::from_sized_iter(path.clone().into_iter()).unwrap(); + + let merkle_root = path.compute_root(leaf_index, node); + let sparse_root = sparse.compute_root(leaf_index, node); + + match (merkle_root, sparse_root) { + (Ok(m), Ok(s)) => prop_assert_eq!(m, s), + (Err(e1), Err(e2)) => { + // Both should have the same error type + prop_assert_eq!(format!("{:?}", e1), format!("{:?}", e2)); + }, + _ => prop_assert!(false, "Inconsistent compute_root results"), + } + } + } - #[test] - fn test_owning_iterator() { - let tree = make_smt(8192); + #[test] + fn verify_consistency( + tree_data in any::(), + node in any::() + ) { + let RandomMerkleTree { tree, leaves, indices } = tree_data; + + for (i, &leaf_index) in indices.iter().enumerate() { + let leaf = leaves[i]; + let path = tree.get_path(NodeIndex::new(tree.depth(), leaf_index).unwrap()).unwrap(); + let sparse = SparseMerklePath::from_sized_iter(path.clone().into_iter()).unwrap(); + + let root = tree.root(); + + let merkle_verify = path.verify(leaf_index, leaf, &root); + let sparse_verify = sparse.verify(leaf_index, leaf, &root); + + match (merkle_verify, sparse_verify) { + (Ok(()), Ok(())) => {}, + (Err(e1), Err(e2)) => { + // Both should have the same error type + prop_assert_eq!(format!("{:?}", e1), format!("{:?}", e2)); + }, + _ => prop_assert!(false, "Inconsistent verify results"), + } + + // Test with wrong node - both should fail + let wrong_verify = path.verify(leaf_index, node, &root); + let wrong_sparse_verify = sparse.verify(leaf_index, node, &root); + + match (wrong_verify, wrong_sparse_verify) { + (Ok(()), Ok(())) => prop_assert!(false, "Verification should have failed with wrong node"), + (Err(_), Err(_)) => {}, + _ => prop_assert!(false, "Inconsistent verification results with wrong node"), + } + } + } - for (key, _value) in tree.entries() { - let control_path = tree.get_path(key); - let path_depth = control_path.depth(); - let sparse_path = SparseMerklePath::try_from(control_path.clone()).unwrap(); - assert_eq!(control_path.depth(), sparse_path.depth()); - assert_eq!(sparse_path.depth(), SMT_DEPTH); - - // Test that both iterators yield the same amount of the same values. - let mut count: u64 = 0; - for (control_node, sparse_node) in itertools::zip_eq(control_path, sparse_path) { - count += 1; - assert_eq!(control_node, sparse_node); + #[test] + fn authenticated_nodes_consistency( + tree_data in any::() + ) { + let RandomMerkleTree { tree, leaves, indices } = tree_data; + + for (i, &leaf_index) in indices.iter().enumerate() { + let leaf = leaves[i]; + let path = tree.get_path(NodeIndex::new(tree.depth(), leaf_index).unwrap()).unwrap(); + let sparse = SparseMerklePath::from_sized_iter(path.clone().into_iter()).unwrap(); + + let merkle_result = path.authenticated_nodes(leaf_index, leaf); + let sparse_result = sparse.authenticated_nodes(leaf_index, leaf); + + match (merkle_result, sparse_result) { + (Ok(m_iter), Ok(s_iter)) => { + let merkle_nodes: Vec<_> = m_iter.collect(); + let sparse_nodes: Vec<_> = s_iter.collect(); + prop_assert_eq!(merkle_nodes.len(), sparse_nodes.len()); + for (m, s) in merkle_nodes.iter().zip(sparse_nodes.iter()) { + prop_assert_eq!(m, s); + } + }, + (Err(e1), Err(e2)) => { + prop_assert_eq!(format!("{:?}", e1), format!("{:?}", e2)); + }, + _ => prop_assert!(false, "Inconsistent authenticated_nodes results"), + } } - assert_eq!(count, path_depth as u64); } } #[test] - fn test_zero_sized() { - let nodes: Vec = Default::default(); + fn test_api_differences() { + // This test documents API differences between MerklePath and SparseMerklePath + + // 1. MerklePath has Deref/DerefMut to Vec - SparseMerklePath does not + let merkle = MerklePath::new(vec![Word::default(); 3]); + let _vec_ref: &Vec = &merkle; // This works due to Deref + let _vec_mut: &mut Vec = &mut merkle.clone(); // This works due to DerefMut + + // 2. SparseMerklePath has from_parts() - MerklePath uses new() or from_iter() + let sparse = SparseMerklePath::from_parts(0b101, vec![Word::default(); 2]).unwrap(); + assert_eq!(sparse.depth(), 4); // depth is 4 because mask has bits set up to depth 4 + + // 3. SparseMerklePath has from_sized_iter() - MerklePath uses from_iter() + let nodes = vec![Word::default(); 3]; + let sparse_from_iter = SparseMerklePath::from_sized_iter(nodes.clone()).unwrap(); + let merkle_from_iter = MerklePath::from_iter(nodes); + assert_eq!(sparse_from_iter.depth(), merkle_from_iter.depth()); + } - // Sparse paths that don't actually contain any nodes should still be well behaved. - let sparse_path = SparseMerklePath::from_sized_iter(nodes).unwrap(); - assert_eq!(sparse_path.depth(), 0); - assert_matches!( - sparse_path.at_depth(NonZero::new(1).unwrap()), - Err(MerkleError::DepthTooBig(1)) - ); - assert_eq!(sparse_path.iter().next(), None); - assert_eq!(sparse_path.into_iter().next(), None); + // Arbitrary instance for MerkleTree with random leaves + #[derive(Debug, Clone)] + struct RandomMerkleTree { + tree: MerkleTree, + leaves: Vec, + indices: Vec, } - #[test] - fn test_root() { - let tree = make_smt(100); + impl Arbitrary for RandomMerkleTree { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + // Generate trees with power-of-2 leaves up to 1024 (2^10) + prop::sample::select(&[2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]) + .prop_flat_map(|num_leaves| { + prop::collection::vec(any::(), num_leaves).prop_map(|leaves| { + let tree = MerkleTree::new(leaves.clone()).unwrap(); + let indices: Vec = (0..leaves.len() as u64).collect(); + RandomMerkleTree { tree, leaves, indices } + }) + }) + .boxed() + } + } - for (key, _value) in tree.entries() { - let leaf = tree.get_leaf(key); - let leaf_node = leaf.hash(); - let index: NodeIndex = Smt::key_to_leaf_index(key).into(); - let control_path = tree.get_path(key); - let sparse_path = SparseMerklePath::try_from(control_path.clone()).unwrap(); - - let authed_nodes: Vec<_> = - sparse_path.authenticated_nodes(index.value(), leaf_node).unwrap().collect(); - let authed_root = authed_nodes.last().unwrap().value; - - let control_root = control_path.compute_root(index.value(), leaf_node).unwrap(); - let sparse_root = sparse_path.compute_root(index.value(), leaf_node).unwrap(); - assert_eq!(control_root, sparse_root); - assert_eq!(authed_root, control_root); - assert_eq!(authed_root, tree.root()); - - let index = index.value(); - let control_auth_nodes = control_path.authenticated_nodes(index, leaf_node).unwrap(); - let sparse_auth_nodes = sparse_path.authenticated_nodes(index, leaf_node).unwrap(); - for (a, b) in control_auth_nodes.zip(sparse_auth_nodes) { - assert_eq!(a, b); + // Arbitrary instance for SimpleSmt with random entries + #[derive(Debug, Clone)] + struct RandomSimpleSmt { + tree: SimpleSmt<10>, // Depth 10 = 1024 leaves + entries: Vec<(u64, Word)>, + } + + impl Arbitrary for RandomSimpleSmt { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (1..=100usize) // 1-100 entries in an 1024-leaf tree + .prop_flat_map(|num_entries| { + prop::collection::vec( + ( + 0..1024u64, // Valid indices for 1024-leaf tree + any::(), + ), + num_entries, + ) + .prop_map(|mut entries| { + // Ensure unique indices to avoid duplicates + let mut seen = alloc::collections::BTreeSet::new(); + entries.retain(|(idx, _)| seen.insert(*idx)); + + let mut tree = SimpleSmt::new().unwrap(); + for (idx, value) in &entries { + let leaf_idx = LeafIndex::new(*idx).unwrap(); + tree.insert(leaf_idx, *value); + } + RandomSimpleSmt { tree, entries } + }) + }) + .boxed() + } + } + + // Arbitrary instance for Smt with random entries + #[derive(Debug, Clone)] + struct RandomSmt { + tree: Smt, + entries: Vec<(Word, Word)>, + } + + impl Arbitrary for RandomSmt { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (1..=100usize) // 1-100 entries in a sparse tree + .prop_flat_map(|num_entries| { + prop::collection::vec((any::(), any::()), num_entries).prop_map( + |indices_n_values| { + let entries: Vec<(Word, Word)> = indices_n_values + .into_iter() + .enumerate() + .map(|(n, (leaf_index, value))| { + // SMT uses the most significant element (index 3) as leaf index + // Ensure we use valid leaf indices for the SMT depth + let valid_leaf_index = leaf_index % (1u64 << 60); // Use large but valid range + let key = Word::new([ + Felt::new(n as u64), // element 0 + Felt::new(n as u64 + 1), // element 1 + Felt::new(n as u64 + 2), // element 2 + Felt::new(valid_leaf_index), // element 3 (leaf index) + ]); + (key, value) + }) + .collect(); + + // Ensure unique keys to avoid duplicates + let mut seen = alloc::collections::BTreeSet::new(); + let unique_entries: Vec<_> = + entries.into_iter().filter(|(key, _)| seen.insert(*key)).collect(); + + let tree = Smt::with_entries(unique_entries.clone()).unwrap(); + RandomSmt { tree, entries: unique_entries } + }, + ) + }) + .boxed() + } + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(20))] + + #[test] + fn simple_smt_path_consistency(tree_data in any::()) { + let RandomSimpleSmt { tree, entries } = tree_data; + + for (leaf_index, value) in &entries { + let merkle_path = tree.get_path(&LeafIndex::new(*leaf_index).unwrap()); + let sparse_path = SparseMerklePath::from_sized_iter(merkle_path.clone().into_iter()).unwrap(); + + // Verify both paths have same depth + prop_assert_eq!(merkle_path.depth(), sparse_path.depth()); + + // Verify both paths produce same root for the same value + let merkle_root = merkle_path.compute_root(*leaf_index, *value).unwrap(); + let sparse_root = sparse_path.compute_root(*leaf_index, *value).unwrap(); + prop_assert_eq!(merkle_root, sparse_root); + + // Verify both paths verify correctly + let tree_root = tree.root(); + prop_assert!(merkle_path.verify(*leaf_index, *value, &tree_root).is_ok()); + prop_assert!(sparse_path.verify(*leaf_index, *value, &tree_root).is_ok()); + + // Test with random additional leaf + let random_leaf = Word::new([Felt::ONE; 4]); + let random_index = *leaf_index ^ 1; // Ensure it's a sibling + + // Both should fail verification with wrong leaf + let merkle_wrong = merkle_path.verify(random_index, random_leaf, &tree_root); + let sparse_wrong = sparse_path.verify(random_index, random_leaf, &tree_root); + prop_assert_eq!(merkle_wrong.is_err(), sparse_wrong.is_err()); + } + } + + #[test] + fn smt_path_consistency(tree_data in any::()) { + let RandomSmt { tree, entries } = tree_data; + + for (key, _value) in &entries { + let (merkle_path, leaf) = tree.open(key).into_parts(); + let sparse_path = SparseMerklePath::from_sized_iter(merkle_path.clone().into_iter()).unwrap(); + + let leaf_index = Smt::key_to_leaf_index(key).value(); + let actual_value = leaf.hash(); // Use the actual leaf hash + + // Verify both paths have same depth + prop_assert_eq!(merkle_path.depth(), sparse_path.depth()); + + // Verify both paths produce same root for the same value + let merkle_root = merkle_path.compute_root(leaf_index, actual_value).unwrap(); + let sparse_root = sparse_path.compute_root(leaf_index, actual_value).unwrap(); + prop_assert_eq!(merkle_root, sparse_root); + + // Verify both paths verify correctly + let tree_root = tree.root(); + prop_assert!(merkle_path.verify(leaf_index, actual_value, &tree_root).is_ok()); + prop_assert!(sparse_path.verify(leaf_index, actual_value, &tree_root).is_ok()); + + // Test authenticated nodes consistency + let merkle_auth = merkle_path.authenticated_nodes(leaf_index, actual_value).unwrap().collect::>(); + let sparse_auth = sparse_path.authenticated_nodes(leaf_index, actual_value).unwrap().collect::>(); + prop_assert_eq!(merkle_auth, sparse_auth); + } + } + + #[test] + fn reverse_conversion_from_sparse(tree_data in any::()) { + let RandomMerkleTree { tree, leaves, indices } = tree_data; + + for (i, &leaf_index) in indices.iter().enumerate() { + let leaf = leaves[i]; + let merkle_path = tree.get_path(NodeIndex::new(tree.depth(), leaf_index).unwrap()).unwrap(); + + // Create SparseMerklePath first, then convert to MerklePath + let sparse_path = SparseMerklePath::from_sized_iter(merkle_path.clone().into_iter()).unwrap(); + let converted_merkle = MerklePath::from(sparse_path.clone()); + + // Verify conversion back and forth works + let back_to_sparse = SparseMerklePath::try_from(converted_merkle.clone()).unwrap(); + prop_assert_eq!(sparse_path, back_to_sparse); + + // Verify all APIs work identically + prop_assert_eq!(merkle_path.depth(), converted_merkle.depth()); + + let merkle_root = merkle_path.compute_root(leaf_index, leaf).unwrap(); + let converted_root = converted_merkle.compute_root(leaf_index, leaf).unwrap(); + prop_assert_eq!(merkle_root, converted_root); } } } diff --git a/miden-crypto/src/merkle/store/mod.rs b/miden-crypto/src/merkle/store/mod.rs index c71c234b23..e52cb82cfe 100644 --- a/miden-crypto/src/merkle/store/mod.rs +++ b/miden-crypto/src/merkle/store/mod.rs @@ -1,13 +1,13 @@ -use alloc::{collections::BTreeMap, vec::Vec}; +use alloc::vec::Vec; use core::borrow::Borrow; use super::{ - EmptySubtreeRoots, InnerNodeInfo, MerkleError, MerklePath, MerkleTree, NodeIndex, - PartialMerkleTree, RootPath, Rpo256, SimpleSmt, Smt, ValuePath, Word, mmr::Mmr, + EmptySubtreeRoots, InnerNodeInfo, MerkleError, MerklePath, MerkleProof, MerkleTree, NodeIndex, + PartialMerkleTree, RootPath, Rpo256, SimpleSmt, Smt, Word, mmr::Mmr, }; -use crate::utils::{ - ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, - collections::{KvMap, RecordingMap}, +use crate::{ + Map, + utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}, }; #[cfg(test)] @@ -16,12 +16,6 @@ mod tests; // MERKLE STORE // ================================================================================================ -/// A default [MerkleStore] which uses a simple [BTreeMap] as the backing storage. -pub type DefaultMerkleStore = MerkleStore>; - -/// A [MerkleStore] with recording capabilities which uses [RecordingMap] as the backing storage. -pub type RecordingMerkleStore = MerkleStore>; - #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct StoreNode { @@ -93,24 +87,24 @@ pub struct StoreNode { /// ``` #[derive(Debug, Clone, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct MerkleStore = BTreeMap> { - nodes: T, +pub struct MerkleStore { + nodes: Map, } -impl> Default for MerkleStore { +impl Default for MerkleStore { fn default() -> Self { Self::new() } } -impl> MerkleStore { +impl MerkleStore { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- /// Creates an empty `MerkleStore` instance. - pub fn new() -> MerkleStore { + pub fn new() -> MerkleStore { // pre-populate the store with the empty hashes - let nodes = empty_hashes().into_iter().collect(); + let nodes = empty_hashes().collect(); MerkleStore { nodes } } @@ -157,7 +151,7 @@ impl> MerkleStore { /// - `RootNotInStore` if the `root` is not present in the store. /// - `NodeNotInStore` if a node needed to traverse from `root` to `index` is not present in the /// store. - pub fn get_path(&self, root: Word, index: NodeIndex) -> Result { + pub fn get_path(&self, root: Word, index: NodeIndex) -> Result { let mut hash = root; let mut path = Vec::with_capacity(index.depth().into()); @@ -183,7 +177,7 @@ impl> MerkleStore { // the path is computed from root to leaf, so it must be reversed path.reverse(); - Ok(ValuePath::new(hash, MerklePath::new(path))) + Ok(MerkleProof::new(hash, MerklePath::new(path))) } // LEAF TRAVERSAL @@ -321,7 +315,7 @@ impl> MerkleStore { /// nodes which are descendants of the specified roots. /// /// The roots for which no descendants exist in this Merkle store are ignored. - pub fn subset(&self, roots: I) -> MerkleStore + pub fn subset(&self, roots: I) -> MerkleStore where I: Iterator, R: Borrow, @@ -436,7 +430,7 @@ impl> MerkleStore { value: Word, ) -> Result { let node = value; - let ValuePath { value, path } = self.get_path(root, index)?; + let MerkleProof { value, path } = self.get_path(root, index)?; // performs the update only if the node value differs from the opening if node != value { @@ -456,17 +450,14 @@ impl> MerkleStore { Ok(parent) } - // DESTRUCTURING + // HELPER METHODS // -------------------------------------------------------------------------------------------- /// Returns the inner storage of this MerkleStore while consuming `self`. - pub fn into_inner(self) -> T { + pub fn into_inner(self) -> Map { self.nodes } - // HELPER METHODS - // -------------------------------------------------------------------------------------------- - /// Recursively clones a tree with the specified root from the specified source into self. /// /// If the source store does not contain a tree with the specified root, this is a noop. @@ -486,56 +477,49 @@ impl> MerkleStore { // CONVERSIONS // ================================================================================================ -impl> From<&MerkleTree> for MerkleStore { +impl From<&MerkleTree> for MerkleStore { fn from(value: &MerkleTree) -> Self { let nodes = combine_nodes_with_empty_hashes(value.inner_nodes()).collect(); Self { nodes } } } -impl, const DEPTH: u8> From<&SimpleSmt> for MerkleStore { +impl From<&SimpleSmt> for MerkleStore { fn from(value: &SimpleSmt) -> Self { let nodes = combine_nodes_with_empty_hashes(value.inner_nodes()).collect(); Self { nodes } } } -impl> From<&Smt> for MerkleStore { +impl From<&Smt> for MerkleStore { fn from(value: &Smt) -> Self { let nodes = combine_nodes_with_empty_hashes(value.inner_nodes()).collect(); Self { nodes } } } -impl> From<&Mmr> for MerkleStore { +impl From<&Mmr> for MerkleStore { fn from(value: &Mmr) -> Self { let nodes = combine_nodes_with_empty_hashes(value.inner_nodes()).collect(); Self { nodes } } } -impl> From<&PartialMerkleTree> for MerkleStore { +impl From<&PartialMerkleTree> for MerkleStore { fn from(value: &PartialMerkleTree) -> Self { let nodes = combine_nodes_with_empty_hashes(value.inner_nodes()).collect(); Self { nodes } } } -impl> From for MerkleStore { - fn from(values: T) -> Self { - let nodes = values.into_iter().chain(empty_hashes()).collect(); - Self { nodes } - } -} - -impl> FromIterator for MerkleStore { +impl FromIterator for MerkleStore { fn from_iter>(iter: I) -> Self { let nodes = combine_nodes_with_empty_hashes(iter).collect(); Self { nodes } } } -impl> FromIterator<(Word, StoreNode)> for MerkleStore { +impl FromIterator<(Word, StoreNode)> for MerkleStore { fn from_iter>(iter: I) -> Self { let nodes = iter.into_iter().chain(empty_hashes()).collect(); Self { nodes } @@ -544,7 +528,7 @@ impl> FromIterator<(Word, StoreNode)> for MerkleStore< // ITERATORS // ================================================================================================ -impl> Extend for MerkleStore { +impl Extend for MerkleStore { fn extend>(&mut self, iter: I) { self.nodes.extend( iter.into_iter() @@ -571,7 +555,7 @@ impl Deserializable for StoreNode { } } -impl> Serializable for MerkleStore { +impl Serializable for MerkleStore { fn write_into(&self, target: &mut W) { target.write_u64(self.nodes.len() as u64); @@ -582,7 +566,7 @@ impl> Serializable for MerkleStore { } } -impl> Deserializable for MerkleStore { +impl Deserializable for MerkleStore { fn read_from(source: &mut R) -> Result { let len = source.read_u64()?; let mut nodes: Vec<(Word, StoreNode)> = Vec::with_capacity(len as usize); @@ -601,7 +585,7 @@ impl> Deserializable for MerkleStore { // ================================================================================================ /// Creates empty hashes for all the subtrees of a tree with a max depth of 255. -fn empty_hashes() -> impl IntoIterator { +fn empty_hashes() -> impl Iterator { let subtrees = EmptySubtreeRoots::empty_hashes(255); subtrees .iter() diff --git a/miden-crypto/src/merkle/store/tests.rs b/miden-crypto/src/merkle/store/tests.rs index 5a35593d08..5b44f17514 100644 --- a/miden-crypto/src/merkle/store/tests.rs +++ b/miden-crypto/src/merkle/store/tests.rs @@ -8,8 +8,8 @@ use { }; use super::{ - DefaultMerkleStore as MerkleStore, EmptySubtreeRoots, MerkleError, MerklePath, NodeIndex, - PartialMerkleTree, RecordingMerkleStore, Rpo256, Word, + EmptySubtreeRoots, MerkleError, MerklePath, MerkleStore, NodeIndex, PartialMerkleTree, Rpo256, + Word, }; use crate::{ Felt, ONE, WORD_SIZE, ZERO, @@ -22,7 +22,6 @@ use crate::{ const KEYS4: [u64; 4] = [0, 1, 2, 3]; const VALUES4: [Word; 4] = [int_to_node(1), int_to_node(2), int_to_node(3), int_to_node(4)]; -const KEYS8: [u64; 8] = [0, 1, 2, 3, 4, 5, 6, 7]; const VALUES8: [Word; 8] = [ int_to_node(1), int_to_node(2), @@ -871,54 +870,3 @@ fn test_serialization() -> Result<(), Box> { assert_eq!(store, decoded); Ok(()) } - -// MERKLE RECORDER -// ================================================================================================ -#[test] -fn test_recorder() { - // instantiate recorder from MerkleTree and SimpleSmt - let mtree = MerkleTree::new(VALUES4).unwrap(); - - const TREE_DEPTH: u8 = 64; - let smtree = - SimpleSmt::::with_leaves(KEYS8.into_iter().zip(VALUES8.into_iter().rev())) - .unwrap(); - - let mut recorder: RecordingMerkleStore = - mtree.inner_nodes().chain(smtree.inner_nodes()).collect(); - - // get nodes from both trees and make sure they are correct - let index_0 = NodeIndex::new(mtree.depth(), 0).unwrap(); - let node = recorder.get_node(mtree.root(), index_0).unwrap(); - assert_eq!(node, mtree.get_node(index_0).unwrap()); - - let index_1 = NodeIndex::new(TREE_DEPTH, 1).unwrap(); - let node = recorder.get_node(smtree.root(), index_1).unwrap(); - assert_eq!(node, smtree.get_node(index_1).unwrap()); - - // insert a value and assert that when we request it next time it is accurate - let new_value = [ZERO, ZERO, ONE, ONE].into(); - let index_2 = NodeIndex::new(TREE_DEPTH, 2).unwrap(); - let root = recorder.set_node(smtree.root(), index_2, new_value).unwrap().root; - assert_eq!(recorder.get_node(root, index_2).unwrap(), new_value); - - // construct the proof - let rec_map = recorder.into_inner(); - let (_, proof) = rec_map.finalize(); - let merkle_store: MerkleStore = proof.into(); - - // make sure the proof contains all nodes from both trees - let node = merkle_store.get_node(mtree.root(), index_0).unwrap(); - assert_eq!(node, mtree.get_node(index_0).unwrap()); - - let node = merkle_store.get_node(smtree.root(), index_1).unwrap(); - assert_eq!(node, smtree.get_node(index_1).unwrap()); - - let node = merkle_store.get_node(smtree.root(), index_2).unwrap(); - assert_eq!(node, smtree.get_leaf(&LeafIndex::::try_from(index_2).unwrap())); - - // assert that is doesnt contain nodes that were not recorded - let not_recorded_index = NodeIndex::new(TREE_DEPTH, 4).unwrap(); - assert!(merkle_store.get_node(smtree.root(), not_recorded_index).is_err()); - assert!(smtree.get_node(not_recorded_index).is_ok()); -} diff --git a/miden-crypto/src/utils/kv_map.rs b/miden-crypto/src/utils/kv_map.rs deleted file mode 100644 index 98acb1ca9b..0000000000 --- a/miden-crypto/src/utils/kv_map.rs +++ /dev/null @@ -1,402 +0,0 @@ -use alloc::{ - boxed::Box, - collections::{BTreeMap, BTreeSet}, -}; -use core::cell::RefCell; - -// KEY-VALUE MAP TRAIT -// ================================================================================================ - -/// A trait that defines the interface for a key-value map. -pub trait KvMap: - Extend<(K, V)> + FromIterator<(K, V)> + IntoIterator -{ - fn get(&self, key: &K) -> Option<&V>; - fn contains_key(&self, key: &K) -> bool; - fn len(&self) -> usize; - fn is_empty(&self) -> bool { - self.len() == 0 - } - fn insert(&mut self, key: K, value: V) -> Option; - fn remove(&mut self, key: &K) -> Option; - - fn iter(&self) -> Box + '_>; -} - -// BTREE MAP `KvMap` IMPLEMENTATION -// ================================================================================================ - -impl KvMap for BTreeMap { - fn get(&self, key: &K) -> Option<&V> { - self.get(key) - } - - fn contains_key(&self, key: &K) -> bool { - self.contains_key(key) - } - - fn len(&self) -> usize { - self.len() - } - - fn insert(&mut self, key: K, value: V) -> Option { - self.insert(key, value) - } - - fn remove(&mut self, key: &K) -> Option { - self.remove(key) - } - - fn iter(&self) -> Box + '_> { - Box::new(self.iter()) - } -} - -// RECORDING MAP -// ================================================================================================ - -/// A [RecordingMap] that records read requests to the underlying key-value map. -/// -/// The data recorder is used to generate a proof for read requests. -/// -/// The [RecordingMap] is composed of three parts: -/// - `data`: which contains the current set of key-value pairs in the map. -/// - `updates`: which tracks keys for which values have been changed since the map was -/// instantiated. updates include both insertions, removals and updates of values under existing -/// keys. -/// - `trace`: which contains the key-value pairs from the original data which have been accesses -/// since the map was instantiated. -#[derive(Debug, Default, Clone, Eq, PartialEq)] -pub struct RecordingMap { - data: BTreeMap, - updates: BTreeSet, - trace: RefCell>, -} - -impl RecordingMap { - // CONSTRUCTOR - // -------------------------------------------------------------------------------------------- - /// Returns a new [RecordingMap] instance initialized with the provided key-value pairs. - /// ([BTreeMap]). - pub fn new(init: impl IntoIterator) -> Self { - RecordingMap { - data: init.into_iter().collect(), - updates: BTreeSet::new(), - trace: RefCell::new(BTreeMap::new()), - } - } - - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - pub fn inner(&self) -> &BTreeMap { - &self.data - } - - // FINALIZER - // -------------------------------------------------------------------------------------------- - - /// Consumes the [RecordingMap] and returns a ([BTreeMap], [BTreeMap]) tuple. The first - /// element of the tuple is a map that represents the state of the map at the time `.finalize()` - /// is called. The second element contains the key-value pairs from the initial data set that - /// were read during recording. - pub fn finalize(self) -> (BTreeMap, BTreeMap) { - (self.data, self.trace.take()) - } - - // TEST HELPERS - // -------------------------------------------------------------------------------------------- - - #[cfg(test)] - pub fn trace_len(&self) -> usize { - self.trace.borrow().len() - } - - #[cfg(test)] - pub fn updates_len(&self) -> usize { - self.updates.len() - } -} - -impl KvMap for RecordingMap { - // PUBLIC ACCESSORS - // -------------------------------------------------------------------------------------------- - - /// Returns a reference to the value associated with the given key if the value exists. - /// - /// If the key is part of the initial data set, the key access is recorded. - fn get(&self, key: &K) -> Option<&V> { - self.data.get(key).inspect(|&value| { - if !self.updates.contains(key) { - self.trace.borrow_mut().insert(key.clone(), value.clone()); - } - }) - } - - /// Returns a boolean to indicate whether the given key exists in the data set. - /// - /// If the key is part of the initial data set, the key access is recorded. - fn contains_key(&self, key: &K) -> bool { - self.get(key).is_some() - } - - /// Returns the number of key-value pairs in the data set. - fn len(&self) -> usize { - self.data.len() - } - - // MUTATORS - // -------------------------------------------------------------------------------------------- - - /// Inserts a key-value pair into the data set. - /// - /// If the key already exists in the data set, the value is updated and the old value is - /// returned. - fn insert(&mut self, key: K, value: V) -> Option { - let new_update = self.updates.insert(key.clone()); - self.data.insert(key.clone(), value).inspect(|old_value| { - if new_update { - self.trace.borrow_mut().insert(key, old_value.clone()); - } - }) - } - - /// Removes a key-value pair from the data set. - /// - /// If the key exists in the data set, the old value is returned. - fn remove(&mut self, key: &K) -> Option { - self.data.remove(key).inspect(|old_value| { - let new_update = self.updates.insert(key.clone()); - if new_update { - self.trace.borrow_mut().insert(key.clone(), old_value.clone()); - } - }) - } - - // ITERATION - // -------------------------------------------------------------------------------------------- - - /// Returns an iterator over the key-value pairs in the data set. - fn iter(&self) -> Box + '_> { - Box::new(self.data.iter()) - } -} - -impl Extend<(K, V)> for RecordingMap { - fn extend>(&mut self, iter: T) { - iter.into_iter().for_each(move |(k, v)| { - self.insert(k, v); - }); - } -} - -impl FromIterator<(K, V)> for RecordingMap { - fn from_iter>(iter: T) -> Self { - Self::new(iter) - } -} - -impl IntoIterator for RecordingMap { - type Item = (K, V); - type IntoIter = alloc::collections::btree_map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.data.into_iter() - } -} - -// TESTS -// ================================================================================================ - -#[cfg(test)] -mod tests { - use super::*; - - const ITEMS: [(u64, u64); 5] = [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)]; - - #[test] - fn test_get_item() { - // instantiate a recording map - let map = RecordingMap::new(ITEMS.to_vec()); - - // get a few items - let get_items = [0, 1, 2]; - for key in get_items.iter() { - map.get(key); - } - - // convert the map into a proof - let (_, proof) = map.finalize(); - - // check that the proof contains the expected values - for (key, value) in ITEMS.iter() { - match get_items.contains(key) { - true => assert_eq!(proof.get(key), Some(value)), - false => assert_eq!(proof.get(key), None), - } - } - } - - #[test] - fn test_contains_key() { - // instantiate a recording map - let map = RecordingMap::new(ITEMS.to_vec()); - - // check if the map contains a few items - let get_items = [0, 1, 2]; - for key in get_items.iter() { - map.contains_key(key); - } - - // convert the map into a proof - let (_, proof) = map.finalize(); - - // check that the proof contains the expected values - for (key, _) in ITEMS.iter() { - match get_items.contains(key) { - true => assert!(proof.contains_key(key)), - false => assert!(!proof.contains_key(key)), - } - } - } - - #[test] - fn test_len() { - // instantiate a recording map - let mut map = RecordingMap::new(ITEMS.to_vec()); - // length of the map should be equal to the number of items - assert_eq!(map.len(), ITEMS.len()); - - // inserting entry with key that already exists should not change the length, but it does - // add entries to the trace and update sets - map.insert(4, 5); - assert_eq!(map.len(), ITEMS.len()); - assert_eq!(map.trace_len(), 1); - assert_eq!(map.updates_len(), 1); - - // inserting entry with new key should increase the length; it should also record the key - // as an updated key, but the trace length does not change since old values were not touched - map.insert(5, 5); - assert_eq!(map.len(), ITEMS.len() + 1); - assert_eq!(map.trace_len(), 1); - assert_eq!(map.updates_len(), 2); - - // get some items so that they are saved in the trace; this should record original items - // in the trace, but should not affect the set of updates - let get_items = [0, 1, 2]; - for key in get_items.iter() { - map.contains_key(key); - } - assert_eq!(map.trace_len(), 4); - assert_eq!(map.updates_len(), 2); - - // read the same items again, this should not have any effect on either length, trace, or - // the set of updates - let get_items = [0, 1, 2]; - for key in get_items.iter() { - map.contains_key(key); - } - assert_eq!(map.trace_len(), 4); - assert_eq!(map.updates_len(), 2); - - // read a newly inserted item; this should not affect either length, trace, or the set of - // updates - let _val = map.get(&5).unwrap(); - assert_eq!(map.trace_len(), 4); - assert_eq!(map.updates_len(), 2); - - // update a newly inserted item; this should not affect either length, trace, or the set - // of updates - map.insert(5, 11); - assert_eq!(map.trace_len(), 4); - assert_eq!(map.updates_len(), 2); - - // Note: The length reported by the proof will be different to the length originally - // reported by the map. - let (_, proof) = map.finalize(); - - // length of the proof should be equal to get_items + 1. The extra item is the original - // value at key = 4u64 - assert_eq!(proof.len(), get_items.len() + 1); - } - - #[test] - fn test_iter() { - let mut map = RecordingMap::new(ITEMS.to_vec()); - assert!(map.iter().all(|(x, y)| ITEMS.contains(&(*x, *y)))); - - // when inserting entry with key that already exists the iterator should return the new - // value - let new_value = 5; - map.insert(4, new_value); - assert_eq!(map.iter().count(), ITEMS.len()); - assert!(map.iter().all(|(x, y)| if x == &4 { - y == &new_value - } else { - ITEMS.contains(&(*x, *y)) - })); - } - - #[test] - fn test_is_empty() { - // instantiate an empty recording map - let empty_map: RecordingMap = RecordingMap::default(); - assert!(empty_map.is_empty()); - - // instantiate a non-empty recording map - let map = RecordingMap::new(ITEMS.to_vec()); - assert!(!map.is_empty()); - } - - #[test] - fn test_remove() { - let mut map = RecordingMap::new(ITEMS.to_vec()); - - // remove an item that exists - let key = 0; - let value = map.remove(&key).unwrap(); - assert_eq!(value, ITEMS[0].1); - assert_eq!(map.len(), ITEMS.len() - 1); - assert_eq!(map.trace_len(), 1); - assert_eq!(map.updates_len(), 1); - - // add the item back and then remove it again - let key = 0; - let value = 0; - map.insert(key, value); - let value = map.remove(&key).unwrap(); - assert_eq!(value, 0); - assert_eq!(map.len(), ITEMS.len() - 1); - assert_eq!(map.trace_len(), 1); - assert_eq!(map.updates_len(), 1); - - // remove an item that does not exist - let key = 100; - let value = map.remove(&key); - assert_eq!(value, None); - assert_eq!(map.len(), ITEMS.len() - 1); - assert_eq!(map.trace_len(), 1); - assert_eq!(map.updates_len(), 1); - - // insert a new item and then remove it - let key = 100; - let value = 100; - map.insert(key, value); - let value = map.remove(&key).unwrap(); - assert_eq!(value, 100); - assert_eq!(map.len(), ITEMS.len() - 1); - assert_eq!(map.trace_len(), 1); - assert_eq!(map.updates_len(), 2); - - // convert the map into a proof - let (_, proof) = map.finalize(); - - // check that the proof contains the expected values - for (key, value) in ITEMS.iter() { - match key { - 0 => assert_eq!(proof.get(key), Some(value)), - _ => assert_eq!(proof.get(key), None), - } - } - } -} diff --git a/miden-crypto/src/utils/mod.rs b/miden-crypto/src/utils/mod.rs index 958def3bf7..dd833e3395 100644 --- a/miden-crypto/src/utils/mod.rs +++ b/miden-crypto/src/utils/mod.rs @@ -4,12 +4,8 @@ use alloc::string::String; use core::fmt::{self, Write}; use thiserror::Error; - -mod kv_map; - -// RE-EXPORTS -// ================================================================================================ - +#[cfg(feature = "std")] +pub use winter_utils::ReadAdapter; pub use winter_utils::{ ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, SliceReader, uninit_vector, @@ -17,10 +13,6 @@ pub use winter_utils::{ use crate::Word; -pub mod collections { - pub use super::kv_map::*; -} - // UTILITY FUNCTIONS // ================================================================================================ diff --git a/miden-crypto/src/word/macros.rs b/miden-crypto/src/word/macros.rs deleted file mode 100644 index 0e0eb84167..0000000000 --- a/miden-crypto/src/word/macros.rs +++ /dev/null @@ -1,81 +0,0 @@ -use super::{Felt, StarkField}; - -// MACROS -// ================================================================================================ - -/// Construct a new [Word](super::Word) from a hex value. -/// -/// Expects a '0x' prefixed hex string followed by up to 64 hex digits. -#[macro_export] -macro_rules! word { - ($hex:expr) => {{ - let felts: [$crate::Felt; 4] = match $crate::word::parse_hex_string_as_word($hex) { - Ok(v) => v, - Err(e) => panic!("{}", e), - }; - - $crate::Word::new(felts) - }}; -} - -/// Parses a hex string into a `[Felt; 4]` array. -pub const fn parse_hex_string_as_word(hex: &str) -> Result<[Felt; 4], &'static str> { - const fn parse_hex_digit(digit: u8) -> Result { - match digit { - b'0'..=b'9' => Ok(digit - b'0'), - b'A'..=b'F' => Ok(digit - b'A' + 0x0a), - b'a'..=b'f' => Ok(digit - b'a' + 0x0a), - _ => Err("Invalid hex character"), - } - } - // Enforce and skip the '0x' prefix. - let hex_bytes = match hex.as_bytes() { - [b'0', b'x', rest @ ..] => rest, - _ => return Err("Hex string must have a \"0x\" prefix"), - }; - - if hex_bytes.len() > 64 { - return Err("Hex string has more than 64 characters"); - } - - let mut felts = [0u64; 4]; - let mut i = 0; - while i < hex_bytes.len() { - let hex_digit = match parse_hex_digit(hex_bytes[i]) { - // SAFETY: u8 cast to u64 is safe. We cannot use u64::from in const context so we - // are forced to cast. - Ok(v) => v as u64, - Err(e) => return Err(e), - }; - - // This digit's nibble offset within the felt. We need to invert the nibbles per - // byte for endianness reasons i.e. ABCD -> BADC. - let inibble = if i.is_multiple_of(2) { - (i + 1) % 16 - } else { - (i - 1) % 16 - }; - - let value = hex_digit << (inibble * 4); - felts[i / 2 / 8] += value; - - i += 1; - } - - // Ensure each felt is within bounds as `Felt::new` silently wraps around. - // This matches the behavior of `Word::try_from(String)`. - let mut idx = 0; - while idx < felts.len() { - if felts[idx] >= Felt::MODULUS { - return Err("Felt overflow"); - } - idx += 1; - } - - Ok([ - Felt::new(felts[0]), - Felt::new(felts[1]), - Felt::new(felts[2]), - Felt::new(felts[3]), - ]) -} diff --git a/miden-crypto/src/word/mod.rs b/miden-crypto/src/word/mod.rs index 9c85fe45c1..e41f24eec8 100644 --- a/miden-crypto/src/word/mod.rs +++ b/miden-crypto/src/word/mod.rs @@ -25,9 +25,6 @@ use crate::{ }, }; -mod macros; -pub use macros::parse_hex_string_as_word; - mod lexicographic; pub use lexicographic::LexicographicWord; @@ -47,11 +44,87 @@ impl Word { /// The serialized size of the word in bytes. pub const SERIALIZED_SIZE: usize = WORD_SIZE_BYTES; - /// Creates a new [Word] from the given field elements. + /// Creates a new [`Word`] from the given field elements. pub const fn new(value: [Felt; WORD_SIZE_FELT]) -> Self { Self(value) } + /// Parses a hex string into a new [`Word`]. + /// + /// The input must contain valid hex prefixed with `0x`. The input after the prefix + /// must contain between 0 and 64 characters (inclusive). + /// + /// The input is interpreted to have little-endian byte ordering. Nibbles are interpreted + /// to have big-endian ordering so that "0x10" represents Felt::new(16), not Felt::new(1). + /// + /// This function is usually used via the `word!` macro. + /// + /// ``` + /// use miden_crypto::{Felt, Word, word}; + /// let word = word!("0x1000000000000000200000000000000030000000000000004000000000000000"); + /// assert_eq!(word, Word::new([Felt::new(16), Felt::new(32), Felt::new(48), Felt::new(64)])); + /// ``` + pub const fn parse(hex: &str) -> Result { + const fn parse_hex_digit(digit: u8) -> Result { + match digit { + b'0'..=b'9' => Ok(digit - b'0'), + b'A'..=b'F' => Ok(digit - b'A' + 0x0a), + b'a'..=b'f' => Ok(digit - b'a' + 0x0a), + _ => Err("Invalid hex character"), + } + } + // Enforce and skip the '0x' prefix. + let hex_bytes = match hex.as_bytes() { + [b'0', b'x', rest @ ..] => rest, + _ => return Err("Hex string must have a \"0x\" prefix"), + }; + + if hex_bytes.len() > 64 { + return Err("Hex string has more than 64 characters"); + } + + let mut felts = [0u64; 4]; + let mut i = 0; + while i < hex_bytes.len() { + let hex_digit = match parse_hex_digit(hex_bytes[i]) { + // SAFETY: u8 cast to u64 is safe. We cannot use u64::from in const context so we + // are forced to cast. + Ok(v) => v as u64, + Err(e) => return Err(e), + }; + + // This digit's nibble offset within the felt. We need to invert the nibbles per + // byte to ensure little-endian ordering i.e. ABCD -> BADC. + let inibble = if i.is_multiple_of(2) { + (i + 1) % 16 + } else { + (i - 1) % 16 + }; + + let value = hex_digit << (inibble * 4); + felts[i / 2 / 8] += value; + + i += 1; + } + + // Ensure each felt is within bounds as `Felt::new` silently wraps around. + // This matches the behavior of `Word::try_from(String)`. + let mut idx = 0; + while idx < felts.len() { + if felts[idx] >= Felt::MODULUS { + return Err("Felt overflow"); + } + idx += 1; + } + + Ok(Self::new([ + Felt::new(felts[0]), + Felt::new(felts[1]), + Felt::new(felts[2]), + Felt::new(felts[3]), + ])) + } + /// Returns a new [Word] consisting of four ZERO elements. pub const fn empty() -> Self { Self([Felt::ZERO; WORD_SIZE_FELT]) @@ -579,3 +652,21 @@ impl IntoIterator for Word { self.0.into_iter() } } + +// MACROS +// ================================================================================================ + +/// Construct a new [Word](super::Word) from a hex value. +/// +/// Expects a '0x' prefixed hex string followed by up to 64 hex digits. +#[macro_export] +macro_rules! word { + ($hex:expr) => {{ + let word: Word = match $crate::word::Word::parse($hex) { + Ok(v) => v, + Err(e) => panic!("{}", e), + }; + + word + }}; +} diff --git a/miden-crypto/src/word/tests.rs b/miden-crypto/src/word/tests.rs index 56378cc020..042c91c728 100644 --- a/miden-crypto/src/word/tests.rs +++ b/miden-crypto/src/word/tests.rs @@ -192,3 +192,13 @@ fn word_macro(#[case] input: &str) { assert_eq!(uut, expected); } + +#[rstest::rstest] +#[case::first_nibble("0x1000000000000000000000000000000000000000000000000000000000000000", crate::Word::new([Felt::new(16), Felt::new(0), Felt::new(0), Felt::new(0)]))] +#[case::second_nibble("0x0100000000000000000000000000000000000000000000000000000000000000", crate::Word::new([Felt::new(1), Felt::new(0), Felt::new(0), Felt::new(0)]))] +#[case::all_first_nibbles("0x1000000000000000100000000000000010000000000000001000000000000000", crate::Word::new([Felt::new(16), Felt::new(16), Felt::new(16), Felt::new(16)]))] +#[case::all_first_nibbles_asc("0x1000000000000000200000000000000030000000000000004000000000000000", crate::Word::new([Felt::new(16), Felt::new(32), Felt::new(48), Felt::new(64)]))] +fn word_macro_endianness(#[case] input: &str, #[case] expected: crate::Word) { + let uut = word!(input); + assert_eq!(uut, expected); +}