From 7410bb4b928ff57a93bdb853939483aa960e5dd0 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 13 Oct 2026 11:00:13 +0300 Subject: [PATCH 1/4] bump `smplx-std` dep to 0.1.0 --- .github/workflows/lint.yml | 2 +- .github/workflows/tests.yml | 2 +- Cargo.lock | 386 +++++++++++++----- Cargo.toml | 2 +- Simplex.toml | 11 +- tests/stdlib/common/core.rs | 22 +- tests/stdlib/common/uint.rs | 5 +- tests/stdlib/common/utxo_helper.rs | 3 +- tests/stdlib/utxo/asserts/asset_id.rs | 10 +- .../utxo/asserts/asset_id_with_amount.rs | 10 +- 10 files changed, 319 insertions(+), 134 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6a67f92..8ce91a2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ concurrency: env: CARGO_TERM_COLOR: always - SIMPLEX_VERSION: 0.0.12 + SIMPLEX_VERSION: 0.1.1 jobs: lint: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c622e6..0ffe655 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ concurrency: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: full - SIMPLEX_VERSION: 0.0.12 + SIMPLEX_VERSION: 0.1.1 jobs: simplex-tests: diff --git a/Cargo.lock b/Cargo.lock index f188c5c..e5edb64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,7 +133,7 @@ version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "365c0acd5b2e8dd0111a46c4faea83fb3cfb6e39a49a7c73a06e090db7b2eff0" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.14.101", ] [[package]] @@ -156,17 +156,36 @@ checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" [[package]] name = "bip39" -version = "2.2.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +checksum = "4559bb9225f8401dcea601cf8dc9ed81ea0dadd9cf7755fdefdd93ef7961e19e" dependencies = [ - "bitcoin_hashes", - "rand", - "rand_core", + "bitcoin_hashes 0.14.101", + "rand 0.8.8", + "rand_core 0.6.4", "serde", "unicode-normalization", ] +[[package]] +name = "bit-set" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269" +dependencies = [ + "borsh", + "serde", +] + [[package]] name = "bitcoin" version = "0.32.102" @@ -176,9 +195,10 @@ dependencies = [ "base58ck", "base64 0.21.7", "bech32", + "bitcoin-consensus-encoding", "bitcoin-io", "bitcoin-units", - "bitcoin_hashes", + "bitcoin_hashes 0.14.101", "hex-conservative 0.2.3", "hex_lit", "secp256k1", @@ -191,11 +211,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9daa31138eb443d5751b207f3f64154e2bb09cd59960562ccc7a7112be38147f" dependencies = [ - "bitcoin-internals", + "bitcoin-internals 0.7.0", "hex-conservative 1.3.0", "serde", ] +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + [[package]] name = "bitcoin-internals" version = "0.7.0" @@ -238,6 +264,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin_hashes" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5304e53726dbe5f93141535e102ed97b5bf4714fbecefdda8f9fb98d7fdaff0e" +dependencies = [ + "bitcoin-consensus-encoding", + "bitcoin-internals 0.6.0", + "hex-conservative 1.3.0", + "serde", +] + [[package]] name = "bitcoincore-rpc" version = "0.19.0" @@ -301,11 +339,35 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "generic-array", + "hybrid-array", +] + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", ] [[package]] @@ -336,6 +398,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "cc" version = "1.9.0" @@ -354,6 +422,23 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] + [[package]] name = "chumsky" version = "0.11.2" @@ -404,12 +489,24 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const_format" version = "0.2.36" @@ -431,11 +528,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" -version = "0.2.17" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -473,23 +576,32 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "generic-array", - "typenum", + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" +dependencies = [ + "cmov", ] [[package]] name = "digest" -version = "0.10.7" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer", + "const-oid", "crypto-common", - "subtle", + "ctutils", ] [[package]] @@ -540,12 +652,16 @@ dependencies = [ [[package]] name = "elements" -version = "0.25.3" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c63a04238f4b7f3564fe9705921452c2900efd00982545e5c634fac9024fe2" +checksum = "34e00f7ddcd826d2f4c9bd72d79a6b6991a0ac4286e7e2b2ff8eecd130549073" dependencies = [ "bech32", "bitcoin", + "bitcoin-consensus-encoding", + "bitcoin-internals 0.6.0", + "bitcoin_hashes 1.2.0", + "hex-conservative 1.3.0", "secp256k1-zkp", "serde", "serde_json", @@ -553,9 +669,9 @@ dependencies = [ [[package]] name = "elements-miniscript" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571fa105690f83c7833df2109eb2e14ca0e62d633d2624ffcb166ff18a3da870" +checksum = "0c18e931c81e6e7e58d63b6fea1c57f5534717f0e6721741c4fce72a6c547e19" dependencies = [ "bitcoin", "elements", @@ -598,11 +714,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand", + "rand 0.8.8", "rustc-hex", "static_assertions", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -645,16 +767,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -677,6 +789,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core 0.10.1", ] [[package]] @@ -685,12 +798,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - [[package]] name = "globset" version = "0.4.20" @@ -764,9 +871,9 @@ checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" [[package]] name = "hmac" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ "digest", ] @@ -780,6 +887,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "ignore" version = "0.4.33" @@ -1093,12 +1209,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.37" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] @@ -1130,6 +1246,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.2", + "chacha20", + "core_detect", + "num-traits", + "rand 0.10.3", + "rand_xorshift", + "regex-syntax 0.8.11", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "psm" version = "0.1.32" @@ -1140,6 +1276,12 @@ dependencies = [ "cc", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.47" @@ -1169,7 +1311,18 @@ checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -1179,7 +1332,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1191,6 +1344,21 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xorshift" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "regex-automata" version = "0.3.9" @@ -1313,6 +1481,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "same-file" version = "1.0.6" @@ -1328,8 +1508,8 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes", - "rand", + "bitcoin_hashes 0.14.101", + "rand 0.8.8", "secp256k1-sys", "serde", ] @@ -1350,7 +1530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403f6ab6ef5189ded7d11cb384a96a9f871001e50b391a8fcbb310c9ebc2099b" dependencies = [ "bitcoin-private", - "rand", + "rand 0.8.8", "secp256k1", "secp256k1-zkp-sys", "serde", @@ -1426,9 +1606,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", "cpufeatures", @@ -1443,36 +1623,36 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simplicity-lang" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13ed081e3046d66c146d7201bcbf3b655ca3436cb83f6efc26d7895bd2b79d06" +checksum = "cff7b10251528d46d9cf60238267911984fedbb0948cccea98dd74fc6bf9f729" dependencies = [ "bitcoin", - "bitcoin_hashes", + "bitcoin_hashes 1.2.0", "byteorder", "elements", "getrandom 0.2.17", "ghost-cell", - "hex-conservative 0.2.3", + "hex-conservative 1.3.0", "miniscript", "simplicity-sys", ] [[package]] name = "simplicity-sys" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d1ec5477c7650b8ef511aa56dccb28f2e8cdb6e87f260ecffdaf0ebfef2d3b" +checksum = "955935733b2735665b4735d9b1361eade6be4c7aea6bca2640345e4c81756f58" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 1.2.0", "cc", ] [[package]] name = "simplicityhl" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae77168fa3367d234b11bb3999cbd6e04d7180db0b9e11f83de259528e3b23d1" +checksum = "9e110f12190a4df92970b50d701f7da979638a60c5bf8294d55e4a55aceebf3e" dependencies = [ "ariadne", "base64 0.21.7", @@ -1496,7 +1676,7 @@ dependencies = [ "num-bigint", "num-traits", "primitive-types", - "rand", + "rand 0.8.8", "secp256k1-zkp", "smplx-std", ] @@ -1509,11 +1689,10 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smplx-build" -version = "0.0.12" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e9561b9da1d788d3e777f8a59b085dd13a6fbace5b109f2b0576069f9f36ff" +checksum = "f886393537eaf341fb389d6cdb5365630d09daf99bcb4b570b228a0cad18dca2" dependencies = [ - "glob", "globwalk", "pathdiff", "prettyplease", @@ -1522,7 +1701,7 @@ dependencies = [ "serde", "serde_json", "simplicityhl", - "syn 2.0.119", + "syn 3.0.9", "thiserror", "toml", "toml_edit", @@ -1530,20 +1709,20 @@ dependencies = [ [[package]] name = "smplx-macros" -version = "0.0.12" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f802ff11501371c4c822f0ecdbf7e9e65180ccbbb43b55f252c9a00cb849d24" +checksum = "c798548362bb93281d5cc553fbd2b8af2eda9417a51243c85efc3ae665f5f23b" dependencies = [ "smplx-build", "smplx-test", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] name = "smplx-regtest" -version = "0.0.12" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881db46f30a472018c26c5f245637a722ccba36a873b845d5e6376e7bd5e1106" +checksum = "cd6bcd96bdbc865762ba32f4f9998be9d8974340fc5c28a7c81557e9c9d3eba8" dependencies = [ "electrsd", "hex", @@ -1557,12 +1736,11 @@ dependencies = [ [[package]] name = "smplx-sdk" -version = "0.0.12" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98b85112288eccd99ba34bd852681d78c3581a52709eab0ce52a073b9d961d8e" +checksum = "160e1785e326d8cb7f8cc89ef1b0be5c49c63a20ee15cd7f179af6988f577163" dependencies = [ "bip39", - "bitcoin_hashes", "bitcoincore-rpc", "dyn-clone", "elements-miniscript", @@ -1577,11 +1755,13 @@ dependencies = [ [[package]] name = "smplx-std" -version = "0.0.12" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05855ac57a1b5f737a4bc244ebf5924453957a2066a0bb0fd8df033498386ba" +checksum = "16413a15bb3c65af6357f2f91ddb1400dc945b303d17db46f0230e77f818762e" dependencies = [ "either", + "rand 0.10.3", + "rand_core 0.10.1", "serde", "simplicityhl", "smplx-macros", @@ -1591,18 +1771,20 @@ dependencies = [ [[package]] name = "smplx-test" -version = "0.0.12" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a66ea20c2701a90faf10f9a18176058dd7a82222797d5ed6da9c83f0f1ca032" +checksum = "05907f61baf71916ed46c7daa76fd528e5605681902686137c8e6261b82a55d3" dependencies = [ "electrsd", "proc-macro2", + "proptest", "quote", + "rand 0.10.3", "serde", "simplicityhl", "smplx-regtest", "smplx-sdk", - "syn 2.0.119", + "syn 3.0.9", "thiserror", "toml", ] @@ -1707,26 +1889,17 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "1.1.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", + "winnow", ] [[package]] @@ -1745,10 +1918,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" dependencies = [ "indexmap", - "toml_datetime 1.1.2+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.4", + "winnow", ] [[package]] @@ -1757,7 +1930,7 @@ version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ - "winnow 1.0.4", + "winnow", ] [[package]] @@ -1784,6 +1957,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.27" @@ -1830,10 +2009,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "version_check" -version = "0.9.5" +name = "wait-timeout" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] [[package]] name = "walkdir" @@ -2023,12 +2205,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - [[package]] name = "winnow" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index 36dee43..67a52ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["simplicity", "liquid", "elements", "smart-contracts"] categories = ["cryptography::cryptocurrencies"] [dependencies] -smplx-std = { version = "0.0.12" } +smplx-std = { version = "0.1.1" } [dev-dependencies] anyhow = { version = "1.0.101" } diff --git a/Simplex.toml b/Simplex.toml index c522f64..80d4c06 100644 --- a/Simplex.toml +++ b/Simplex.toml @@ -6,10 +6,10 @@ # out_dir = "./src/artifacts" # [dependencies] -# some_dep = { git = "", path = "" } +# some_dep = { git = "", path = "", = "", package = "" } # [regtest] -# mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" +# mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" # bitcoins = 10_000_000 # rpc_port = 18443 # esplora_port = 3000 @@ -17,12 +17,17 @@ # rpc_password = "password" # [test] -# mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" +# mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" # bitcoins = 10_000_000 # verbosity = 0 # [test.esplora] # url = "" +# network = "" + + [test.fuzz] + cases = 10 +# seed = 12345 # network = "" # [test.rpc] diff --git a/tests/stdlib/common/core.rs b/tests/stdlib/common/core.rs index 96ac097..53d4736 100644 --- a/tests/stdlib/common/core.rs +++ b/tests/stdlib/common/core.rs @@ -3,10 +3,14 @@ #![allow(dead_code)] use secp256k1_zkp::Secp256k1; -use simplex::program::{Program, WitnessTrait}; + +use simplex::program::Program; use simplex::signer::Signer; use simplex::simplicityhl::elements::bitcoin::PublicKey; -use simplex::simplicityhl::elements::{AssetId, Script, Txid}; +use simplex::simplicityhl::{ + WitnessValues, + elements::{AssetId, Script, Txid}, +}; use simplex::transaction::partial_input::IssuanceInput; use simplex::transaction::{ FinalTransaction, PartialInput, PartialOutput, ProgramInput, RequiredSignature, UTXO, @@ -113,7 +117,7 @@ pub fn construct_final_tx( program_for_test: Option, ) -> anyhow::Result where - W: WitnessTrait + 'static, + W: Into + 'static, { let signer = context.get_default_signer(); let network = context.get_network(); @@ -128,7 +132,7 @@ where ft.add_program_input( PartialInput::new(program_utxo.clone()), - ProgramInput::new(Box::new(program.as_ref().clone()), Box::new(witness)), + ProgramInput::new(Box::new(program.as_ref().clone()), witness.into()), RequiredSignature::None, ); @@ -160,7 +164,7 @@ where ft.add_program_input( PartialInput::new(last_utxo.clone()), - ProgramInput::new(Box::new(program_for_test.clone()), Box::new(MockWitness {})), + ProgramInput::new(Box::new(program_for_test.clone()), MockWitness {}), RequiredSignature::None, ); @@ -280,7 +284,7 @@ pub fn spend( program_for_test: Option, ) -> anyhow::Result where - W: WitnessTrait + 'static, + W: Into + 'static, { let ft = construct_final_tx(context, program, witness, data, txids, program_for_test)?; @@ -315,7 +319,7 @@ pub fn run( expect: Expect, ) -> anyhow::Result<()> where - W: WitnessTrait + 'static, + W: Into + 'static, { fund(context, &program, DEFAULT_SEND_AMOUNT)?; let result = spend(context, &program, witness, None, Vec::new(), None); @@ -333,7 +337,7 @@ pub fn run_with_op_return( data: &[u8], ) -> anyhow::Result<()> where - W: WitnessTrait + 'static, + W: Into + 'static, { fund(context, &program, DEFAULT_SEND_AMOUNT)?; let result = spend(context, &program, witness, Some(data), Vec::new(), None); @@ -351,7 +355,7 @@ pub fn run_with_inputs_outputs( program_for_test: Option, ) -> anyhow::Result<()> where - W: WitnessTrait + 'static, + W: Into + 'static, { let result = spend(context, &program, witness, None, txids, program_for_test); diff --git a/tests/stdlib/common/uint.rs b/tests/stdlib/common/uint.rs index 1169c33..ab997d1 100644 --- a/tests/stdlib/common/uint.rs +++ b/tests/stdlib/common/uint.rs @@ -8,7 +8,8 @@ use std::ops::{Add, Div, Mul, Sub}; use rand::Rng; use rand::distributions::uniform::SampleUniform; -use simplex::program::{Program, WitnessTrait}; +use simplex::program::Program; +use simplex::simplicityhl::WitnessValues; use crate::common::u256_wrapper::U256Wrapper; @@ -47,7 +48,7 @@ pub trait TestUint: + Div { type Program: AsRef; - type Witness: WitnessTrait + 'static; + type Witness: Into + 'static; const ZERO: Self; const ONE: Self; diff --git a/tests/stdlib/common/utxo_helper.rs b/tests/stdlib/common/utxo_helper.rs index 90cad21..3baa243 100644 --- a/tests/stdlib/common/utxo_helper.rs +++ b/tests/stdlib/common/utxo_helper.rs @@ -1,6 +1,5 @@ use primitive_types::U256; use rand::Rng; -use secp256k1_zkp::hashes::Hash; use simplex::program::Program; use simplex::simplicityhl::elements::AssetId; @@ -184,7 +183,7 @@ pub fn create_utxos_with_new_asset( } } - Ok((new_asset.unwrap().into_inner().0, txids)) + Ok((new_asset.unwrap().to_byte_array(), txids)) } fn get_asset_id_amount_from_conf_utxo(conf_utxo: UTXO) -> anyhow::Result { diff --git a/tests/stdlib/utxo/asserts/asset_id.rs b/tests/stdlib/utxo/asserts/asset_id.rs index 6408829..2ed19c4 100644 --- a/tests/stdlib/utxo/asserts/asset_id.rs +++ b/tests/stdlib/utxo/asserts/asset_id.rs @@ -186,7 +186,7 @@ fn assert_false_explicit_asset_id_for_input(context: simplex::TestContext) -> an case(Explicit) .index(index) .index_type(IndexType::Input) - .expect(context.get_network().policy_asset().into_inner().0) + .expect(context.get_network().policy_asset().to_byte_array()) .expecting(&context, Expect::AssertFailed, txids) } @@ -224,7 +224,7 @@ fn assert_false_explicit_asset_id_for_output(context: simplex::TestContext) -> a case(Explicit) .index(index) .index_type(IndexType::Output) - .expect(context.get_network().policy_asset().into_inner().0) + .expect(context.get_network().policy_asset().to_byte_array()) .expecting(&context, Expect::AssertFailed, txids) } @@ -321,7 +321,7 @@ fn assert_false_explicit_input_asset_id(context: simplex::TestContext) -> anyhow case(ExplicitInput) .index(index) - .expect(context.get_network().policy_asset().into_inner().0) + .expect(context.get_network().policy_asset().to_byte_array()) .expecting(&context, Expect::AssertFailed, txids) } @@ -376,7 +376,7 @@ fn assert_false_explicit_output_asset_id(context: simplex::TestContext) -> anyho case(ExplicitOutput) .index(index) - .expect(context.get_network().policy_asset().into_inner().0) + .expect(context.get_network().policy_asset().to_byte_array()) .expecting(&context, Expect::AssertFailed, txids) } @@ -434,7 +434,7 @@ fn assert_false_explicit_current_asset_id(context: simplex::TestContext) -> anyh case(CurrentExplicit) .index(index) - .expect(context.get_network().policy_asset().into_inner().0) + .expect(context.get_network().policy_asset().to_byte_array()) .expecting(&context, Expect::AssertFailed, txids) } diff --git a/tests/stdlib/utxo/asserts/asset_id_with_amount.rs b/tests/stdlib/utxo/asserts/asset_id_with_amount.rs index de17bc4..f9331ee 100644 --- a/tests/stdlib/utxo/asserts/asset_id_with_amount.rs +++ b/tests/stdlib/utxo/asserts/asset_id_with_amount.rs @@ -210,7 +210,7 @@ fn assert_false_explicit_asset_id_with_amount_for_input( .index(index) .index_type(IndexType::Input) .expect( - context.get_network().policy_asset().into_inner().0, + context.get_network().policy_asset().to_byte_array(), real_amount, ) .expecting(&context, Expect::AssertFailed, txids) @@ -281,7 +281,7 @@ fn assert_false_explicit_asset_id_with_amount_for_output( .index(index) .index_type(IndexType::Output) .expect( - context.get_network().policy_asset().into_inner().0, + context.get_network().policy_asset().to_byte_array(), real_amount, ) .expecting(&context, Expect::AssertFailed, txids) @@ -432,7 +432,7 @@ fn assert_false_explicit_input_asset_id_with_amount( case(ExplicitInput) .index(index) .expect( - context.get_network().policy_asset().into_inner().0, + context.get_network().policy_asset().to_byte_array(), real_amount, ) .expecting(&context, Expect::AssertFailed, txids) @@ -533,7 +533,7 @@ fn assert_false_explicit_output_asset_id_with_amount( case(ExplicitOutput) .index(index) .expect( - context.get_network().policy_asset().into_inner().0, + context.get_network().policy_asset().to_byte_array(), real_amount, ) .expecting(&context, Expect::AssertFailed, txids) @@ -631,7 +631,7 @@ fn assert_false_explicit_current_asset_id_with_amount( case(CurrentExplicit) .index(index) .expect( - context.get_network().policy_asset().into_inner().0, + context.get_network().policy_asset().to_byte_array(), real_amount, ) .expecting(&context, Expect::AssertFailed, txids) From 527384fb5ce5fb11a9f9524bbc17971b9ce85cd5 Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 13 Oct 2026 14:20:25 +0300 Subject: [PATCH 2/4] Add fuzz entry to the CI, limit fuzz tests runs --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0ffe655..6f1eb77 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -59,6 +59,10 @@ jobs: run: | cargo build --workspace --all-features --verbose + - name: Fuzz + run: | + simplex test --fuzz --test-threads 4 -v + - name: Test run: | simplex test --test-threads 4 -v From d520162cb5611d7ace9908565475be753604164d Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Tue, 13 Oct 2026 17:24:13 +0300 Subject: [PATCH 3/4] feat(fuzz): add fuzzing tests * add description for u128, u256 tests, because they're much complexer than for ordinary ones * add fuzzing in separate modules to separate logic * introduce explicit Aliases for types in secp256k1::operations --- .gitignore | 1 + Simplex.toml | 2 +- tests/stdlib/asserts.rs | 535 +++++++++++++++- tests/stdlib/common/mod.rs | 1 + tests/stdlib/common/uint_fuzz.rs | 351 +++++++++++ tests/stdlib/op_return.rs | 166 +++++ tests/stdlib/secp256k1/operations.rs | 362 +++++++++-- tests/stdlib/u1/convert.rs | 124 ++++ tests/stdlib/u128/bit.rs | 306 +++++++++ tests/stdlib/u128/comparison.rs | 231 +++++++ tests/stdlib/u128/convert.rs | 256 ++++++++ tests/stdlib/u128/math/api.rs | 35 ++ tests/stdlib/u128/math/primitives.rs | 908 +++++++++++++++++++++++++++ tests/stdlib/u128/mul_div.rs | 196 ++++++ tests/stdlib/u16/convert.rs | 161 +++++ tests/stdlib/u16/math.rs | 32 + tests/stdlib/u16/mul_div.rs | 178 ++++++ tests/stdlib/u256/bit.rs | 242 +++++++ tests/stdlib/u256/comparison.rs | 205 ++++++ tests/stdlib/u256/convert.rs | 326 ++++++++++ tests/stdlib/u256/math/add.rs | 361 +++++++++++ tests/stdlib/u256/math/api.rs | 50 ++ tests/stdlib/u256/math/div.rs | 640 +++++++++++++++++++ tests/stdlib/u256/math/sub_mul.rs | 426 +++++++++++++ tests/stdlib/u256/mul_div.rs | 407 ++++++++++++ tests/stdlib/u32/convert.rs | 185 ++++++ tests/stdlib/u32/math.rs | 32 + tests/stdlib/u32/mul_div.rs | 178 ++++++ tests/stdlib/u64/convert.rs | 205 ++++++ tests/stdlib/u64/math.rs | 32 + tests/stdlib/u64/mul_div.rs | 179 ++++++ tests/stdlib/u8/convert.rs | 143 +++++ tests/stdlib/u8/math.rs | 32 + tests/stdlib/u8/mul_div.rs | 169 +++++ 34 files changed, 7615 insertions(+), 42 deletions(-) create mode 100644 tests/stdlib/common/uint_fuzz.rs diff --git a/.gitignore b/.gitignore index ddafd0a..ab9046a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ target/ # Simplex output src/artifacts +/fuzz-failures # IDE/editors (optional but common minimal) **/.DS_Store diff --git a/Simplex.toml b/Simplex.toml index 80d4c06..72d5f6c 100644 --- a/Simplex.toml +++ b/Simplex.toml @@ -26,7 +26,7 @@ # network = "" [test.fuzz] - cases = 10 + cases = 25 # seed = 12345 # network = "" diff --git a/tests/stdlib/asserts.rs b/tests/stdlib/asserts.rs index b3ca487..835969b 100644 --- a/tests/stdlib/asserts.rs +++ b/tests/stdlib/asserts.rs @@ -11,6 +11,7 @@ use simplicityhl_std::artifacts::tests::asserts::derived_asserts::{ // simf/tests/asserts.simf. use FunctionToTest::*; +#[derive(Clone, Copy)] enum FunctionToTest { AssertEq1, AssertEq8, @@ -63,11 +64,9 @@ pub fn generate_uints_in_one_range(same: bool, min_val: u128, max_val: u128) -> (some_u, other_u) } -/// Builds the witness for one assert call. `same` controls the two `assert_eq` -/// args; `none` makes the single `assert_none` arg `None`. -fn build_witness(function: FunctionToTest, same: bool, none: bool) -> AssertsTestWitness { - let mut witness = AssertsTestWitness { - function_index: 0, +fn default_witness(function_index: u8) -> AssertsTestWitness { + AssertsTestWitness { + function_index, first_arg_u1: DEFAULT_SOME_U8, // u1 in Simplicity is represented as u8 second_arg_u1: DEFAULT_SOME_U8, first_arg_u8: DEFAULT_SOME_U8, @@ -82,7 +81,13 @@ fn build_witness(function: FunctionToTest, same: bool, none: bool) -> AssertsTes second_arg_u128: DEFAULT_SOME_U128, first_arg_u256: DEFAULT_SOME_U256, second_arg_u256: DEFAULT_SOME_U256, - }; + } +} + +/// Builds the witness for one assert call. `same` controls the two `assert_eq` +/// args; `none` makes the single `assert_none` arg `None`. +fn build_witness(function: FunctionToTest, same: bool, none: bool) -> AssertsTestWitness { + let mut witness = default_witness(0); match function { FunctionToTest::AssertEq1 => { @@ -350,3 +355,521 @@ fn assert_none_256_happy_path(context: simplex::TestContext) -> anyhow::Result<( fn assert_none_256_unhappy_path(context: simplex::TestContext) -> anyhow::Result<()> { case(AssertNone256).expecting(&context, Expect::AssertFailed) } + +mod asserts_test_fuzz { + use std::fmt::Debug; + + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + fn arb_u1() -> impl Strategy { + any::().prop_map(u8::from) + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_u32() -> impl Strategy { + any::() + } + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>() + } + + type ArgumentFields = fn(&mut AssertsTestWitness) -> (&mut Option, &mut Option); + + fn equal_values_strategy( + function_index: u8, + values: impl Strategy + 'static, + fields: ArgumentFields, + ) -> BoxedStrategy + where + T: Clone + Debug + Into + 'static, + V: 'static, + { + values + .prop_map(move |value| { + let mut witness = default_witness(function_index); + let first: V = value.clone().into(); + let second: V = value.into(); + + Case::set_arguments(fields(&mut witness), first, second); + witness + }) + .boxed() + } + + fn distinct_values_strategy( + function_index: u8, + values: impl Strategy + 'static, + fields: ArgumentFields, + ) -> BoxedStrategy + where + T: PartialEq + Debug + Into + 'static, + V: 'static, + { + let values = values.boxed(); + + (values.clone(), values) + .prop_filter("assert_eq arguments must be distinct", |(first, second)| { + first != second + }) + .prop_map(move |(first, second)| { + let mut witness = default_witness(function_index); + let first: V = first.into(); + let second: V = second.into(); + + Case::set_arguments(fields(&mut witness), first, second); + witness + }) + .boxed() + } + + fn none_strategy( + none: bool, + function_index: u8, + values: impl Strategy + 'static, + fields: ArgumentFields, + ) -> BoxedStrategy { + values + .prop_map(move |value| { + let mut witness = default_witness(function_index); + + let (first, second) = if none { + (None, Some(value)) + } else { + (Some(value), Some(T::default())) + }; + + Case::set_arguments(fields(&mut witness), first, second); + witness + }) + .boxed() + } + + impl Case { + fn set_arguments( + fields: (&mut Option, &mut Option), + first: impl Into>, + second: impl Into>, + ) { + *fields.0 = first.into(); + *fields.1 = second.into(); + } + + fn u1_arguments(witness: &mut AssertsTestWitness) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u1, &mut witness.second_arg_u1) + } + + fn u8_arguments(witness: &mut AssertsTestWitness) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u8, &mut witness.second_arg_u8) + } + + fn u16_arguments(witness: &mut AssertsTestWitness) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u16, &mut witness.second_arg_u16) + } + + fn u32_arguments(witness: &mut AssertsTestWitness) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u32, &mut witness.second_arg_u32) + } + + fn u64_arguments(witness: &mut AssertsTestWitness) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u64, &mut witness.second_arg_u64) + } + + fn u128_arguments( + witness: &mut AssertsTestWitness, + ) -> (&mut Option, &mut Option) { + (&mut witness.first_arg_u128, &mut witness.second_arg_u128) + } + + fn u256_arguments( + witness: &mut AssertsTestWitness, + ) -> (&mut Option<[u8; 32]>, &mut Option<[u8; 32]>) { + (&mut witness.first_arg_u256, &mut witness.second_arg_u256) + } + + fn strategy(self) -> BoxedStrategy { + match (self.function, self.same) { + ( + AssertNone1 | AssertNone8 | AssertNone16 | AssertNone32 | AssertNone64 + | AssertNone128 | AssertNone256, + _, + ) => self.strategy_with_none_values(), + (_, true) => self.strategy_with_same_values(), + (_, false) => self.strategy_with_distinct_values(), + } + } + + fn strategy_with_none_values(&self) -> BoxedStrategy { + let function_index = self.function as u8; + + match self.function { + AssertNone1 => { + none_strategy(self.none, function_index, arb_u1(), Self::u1_arguments) + } + AssertNone8 => { + none_strategy(self.none, function_index, arb_u8(), Self::u8_arguments) + } + AssertNone16 => { + none_strategy(self.none, function_index, arb_u16(), Self::u16_arguments) + } + AssertNone32 => { + none_strategy(self.none, function_index, arb_u32(), Self::u32_arguments) + } + AssertNone64 => { + none_strategy(self.none, function_index, arb_u64(), Self::u64_arguments) + } + AssertNone128 => { + none_strategy(self.none, function_index, arb_u128(), Self::u128_arguments) + } + AssertNone256 => { + none_strategy(self.none, function_index, arb_u256(), Self::u256_arguments) + } + _ => unreachable!("assert_eq cases aren't handled"), + } + } + + fn strategy_with_same_values(&self) -> BoxedStrategy { + let function_index = self.function as u8; + + match self.function { + AssertEq1 => equal_values_strategy(function_index, arb_u1(), Self::u1_arguments), + AssertEq8 => equal_values_strategy(function_index, arb_u8(), Self::u8_arguments), + AssertEq16 => equal_values_strategy(function_index, arb_u16(), Self::u16_arguments), + AssertEq32 => equal_values_strategy(function_index, arb_u32(), Self::u32_arguments), + AssertEq64 => equal_values_strategy(function_index, arb_u64(), Self::u64_arguments), + AssertEq128 => { + equal_values_strategy(function_index, arb_u128(), Self::u128_arguments) + } + AssertEq256 => { + equal_values_strategy(function_index, arb_u256(), Self::u256_arguments) + } + AssertEqBool => { + equal_values_strategy(function_index, arb_bool(), Self::u1_arguments) + } + _ => unreachable!("assert_none cases aren't handled"), + } + } + + fn strategy_with_distinct_values(&self) -> BoxedStrategy { + let function_index = self.function as u8; + + match self.function { + AssertEq1 => distinct_values_strategy(function_index, arb_u1(), Self::u1_arguments), + AssertEq8 => distinct_values_strategy(function_index, arb_u8(), Self::u8_arguments), + AssertEq16 => { + distinct_values_strategy(function_index, arb_u16(), Self::u16_arguments) + } + AssertEq32 => { + distinct_values_strategy(function_index, arb_u32(), Self::u32_arguments) + } + AssertEq64 => { + distinct_values_strategy(function_index, arb_u64(), Self::u64_arguments) + } + AssertEq128 => { + distinct_values_strategy(function_index, arb_u128(), Self::u128_arguments) + } + AssertEq256 => { + distinct_values_strategy(function_index, arb_u256(), Self::u256_arguments) + } + AssertEqBool => { + distinct_values_strategy(function_index, arb_bool(), Self::u1_arguments) + } + _ => unreachable!("assert_none cases aren't handled"), + } + } + } + + struct CaseFuzz { + case: Case, + context: TestContext, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn equal(mut self) -> Self { + self.case = self.case.equal(); + self + } + + fn none(mut self) -> Self { + self.case = self.case.none(); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let strategy = self + .case + .strategy() + .prop_map(|witness| { + let arguments: Arguments = AssertsTestArguments {}.into(); + let witness: WitnessValues = witness.into(); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + #[simplex::fuzz] + fn assert_eq_1_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq1, context, "assert_eq_1_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_1_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq1, context, "assert_eq_1_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_8_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq8, context, "assert_eq_8_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_8_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq8, context, "assert_eq_8_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_16_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq16, context, "assert_eq_16_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_16_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq16, context, "assert_eq_16_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_32_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq32, context, "assert_eq_32_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_32_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq32, context, "assert_eq_32_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_64_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq64, context, "assert_eq_64_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_64_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq64, context, "assert_eq_64_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_128_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq128, context, "assert_eq_128_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_128_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq128, context, "assert_eq_128_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_256_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq256, context, "assert_eq_256_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_256_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEq256, context, "assert_eq_256_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_eq_bool_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEqBool, context, "assert_eq_bool_happy_path") + .equal() + .run() + } + + #[simplex::fuzz] + fn assert_eq_bool_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertEqBool, context, "assert_eq_bool_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_1_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone1, context, "assert_none_1_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_1_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone1, context, "assert_none_1_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_8_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone8, context, "assert_none_8_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_8_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone8, context, "assert_none_8_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_16_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone16, context, "assert_none_16_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_16_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone16, context, "assert_none_16_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_32_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone32, context, "assert_none_32_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_32_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone32, context, "assert_none_32_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_64_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone64, context, "assert_none_64_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_64_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone64, context, "assert_none_64_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_128_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone128, context, "assert_none_128_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_128_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone128, context, "assert_none_128_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_none_256_happy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone256, context, "assert_none_256_happy_path") + .none() + .run() + } + + #[simplex::fuzz] + fn assert_none_256_unhappy_path(context: TestContext) -> anyhow::Result<()> { + case_fuzz(AssertNone256, context, "assert_none_256_unhappy_path") + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/common/mod.rs b/tests/stdlib/common/mod.rs index 16f0294..862f5b7 100644 --- a/tests/stdlib/common/mod.rs +++ b/tests/stdlib/common/mod.rs @@ -2,4 +2,5 @@ pub mod core; pub mod helper; pub mod u256_wrapper; pub mod uint; +pub mod uint_fuzz; pub mod utxo_helper; diff --git a/tests/stdlib/common/uint_fuzz.rs b/tests/stdlib/common/uint_fuzz.rs new file mode 100644 index 0000000..646f511 --- /dev/null +++ b/tests/stdlib/common/uint_fuzz.rs @@ -0,0 +1,351 @@ +// Each `tests/*.rs` is a separate crate that mounts this module but uses only +// part of it, so per-crate dead-code analysis would warn about the rest. +#![allow(dead_code)] + +use std::fmt::Debug; + +use super::uint::{CommonOp, TestUint}; + +use simplex::simplicityhl::{Arguments, WitnessValues}; + +use simplex::fuzz::FuzzableProgram; +use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; +use simplex::fuzz::proptest::prelude::Just; +use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; +use simplex::program::{ProgramFactory, ProgramSchema}; +use simplex::{FuzzMode, TestContext}; + +/// Fuzz-specific program data and number generators for a common unsigned +/// integer width. +pub trait TestUintFuzz: TestUint + Debug + 'static { + /// Program arguments used for each generated test case. + type Arguments: Debug + Clone + Into + 'static; + + /// Execution outcome for division by zero in this width's target library. + const DIV_BY_ZERO_EXPECT: Expect = Expect::PrunedBranch; + + /// Returns the fixed program arguments for this uint test program. + fn arguments() -> Self::Arguments; + + /// Generates any representable value of this uint type. + fn arb_any() -> BoxedStrategy; + + /// Generates any representable value except zero. + fn arb_non_zero() -> BoxedStrategy; + + /// Generates a value in the inclusive interval `low..=high`. + /// + /// Implementations should preserve both bounds exactly. This lets shared + /// scenarios construct ranges such as `0..=a` and `(a + 1)..=MAX` without + /// changing their input shape. + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy; + + /// Generates an addend in the fitting range used by `uint.rs`. + fn arb_fitting_addend() -> BoxedStrategy { + Self::arb_fitting(Self::ZERO, Self::HALF_MAX) + } + + /// Generates a multiplier in the fitting range used by `uint.rs`. + fn arb_fitting_multiplier() -> BoxedStrategy { + Self::arb_fitting(Self::ZERO, Self::MUL_BOUND - Self::ONE) + } +} + +fn uint_strategy( + operation: CommonOp, + inputs: BoxedStrategy<(T, T, Option)>, +) -> BoxedStrategy<(Arguments, WitnessValues)> { + let function_index = operation as u8; + + inputs + .prop_map(move |(a, b, expected)| { + let arguments: Arguments = T::arguments().into(); + let witness: WitnessValues = T::witness(function_index, a, b, expected).into(); + (arguments, witness) + }) + .boxed() +} + +pub fn checked_add_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedAdd, + (T::arb_fitting_addend(), T::arb_fitting_addend()) + .prop_map(|(a, b)| (a, b, Some(a + b))) + .boxed(), + ) +} + +pub fn checked_add_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedAdd, + (Just(T::MAX), T::arb_non_zero()) + .prop_map(|(a, b)| (a, b, None)) + .boxed(), + ) +} + +pub fn safe_add_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Add, + (T::arb_fitting_addend(), T::arb_fitting_addend()) + .prop_map(|(a, b)| (a, b, Some(a + b))) + .boxed(), + ) +} + +pub fn safe_add_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Add, + (Just(T::MAX), T::arb_non_zero()) + .prop_map(|(a, b)| (a, b, None)) + .boxed(), + ) +} + +pub fn checked_sub_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedSub, + T::arb_fitting(T::ZERO, T::MAX) + .prop_flat_map(|a| T::arb_fitting(T::ZERO, a).prop_map(move |b| (a, b, Some(a - b)))) + .boxed(), + ) +} + +pub fn checked_sub_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedSub, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|a| T::arb_fitting(a + T::ONE, T::MAX).prop_map(move |b| (a, b, None))) + .boxed(), + ) +} + +pub fn safe_sub_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Sub, + T::arb_fitting(T::ZERO, T::MAX) + .prop_flat_map(|a| T::arb_fitting(T::ZERO, a).prop_map(move |b| (a, b, Some(a - b)))) + .boxed(), + ) +} + +pub fn safe_sub_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Sub, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|a| T::arb_fitting(a + T::ONE, T::MAX).prop_map(move |b| (a, b, None))) + .boxed(), + ) +} + +pub fn checked_mul_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedMul, + (T::arb_fitting_multiplier(), T::arb_fitting_multiplier()) + .prop_map(|(a, b)| (a, b, Some(a * b))) + .boxed(), + ) +} + +pub fn checked_mul_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedMul, + (Just(T::MAX), T::arb_fitting(T::ONE + T::ONE, T::MAX)) + .prop_map(|(a, b)| (a, b, None)) + .boxed(), + ) +} + +pub fn safe_mul_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Mul, + (T::arb_fitting_multiplier(), T::arb_fitting_multiplier()) + .prop_map(|(a, b)| (a, b, Some(a * b))) + .boxed(), + ) +} + +pub fn safe_mul_overflow_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Mul, + (Just(T::MAX), T::arb_fitting(T::ONE + T::ONE, T::MAX)) + .prop_map(|(a, b)| (a, b, None)) + .boxed(), + ) +} + +pub fn checked_div_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedDiv, + (T::arb_any(), T::arb_non_zero()) + .prop_map(|(a, b)| (a, b, Some(a / b))) + .boxed(), + ) +} + +pub fn checked_div_by_zero_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> +{ + uint_strategy( + CommonOp::CheckedDiv, + T::arb_any().prop_map(|a| (a, T::ZERO, None)).boxed(), + ) +} + +pub fn safe_div_fitting_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Div, + (T::arb_any(), T::arb_non_zero()) + .prop_map(|(a, b)| (a, b, Some(a / b))) + .boxed(), + ) +} + +pub fn safe_div_by_zero_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Div, + T::arb_any().prop_map(|a| (a, T::ZERO, None)).boxed(), + ) +} + +pub fn gt_greater_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Gt, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|b| { + T::arb_fitting(b + T::ONE, T::MAX).prop_map(move |a| (a, b, Some(T::ZERO))) + }) + .boxed(), + ) +} + +pub fn gt_equal_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Gt, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_map(|a| (a, a, None)) + .boxed(), + ) +} + +pub fn gt_less_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Gt, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|a| T::arb_fitting(a + T::ONE, T::MAX).prop_map(move |b| (a, b, None))) + .boxed(), + ) +} + +pub fn ge_greater_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Ge, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|b| { + T::arb_fitting(b + T::ONE, T::MAX).prop_map(move |a| (a, b, Some(T::ZERO))) + }) + .boxed(), + ) +} + +pub fn ge_equal_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Ge, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_map(|a| (a, a, Some(T::ZERO))) + .boxed(), + ) +} + +pub fn ge_less_strategy() -> BoxedStrategy<(Arguments, WitnessValues)> { + uint_strategy( + CommonOp::Ge, + T::arb_fitting(T::ZERO, T::MAX - T::ONE) + .prop_flat_map(|a| T::arb_fitting(a + T::ONE, T::MAX).prop_map(move |b| (a, b, None))) + .boxed(), + ) +} + +pub fn run_uint_fuzz( + context: TestContext, + strategy: BoxedStrategy<(Arguments, WitnessValues)>, + expect: Expect, +) -> anyhow::Result<()> +where + T::Program: + FuzzableProgram + ProgramFactory + ProgramSchema + Clone + 'static, + T::Witness: Debug + Clone + 'static, +{ + context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new("uint operation", expect)) + .run(); + + Ok(()) +} + +/// Stamps the common `#[simplex::fuzz]` entry points for one unsigned width. +/// +/// Each expansion still emits one independently selectable fuzz test per +/// scenario. The macro only removes the identical runner wiring from the +/// width-specific test modules. +/// +/// # Usage +/// +/// ```ignore +/// uint_fuzz_tests!(u8); +/// ``` +/// +/// The unsigned type must implement [`TestUintFuzz`], including its bounded +/// number-generation strategy. +#[macro_export] +macro_rules! uint_fuzz_tests { + ($t:ty) => { + + $crate::uint_fuzz_tests!(@stub $t; + checked_add_fitting checked_add_fitting_strategy simplex::fuzz::fuzz_check::Expect::Ok; + checked_add_overflow checked_add_overflow_strategy simplex::fuzz::fuzz_check::Expect::Ok; + safe_add_fitting safe_add_fitting_strategy simplex::fuzz::fuzz_check::Expect::Ok; + safe_add_overflow safe_add_overflow_strategy simplex::fuzz::fuzz_check::Expect::PrunedBranch; + checked_sub_fitting checked_sub_fitting_strategy simplex::fuzz::fuzz_check::Expect::Ok; + checked_sub_overflow checked_sub_overflow_strategy simplex::fuzz::fuzz_check::Expect::Ok; + safe_sub_fitting safe_sub_fitting_strategy simplex::fuzz::fuzz_check::Expect::Ok; + safe_sub_overflow safe_sub_overflow_strategy simplex::fuzz::fuzz_check::Expect::PrunedBranch; + checked_mul_fitting checked_mul_fitting_strategy simplex::fuzz::fuzz_check::Expect::Ok; + checked_mul_overflow checked_mul_overflow_strategy simplex::fuzz::fuzz_check::Expect::Ok; + safe_mul_fitting safe_mul_fitting_strategy simplex::fuzz::fuzz_check::Expect::Ok; + safe_mul_overflow safe_mul_overflow_strategy simplex::fuzz::fuzz_check::Expect::PrunedBranch; + checked_div_fitting checked_div_fitting_strategy simplex::fuzz::fuzz_check::Expect::Ok; + checked_div_by_zero checked_div_by_zero_strategy simplex::fuzz::fuzz_check::Expect::Ok; + safe_div_fitting safe_div_fitting_strategy simplex::fuzz::fuzz_check::Expect::Ok; + safe_div_by_zero safe_div_by_zero_strategy <$t as $crate::common::uint_fuzz::TestUintFuzz>::DIV_BY_ZERO_EXPECT; + gt_greater gt_greater_strategy simplex::fuzz::fuzz_check::Expect::Ok; + gt_equal gt_equal_strategy simplex::fuzz::fuzz_check::Expect::Ok; + gt_less gt_less_strategy simplex::fuzz::fuzz_check::Expect::Ok; + ge_greater ge_greater_strategy simplex::fuzz::fuzz_check::Expect::Ok; + ge_equal ge_equal_strategy simplex::fuzz::fuzz_check::Expect::Ok; + ge_less ge_less_strategy simplex::fuzz::fuzz_check::Expect::Ok; + ); + }; + (@stub $t:ty; $( + $name:ident $strategy:ident $expected_execution:expr; + )+) => { + $( + #[simplex::fuzz] + fn $name(context: simplex::TestContext) -> anyhow::Result<()> { + $crate::common::uint_fuzz::run_uint_fuzz::<$t>( + context, + $crate::common::uint_fuzz::$strategy::<$t>(), + $expected_execution, + ) + } + )+ + }; +} diff --git a/tests/stdlib/op_return.rs b/tests/stdlib/op_return.rs index 2b83226..2d05743 100644 --- a/tests/stdlib/op_return.rs +++ b/tests/stdlib/op_return.rs @@ -129,3 +129,169 @@ fn assert_output_is_op_return_fail(context: simplex::TestContext) -> anyhow::Res .index(index) .expecting(&context, Expect::AssertFailed) } + +mod op_return_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::fuzz_transaction::{FuzzTransaction, ProgramTarget}; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::transaction::{ + FinalTransaction, PartialInput, PartialOutput, RequiredSignature, UTXO, + }; + use simplex::{FuzzMode, TestContext}; + + const EXPECTED_IS_OP_RETURN: bool = true; + + const PROGRAM_TARGET: ProgramTarget = ProgramTarget::Input(0); + + fn arb_non_zero_u32() -> impl Strategy { + any::().prop_filter("output index should not be zero", |index| *index != 0) + } + + struct CaseFuzz { + case: Case, + context: TestContext, + indices: BoxedStrategy, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + indices: Just(0_u32).boxed(), + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, indices: impl Strategy + 'static) -> Self { + self.indices = indices.boxed(); + self + } + + fn flag(mut self, flag: bool) -> Self { + self.case = self.case.flag(flag); + self + } + + fn op_return(mut self, data: &'static [u8]) -> Self { + self.case = self.case.op_return(data); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn build_initial_tx(data: Option<&[u8]>) -> FinalTransaction { + let mut tx = FinalTransaction::new(); + tx.add_input(PartialInput::new(UTXO::default()), RequiredSignature::None); + + if let Some(data) = data { + tx.add_output(PartialOutput::new_metadata(data)); + } + + tx + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness, data } = self.case; + + let strategy = self + .indices + .prop_map(move |index| { + let arguments: Arguments = OpReturnTestArguments {}.into(); + let witness: WitnessValues = OpReturnTestWitness { + index, + ..witness.clone() + } + .into(); + + (arguments, witness) + }) + .boxed(); + + let transaction_builder = + FuzzTransaction::new(CaseFuzz::build_initial_tx(data), [PROGRAM_TARGET])?; + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_transaction(transaction_builder) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + #[simplex::fuzz] + fn is_output_op_return_true(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsOpReturn, context, "is_output_op_return with OP_RETURN") + .flag(EXPECTED_IS_OP_RETURN) + .op_return(DEFAULT_DATA) + .run() + } + + #[simplex::fuzz] + fn is_output_op_return_empty_index_false(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsOpReturn, context, "is_output_op_return outside outputs") + .strategy(arb_non_zero_u32()) + .op_return(DEFAULT_DATA) + .run() + } + + #[simplex::fuzz] + fn is_output_op_return_false(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsOpReturn, context, "is_output_op_return without outputs").run() + } + + #[simplex::fuzz] + fn assert_output_is_op_return_pass(context: TestContext) -> anyhow::Result<()> { + case_fuzz( + AssertOutputIsOpReturn, + context, + "assert_output_is_op_return with OP_RETURN", + ) + .op_return(DEFAULT_DATA) + .run() + } + + #[simplex::fuzz] + fn assert_output_is_op_return_empty_index_fail( + context: TestContext, + ) -> anyhow::Result<()> { + case_fuzz( + AssertOutputIsOpReturn, + context, + "assert_output_is_op_return outside outputs", + ) + .strategy(arb_non_zero_u32()) + .op_return(DEFAULT_DATA) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn assert_output_is_op_return_fail(context: TestContext) -> anyhow::Result<()> { + case_fuzz( + AssertOutputIsOpReturn, + context, + "assert_output_is_op_return without outputs", + ) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/secp256k1/operations.rs b/tests/stdlib/secp256k1/operations.rs index 3bc6798..3b24096 100644 --- a/tests/stdlib/secp256k1/operations.rs +++ b/tests/stdlib/secp256k1/operations.rs @@ -13,6 +13,18 @@ use simplicityhl_std::artifacts::tests::secp256k1::operations::derived_operation use FunctionToTest::*; +type U256Alias = [u8; 32]; +/// A **G**roup **E**lement, represented as an affine curve point (u256, u256) +type Ge = (U256Alias, U256Alias); +/// A **G**roup **E**lement, represented using **J**acobian coordinates ((X, Y), Z) +type Gej = (Ge, U256Alias); +/// A compressed curve point (y_parity, x) +type Point = (u8, U256Alias); +// **F**ield **e**lement +type Fe = U256Alias; +// A multiplier for curve points, with arithmetic modulo the group order **n** +type Scalar = U256Alias; + enum FunctionToTest { GeToPoint, PointToGej, @@ -43,6 +55,7 @@ fn program() -> Secp256k1OperationsTestProgram { } /// One dispatch arm of the contract, plus the witness it reads. +#[derive(Clone, Debug)] struct Case { witness: Secp256k1OperationsTestWitness, } @@ -51,85 +64,81 @@ fn case(function: FunctionToTest) -> Case { Case { witness: Secp256k1OperationsTestWitness { function_index: function as u8, - first_uint: [0; 32], - second_uint: [0; 32], - first_ge: ([0; 32], [0; 32]), - second_ge: ([0; 32], [0; 32]), - first_gej: (([0; 32], [0; 32]), [0; 32]), - second_gej: (([0; 32], [0; 32]), [0; 32]), - first_point: (0, [0; 32]), - expected_uint: [0; 32], - expected_ge: ([0; 32], [0; 32]), - expected_gej: (([0; 32], [0; 32]), [0; 32]), - expected_point: (0, [0; 32]), + first_uint: U256Alias::default(), + second_uint: U256Alias::default(), + first_ge: Ge::default(), + second_ge: Ge::default(), + first_gej: Gej::default(), + second_gej: Gej::default(), + first_point: Point::default(), + expected_uint: U256Alias::default(), + expected_ge: Ge::default(), + expected_gej: Gej::default(), + expected_point: Point::default(), }, } } impl Case { /// The two field or scalar operands, `first_uint` and `second_uint`. - fn uints(mut self, first_uint: [u8; 32], second_uint: [u8; 32]) -> Self { + fn uints(mut self, first_uint: U256Alias, second_uint: U256Alias) -> Self { self.witness.first_uint = first_uint; self.witness.second_uint = second_uint; self } /// Only `first_ge`. - fn ge(mut self, first_ge: ([u8; 32], [u8; 32])) -> Self { + fn ge(mut self, first_ge: Ge) -> Self { self.witness.first_ge = first_ge; self } /// The two affine points, `first_ge` and `second_ge`. - fn ges(mut self, first_ge: ([u8; 32], [u8; 32]), second_ge: ([u8; 32], [u8; 32])) -> Self { + fn ges(mut self, first_ge: Ge, second_ge: Ge) -> Self { self.witness.first_ge = first_ge; self.witness.second_ge = second_ge; self } /// Only `first_gej`. - fn gej(mut self, first_gej: (([u8; 32], [u8; 32]), [u8; 32])) -> Self { + fn gej(mut self, first_gej: Gej) -> Self { self.witness.first_gej = first_gej; self } /// The two Jacobian points, `first_gej` and `second_gej`. - fn gejs( - mut self, - first_gej: (([u8; 32], [u8; 32]), [u8; 32]), - second_gej: (([u8; 32], [u8; 32]), [u8; 32]), - ) -> Self { + fn gejs(mut self, first_gej: Gej, second_gej: Gej) -> Self { self.witness.first_gej = first_gej; self.witness.second_gej = second_gej; self } /// `first_point`: a compressed point, parity byte and x coordinate. - fn point(mut self, first_point: (u8, [u8; 32])) -> Self { + fn point(mut self, first_point: Point) -> Self { self.witness.first_point = first_point; self } /// `expected_uint`: the field or scalar the arm should produce. - fn expect_uint(mut self, expected_uint: [u8; 32]) -> Self { + fn expect_uint(mut self, expected_uint: U256Alias) -> Self { self.witness.expected_uint = expected_uint; self } /// `expected_ge`: the affine point the arm should produce. - fn expect_ge(mut self, expected_ge: ([u8; 32], [u8; 32])) -> Self { + fn expect_ge(mut self, expected_ge: Ge) -> Self { self.witness.expected_ge = expected_ge; self } /// `expected_gej`: the Jacobian point the arm should produce. - fn expect_gej(mut self, expected_gej: (([u8; 32], [u8; 32]), [u8; 32])) -> Self { + fn expect_gej(mut self, expected_gej: Gej) -> Self { self.witness.expected_gej = expected_gej; self } /// `expected_point`: the compressed point the arm should produce. - fn expect_point(mut self, expected_point: (u8, [u8; 32])) -> Self { + fn expect_point(mut self, expected_point: Point) -> Self { self.witness.expected_point = expected_point; self } @@ -168,7 +177,7 @@ fn mod_n(x: &BigUint) -> BigUint { x % secp_n() } -fn fe_sub_ref(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { +fn fe_sub_ref(a: Fe, b: Fe) -> Fe { let p = secp_p(); let a = BigUint::from_bytes_be(&a) % &p; let b = BigUint::from_bytes_be(&b) % &p; @@ -176,7 +185,7 @@ fn fe_sub_ref(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { to_32_be(&((a + &p - b) % &p)) } -fn scalar_sub_ref(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { +fn scalar_sub_ref(a: Scalar, b: Scalar) -> Scalar { let n = secp_n(); let a = BigUint::from_bytes_be(&a) % &n; let b = BigUint::from_bytes_be(&b) % &n; @@ -184,7 +193,7 @@ fn scalar_sub_ref(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { to_32_be(&((a + &n - b) % &n)) } -fn fe_negate_ref(a: [u8; 32]) -> [u8; 32] { +fn fe_negate_ref(a: Fe) -> Fe { let p = secp_p(); let a = BigUint::from_bytes_be(&a) % &p; @@ -195,7 +204,7 @@ fn fe_negate_ref(a: [u8; 32]) -> [u8; 32] { } } -fn fe_mul_ref(a: [u8; 32], b: [u8; 32]) -> [u8; 32] { +fn fe_mul_ref(a: Fe, b: Fe) -> Fe { let p = secp_p(); let a = BigUint::from_bytes_be(&a); let b = BigUint::from_bytes_be(&b); @@ -211,15 +220,15 @@ fn random_uint_bytes() -> [u8; 32] { b } -fn random_fe_bytes() -> [u8; 32] { +fn random_fe_bytes() -> Fe { to_32_be(&mod_p(&BigUint::from_bytes_be(&random_uint_bytes()))) } -fn random_scalar_bytes() -> [u8; 32] { +fn random_scalar_bytes() -> Scalar { to_32_be(&mod_n(&BigUint::from_bytes_be(&random_uint_bytes()))) } -fn random_ge_bytes() -> ([u8; 32], [u8; 32]) { +fn random_ge_bytes() -> Ge { let secp = Secp256k1::new(); let sk = SecretKey::new(&mut SecpOsRng); let pk = PublicKey::from_secret_key(&secp, &sk); @@ -234,18 +243,18 @@ fn random_ge_bytes() -> ([u8; 32], [u8; 32]) { (x, y) } -fn ge_to_gej(ge: ([u8; 32], [u8; 32])) -> (([u8; 32], [u8; 32]), [u8; 32]) { +fn ge_to_gej(ge: Ge) -> Gej { let mut one = [0u8; 32]; one[31] = 1; (ge, one) } -fn compress(ge: ([u8; 32], [u8; 32])) -> (u8, [u8; 32]) { +fn compress(ge: Ge) -> Point { (ge.1[31] & 1, ge.0) } -fn pk_to_gej(pk: &PublicKey) -> (([u8; 32], [u8; 32]), [u8; 32]) { +fn pk_to_gej(pk: &PublicKey) -> Gej { let ser = pk.serialize_uncompressed(); let mut x = [0u8; 32]; @@ -432,3 +441,286 @@ fn safe_gej_normalize_roundtrip(context: simplex::TestContext) -> anyhow::Result .expect_ge(ge) .run(&context) } + +mod fuzz { + use std::fmt::Debug; + + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + fn arb_fe() -> impl Strategy { + any::<[u8; 32]>().prop_filter("valid secp256k1 field element", |value| *value < SECP_P) + } + + fn arb_non_zero_fe() -> impl Strategy { + arb_fe().prop_filter("non-zero secp256k1 field element", |value| { + *value != [0; 32] + }) + } + + fn arb_scalar() -> impl Strategy { + any::<[u8; 32]>().prop_filter("valid secp256k1 scalar", |value| *value < SECP_N) + } + + fn arb_secret_key() -> impl Strategy { + any::<[u8; 32]>().prop_filter_map("valid secp256k1 secret key", |bytes| { + SecretKey::from_slice(&bytes).ok() + }) + } + + fn ge_from_secret_key(secret_key: SecretKey) -> Ge { + let secp = Secp256k1::new(); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + let serialized = public_key.serialize_uncompressed(); + + let mut x = [0; 32]; + x.copy_from_slice(&serialized[1..33]); + + let mut y = [0; 32]; + y.copy_from_slice(&serialized[33..65]); + + (x, y) + } + + fn arb_ge() -> impl Strategy { + arb_secret_key().prop_map(ge_from_secret_key) + } + + /// Fuzz counterparts for every `#[simplex::test]` case with randomized inputs. + /// Deterministic cases stay as regular tests. + struct CaseFuzz { + cases: BoxedStrategy, + context: TestContext, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + cases: Just(case(function)).boxed(), + context, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn inject( + mut self, + values: impl Strategy + 'static, + inject: impl Fn(Case, T) -> Case + 'static, + ) -> Self + where + T: Debug + 'static, + { + self.cases = (self.cases, values) + .prop_map(move |(case, values)| inject(case, values)) + .boxed(); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let strategy = self.cases.prop_map(|case| { + let arguments: Arguments = Secp256k1OperationsTestArguments {}.into(); + let witness: WitnessValues = case.witness.into(); + + (arguments, witness) + }); + + self.context + .engine::< + Secp256k1OperationsTestProgram, + Secp256k1OperationsTestArguments, + Secp256k1OperationsTestWitness, + >() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + // 0. ge_to_point + #[simplex::fuzz] + fn ge_to_point_matches_parity(context: TestContext) -> anyhow::Result<()> { + // Sample one on-curve point; whatever parity it has, that's what we expect + // ge_to_point to produce. + case_fuzz(GeToPoint, context, "ge_to_point") + .inject(arb_ge(), |case, ge| { + let expected_parity = ge.1[31] & 1; // 0 (even y) or 1 (odd y) + + case.ge(ge).expect_point((expected_parity, ge.0)) + }) + .run() + } + + // 1. point_to_gej + #[simplex::fuzz] + fn point_to_gej_roundtrip(context: TestContext) -> anyhow::Result<()> { + case_fuzz(PointToGej, context, "point_to_gej") + .inject(arb_ge(), |case, ge| { + let point = compress(ge); + + case.point(point).expect_point(point) + }) + .run() + } + + // 2. fe_sub + #[simplex::fuzz] + fn fe_sub_is_zero(context: TestContext) -> anyhow::Result<()> { + case_fuzz(FeSub, context, "fe_sub self is zero") + .inject(arb_fe(), |case, a| case.uints(a, a).expect_uint([0u8; 32])) + .run() + } + + #[simplex::fuzz] + fn fe_sub(context: TestContext) -> anyhow::Result<()> { + case_fuzz(FeSub, context, "fe_sub") + .inject((arb_fe(), arb_fe()), |case, (a, b)| { + let exp = fe_sub_ref(a, b); + + case.uints(a, b).expect_uint(exp) + }) + .run() + } + + // 3. scalar_sub + #[simplex::fuzz] + fn scalar_sub(context: TestContext) -> anyhow::Result<()> { + case_fuzz(ScalarSub, context, "scalar_sub") + .inject((arb_scalar(), arb_scalar()), |case, (a, b)| { + let exp = scalar_sub_ref(a, b); + + case.uints(a, b).expect_uint(exp) + }) + .run() + } + + // 4. gej_sub + #[simplex::fuzz] + fn gej_sub(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GejSub, context, "gej_sub") + .inject( + (arb_secret_key(), arb_secret_key()) + .prop_filter("distinct secp256k1 points", |(p, q)| p != q), + |case, (p_secret, q_secret)| { + let secp = Secp256k1::new(); + let p = PublicKey::from_secret_key(&secp, &p_secret); + let q = PublicKey::from_secret_key(&secp, &q_secret); + let diff = p.combine(&q.negate(&secp)).expect("p - q non-infinity"); + + case.gejs(pk_to_gej(&p), pk_to_gej(&q)) + .expect_gej(pk_to_gej(&diff)) + }, + ) + .run() + } + + // 4.1 gej_sub_gives_inf and fails + #[simplex::fuzz] + fn gej_sub_gives_inf(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GejSub, context, "gej_sub") + .inject(arb_secret_key(), |case, p_secret| { + let secp = Secp256k1::new(); + let p = PublicKey::from_secret_key(&secp, &p_secret); + + // we expect zeroed arrays + case.gejs(pk_to_gej(&p), pk_to_gej(&p)) + .expect_gej(Default::default()) + }) + .expect(Expect::Ok) + .run() + } + + // 5. fe_eq + #[simplex::fuzz] + fn fe_eq_reflexive(context: TestContext) -> anyhow::Result<()> { + case_fuzz(FeEq, context, "fe_eq_reflexive") + .inject(arb_fe(), |case, value| case.uints(value, value)) + .run() + } + + // 6. scalar_eq has only a deterministic case and remains a regular test. + + // 7. ge_eq + #[simplex::fuzz] + fn ge_eq_rejects_negation(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GeEq, context, "ge_eq negation") + .inject(arb_ge(), |case, ge| { + case.ges(ge, (ge.0, fe_negate_ref(ge.1))) + }) + .expect(Expect::AssertFailed) + .run() + } + + // 8. gej_point_eq + #[simplex::fuzz] + fn gej_point_eq_rescaled(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GejPointEq, context, "gej_point_eq_rescaled") + .inject((arb_ge(), arb_non_zero_fe()), |case, (ge, lambda)| { + let lambda_squared = fe_mul_ref(lambda, lambda); + let lambda_cubed = fe_mul_ref(lambda_squared, lambda); + + let gej: Gej = ( + ( + fe_mul_ref(ge.0, lambda_squared), + fe_mul_ref(ge.1, lambda_cubed), + ), + lambda, + ); + + case.gej(gej).point(compress(ge)) + }) + .run() + } + + #[simplex::fuzz] + fn gej_point_eq_rejects_negation(context: TestContext) -> anyhow::Result<()> { + case_fuzz(GejPointEq, context, "gej_point_eq negation") + .inject(arb_ge(), |case, ge| { + let (parity, x) = compress(ge); + + case.gej(ge_to_gej(ge)).point((parity ^ 1, x)) + }) + .expect(Expect::AssertFailed) + .run() + } + + // 9. safe_gej_normalize + #[simplex::fuzz] + fn safe_gej_normalize_roundtrip(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeGejNormalize, context, "safe_gej_normalize_roundtrip") + .inject((arb_ge(), arb_non_zero_fe()), |case, (ge, lambda)| { + let lambda_squared = fe_mul_ref(lambda, lambda); + let lambda_cubed = fe_mul_ref(lambda_squared, lambda); + + let gej: Gej = ( + ( + fe_mul_ref(ge.0, lambda_squared), + fe_mul_ref(ge.1, lambda_cubed), + ), + lambda, + ); + + case.gej(gej).expect_ge(ge) + }) + .run() + } +} diff --git a/tests/stdlib/u1/convert.rs b/tests/stdlib/u1/convert.rs index 69e84e4..be32258 100644 --- a/tests/stdlib/u1/convert.rs +++ b/tests/stdlib/u1/convert.rs @@ -132,3 +132,127 @@ fn split_u1_to_u1(context: simplex::TestContext) -> anyhow::Result<()> { .expect(U256::from(a).to_big_endian()) .run(&context) } + +mod convert_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + } + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_bool_u8() -> impl Strategy { + arb_bool().prop_map(u8::from) + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy: BoxedStrategy<(Arguments, WitnessValues)> = inputs + .prop_map(move |a| { + let arguments: Arguments = U1ConvertTestArguments {}.into(); + + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, Expect::Ok)) + .run(); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u1_to_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU8, context, "u1 to u8") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU16, context, "u1 to u16") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU32, context, "u1 to u32") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU64, context, "u1 to u64") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u128(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU128, context, "u1 to u128") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn u1_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToU256, context, "u1 to u256") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn split_u1_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U1ToBool, context, "split u1 to u1") + .strategy(arb_bool_u8()) + .run() + } +} diff --git a/tests/stdlib/u128/bit.rs b/tests/stdlib/u128/bit.rs index bba6b0b..778d12b 100644 --- a/tests/stdlib/u128/bit.rs +++ b/tests/stdlib/u128/bit.rs @@ -166,3 +166,309 @@ fn right_shift_128_out_of_range(context: simplex::TestContext) -> anyhow::Result case(RightShift128).args(shift, val).expect(0).run(&context) } + +mod bit_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + const EXPECTED_FALSE: bool = false; + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u128() -> impl Strategy { + any::() + } + + /// Values for one bit operation before building the contract witness. + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + expected_bool: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: u128) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: u128) -> Self { + let _ = self.second_arg.insert(second_arg); + self + } + + fn expect(mut self, expected: u128) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn flag(mut self, expected_bool: bool) -> Self { + let _ = self.expected_bool.insert(expected_bool); + self + } + + fn into_witness(self, witness: U128TestBitsWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.expect("no first_arg in witness"), + self.second_arg.expect("no second_arg in witness"), + ) + .expect(self.expected.expect("no expected in witness")) + .flag(self.expected_bool.expect("no expected_bool in witness")) + .witness + .into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn run(self) -> anyhow::Result<()> { + let witness = self.case.witness; + + let strategy = self + .inputs + .expect("a fuzz strategy must be specified") + .prop_map(move |case| { + let arguments: Arguments = U128TestBitsArguments {}.into(); + let witness = case.into_witness(witness.clone()); + (arguments, witness) + }); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.name, Expect::Ok)) + .run(); + Ok(()) + } + } + + // and_128 - accepts custom inputs for the same operation. + // Both operands are full-width u128; compares against the independent host bitwise result. + #[simplex::fuzz] + fn and(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + FuzzCase::first_arg(a) + .second_arg(b) + .expect(a & b) + .flag(false) + }) + }; + + case_fuzz(And128, context, "u128 bitwise and") + .strategy(strategy) + .run() + } + + // or_128 - accepts custom inputs for the same operation. + // Both operands are full-width u128; compares against the independent host bitwise result. + #[simplex::fuzz] + fn or(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + FuzzCase::first_arg(a) + .second_arg(b) + .expect(a | b) + .flag(false) + }) + }; + + case_fuzz(Or128, context, "u128 bitwise or") + .strategy(strategy) + .run() + } + + // eq_128_true - accepts custom inputs for the same operation. + // eq_128_false - accepts custom inputs for the same operation. + // Both operands are full-width u128; compares the equality flag, with dedicated equal/different + // cases below. + #[simplex::fuzz] + fn eq(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).flag(a == b).expect(0)) + }; + + case_fuzz(Eq128, context, "u128 equality") + .strategy(strategy) + .run() + } + + // eq_128_true - accepts custom inputs for the same operation. + // a=b across the full width; expected equality=true. + #[simplex::fuzz] + fn eq_same(context: TestContext) -> anyhow::Result<()> { + let strategy = + { arb_u128().prop_map(|a| FuzzCase::first_arg(a).second_arg(a).expect(0).flag(true)) }; + + case_fuzz(Eq128, context, "u128 equality with equal operands") + .strategy(strategy) + .run() + } + + // eq_128_false - accepts custom inputs for the same operation. + // b=a.wrapping_add(1); operands are guaranteed different, including wraparound at MAX. + #[simplex::fuzz] + fn eq_different(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|a| { + FuzzCase::first_arg(a) + .second_arg(a.wrapping_add(1)) + .expect(0) + .flag(false) + }) + }; + + case_fuzz(Eq128, context, "u128 inequality") + .strategy(strategy) + .run() + } + + // left_shift_128 - accepts custom inputs for the same operation. + // 0<=shift<=255; shifts below 128 use the host result and larger shifts expect zero. + #[simplex::fuzz] + fn left_shift(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u8(), arb_u128()).prop_map(|(shift, value)| { + let expected = if shift >= 128 { 0 } else { value << shift }; + + FuzzCase::first_arg(shift as u128) + .second_arg(value) + .expect(expected) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(LeftShift128, context, "u128 left shift") + .strategy(strategy) + .run() + } + + // right_shift_128 - accepts custom inputs for the same operation. + // 0<=shift<=255; shifts below 128 use the host result and larger shifts expect zero. + #[simplex::fuzz] + fn right_shift(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u8(), arb_u128()).prop_map(|(shift, value)| { + let expected = if shift >= 128 { 0 } else { value >> shift }; + + FuzzCase::first_arg(shift as u128) + .second_arg(value) + .expect(expected) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(RightShift128, context, "u128 right shift") + .strategy(strategy) + .run() + } + + // left_shift_128_by_zero - accepts custom inputs for the same operation. + // shift=0 and value is full-width u128; expected result=value. + #[simplex::fuzz] + fn left_shift_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|value| { + FuzzCase::first_arg(0) + .second_arg(value) + .expect(value) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(LeftShift128, context, "u128 left shift by zero") + .strategy(strategy) + .run() + } + + // right_shift_128_by_zero - accepts custom inputs for the same operation. + // shift=0 and value is full-width u128; expected result=value. + #[simplex::fuzz] + fn right_shift_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u128().prop_map(|value| { + FuzzCase::first_arg(0) + .second_arg(value) + .expect(value) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(RightShift128, context, "u128 right shift by zero") + .strategy(strategy) + .run() + } + + // left_shift_128_out_of_range - accepts custom inputs for the same operation. + // 128<=shift<=255 and value is full-width u128; expected result=0. + #[simplex::fuzz] + fn left_shift_out_of_range(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (128u8..=u8::MAX, arb_u128()).prop_map(|(shift, value)| { + FuzzCase::first_arg(shift as u128) + .second_arg(value) + .expect(0) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(LeftShift128, context, "u128 left shift out of range") + .strategy(strategy) + .run() + } + + // right_shift_128_out_of_range - accepts custom inputs for the same operation. + // 128<=shift<=255 and value is full-width u128; expected result=0. + #[simplex::fuzz] + fn right_shift_out_of_range(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (128u8..=u8::MAX, arb_u128()).prop_map(|(shift, value)| { + FuzzCase::first_arg(shift as u128) + .second_arg(value) + .expect(0) + .flag(EXPECTED_FALSE) + }) + }; + + case_fuzz(RightShift128, context, "u128 right shift out of range") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u128/comparison.rs b/tests/stdlib/u128/comparison.rs index b97c596..51e07ac 100644 --- a/tests/stdlib/u128/comparison.rs +++ b/tests/stdlib/u128/comparison.rs @@ -127,3 +127,234 @@ fn le_128_bigger(context: simplex::TestContext) -> anyhow::Result<()> { case(Le128).args(a, b).run(&context) } + +mod comparison_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + const EXPECTED_TRUE: bool = true; + const EXPECTED_FALSE: bool = false; + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + any::().prop_filter("u128 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: u128) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: u128) -> Self { + let _ = self.second_arg.insert(second_arg); + self + } + + fn expect(mut self, expected: bool) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness(self, witness: U128TestCompareWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.expect("no first arg in witness"), + self.second_arg.expect("no second arg in witness"), + ) + .flag(self.expected.expect("no expected arg in witness")) + .witness + .into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn run(self) -> anyhow::Result<()> { + let witness = self.case.witness; + + let strategy = self + .inputs + .expect("a fuzz strategy must be specified") + .prop_map(move |fuzz_case| { + let arguments: Arguments = U128TestCompareArguments {}.into(); + let witness: WitnessValues = fuzz_case.into_witness(witness.clone()); + (arguments, witness) + }); + + self.context + .engine::( + ) + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.name, Expect::Ok)) + .run(); + Ok(()) + } + } + + // is_zero_128_true - accepts custom inputs for the same operation. + // a is full-width u128; checks a==0, but does not force the original fixed-zero case. + #[simplex::fuzz] + fn is_zero(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsZero128, context, "u128 is zero") + .strategy(arb_u128().prop_map(|a| FuzzCase::first_arg(a).second_arg(0).expect(a == 0))) + .run() + } + + // lt_128_less - accepts custom inputs for the same operation. + // lt_128_eq - accepts custom inputs for the same operation. + // lt_128_bigger - accepts custom inputs for the same operation. + // Both operands are full-width u128; compares the host boolean across all orderings. + #[simplex::fuzz] + fn lt(context: TestContext) -> anyhow::Result<()> { + case_fuzz(Lt128, context, "u128 less than") + .strategy( + (arb_u128(), arb_u128()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).expect(a < b)), + ) + .run() + } + + // le_128_less - accepts custom inputs for the same operation. + // le_128_eq - accepts custom inputs for the same operation. + // le_128_bigger - accepts custom inputs for the same operation. + // Both operands are full-width u128; compares the host boolean across all orderings. + #[simplex::fuzz] + fn le(context: TestContext) -> anyhow::Result<()> { + case_fuzz(Le128, context, "u128 less than or equal") + .strategy( + (arb_u128(), arb_u128()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).expect(a <= b)), + ) + .run() + } + + // le_128_eq - accepts custom inputs for the same operation. + // a=b across the full width; expected le=true. + #[simplex::fuzz] + fn equal_operands(context: TestContext) -> anyhow::Result<()> { + case_fuzz( + Le128, + context, + "u128 less than or equal with equal operands", + ) + .strategy( + arb_u128().prop_map(|a| FuzzCase::first_arg(a).second_arg(a).expect(EXPECTED_TRUE)), + ) + .run() + } + + // is_zero_128_false - accepts custom inputs for the same operation. + // 1<=a<=u128::MAX; expected is_zero=false. + #[simplex::fuzz] + fn non_zero_operand(context: TestContext) -> anyhow::Result<()> { + case_fuzz(IsZero128, context, "u128 nonzero operand") + .strategy(arb_non_zero_u128().prop_map(|value| { + FuzzCase::first_arg(value) + .second_arg(0) + .expect(EXPECTED_FALSE) + })) + .run() + } + + // lt_128_less - accepts custom inputs for the same operation. + // 0<=a) -> anyhow::Result<()> { + case_fuzz(Lt128, context, "u128 strict less than") + .strategy((0u128..u128::MAX).prop_flat_map(|a| { + (a + 1..=u128::MAX) + .prop_map(move |b| FuzzCase::first_arg(a).second_arg(b).expect(EXPECTED_TRUE)) + })) + .run() + } + + // lt_128_eq - accepts custom inputs for the same operation. + // a=b across the full width; expected lt=false. + #[simplex::fuzz] + fn lt_equal(context: TestContext) -> anyhow::Result<()> { + case_fuzz(Lt128, context, "u128 less than equal operands") + .strategy( + arb_u128() + .prop_map(|a| FuzzCase::first_arg(a).second_arg(a).expect(EXPECTED_FALSE)), + ) + .run() + } + + // lt_128_bigger - accepts custom inputs for the same operation. + // 1<=a<=MAX and 0<=b) -> anyhow::Result<()> { + case_fuzz(Lt128, context, "u128 less than with greater first operand") + .strategy(arb_non_zero_u128().prop_flat_map(|a| { + (0u128..a) + .prop_map(move |b| FuzzCase::first_arg(a).second_arg(b).expect(EXPECTED_FALSE)) + })) + .run() + } + + // le_128_less - accepts custom inputs for the same operation. + // 0<=a) -> anyhow::Result<()> { + case_fuzz(Le128, context, "u128 strict less than or equal") + .strategy((0u128..u128::MAX).prop_flat_map(|a| { + (a + 1..=u128::MAX) + .prop_map(move |b| FuzzCase::first_arg(a).second_arg(b).expect(EXPECTED_TRUE)) + })) + .run() + } + + // le_128_bigger - accepts custom inputs for the same operation. + // 1<=a<=MAX and 0<=b) -> anyhow::Result<()> { + case_fuzz(Le128, context, "u128 strict greater than") + .strategy((1u128..=u128::MAX).prop_flat_map(|a| { + (0u128..a) + .prop_map(move |b| FuzzCase::first_arg(a).second_arg(b).expect(EXPECTED_FALSE)) + })) + .run() + } +} diff --git a/tests/stdlib/u128/convert.rs b/tests/stdlib/u128/convert.rs index b1cf059..aecb00d 100644 --- a/tests/stdlib/u128/convert.rs +++ b/tests/stdlib/u128/convert.rs @@ -215,3 +215,259 @@ fn safe_u128_to_u64_overflow(context: simplex::TestContext) -> anyhow::Result<() .expect(U256::from(a).to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + expect: Expect, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + expect: Expect::Ok, + name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let witness = self.case.witness; + + let strategy = self + .inputs + .expect("a fuzz strategy must be specified") + .prop_map(move |a| { + let arguments: Arguments = U128ConvertTestArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + (arguments, witness) + }); + + self.context + .engine::( + ) + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.name, self.expect)) + .run(); + Ok(()) + } + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_u32() -> impl Strategy { + any::() + } + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_bool_u128() -> impl Strategy { + arb_bool().prop_map(u128::from) + } + + fn arb_u8_u128() -> impl Strategy { + arb_u8().prop_map(u128::from) + } + + fn arb_u16_u128() -> impl Strategy { + arb_u16().prop_map(u128::from) + } + + fn arb_u32_u128() -> impl Strategy { + arb_u32().prop_map(u128::from) + } + + fn arb_u64_u128() -> impl Strategy { + arb_u64().prop_map(u128::from) + } + + // u128_to_u256 - accepts custom inputs for the same operation. + // Custom full-width u128 input; checks exact zero extension to the larger type. + #[simplex::fuzz] + fn u128_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U128ToU256, context, "u128 to u256") + .strategy(arb_u128()) + .run() + } + + // split_u128_into_u8 - accepts custom inputs for the same operation. + // Custom full-width u128 input; checks every split component in the original order. + #[simplex::fuzz] + fn split_u128_into_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU8, context, "split u128 into u8") + .strategy(arb_u128()) + .run() + } + + // split_u128_into_u16 - accepts custom inputs for the same operation. + // Custom full-width u128 input; checks every split component in the original order. + #[simplex::fuzz] + fn split_u128_into_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU16, context, "split u128 into u16") + .strategy(arb_u128()) + .run() + } + + // split_u128_into_u32 - accepts custom inputs for the same operation. + // Custom full-width u128 input; checks every split component in the original order. + #[simplex::fuzz] + fn split_u128_into_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU32, context, "split u128 into u32") + .strategy(arb_u128()) + .run() + } + + // split_u128_into_u64 - accepts custom inputs for the same operation. + // Custom full-width u128 input; checks every split component in the original order. + #[simplex::fuzz] + fn split_u128_into_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU128IntoU64, context, "split u128 into u64") + .strategy(arb_u128()) + .run() + } + + // safe_u128_to_u1 - accepts custom inputs for the same operation. + // 0<=a<=1; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u128_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU1, context, "safe u128 to u1") + .strategy(arb_bool_u128()) + .run() + } + + // safe_u128_to_u1_overflow - accepts custom inputs for the same operation. + // 1) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU1, context, "safe u128 to u1 overflow") + .strategy(2u128..=u128::MAX) + .expect(Expect::AssertFailed) + .run() + } + + // safe_u128_to_u8 - accepts custom inputs for the same operation. + // 0<=a<=u8::MAX; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u128_to_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU8, context, "safe u128 to u8") + .strategy(arb_u8_u128()) + .run() + } + + // safe_u128_to_u8_overflow - accepts custom inputs for the same operation. + // u8::MAX) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU8, context, "safe u128 to u8 overflow") + .strategy((u128::from(u8::MAX) + 1)..=u128::MAX) + .expect(Expect::AssertFailed) + .run() + } + + // safe_u128_to_u16 - accepts custom inputs for the same operation. + // 0<=a<=u16::MAX; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u128_to_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU16, context, "safe u128 to u16") + .strategy(arb_u16_u128()) + .run() + } + + // safe_u128_to_u16_overflow - accepts custom inputs for the same operation. + // u16::MAX) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU16, context, "safe u128 to u16 overflow") + .strategy((u128::from(u16::MAX) + 1)..=u128::MAX) + .expect(Expect::AssertFailed) + .run() + } + + // safe_u128_to_u32 - accepts custom inputs for the same operation. + // 0<=a<=u32::MAX; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u128_to_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU32, context, "safe u128 to u32") + .strategy(arb_u32_u128()) + .run() + } + + // safe_u128_to_u32_overflow - accepts custom inputs for the same operation. + // u32::MAX) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU32, context, "safe u128 to u32 overflow") + .strategy((u128::from(u32::MAX) + 1)..=u128::MAX) + .expect(Expect::AssertFailed) + .run() + } + + // safe_u128_to_u64 - accepts custom inputs for the same operation. + // 0<=a<=u64::MAX; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u128_to_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU64, context, "safe u128 to u64") + .strategy(arb_u64_u128()) + .run() + } + + // safe_u128_to_u64_overflow - accepts custom inputs for the same operation. + // u64::MAX) -> anyhow::Result<()> { + case_fuzz(SafeU128ToU64, context, "safe u128 to u64 overflow") + .strategy((u128::from(u64::MAX) + 1)..=u128::MAX) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u128/math/api.rs b/tests/stdlib/u128/math/api.rs index c624b3f..111b981 100644 --- a/tests/stdlib/u128/math/api.rs +++ b/tests/stdlib/u128/math/api.rs @@ -32,3 +32,38 @@ impl TestUint for u128 { // Stamps the 22 `#[simplex::test]` entry points for u128. Logic lives in common::uint. crate::uint_tests!(u128); + +mod math_api_tests_fuzz { + use crate::common::uint_fuzz::TestUintFuzz; + + use super::*; + + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + impl TestUintFuzz for u128 { + type Arguments = U128MathTestArguments; + + const DIV_BY_ZERO_EXPECT: simplex::fuzz::fuzz_check::Expect = + simplex::fuzz::fuzz_check::Expect::AssertFailed; + + fn arguments() -> Self::Arguments { + U128MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::().boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + any::().prop_map(|value| value.max(1)).boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + (low..=high).boxed() + } + } + + crate::uint_fuzz_tests!(u128); +} diff --git a/tests/stdlib/u128/math/primitives.rs b/tests/stdlib/u128/math/primitives.rs index 3d39661..d542375 100644 --- a/tests/stdlib/u128/math/primitives.rs +++ b/tests/stdlib/u128/math/primitives.rs @@ -636,3 +636,911 @@ fn div_mod_128_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod primitives_tests_fuzz { + use crate::common::uint_fuzz::{ + run_uint_fuzz, safe_div_by_zero_strategy, safe_div_fitting_strategy, + }; + + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + const EXPECT_CARRY: bool = true; + const EXPECT_BORROW: bool = true; + const NORMALIZER_U128_DIVISOR: bool = true; + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_non_zero_u64() -> impl Strategy { + any::().prop_filter("u64 should not be zero", |index| *index != 0) + } + + fn arb_non_zero_u64_u128() -> impl Strategy { + any::() + .prop_filter("u64 should not be zero", |index| *index != 0) + .prop_map(u128::from) + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + arb_u128().prop_filter("u128 should not be zero", |index| *index != 0) + } + + /// Values for one primitive operation before building the contract witness. + #[derive(Debug, Default, Clone)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + expected_bool: bool, + second_expected: u128, + } + + impl FuzzCase { + fn first_argument(first_arg: u128) -> Self { + Self { + first_arg: Some(first_arg), + ..Self::default() + } + } + + fn second_argument(mut self, second_arg: u128) -> Self { + self.second_arg = Some(second_arg); + self + } + + fn expect(mut self, expected: u128) -> Self { + self.expected = Some(expected); + self + } + + fn expect_bool(mut self, expected_bool: bool) -> Self { + self.expected_bool = expected_bool; + self + } + + fn second_expected(mut self, second_expected: u128) -> Self { + self.second_expected = second_expected; + self + } + + fn into_witness(self, witness: U128BasicMathTestWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.expect("no first arg in witness"), + self.second_arg.expect("no second arg in witness"), + ) + .expect(self.expected.expect("no expected result in witness")) + .flag(self.expected_bool) + .second(self.second_expected) + .witness + .into() + } + } + + fn div_mod_case(a: u128, b: u128) -> FuzzCase { + FuzzCase::first_argument(a) + .second_argument(b) + .expect(a / b) + .second_expected(a % b) + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + expect: Expect, + name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + expect: Expect::Ok, + name, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U128BasicMathTestArguments {}.into(); + let witness = fuzz_case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.name, self.expect)) + .run(); + Ok(()) + } + } + + // add_128_carry_not_overflow - accepts custom inputs for the same operation. + // add_128_carry_overflow - accepts custom inputs for the same operation. + // Full-width a and b; checks the wrapped sum and output carry for either outcome. + #[simplex::fuzz] + fn add(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (sum, carry) = a.overflowing_add(b); + + FuzzCase::first_argument(a) + .second_argument(b) + .expect(sum) + .expect_bool(carry) + }) + }; + + case_fuzz(Add128Carry, context, "add") + .strategy(strategy) + .run() + } + + // add_128_64_carry_not_overflow - accepts custom inputs for the same operation. + // add_128_64_carry_overflow - accepts custom inputs for the same operation. + // a is u128 and b is u64; checks the wrapped sum and output carry for either outcome. + #[simplex::fuzz] + fn add_128_64(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u64()).prop_map(|(a, b)| { + let b = b as u128; + let (sum, carry) = a.overflowing_add(b); + + FuzzCase::first_argument(a) + .second_argument(b) + .expect(sum) + .expect_bool(carry) + }) + }; + + case_fuzz(Add128_64Carry, context, "add_128_64") + .strategy(strategy) + .run() + } + + // full_add_128_not_overflow_carry_low_false - accepts custom inputs for the same operation. + // full_add_128_overflow_carry_low_false - accepts custom inputs for the same operation. + // Full-width a and b; carry_in=0; checks a+b and its output carry, including overflow. + #[simplex::fuzz] + fn full_add_no_carry(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (sum, carry) = a.overflowing_add(b); + + FuzzCase::first_argument(a) + .second_argument(b) + .second_expected(0) + .expect(sum) + .expect_bool(carry) + }) + }; + + case_fuzz(FullAdd128, context, "full_add_no_carry") + .strategy(strategy) + .run() + } + + // full_add_128_not_overflow_carry_low_true - accepts custom inputs for the same operation. + // full_add_128_overflow_carry_low_true - accepts custom inputs for the same operation. + // Full-width a and b; carry_in=1; checks wrapped a+b+1 and both carry steps. + #[simplex::fuzz] + fn full_add_with_carry(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (sum, carry1) = a.overflowing_add(b); + let (sum, carry2) = sum.overflowing_add(1); + + FuzzCase::first_argument(a) + .second_argument(b) + .second_expected(1) + .expect(sum) + .expect_bool(carry1 || carry2) + }) + }; + + case_fuzz(FullAdd128, context, "full_add_with_carry") + .strategy(strategy) + .run() + } + + // sub_128_borrow_not_overflow - accepts custom inputs for the same operation. + // sub_128_borrow_overflow - accepts custom inputs for the same operation. + // Full-width a and b; checks wrapped a-b and its output borrow for either outcome. + #[simplex::fuzz] + fn sub(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (diff, borrow) = a.overflowing_sub(b); + + FuzzCase::first_argument(a) + .second_argument(b) + .expect(diff) + .expect_bool(borrow) + }) + }; + + case_fuzz(Sub128Borrow, context, "sub") + .strategy(strategy) + .run() + } + + // sub_128_borrow_a_eq_b - accepts custom inputs for the same operation. + // a=b across the full width; expected result=0 and output borrow=false. + #[simplex::fuzz] + fn sub_equal(context: TestContext) -> anyhow::Result<()> { + let strategy = + { arb_u128().prop_map(|a| FuzzCase::first_argument(a).second_argument(a).expect(0)) }; + + case_fuzz(Sub128Borrow, context, "sub_equal") + .strategy(strategy) + .run() + } + + // sub_128_borrow_a_low_eq_b_low - accepts custom inputs for the same operation. + // a and b share the low u64 word; high words vary independently. + #[simplex::fuzz] + fn sub_equal_low_words(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u64()).prop_map(|(a, high)| { + let b = ((high as u128) << 64) | (a as u64 as u128); + let (diff, borrow) = a.overflowing_sub(b); + + FuzzCase::first_argument(a) + .second_argument(b) + .expect(diff) + .expect_bool(borrow) + }) + }; + + case_fuzz(Sub128Borrow, context, "sub_equal_low_words") + .strategy(strategy) + .run() + } + + // sub_128_borrow_diff_is_u64_max - accepts custom inputs for the same operation. + // a_low=u64::MAX and b_low=0; high words vary independently. + #[simplex::fuzz] + fn sub_max_low_word(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u64(), arb_u64()).prop_map(|(a_high, b_high)| { + let a = ((a_high as u128) << 64) | (u64::MAX as u128); + let b = (b_high as u128) << 64; + let (diff, borrow) = a.overflowing_sub(b); + + FuzzCase::first_argument(a) + .second_argument(b) + .expect(diff) + .expect_bool(borrow) + }) + }; + + case_fuzz(Sub128Borrow, context, "sub_max_low_word") + .strategy(strategy) + .run() + } + + // full_sub_128_borrow_low_false - accepts custom inputs for the same operation. + // full_sub_128_overflow_borrow_low_false - accepts custom inputs for the same operation. + // Full-width a and b; borrow_in=0; checks a-b and its output borrow, including underflow. + #[simplex::fuzz] + fn full_sub_no_borrow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (diff, borrow) = a.overflowing_sub(b); + + FuzzCase::first_argument(a) + .second_argument(b) + .second_expected(0) + .expect(diff) + .expect_bool(borrow) + }) + }; + + case_fuzz(FullSub128, context, "full_sub_no_borrow") + .strategy(strategy) + .run() + } + + // full_sub_128_borrow_low_true - accepts custom inputs for the same operation. + // full_sub_128_overflow_borrow_low_true - accepts custom inputs for the same operation. + // Full-width a and b; borrow_in=1; checks wrapped a-b-1 and both borrow steps. + #[simplex::fuzz] + fn full_sub_with_borrow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (diff, borrow1) = a.overflowing_sub(b); + let (diff, borrow2) = diff.overflowing_sub(1); + + FuzzCase::first_argument(a) + .second_argument(b) + .second_expected(1) + .expect(diff) + .expect_bool(borrow1 || borrow2) + }) + }; + + case_fuzz(FullSub128, context, "full_sub_with_borrow") + .strategy(strategy) + .run() + } + + // mul_128_wide - accepts custom inputs for the same operation. + // Full-width u128 operands; checks both product halves, including the high half omitted by the + // original. + #[simplex::fuzz] + fn mul(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u128()).prop_map(|(a, b)| { + let (high, low) = split_helper(U256::from(a) * U256::from(b)); + + FuzzCase::first_argument(a) + .second_argument(b) + .expect(high) + .second_expected(low) + }) + }; + + case_fuzz(Mul128Wide, context, "mul") + .strategy(strategy) + .run() + } + + // mul_128_64_wide - accepts custom inputs for the same operation. + // a is u128 and b is u64; checks both halves of the widened product. + #[simplex::fuzz] + fn mul_128_64(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_u64()).prop_map(|(a, b)| { + let b = b as u128; + let (high, low) = split_helper(U256::from(a) * U256::from(b)); + + FuzzCase::first_argument(a) + .second_argument(b) + .expect(high) + .second_expected(low) + }) + }; + + case_fuzz(Mul128_64Wide, context, "mul_128_64") + .strategy(strategy) + .run() + } + + const THRESHOLD: u128 = 1u128 << 63; + + // calculate_normalizer_base_64_b_is_u64 - accepts custom inputs for the same operation. + // 1<=b<2^63; selects the low word; expected normalizer=ceil(2^63/b). + #[simplex::fuzz] + fn normalizer_u64_small(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (1u128..THRESHOLD).prop_map(|b| { + FuzzCase::first_argument(0) + .second_argument(b) + .expect(THRESHOLD.div_ceil(b)) + }) + }; + + case_fuzz(CalculateNormalizerBase64, context, "normalizer_u64_small") + .strategy(strategy) + .run() + } + + // calculate_normalizer_base_64_b_is_big_enough_not_normalize - accepts custom inputs for the + // same operation. + // 2^63<=b<=u64::MAX; selects the low word; expected normalizer=1. + #[simplex::fuzz] + fn normalizer_u64_large(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (THRESHOLD..=u64::MAX as u128) + .prop_map(|b| FuzzCase::first_argument(0).second_argument(b).expect(1)) + }; + + case_fuzz(CalculateNormalizerBase64, context, "normalizer_u64_large") + .strategy(strategy) + .run() + } + + // calculate_normalizer_base_64_b_is_u128 - accepts custom inputs for the same operation. + // u64::MAX) -> anyhow::Result<()> { + let strategy = { + ((u64::MAX as u128 + 1)..=u128::MAX).prop_map(|b| { + FuzzCase::first_argument(0) + .second_argument(b) + .expect(THRESHOLD.div_ceil(b >> 64)) + .expect_bool(NORMALIZER_U128_DIVISOR) + }) + }; + + case_fuzz(CalculateNormalizerBase64, context, "normalizer_u128") + .strategy(strategy) + .run() + } + + // calculate_normalizer_base_64_b_is_u64_fail - accepts custom inputs for the same operation. + // b>u64::MAX with the low-word selector; expects an assertion failure. + #[simplex::fuzz] + fn normalizer_u64_wrong_width(context: TestContext) -> anyhow::Result<()> { + let strategy = { + ((u64::MAX as u128 + 1)..=u128::MAX) + .prop_map(|b| FuzzCase::first_argument(0).second_argument(b).expect(0)) + }; + + case_fuzz( + CalculateNormalizerBase64, + context, + "normalizer_u64_wrong_width", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // calculate_normalizer_base_64_b_is_u128_fail - accepts custom inputs for the same operation. + // 1<=b<=u64::MAX with the high-word selector; expects an assertion failure. + #[simplex::fuzz] + fn normalizer_u128_wrong_width(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u64_u128().prop_map(|value| { + FuzzCase::first_argument(0) + .second_argument(value) + .expect(0) + .expect_bool(NORMALIZER_U128_DIVISOR) + }) + }; + + case_fuzz( + CalculateNormalizerBase64, + context, + "normalizer_u128_wrong_width", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // calculate_normalizer_base_64_b_is_zero_fail - accepts custom inputs for the same operation. + // b=0 is fixed with Just; expects an assertion failure. + #[simplex::fuzz] + fn normalizer_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { Just(FuzzCase::first_argument(0).second_argument(0).expect(0)) }; + + case_fuzz(CalculateNormalizerBase64, context, "normalizer_zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // estimate_quotient_digit_base_64 - accepts custom inputs for the same operation. + // 2^63<=b_high<=u64::MAX and a_high) -> anyhow::Result<()> { + let strategy = { + ((1u64 << 63)..=u64::MAX).prop_flat_map(|b_high| { + (Just(b_high), arb_u64(), 0..b_high, arb_u128()).prop_map( + |(b_high, b_low, a_high, a_low)| { + let a = (U256::from(a_high) << 128) | U256::from(a_low); + let b = ((b_high as u128) << 64) | (b_low as u128); + FuzzCase::first_argument(a_high as u128) + .second_argument(a_low) + .expect((a / U256::from(b)).as_u128()) + .second_expected(b) + }, + ) + }) + }; + + case_fuzz(EstimateQuotientDigitBase64, context, "quotient_digit") + .strategy(strategy) + .run() + } + + // estimate_quotient_digit_base_64_fail - accepts custom inputs for the same operation. + // 2^63<=b_highb_high; expects the original assertion-failure outcome. + #[simplex::fuzz] + fn quotient_digit_too_large(context: TestContext) -> anyhow::Result<()> { + let strategy = { + ((1u64 << 63)..u64::MAX).prop_flat_map(|b_high| { + (Just(b_high), arb_u64(), (b_high + 1)..=u64::MAX, arb_u128()).prop_map( + |(b_high, b_low, a_high, a_low)| { + let a = (U256::from(a_high) << 128) | U256::from(a_low); + let b = ((b_high as u128) << 64) | (b_low as u128); + FuzzCase::first_argument(a_high as u128) + .second_argument(a_low) + .expect((a / U256::from(b)).as_u128()) + .second_expected(b) + }, + ) + }) + }; + + case_fuzz( + EstimateQuotientDigitBase64, + context, + "quotient_digit_too_large", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // div_mod_128_64 - accepts custom inputs for the same operation. + // a is full-width u128; 1<=b<=u64::MAX; checks quotient and remainder. + #[simplex::fuzz] + fn div_mod_128_64(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u128(), arb_non_zero_u64().prop_map(u128::from)) + .prop_map(|(a, b)| div_mod_case(a, b)) + }; + + case_fuzz(DivMod128_64, context, "div_mod_128_64") + .strategy(strategy) + .run() + } + + // div_mod_128_64_div_by_zero - accepts custom inputs for the same operation. + // a is full-width u128 and b=0; expects an assertion failure. + #[simplex::fuzz] + fn div_mod_128_64_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = + { arb_u128().prop_map(|a| FuzzCase::first_argument(a).second_argument(0).expect(0)) }; + + case_fuzz(DivMod128_64, context, "div_mod_128_64_zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // div_mod_128_a_less_than_b - accepts custom inputs for the same operation. + // div_mod_128_b_is_u64 - accepts custom inputs for the same operation. + // div_mod_128_b_is_u128 - accepts custom inputs for the same operation. + // div_mod_128_a_equal_b - accepts custom inputs for the same operation. + // a is full-width u128 and b is nonzero u128; checks quotient and remainder across branches. + #[simplex::fuzz] + fn div_mod_128(context: TestContext) -> anyhow::Result<()> { + let strategy = { (arb_u128(), arb_non_zero_u128()).prop_map(|(a, b)| div_mod_case(a, b)) }; + + case_fuzz(DivMod128, context, "div_mod_128") + .strategy(strategy) + .run() + } + + // div_mod_128_a_less_than_b - accepts custom inputs for the same operation. + // 0<=a) -> anyhow::Result<()> { + let strategy = { + (0u128..u128::MAX) + .prop_flat_map(|a| (a + 1..=u128::MAX).prop_map(move |b| div_mod_case(a, b))) + }; + + case_fuzz(DivMod128, context, "div_mod_a_less_than_b") + .strategy(strategy) + .run() + } + + // div_mod_128_a_equal_b - accepts custom inputs for the same operation. + // a=b>0; expects q=1 and r=0. + #[simplex::fuzz] + fn div_mod_equal(context: TestContext) -> anyhow::Result<()> { + let strategy = { arb_non_zero_u128().prop_map(|value| div_mod_case(value, value)) }; + + case_fuzz(DivMod128, context, "div_mod_equal") + .strategy(strategy) + .run() + } + + // div_mod_128_q_is_1 - accepts custom inputs for the same operation. + // div_mod_128_eq_high_words_a_less_than_b - accepts custom inputs for the same operation. + // a_high=b_high>0; low words are independent, covering q=0 or q=1. + #[simplex::fuzz] + fn div_mod_same_high_words(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u64(), arb_u64(), arb_u64()).prop_map(|(high, a_low, b_low)| { + let a = ((high as u128) << 64) | (a_low as u128); + let b = ((high as u128) << 64) | (b_low as u128); + + div_mod_case(a, b) + }) + }; + + case_fuzz(DivMod128, context, "div_mod_same_high_words") + .strategy(strategy) + .run() + } + + // No direct simplex::test counterpart - additional general property. + // Additional API division property, not a direct primitive-dispatch counterpart; a is u128 and + // b>0. + #[simplex::fuzz] + fn div(context: TestContext) -> anyhow::Result<()> { + run_uint_fuzz::(context, safe_div_fitting_strategy::(), Expect::Ok) + } + + // div_mod_128_div_by_zero - accepts custom inputs for the same operation. + // Uses API div_128, which calls div_mod_128 indirectly; a is u128, b=0, and assertion failure + // is expected. + #[simplex::fuzz] + fn div_by_zero(context: TestContext) -> anyhow::Result<()> { + run_uint_fuzz::( + context, + safe_div_by_zero_strategy::(), + Expect::AssertFailed, + ) + } + + // Explicit edge families complement the unrestricted arithmetic properties. + // add_128_carry_overflow - accepts custom inputs for the same operation. + // a=u128::MAX and b>0; wrapped result=b-1 and output carry=true. + #[simplex::fuzz] + fn add_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u128().prop_map(|value| { + FuzzCase::first_argument(u128::MAX) + .second_argument(value) + .expect(value - 1) + .expect_bool(EXPECT_CARRY) + }) + }; + + case_fuzz(Add128Carry, context, "add_overflow") + .strategy(strategy) + .run() + } + + // add_128_64_carry_overflow - accepts custom inputs for the same operation. + // a=u128::MAX and 1<=b<=u64::MAX; result=b-1 and output carry=true. + #[simplex::fuzz] + fn add_128_64_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u64_u128().prop_map(|value| { + FuzzCase::first_argument(u128::MAX) + .second_argument(value) + .expect(value - 1) + .expect_bool(EXPECT_CARRY) + }) + }; + + case_fuzz(Add128_64Carry, context, "add_128_64_overflow") + .strategy(strategy) + .run() + } + + // full_add_128_overflow_carry_low_true - accepts custom inputs for the same operation. + // a=u128::MAX, b>0, carry_in=1; wrapped result=b and output carry=true. + #[simplex::fuzz] + fn full_add_overflow_with_carry(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u128().prop_map(|value| { + FuzzCase::first_argument(u128::MAX) + .second_argument(value) + .second_expected(1) + .expect(value) + .expect_bool(EXPECT_CARRY) + }) + }; + + case_fuzz(FullAdd128, context, "full_add_overflow_with_carry") + .strategy(strategy) + .run() + } + + // sub_128_borrow_overflow - accepts custom inputs for the same operation. + // 0<=a) -> anyhow::Result<()> { + let strategy = { + (0u128..u128::MAX).prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(u128::MAX) + .expect(a + 1) + .expect_bool(EXPECT_BORROW) + }) + }; + + case_fuzz(Sub128Borrow, context, "sub_overflow") + .strategy(strategy) + .run() + } + + // full_sub_128_overflow_borrow_low_true - accepts custom inputs for the same operation. + // 0<=a) -> anyhow::Result<()> { + let strategy = { + (0..u128::MAX).prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(u128::MAX) + .second_expected(1) + .expect(a) + .expect_bool(EXPECT_BORROW) + }) + }; + + case_fuzz(FullSub128, context, "full_sub_overflow_with_borrow") + .strategy(strategy) + .run() + } + + // No direct simplex::test counterpart - new input-bit boundary. + // New boundary: a=u128::MAX, b=0, carry_in=1; only the input carry causes result=0 and + // carry_out=true. + #[simplex::fuzz] + fn full_add_carry_in_causes_overflow(context: TestContext) -> anyhow::Result<()> { + // MAX + 0 fits; only the input carry causes overflow. + let strategy = Just(u128::MAX).prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(0) + .second_expected(1) + .expect(0) + .expect_bool(true) + }); + + case_fuzz(FullAdd128, context, "full_add_carry_in_causes_overflow") + .strategy(strategy) + .run() + } + + // No direct simplex::test counterpart - new input-bit boundary. + // New boundary: a=b across the full width, borrow_in=1; only the input borrow causes result=MAX + // and borrow_out=true. + #[simplex::fuzz] + fn full_sub_borrow_in_causes_underflow(context: TestContext) -> anyhow::Result<()> { + // a - a is zero; only the input borrow causes underflow. + let strategy = arb_u128().prop_map(|a| { + FuzzCase::first_argument(a) + .second_argument(a) + .second_expected(1) + .expect(u128::MAX) + .expect_bool(true) + }); + + case_fuzz(FullSub128, context, "full_sub_borrow_in_causes_underflow") + .strategy(strategy) + .run() + } + + // div_mod_128_div_64 - accepts custom inputs for the same operation. + // 1<=b<=a<=u64::MAX; checks the small-operands branch of div_mod_128. + #[simplex::fuzz] + fn div_mod_128_div_64(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_non_zero_u64().prop_flat_map(|b| { + (b..=u64::MAX).prop_map(move |a| div_mod_case(u128::from(a), u128::from(b))) + }) + }; + + case_fuzz(DivMod128, context, "div_mod_128_div_64") + .strategy(strategy) + .run() + } + + // div_mod_128_q_is_1 - accepts custom inputs for the same operation. + // a_high=b_high>0 and a_low>=b_low; expects q=1 and the exact remainder. + #[simplex::fuzz] + fn div_mod_quotient_one(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u64(), arb_u64()).prop_flat_map(|(high, b_low)| { + (b_low..=u64::MAX).prop_map(move |a_low| { + let a = (u128::from(high) << 64) | u128::from(a_low); + let b = (u128::from(high) << 64) | u128::from(b_low); + div_mod_case(a, b) + }) + }) + }; + + case_fuzz(DivMod128, context, "div_mod_quotient_one") + .strategy(strategy) + .run() + } + + // div_mod_128_b_is_u64 - accepts custom inputs for the same operation. + // a>u64::MAX and 1<=b<=u64::MAX; checks the small-divisor branch. + #[simplex::fuzz] + fn div_mod_b_is_u64(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u64(), (u128::from(u64::MAX) + 1)..=u128::MAX) + .prop_map(|(b, a)| div_mod_case(a, u128::from(b))) + }; + + case_fuzz(DivMod128, context, "div_mod_b_is_u64") + .strategy(strategy) + .run() + } + + // div_mod_128_b_is_u128 - accepts custom inputs for the same operation. + // a_high>b_high>0 and both low words are nonzero; checks the wide-divisor branch. + #[simplex::fuzz] + fn div_mod_b_is_u128(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (1u64..u64::MAX).prop_flat_map(|b_high| { + ( + (b_high + 1)..=u64::MAX, + arb_non_zero_u64(), + arb_non_zero_u64(), + ) + .prop_map(move |(a_high, a_low, b_low)| { + let a = (u128::from(a_high) << 64) | u128::from(a_low); + let b = (u128::from(b_high) << 64) | u128::from(b_low); + + div_mod_case(a, b) + }) + }) + }; + + case_fuzz(DivMod128, context, "div_mod_b_is_u128") + .strategy(strategy) + .run() + } + + // div_mod_128_equal_high_words_max_low_diff - accepts custom inputs for the same operation. + // a_high=b_high>0, a_low=u64::MAX, b_low=0; expects q=1 and r=u64::MAX. + #[simplex::fuzz] + fn div_mod_equal_high_words_max_low_diff(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u64().prop_map(|value| { + let high = u128::from(value.max(1)) << 64; + + div_mod_case(high | u128::from(u64::MAX), high) + }) + }; + + case_fuzz(DivMod128, context, "div_mod_equal_high_words_max_low_diff") + .strategy(strategy) + .run() + } + + // div_mod_128_eq_high_words_a_less_than_b - accepts custom inputs for the same operation. + // a_high=b_high>0, a_low=0, b_low=u64::MAX; expects q=0 and r=a. + #[simplex::fuzz] + fn div_mod_equal_high_words_less(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u64().prop_map(|value| { + let high = u128::from(value.max(1)) << 64; + + div_mod_case(high, high | u128::from(u64::MAX)) + }) + }; + + case_fuzz(DivMod128, context, "div_mod_equal_high_words_less") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u128/mul_div.rs b/tests/stdlib/u128/mul_div.rs index 19ed754..a111ff2 100644 --- a/tests/stdlib/u128/mul_div.rs +++ b/tests/stdlib/u128/mul_div.rs @@ -110,3 +110,199 @@ fn mul_div_128_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod mul_div_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + any::().prop_filter("u128 should not be zero", |value| *value != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_numerator: Option, + second_numerator: Option, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn from_numerator(first_numerator: u128) -> Self { + Self { + first_numerator: Some(first_numerator), + ..Self::default() + } + } + + fn mul(mut self, second_numerator: u128) -> Self { + self.second_numerator = Some(second_numerator); + self + } + + fn div(mut self, denominator: u128) -> Self { + self.denominator = Some(denominator); + self + } + + fn expect(mut self, expected: u128) -> Self { + self.expected = Some(expected); + self + } + + fn into_witness( + self, + witness: U128MulDivTestWitness, + expect_failure: bool, + ) -> WitnessValues { + let case = Case { witness }.args( + self.first_numerator.expect("no first numerator in witness"), + self.second_numerator + .expect("no second numerator in witness"), + self.denominator.expect("no denominator in witness"), + ); + + let case = if expect_failure { + case + } else { + case.expect(self.expected.expect("no expected result in witness")) + }; + + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + expect: Expect, + test_name: &'static str, + } + + fn case_fuzz(context: TestContext, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + context, + inputs: None, + expect: Expect::Ok, + test_name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U128MulDivTestArguments {}.into(); + let witness = case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + // mul_div_128_product_is_u128 - accepts custom inputs for the same operation. + // 0<=a,b<=u64::MAX and c>0; a*b fits u128; checks floor(a*b/c). + #[simplex::fuzz] + fn product_fits_u128(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u64(), arb_u64(), arb_non_zero_u128()).prop_map(|(a, b, c)| { + let (a, b) = (a as u128, b as u128); + + FuzzCase::from_numerator(a).mul(b).div(c).expect(a * b / c) + }) + }; + + case_fuzz(context, "u128 mul div with fitting product") + .strategy(strategy) + .run() + } + + // mul_div_128_intermediate_overflow - accepts custom inputs for the same operation. + // 2<=a<=MAX, b=MAX, a<=c<=MAX; the widened product overflows u128 but the quotient fits. + #[simplex::fuzz] + fn intermediate_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (2u128..=u128::MAX).prop_flat_map(|a| { + (a..=u128::MAX).prop_map(move |c| { + let expected = + (U256::from(a) * U256::from(u128::MAX) / U256::from(c)).low_u128(); + + FuzzCase::from_numerator(a) + .mul(u128::MAX) + .div(c) + .expect(expected) + }) + }) + }; + + case_fuzz(context, "u128 mul div with intermediate overflow") + .strategy(strategy) + .run() + } + + // mul_div_128_result_overflow - accepts custom inputs for the same operation. + // 2<=a<=MAX, b=MAX, 1<=c) -> anyhow::Result<()> { + let strategy = { + (2u128..=u128::MAX).prop_flat_map(|a| { + (1..a).prop_map(move |c| FuzzCase::from_numerator(a).mul(u128::MAX).div(c)) + }) + }; + + case_fuzz(context, "u128 mul div result overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // mul_div_128_div_by_zero - accepts custom inputs for the same operation. + // a,b>0 and c=0; expects an assertion failure; the failure witness has no expected result. + #[simplex::fuzz] + fn divide_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u128(), arb_non_zero_u128()) + .prop_map(|(a, b)| FuzzCase::from_numerator(a).mul(b).div(0).expect(0)) + }; + + case_fuzz(context, "u128 mul div by zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u16/convert.rs b/tests/stdlib/u16/convert.rs index 036a8a4..90f316e 100644 --- a/tests/stdlib/u16/convert.rs +++ b/tests/stdlib/u16/convert.rs @@ -152,3 +152,164 @@ fn safe_u16_to_u8_overflow(context: simplex::TestContext) -> anyhow::Result<()> .expect(U256::from(a).to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + // (first_arg) + type ConvertInputs = u16; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_bool_u16() -> impl Strategy { + arb_bool().prop_map(u16::from) + } + + fn arb_u8_u16() -> impl Strategy { + arb_u8().prop_map(u16::from) + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let strategy = inputs + .prop_map(move |a| { + let arguments: Arguments = U16ConvertTestArguments {}.into(); + + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u16_to_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U16ToU32, context, "u16_to_u32") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn u16_to_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U16ToU64, context, "u16_to_u64") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn u16_to_u128(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U16ToU128, context, "u16_to_u128") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn u16_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U16ToU256, context, "u16_to_u256") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn split_u16_into_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU16IntoU8, context, "split_u16_into_u8") + .strategy(arb_u16()) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU1, context, "safe_u16_to_u1") + .strategy(arb_bool_u16()) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u1_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU1, context, "safe_u16_to_u1_overflow") + .strategy(2u16..=u16::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU8, context, "safe_u16_to_u8") + .strategy(arb_u8_u16()) + .run() + } + + #[simplex::fuzz] + fn safe_u16_to_u8_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU16ToU8, context, "safe_u16_to_u8_overflow") + .strategy(u8::MAX as u16 + 1..=u16::MAX) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u16/math.rs b/tests/stdlib/u16/math.rs index a343cfb..9ccd110 100644 --- a/tests/stdlib/u16/math.rs +++ b/tests/stdlib/u16/math.rs @@ -32,3 +32,35 @@ impl TestUint for u16 { // Stamps the 22 `#[simplex::test]` entry points for u16. Logic lives in common::uint. crate::uint_tests!(u16); + +mod math_tests_fuzz { + use crate::common::uint_fuzz::TestUintFuzz; + + use super::*; + + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + impl TestUintFuzz for u16 { + type Arguments = U16MathTestArguments; + + fn arguments() -> Self::Arguments { + U16MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::().boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + any::().prop_map(|value| value.max(1)).boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + (low..=high).boxed() + } + } + + crate::uint_fuzz_tests!(u16); +} diff --git a/tests/stdlib/u16/mul_div.rs b/tests/stdlib/u16/mul_div.rs index dbdbcab..3087fec 100644 --- a/tests/stdlib/u16/mul_div.rs +++ b/tests/stdlib/u16/mul_div.rs @@ -105,3 +105,181 @@ fn mul_div_16_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod mul_div_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_non_zero_u16() -> impl Strategy { + any::().prop_filter("u16 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_numerator: Option, + second_numerator: Option, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn from_numerator(first_numerator: u16) -> Self { + let mut x = Self::default(); + let _ = x.first_numerator.insert(first_numerator); + x + } + + fn mul(mut self, second_numerator: u16) -> Self { + let _ = self.second_numerator.insert(second_numerator); + self + } + + fn div(mut self, denominator: u16) -> Self { + let _ = self.denominator.insert(denominator); + self + } + + fn expect(mut self, expected: u16) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness( + self, + witness: U16MulDivTestWitness, + expect_failure: bool, + ) -> WitnessValues { + let case = Case { witness }.args( + self.first_numerator.expect("no first numerator in witness"), + self.second_numerator + .expect("no second numerator in witness"), + self.denominator.expect("no denominator in witness"), + ); + + let case = if expect_failure { + case + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(context: TestContext, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U16MulDivTestArguments {}.into(); + let witness: WitnessValues = + fuzz_case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_16_product_is_u16(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_u8(), arb_u8(), arb_non_zero_u16()).prop_map(|(a, b, c)| { + let a = u16::from(a); + let b = u16::from(b); + + FuzzCase::from_numerator(a).mul(b).div(c).expect(a * b / c) + }); + + case_fuzz(context, "mul_div_16_product_is_u16") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_16_intermediate_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2u16..=u16::MAX).prop_flat_map(|a| { + (a..=u16::MAX).prop_map(move |c| { + let b = u16::MAX; + let expected = ((u32::from(a) * u32::from(b)) / u32::from(c)) as u16; + + FuzzCase::from_numerator(a).mul(b).div(c).expect(expected) + }) + }); + + case_fuzz(context, "mul_div_16_intermediate_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_16_result_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2u16..=u16::MAX).prop_flat_map(|a| { + (1u16..a).prop_map(move |c| FuzzCase::from_numerator(a).mul(u16::MAX).div(c)) + }); + + case_fuzz(context, "mul_div_16_result_overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn mul_div_16_div_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u16(), arb_non_zero_u16()) + .prop_map(|(a, b)| FuzzCase::from_numerator(a).mul(b).div(0).expect(0)); + + case_fuzz(context, "mul_div_16_div_by_zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u256/bit.rs b/tests/stdlib/u256/bit.rs index 0d0db36..c144009 100644 --- a/tests/stdlib/u256/bit.rs +++ b/tests/stdlib/u256/bit.rs @@ -159,3 +159,245 @@ fn right_shift_256_max(context: simplex::TestContext) -> anyhow::Result<()> { .expect(result) .run(&context) } + +mod bit_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u8() -> impl Strategy { + any::().prop_filter("u8 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_arg: Option, + second_arg: Option, + expected: Option, + } + + impl FuzzCase { + fn first_arg(first_arg: U256) -> Self { + let mut x = Self::default(); + let _ = x.first_arg.insert(first_arg); + x + } + + fn second_arg(mut self, second_arg: U256) -> Self { + let _ = self.second_arg.insert(second_arg); + self + } + + fn expect(mut self, expected: U256) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness(self, witness: U256TestBitsWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg + .expect("no first arg in witness") + .to_big_endian(), + self.second_arg + .expect("no second arg in witness") + .to_big_endian(), + ) + .expect( + self.expected + .expect("no expected arg in witness") + .to_big_endian(), + ) + .witness + .into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U256TestBitsArguments {}.into(); + let witness = case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, Expect::Ok)) + .run(); + + Ok(()) + } + } + + // and_256 - accepts custom inputs for the same operation. + // Both operands are full-width u256; compares against the independent host bitwise result. + #[simplex::fuzz] + fn and_256(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u256(), arb_u256()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).expect(a & b)) + }; + + case_fuzz(And256, context, "u256 bitwise and") + .strategy(strategy) + .run() + } + + // or_256 - accepts custom inputs for the same operation. + // Both operands are full-width u256; compares against the independent host bitwise result. + #[simplex::fuzz] + fn or_256(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_u256(), arb_u256()) + .prop_map(|(a, b)| FuzzCase::first_arg(a).second_arg(b).expect(a | b)) + }; + + case_fuzz(Or256, context, "u256 bitwise or") + .strategy(strategy) + .run() + } + + // left_shift_256 - accepts custom inputs for the same operation. + // 1<=shift<=255 and value is full-width u256; checks the host shift result. + #[simplex::fuzz] + fn left_shift_256(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u8(), arb_u256()).prop_map(|(shift, value)| { + FuzzCase::first_arg(U256::from(shift)) + .second_arg(value) + .expect(value << shift) + }) + }; + + case_fuzz(LeftShift256, context, "u256 left shift") + .strategy(strategy) + .run() + } + + // left_shift_256_by_zero - accepts custom inputs for the same operation. + // shift=0 and value is full-width u256; expected result=value. + #[simplex::fuzz] + fn left_shift_256_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u256().prop_map(|value| { + FuzzCase::first_arg(U256::zero()) + .second_arg(value) + .expect(value) + }) + }; + + case_fuzz(LeftShift256, context, "u256 left shift by zero") + .strategy(strategy) + .run() + } + + // left_shift_256_max - accepts custom inputs for the same operation. + // shift=255 and value is full-width u256; checks the maximum accepted u8 shift. + #[simplex::fuzz] + fn left_shift_256_max(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u256().prop_map(|value| { + FuzzCase::first_arg(U256::from(u8::MAX)) + .second_arg(value) + .expect(value << u8::MAX) + }) + }; + + case_fuzz(LeftShift256, context, "u256 left shift by max") + .strategy(strategy) + .run() + } + + // right_shift_256 - accepts custom inputs for the same operation. + // 1<=shift<=255 and value is full-width u256; checks the host shift result. + #[simplex::fuzz] + fn right_shift_256(context: TestContext) -> anyhow::Result<()> { + let strategy = { + (arb_non_zero_u8(), arb_u256()).prop_map(|(shift, value)| { + FuzzCase::first_arg(U256::from(shift)) + .second_arg(value) + .expect(value >> shift) + }) + }; + + case_fuzz(RightShift256, context, "u256 right shift") + .strategy(strategy) + .run() + } + + // right_shift_256_by_zero - accepts custom inputs for the same operation. + // shift=0 and value is full-width u256; expected result=value. + #[simplex::fuzz] + fn right_shift_256_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u256().prop_map(|value| { + FuzzCase::first_arg(U256::zero()) + .second_arg(value) + .expect(value) + }) + }; + + case_fuzz(RightShift256, context, "u256 right shift by zero") + .strategy(strategy) + .run() + } + + // right_shift_256_max - accepts custom inputs for the same operation. + // shift=255 and value is full-width u256; checks the maximum accepted u8 shift. + #[simplex::fuzz] + fn right_shift_256_max(context: TestContext) -> anyhow::Result<()> { + let strategy = { + arb_u256().prop_map(|value| { + FuzzCase::first_arg(U256::from(u8::MAX)) + .second_arg(value) + .expect(value >> u8::MAX) + }) + }; + + case_fuzz(RightShift256, context, "u256 right shift by max") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u256/comparison.rs b/tests/stdlib/u256/comparison.rs index 27bdbb5..6a11d0d 100644 --- a/tests/stdlib/u256/comparison.rs +++ b/tests/stdlib/u256/comparison.rs @@ -136,3 +136,208 @@ fn le_256_bigger(context: simplex::TestContext) -> anyhow::Result<()> { .args(a.to_big_endian(), b.to_big_endian()) .run(&context) } + +mod comparison_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + const EXPECTED_TRUE: bool = true; + const EXPECTED_FALSE: bool = false; + + // (A, B, Expected) + type ComparisonInputs = (U256, U256, bool); + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u256() -> impl Strategy { + arb_u256().prop_map(|value| value.max(U256::one())) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |(a, b, expected)| (a.to_big_endian(), b.to_big_endian(), expected)) + .prop_map(move |(a, b, expected)| { + let arguments: Arguments = U256TestCompareArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .args(a, b) + .flag(expected) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::( + ) + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, Expect::Ok)) + .run(); + + Ok(()) + } + } + + // is_zero_256_false - accepts custom inputs for the same operation. + // 1<=a<=u256::MAX; expected is_zero=false. + #[simplex::fuzz] + fn is_zero_256_false(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_non_zero_u256().prop_map(|a| (a, U256::zero(), EXPECTED_FALSE)); + + case_fuzz(IsZero256, context, "u256 is non-zero") + .strategy(strategy) + .run() + } + + // lt_256_less - accepts custom inputs for the same operation. + // 0<=a) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::zero(), U256::MAX - 1); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(a + U256::one(), U256::MAX); + + b.prop_map(move |b| (a, b, EXPECTED_TRUE)) + }) + }; + + case_fuzz(Lt256, context, "u256 less than") + .strategy(strategy) + .run() + } + + // lt_256_eq - accepts custom inputs for the same operation. + // a=b across the full width; expected lt=false. + #[simplex::fuzz] + fn lt_256_eq(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(|a| (a, a, EXPECTED_FALSE)); + + case_fuzz(Lt256, context, "u256 less than equal") + .strategy(strategy) + .run() + } + + // lt_256_bigger - accepts custom inputs for the same operation. + // 1<=a<=MAX and 0<=b) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), a - U256::one()); + + b.prop_map(move |b| (a, b, EXPECTED_FALSE)) + }) + }; + + case_fuzz(Lt256, context, "u256 greater than") + .strategy(strategy) + .run() + } + + // le_256_less - accepts custom inputs for the same operation. + // 0<=a) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::zero(), U256::MAX - 1); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(a + U256::one(), U256::MAX); + + b.prop_map(move |b| (a, b, EXPECTED_TRUE)) + }) + }; + + case_fuzz(Le256, context, "u256 less than or equal") + .strategy(strategy) + .run() + } + + // le_256_eq - accepts custom inputs for the same operation. + // a=b across the full width; expected le=true. + #[simplex::fuzz] + fn le_256_eq(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(|a| (a, a, EXPECTED_TRUE)); + + case_fuzz(Le256, context, "u256 less than or equal equal") + .strategy(strategy) + .run() + } + + // le_256_bigger - accepts custom inputs for the same operation. + // 1<=a<=MAX and 0<=b) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), a - U256::one()); + + b.prop_map(move |b| (a, b, EXPECTED_FALSE)) + }) + }; + + case_fuzz(Le256, context, "u256 greater than") + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u256/convert.rs b/tests/stdlib/u256/convert.rs index 2bd68cb..3bf7745 100644 --- a/tests/stdlib/u256/convert.rs +++ b/tests/stdlib/u256/convert.rs @@ -237,3 +237,329 @@ fn safe_u256_to_u128_overflow(context: simplex::TestContext) -> anyhow::Result<( .expect(a.to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + // (A, Result) + type ConvertInputs = (U256, U256); + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + #[inline] + fn split_number(n: T) -> (T, T) { + (n.clone(), n) + } + + fn u256_duplicated_strategy() -> impl Strategy { + arb_u256().prop_map(split_number) + } + + fn safe_u256_to_u1_strategy() -> impl Strategy { + any::().prop_map(|x| U256::from(x as u8)) + } + + fn safe_u256_to_u8_strategy() -> impl Strategy { + any::().prop_map(U256::from) + } + + fn safe_u256_to_u16_strategy() -> impl Strategy { + any::().prop_map(U256::from) + } + + fn safe_u256_to_u32_strategy() -> impl Strategy { + any::().prop_map(U256::from) + } + + fn safe_u256_to_u64_strategy() -> impl Strategy { + any::().prop_map(U256::from) + } + + fn safe_u256_to_u128_strategy() -> impl Strategy { + any::().prop_map(U256::from) + } + + fn overflow_strategy(max: U256) -> impl Strategy { + arb_u256_in_range(max + U256::one(), U256::MAX) + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |(a, expected)| (a.to_big_endian(), expected.to_big_endian())) + .prop_map(move |(a, expected)| { + let arguments: Arguments = U256ConvertTestArguments {}.into(); + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(expected) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::( + ) + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + // u256_into_u8 - accepts custom inputs for the same operation. + // Custom full-width u256 input; checks every split component in the original order. + #[simplex::fuzz] + fn u256_into_u8(context: TestContext) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU8, context, "u256 split into u8") + .strategy(strategy) + .run() + } + + // u256_into_u16 - accepts custom inputs for the same operation. + // Custom full-width u256 input; checks every split component in the original order. + #[simplex::fuzz] + fn u256_into_u16(context: TestContext) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU16, context, "u256 split into u16") + .strategy(strategy) + .run() + } + + // u256_into_u32 - accepts custom inputs for the same operation. + // Custom full-width u256 input; checks every split component in the original order. + #[simplex::fuzz] + fn u256_into_u32(context: TestContext) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU32, context, "u256 split into u32") + .strategy(strategy) + .run() + } + + // u256_into_u64 - accepts custom inputs for the same operation. + // Custom full-width u256 input; checks every split component in the original order. + #[simplex::fuzz] + fn u256_into_u64(context: TestContext) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU64, context, "u256 split into u64") + .strategy(strategy) + .run() + } + + // u256_into_u128 - accepts custom inputs for the same operation. + // Custom full-width u256 input; checks every split component in the original order. + #[simplex::fuzz] + fn u256_into_u128(context: TestContext) -> anyhow::Result<()> { + let strategy = u256_duplicated_strategy(); + + case_fuzz(SplitU256IntoU128, context, "u256 split into u128") + .strategy(strategy) + .run() + } + + // safe_u256_to_u1 - accepts custom inputs for the same operation. + // 0<=a<=1; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u256_to_u1(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u1_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU1, context, "safe u256 to u1") + .strategy(strategy) + .run() + } + + // safe_u256_to_u1_overflow - accepts custom inputs for the same operation. + // 1) -> anyhow::Result<()> { + let strategy = overflow_strategy(U256::one()).prop_map(split_number); + + case_fuzz(SafeU256ToU1, context, "safe u256 to u1 overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // safe_u256_to_u8 - accepts custom inputs for the same operation. + // 0<=a<=u8::MAX; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u256_to_u8(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u8_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU8, context, "safe u256 to u8") + .strategy(strategy) + .run() + } + + // safe_u256_to_u8_overflow - accepts custom inputs for the same operation. + // u8::MAX) -> anyhow::Result<()> { + let strategy = overflow_strategy(u8::MAX.into()).prop_map(split_number); + + case_fuzz(SafeU256ToU8, context, "safe u256 to u8 overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // safe_u256_to_u16 - accepts custom inputs for the same operation. + // 0<=a<=u16::MAX; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u256_to_u16(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u16_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU16, context, "safe u256 to u16") + .strategy(strategy) + .run() + } + + // safe_u256_to_u16_overflow - accepts custom inputs for the same operation. + // u16::MAX) -> anyhow::Result<()> { + let strategy = overflow_strategy(u16::MAX.into()).prop_map(split_number); + + case_fuzz(SafeU256ToU16, context, "safe u256 to u16 overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // safe_u256_to_u32 - accepts custom inputs for the same operation. + // 0<=a<=u32::MAX; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u256_to_u32(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u32_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU32, context, "safe u256 to u32") + .strategy(strategy) + .run() + } + + // safe_u256_to_u32_overflow - accepts custom inputs for the same operation. + // u32::MAX) -> anyhow::Result<()> { + let strategy = overflow_strategy(u32::MAX.into()).prop_map(split_number); + + case_fuzz(SafeU256ToU32, context, "safe u256 to u32 overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // safe_u256_to_u64 - accepts custom inputs for the same operation. + // 0<=a<=u64::MAX; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u256_to_u64(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u64_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU64, context, "safe u256 to u64") + .strategy(strategy) + .run() + } + + // safe_u256_to_u64_overflow - accepts custom inputs for the same operation. + // u64::MAX) -> anyhow::Result<()> { + let strategy = overflow_strategy(u64::MAX.into()).prop_map(split_number); + + case_fuzz(SafeU256ToU64, context, "safe u256 to u64 overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // safe_u256_to_u128 - accepts custom inputs for the same operation. + // 0<=a<=u128::MAX; checks a fitting narrowing conversion with exact output. + #[simplex::fuzz] + fn safe_u256_to_u128(context: TestContext) -> anyhow::Result<()> { + let strategy = safe_u256_to_u128_strategy().prop_map(split_number); + + case_fuzz(SafeU256ToU128, context, "safe u256 to u128") + .strategy(strategy) + .run() + } + + // safe_u256_to_u128_overflow - accepts custom inputs for the same operation. + // u128::MAX) -> anyhow::Result<()> { + let strategy = overflow_strategy(u128::MAX.into()).prop_map(split_number); + + case_fuzz(SafeU256ToU128, context, "safe u256 to u128 overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u256/math/add.rs b/tests/stdlib/u256/math/add.rs index 5af5660..043a017 100644 --- a/tests/stdlib/u256/math/add.rs +++ b/tests/stdlib/u256/math/add.rs @@ -193,3 +193,364 @@ fn full_add_256_overflow_carry_low_true(context: simplex::TestContext) -> anyhow .flag(result_carry) .run(&context) } + +mod add_tests_fuzz { + use std::cmp::max; + + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u256() -> impl Strategy { + arb_u256().prop_map(|value| max(value, U256::one())) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + #[derive(Debug)] + struct FuzzCase { + first_arg: U256, + second_arg: U256, + carry_in: bool, + expected: U256, + expected_bool: bool, + } + + impl FuzzCase { + fn new(first_arg: U256, second_arg: U256) -> Self { + Self { + first_arg, + second_arg, + carry_in: false, + expected: U256::zero(), + expected_bool: false, + } + } + + fn carry_in(mut self, carry_in: bool) -> Self { + self.carry_in = carry_in; + self + } + + fn expect(mut self, expected: U256) -> Self { + self.expected = expected; + self + } + + fn expect_bool(mut self, expected_bool: bool) -> Self { + self.expected_bool = expected_bool; + self + } + + fn into_witness(self, witness: U256TestAddWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.to_big_endian(), + self.second_arg.to_big_endian(), + ) + .third_arg(self.carry_in) + .expect(self.expected.to_big_endian()) + .flag(self.expected_bool) + .witness + .into() + } + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U256TestAddArguments {}.into(); + let witness = case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, Expect::Ok)) + .run(); + + Ok(()) + } + } + + // add_256_carry_not_overflow - accepts custom inputs for the same operation. + // Custom full-width a; 0<=b<=MAX-a guarantees no output carry. + #[simplex::fuzz] + fn add_256_not_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), U256::MAX - a); + + b.prop_map(move |b| FuzzCase::new(a, b).expect(a + b)) + }) + }; + + case_fuzz(Add256Carry, context, "u256 add without overflow") + .strategy(strategy) + .run() + } + + // add_256_carry_overflow - accepts custom inputs for the same operation. + // Custom a>0; MAX-a+1<=b<=MAX guarantees overflow; checks wrapped result and carry. + #[simplex::fuzz] + fn add_256_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::MAX - a + U256::one(), U256::MAX); + + b.prop_map(move |b| { + let (expected, carry) = a.overflowing_add(b); + + FuzzCase::new(a, b).expect(expected).expect_bool(carry) + }) + }) + }; + + case_fuzz(Add256Carry, context, "u256 add with overflow") + .strategy(strategy) + .run() + } + + // add_256_128_carry_not_overflow - accepts custom inputs for the same operation. + // Custom full-width a; 0<=b<=min(MAX-a,u128::MAX) guarantees a fitting sum. + #[simplex::fuzz] + fn add_256_128_not_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), (U256::MAX - a).min(u128::MAX.into())); + + b.prop_map(move |b| FuzzCase::new(a, b).expect(a + b)) + }) + }; + + case_fuzz(Add256_128Carry, context, "u256 add u128 without overflow") + .strategy(strategy) + .run() + } + + // add_256_128_carry_overflow - accepts custom inputs for the same operation. + // MAX-u128::MAX+1<=a<=MAX; MAX-a+1<=b<=u128::MAX guarantees overflow. + #[simplex::fuzz] + fn add_256_128_overflow(context: TestContext) -> anyhow::Result<()> { + let max_u128 = U256::from(u128::MAX); + let strategy = { + let a = arb_u256_in_range(U256::MAX - max_u128 + U256::one(), U256::MAX); + + a.prop_flat_map(move |a| { + let b = arb_u256_in_range(U256::MAX - a + U256::one(), max_u128); + + b.prop_map(move |b| { + let (expected, carry) = a.overflowing_add(b); + + FuzzCase::new(a, b).expect(expected).expect_bool(carry) + }) + }) + }; + + case_fuzz(Add256_128Carry, context, "u256 add u128 with overflow") + .strategy(strategy) + .run() + } + + // full_add_256_not_overflow_carry_low_false - accepts custom inputs for the same operation. + // carry_in=false; 0<=a<=MAX and 0<=b<=MAX-a; expects sum=a+b and carry_out=false. + #[simplex::fuzz] + fn full_add_256_not_overflow_carry_in_false( + context: TestContext, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), U256::MAX - a); + + b.prop_map(move |b| { + FuzzCase::new(a, b) + .carry_in(false) + .expect(a + b) + .expect_bool(false) + }) + }) + }; + + case_fuzz( + FullAdd256, + context, + "u256 full add without overflow and input carry false", + ) + .strategy(strategy) + .run() + } + + // full_add_256_overflow_carry_low_false - accepts custom inputs for the same operation. + // carry_in=false; a>0 and MAX-a+1<=b<=MAX; checks wrapped sum and carry_out=true. + #[simplex::fuzz] + fn full_add_256_overflow_carry_in_false(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::MAX - a + U256::one(), U256::MAX); + + b.prop_map(move |b| { + let (expected, carry) = a.overflowing_add(b); + + FuzzCase::new(a, b) + .carry_in(false) + .expect(expected) + .expect_bool(carry) + }) + }) + }; + + case_fuzz( + FullAdd256, + context, + "u256 full add with overflow and input carry false", + ) + .strategy(strategy) + .run() + } + + // full_add_256_not_overflow_carry_low_true - accepts custom inputs for the same operation. + // carry_in=true; 0<=a, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::zero(), U256::MAX - U256::one()); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::zero(), U256::MAX - U256::one() - a); + + b.prop_map(move |b| { + FuzzCase::new(a, b) + .carry_in(true) + .expect(a + b + U256::one()) + .expect_bool(false) + }) + }) + }; + + case_fuzz( + FullAdd256, + context, + "u256 full add without overflow and input carry true", + ) + .strategy(strategy) + .run() + } + + // full_add_256_overflow_carry_low_true - accepts custom inputs for the same operation. + // carry_in=true; 0<=a<=MAX and MAX-a<=b<=MAX; checks wrapped a+b+1 and carry_out=true. + #[simplex::fuzz] + fn full_add_256_overflow_carry_in_true(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + + a.prop_flat_map(|a| { + let b = arb_u256_in_range(U256::MAX - a, U256::MAX); + + b.prop_map(move |b| { + let (result, carry) = a.overflowing_add(b); + let (expected, carry_from_input) = result.overflowing_add(U256::one()); + + FuzzCase::new(a, b) + .carry_in(true) + .expect(expected) + .expect_bool(carry || carry_from_input) + }) + }) + }; + + case_fuzz( + FullAdd256, + context, + "u256 full add with overflow and input carry true", + ) + .strategy(strategy) + .run() + } + + // No direct simplex::test counterpart - new input-bit boundary. + // New boundary: a=MAX, b=0, carry_in=true; only the input carry causes result=0 and + // carry_out=true. + #[simplex::fuzz] + fn full_add_256_carry_in_causes_overflow(context: TestContext) -> anyhow::Result<()> { + // MAX + 0 fits; only the input carry causes overflow. + let strategy = Just(U256::MAX).prop_map(|a| { + FuzzCase::new(a, U256::zero()) + .carry_in(true) + .expect(U256::zero()) + .expect_bool(true) + }); + + case_fuzz( + FullAdd256, + context, + "u256 input carry alone causes overflow", + ) + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u256/math/api.rs b/tests/stdlib/u256/math/api.rs index 3964165..a10ffe8 100644 --- a/tests/stdlib/u256/math/api.rs +++ b/tests/stdlib/u256/math/api.rs @@ -40,3 +40,53 @@ impl TestUint for U256Wrapper { // Stamps the 22 `#[simplex::test]` entry points for U256Wrapper. Logic lives in common::uint. crate::uint_tests!(U256Wrapper); + +mod u256_math_tests_fuzz { + use crate::common::uint_fuzz::TestUintFuzz; + + use super::*; + + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + impl TestUintFuzz for U256Wrapper { + type Arguments = U256MathTestArguments; + + const DIV_BY_ZERO_EXPECT: simplex::fuzz::fuzz_check::Expect = + simplex::fuzz::fuzz_check::Expect::AssertFailed; + + fn arguments() -> Self::Arguments { + U256MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::<[u8; 32]>() + .prop_map(|bytes| U256Wrapper(U256::from_big_endian(&bytes))) + .boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + Self::arb_any() + .prop_map(|value| U256Wrapper(value.0.max(U256::one()))) + .boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + + let range = high.0 - low.0; + + Self::arb_any() + .prop_map(move |value| { + if range == U256::MAX { + value + } else { + U256Wrapper(low.0 + value.0 % (range + U256::one())) + } + }) + .boxed() + } + } + + crate::uint_fuzz_tests!(U256Wrapper); +} diff --git a/tests/stdlib/u256/math/div.rs b/tests/stdlib/u256/math/div.rs index 7b40598..2dbdd62 100644 --- a/tests/stdlib/u256/math/div.rs +++ b/tests/stdlib/u256/math/div.rs @@ -438,3 +438,643 @@ fn div_mod_256_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> .expect([0; 32]) .expecting(&context, Expect::AssertFailed) } + +mod div_tests_fuzz { + use std::cmp::max; + + use crate::common::u256_wrapper::U256Wrapper; + use crate::common::uint_fuzz::{ + run_uint_fuzz, safe_div_by_zero_strategy, safe_div_fitting_strategy, + }; + + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_non_zero_u64() -> impl Strategy { + arb_u64().prop_map(|value| max(value, 1)) + } + + fn arb_non_zero_64bit_u256() -> impl Strategy { + arb_non_zero_u64().prop_map(U256::from) + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + arb_u128().prop_map(|value| max(value, 1)) + } + + fn arb_128bit_u256() -> impl Strategy { + arb_u128().prop_map(U256::from) + } + + fn arb_non_zero_128bit_u256() -> impl Strategy { + arb_non_zero_u128().prop_map(U256::from) + } + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u256() -> impl Strategy { + arb_u256().prop_map(|value| max(value, U256::one())) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + #[derive(Debug)] + struct FuzzCase { + first_arg: U256, + second_arg: U256, + expected: U256, + second_expected: U256, + } + + impl FuzzCase { + fn arg(first_arg: U256) -> Self { + Self::new(first_arg, U256::zero()) + } + + fn new(first_arg: U256, second_arg: U256) -> Self { + Self { + first_arg, + second_arg, + expected: U256::zero(), + second_expected: U256::zero(), + } + } + + fn expect(mut self, expected: U256) -> Self { + self.expected = expected; + self + } + + fn second_expected(mut self, second_expected: U256) -> Self { + self.second_expected = second_expected; + self + } + + fn into_witness(self, witness: U256TestDivWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.to_big_endian(), + self.second_arg.to_big_endian(), + ) + .expect(self.expected.to_big_endian()) + .second(self.second_expected.to_big_endian()) + .witness + .into() + } + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U256TestDivArguments {}.into(); + let witness = case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + // calculate_normalizer_base_128 - accepts custom inputs for the same operation. + // 2^128<=a<=MAX; expects ceil(2^127/a_high). + #[simplex::fuzz] + fn calculate_normalizer_base_128(context: TestContext) -> anyhow::Result<()> { + let threshold = U256::one() << 127; + let strategy = { + let a = arb_u256_in_range(U256::one() << 128, U256::MAX); + + a.prop_map(move |a| { + let high = (a >> 128).as_u128(); + let normalizer = threshold.as_u128().div_ceil(high); + + FuzzCase::arg(a).expect(U256::from(normalizer)) + }) + }; + + case_fuzz( + CalculateNormalizerBase128, + context, + "u256 calculate base 128 normalizer", + ) + .strategy(strategy) + .run() + } + + // calculate_normalizer_base_128_norm_is_1 - accepts custom inputs for the same operation. + // 2^255<=a<=MAX; expects normalizer=1. + #[simplex::fuzz] + fn calculate_normalizer_base_128_norm_is_1( + context: TestContext, + ) -> anyhow::Result<()> { + let threshold = U256::one() << 127; + let strategy = { + let a = arb_u256_in_range(U256::one() << 255, U256::MAX); + + a.prop_map(move |a| { + let high = (a >> 128).as_u128(); + let normalizer = threshold.as_u128().div_ceil(high); + + FuzzCase::arg(a).expect(U256::from(normalizer)) + }) + }; + + case_fuzz( + CalculateNormalizerBase128, + context, + "u256 calculate base 128 normalizer is one", + ) + .strategy(strategy) + .run() + } + + // calculate_normalizer_base_128_norm_greater_than_1 - accepts custom inputs for the same + // operation. + // 2^128<=a<2^255; expects normalizer>1. + #[simplex::fuzz] + fn calculate_normalizer_base_128_norm_greater_than_1( + context: TestContext, + ) -> anyhow::Result<()> { + let threshold = U256::one() << 127; + let strategy = { + let a = arb_u256_in_range(U256::one() << 128, (U256::one() << 255) - 1); + + a.prop_map(move |a| { + let high = (a >> 128).as_u128(); + let normalizer = threshold.as_u128().div_ceil(high); + + FuzzCase::arg(a).expect(U256::from(normalizer)) + }) + }; + + case_fuzz( + CalculateNormalizerBase128, + context, + "u256 calculate base 128 normalizer is greater than one", + ) + .strategy(strategy) + .run() + } + + // calculate_normalizer_base_128_a_is_u128_fail - accepts custom inputs for the same operation. + // 1<=a<=u128::MAX has a zero high half; expects assertion failure. + #[simplex::fuzz] + fn calculate_normalizer_base_128_a_is_u128_fail( + context: TestContext, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::one(), (U256::one() << 128) - 1); + + a.prop_map(FuzzCase::arg) + }; + + case_fuzz( + CalculateNormalizerBase128, + context, + "u256 calculate base 128 normalizer rejects u128", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // calculate_normalizer_base_128_b_is_zero_fail - accepts custom inputs for the same operation. + // a=0 is fixed with Just; expects assertion failure. + #[simplex::fuzz] + fn calculate_normalizer_base_128_b_is_zero_fail( + context: TestContext, + ) -> anyhow::Result<()> { + let strategy = Just(U256::zero()).prop_map(FuzzCase::arg); + + case_fuzz( + CalculateNormalizerBase128, + context, + "u256 calculate base 128 normalizer rejects zero", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // div_mod_256_64 - accepts custom inputs for the same operation. + // a is full-width u256; 1<=b<=u64::MAX; checks quotient and remainder. + #[simplex::fuzz] + fn div_mod_256_64(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_non_zero_64bit_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second_expected(a % b)) + }; + + case_fuzz(DivMod256_64, context, "u256 div mod by u64") + .strategy(strategy) + .run() + } + + // div_mod_256_64_div_by_zero - accepts custom inputs for the same operation. + // a is full-width u256 and b=0; this is division by zero, not arithmetic overflow. + #[simplex::fuzz] + fn div_mod_256_64_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(FuzzCase::arg); + + case_fuzz(DivMod256_64, context, "u256 div mod by u64 zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // algorithm_d_256_128 - accepts custom inputs for the same operation. + // a is full-width u256; u64::MAX) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_u256_in_range(U256::from(u64::MAX) + 1, U256::from(u128::MAX)); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second_expected(a % b)) + }; + + case_fuzz(AlgorithmD256_128, context, "u256 algorithm d by u128") + .strategy(strategy) + .run() + } + + // algorithm_d_256_128_fail_b_fits_into_u64 - accepts custom inputs for the same operation. + // a is u256; 1<=b<=u64::MAX violates the Algorithm D divisor-width precondition. + #[simplex::fuzz] + fn algorithm_d_256_128_fail_b_fits_into_u64( + context: TestContext, + ) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_non_zero_64bit_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second_expected(a % b)) + }; + + case_fuzz( + AlgorithmD256_128, + context, + "u256 algorithm d rejects u64 divisor", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // algorithm_d_256_128_div_by_zero - accepts custom inputs for the same operation. + // a is full-width u256 and b=0; this is division by zero, not arithmetic overflow. + #[simplex::fuzz] + fn algorithm_d_256_128_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(FuzzCase::arg); + + case_fuzz( + AlgorithmD256_128, + context, + "u256 algorithm d rejects zero divisor", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // algorithm_d_256_128_a_eq_b - accepts custom inputs for the same operation. + // u64::MAX) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::from(u64::MAX) + 1, U256::from(u128::MAX)); + + a.prop_map(|a| FuzzCase::new(a, a).expect(U256::one())) + }; + + case_fuzz( + AlgorithmD256_128, + context, + "u256 algorithm d equal arguments", + ) + .strategy(strategy) + .run() + } + + // div_mod_256_128 - accepts custom inputs for the same operation. + // a is u256; u64::MAX) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_u256_in_range(U256::from(u64::MAX) + 1, U256::from(u128::MAX)); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second_expected(a % b)) + }; + + case_fuzz(DivMod256_128, context, "u256 div mod by u128") + .strategy(strategy) + .run() + } + + // div_mod_256_128_b_fits_into_u64 - accepts custom inputs for the same operation. + // a is u256; 1<=b<=u64::MAX; exercises the fallback through the u128-divisor entry point. + #[simplex::fuzz] + fn div_mod_256_128_b_fits_into_u64(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b = arb_non_zero_64bit_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second_expected(a % b)) + }; + + case_fuzz(DivMod256_128, context, "u256 div mod by u64 through u128") + .strategy(strategy) + .run() + } + + // div_mod_256_128_div_by_zero - accepts custom inputs for the same operation. + // a is full-width u256 and b=0; this is division by zero, not arithmetic overflow. + #[simplex::fuzz] + fn div_mod_256_128_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(FuzzCase::arg); + + case_fuzz(DivMod256_128, context, "u256 div mod by u128 zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // div_mod_256_128_a_eq_b - accepts custom inputs for the same operation. + // 1<=a=b<=u128::MAX; expects q=1 and r=0. + #[simplex::fuzz] + fn div_mod_256_128_a_eq_b(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_128bit_u256(); + + a.prop_map(|a| FuzzCase::new(a, a).expect(U256::one())) + }; + + case_fuzz( + DivMod256_128, + context, + "u256 div mod by u128 equal arguments", + ) + .strategy(strategy) + .run() + } + + // div_mod_256_a_less_than_b - accepts custom inputs for the same operation. + // 1<=a) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::one(), U256::MAX - 1); + + a.prop_flat_map(|a| { + arb_u256_in_range(a + 1, U256::MAX) + .prop_map(move |b| FuzzCase::new(a, b).expect(a / b).second_expected(a % b)) + }) + }; + + case_fuzz(DivMod256, context, "u256 div mod a less than b") + .strategy(strategy) + .run() + } + + // div_mod_256_div_128 - accepts custom inputs for the same operation. + // 1<=b<=a<=u128::MAX; checks the small-operands branch. + #[simplex::fuzz] + fn div_mod_256_div_128(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let b = arb_non_zero_128bit_u256(); + + b.prop_flat_map(|b| { + arb_u256_in_range(b, U256::from(u128::MAX)) + .prop_map(move |a| FuzzCase::new(a, b).expect(a / b).second_expected(a % b)) + }) + }; + + case_fuzz(DivMod256, context, "u256 div mod u128 dividend") + .strategy(strategy) + .run() + } + + // div_mod_256_q_is_1 - accepts custom inputs for the same operation. + // a_high=b_high>0 and a_low>=b_low; expects q=1 and the exact remainder. + #[simplex::fuzz] + fn div_mod_256_q_is_1(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let b_low = arb_128bit_u256(); + let high = arb_non_zero_128bit_u256(); + + (b_low, high).prop_flat_map(|(b_low, high)| { + arb_u256_in_range(b_low, U256::from(u128::MAX)).prop_map(move |a_low| { + let a = (high << 128) | a_low; + let b = (high << 128) | b_low; + + FuzzCase::new(a, b).expect(a / b).second_expected(a % b) + }) + }) + }; + + case_fuzz(DivMod256, context, "u256 div mod quotient is one") + .strategy(strategy) + .run() + } + + // div_mod_256_b_fits_into_u128 - accepts custom inputs for the same operation. + // a>u128::MAX and 1<=b<=u128::MAX; checks the small-divisor branch. + #[simplex::fuzz] + fn div_mod_256_b_fits_into_u128(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::from(u128::MAX) + 1, U256::MAX); + let b = arb_non_zero_128bit_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a / b).second_expected(a % b)) + }; + + case_fuzz(DivMod256, context, "u256 div mod u128 divisor") + .strategy(strategy) + .run() + } + + // div_mod_256_b_is_u256 - accepts custom inputs for the same operation. + // 1<=b) -> anyhow::Result<()> { + let strategy = { + let b = arb_u256_in_range(U256::one(), U256::MAX - 1); + + b.prop_flat_map(|b| { + arb_u256_in_range(b + 1, U256::MAX) + .prop_map(move |a| FuzzCase::new(a, b).expect(a / b).second_expected(a % b)) + }) + }; + + case_fuzz(DivMod256, context, "u256 div mod u256 divisor") + .strategy(strategy) + .run() + } + + // div_mod_256_a_equal_b - accepts custom inputs for the same operation. + // a=b>0 across the full width; expects q=1 and r=0. + #[simplex::fuzz] + fn div_mod_256_a_equal_b(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + + a.prop_map(|a| FuzzCase::new(a, a).expect(U256::one())) + }; + + case_fuzz(DivMod256, context, "u256 div mod equal arguments") + .strategy(strategy) + .run() + } + + // div_mod_256_equal_high_words_max_low_diff - accepts custom inputs for the same operation. + // a_high=b_high>0, a_low=u128::MAX, b_low=0; expects q=1 and r=u128::MAX. + #[simplex::fuzz] + fn div_mod_256_equal_high_words_max_low_diff( + context: TestContext, + ) -> anyhow::Result<()> { + let strategy = { + let high = arb_non_zero_128bit_u256(); + + high.prop_map(|high| { + let a = (high << 128) | U256::from(u128::MAX); + let b = high << 128; + + FuzzCase::new(a, b) + .expect(U256::one()) + .second_expected(U256::from(u128::MAX)) + }) + }; + + case_fuzz( + DivMod256, + context, + "u256 div mod equal high words maximum low difference", + ) + .strategy(strategy) + .run() + } + + // div_mod_256_eq_high_words_a_less_than_b - accepts custom inputs for the same operation. + // a_high=b_high>0, a_low=0, b_low=u128::MAX; expects q=0 and r=a. + #[simplex::fuzz] + fn div_mod_256_eq_high_words_a_less_than_b( + context: TestContext, + ) -> anyhow::Result<()> { + let strategy = { + let high = arb_non_zero_128bit_u256(); + + high.prop_map(|high| { + let a = high << 128; + let b = (high << 128) | U256::from(u128::MAX); + + FuzzCase::new(a, b).second_expected(a) + }) + }; + + case_fuzz( + DivMod256, + context, + "u256 div mod equal high words a less than b", + ) + .strategy(strategy) + .run() + } + + // No direct simplex::test counterpart - additional general property. + // Additional API division property, not the original fixed div_mod_256_edge_case; a is u256 and + // b>0. + #[simplex::fuzz] + fn div_256(context: TestContext) -> anyhow::Result<()> { + run_uint_fuzz::( + context, + safe_div_fitting_strategy::(), + Expect::Ok, + ) + } + + // div_mod_256_div_by_zero - accepts custom inputs for the same operation. + // Uses API div_256, which calls div_mod_256 indirectly; a is u256, b=0, and assertion failure + // is expected. + #[simplex::fuzz] + fn div_256_div_by_zero(context: TestContext) -> anyhow::Result<()> { + run_uint_fuzz::( + context, + safe_div_by_zero_strategy::(), + Expect::AssertFailed, + ) + } +} diff --git a/tests/stdlib/u256/math/sub_mul.rs b/tests/stdlib/u256/math/sub_mul.rs index 3773232..68bea32 100644 --- a/tests/stdlib/u256/math/sub_mul.rs +++ b/tests/stdlib/u256/math/sub_mul.rs @@ -274,3 +274,429 @@ fn mul_256_128_overflow(context: simplex::TestContext) -> anyhow::Result<()> { .expect([0; 32]) .expecting(&context, Expect::AssertFailed) } + +mod sub_mul_tests_fuzz { + use std::cmp::max; + + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + const BORROW_TRUE: bool = true; + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_non_zero_u64() -> impl Strategy { + arb_u64().prop_map(|value| max(value, 1)) + } + + fn arb_non_zero_64bit_u256() -> impl Strategy { + arb_non_zero_u64().prop_map(U256::from) + } + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + arb_u128().prop_map(|value| max(value, 1)) + } + + fn arb_128bit_u256() -> impl Strategy { + arb_u128().prop_map(U256::from) + } + + fn arb_non_zero_128bit_u256() -> impl Strategy { + arb_non_zero_u128().prop_map(U256::from) + } + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u256() -> impl Strategy { + arb_u256().prop_map(|value| max(value, U256::one())) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + #[derive(Debug)] + struct FuzzCase { + first_arg: U256, + second_arg: U256, + third_arg: u128, + expected: U256, + expected_bool: bool, + second_expected: U256, + third_expected: U256, + } + + impl FuzzCase { + fn new(first_arg: U256, second_arg: U256) -> Self { + Self { + first_arg, + second_arg, + third_arg: 0, + expected: U256::zero(), + expected_bool: false, + second_expected: U256::zero(), + third_expected: U256::zero(), + } + } + + fn third_arg(mut self, third_arg: u128) -> Self { + self.third_arg = third_arg; + self + } + + fn expect(mut self, expected: U256) -> Self { + self.expected = expected; + self + } + + fn expect_bool(mut self, expected_bool: bool) -> Self { + self.expected_bool = expected_bool; + self + } + + fn second(mut self, second_expected: U256) -> Self { + self.second_expected = second_expected; + self + } + + fn third(mut self, third_expected: U256) -> Self { + self.third_expected = third_expected; + self + } + + fn into_witness(self, witness: U256TestSubMulWitness) -> WitnessValues { + Case { witness } + .args( + self.first_arg.to_big_endian(), + self.second_arg.to_big_endian(), + ) + .third_arg(self.third_arg) + .expect(self.expected.to_big_endian()) + .flag(self.expected_bool) + .second(self.second_expected.to_big_endian()) + .third(self.third_expected.to_big_endian()) + .witness + .into() + } + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |case| { + let arguments: Arguments = U256TestSubMulArguments {}.into(); + let witness = case.into_witness(witness.clone()); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + // sub_256_borrow_not_overflow - accepts custom inputs for the same operation. + // 0<=b<=a<=MAX; expects a-b and borrow_out=false. + #[simplex::fuzz] + fn sub_256_not_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + + a.prop_flat_map(|a| { + arb_u256_in_range(U256::zero(), a).prop_map(move |b| { + let result = a - b; + + FuzzCase::new(a, b).expect(result) + }) + }) + }; + + case_fuzz(Sub256Borrow, context, "u256 subtraction without overflow") + .strategy(strategy) + .run() + } + + // sub_256_borrow_a_eq_b - accepts custom inputs for the same operation. + // a=b across the full width; expects result=0 and borrow_out=false. + #[simplex::fuzz] + fn sub_256_a_eq_b(context: TestContext) -> anyhow::Result<()> { + let strategy = arb_u256().prop_map(|a| FuzzCase::new(a, a).expect(U256::zero())); + + case_fuzz(Sub256Borrow, context, "u256 subtraction equal operands") + .strategy(strategy) + .run() + } + + // sub_256_borrow_a_low_eq_b_low - accepts custom inputs for the same operation. + // a and b share their low u128 half; high halves vary independently. + #[simplex::fuzz] + fn sub_256_a_low_eq_b_low(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256(); + let b_high = arb_u128(); + + (a, b_high).prop_map(|(a, b_high)| { + let low = U256::from(a.low_u128()); + let b = (U256::from(b_high) << 128) | low; + let (result, borrow) = a.overflowing_sub(b); + + FuzzCase::new(a, b).expect(result).expect_bool(borrow) + }) + }; + + case_fuzz(Sub256Borrow, context, "u256 subtraction equal low halves") + .strategy(strategy) + .run() + } + + // sub_256_borrow_diff_is_u128_max - accepts custom inputs for the same operation. + // a_low=u128::MAX and b_low=0; high halves vary independently. + #[simplex::fuzz] + fn sub_256_diff_is_u128_max(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a_high = arb_u128(); + let b_high = arb_u128(); + + (a_high, b_high).prop_map(|(a_high, b_high)| { + let a = (U256::from(a_high) << 128) | U256::from(u128::MAX); + let b = U256::from(b_high) << 128; + let (result, borrow) = a.overflowing_sub(b); + + FuzzCase::new(a, b).expect(result).expect_bool(borrow) + }) + }; + + case_fuzz( + Sub256Borrow, + context, + "u256 subtraction u128 max difference", + ) + .strategy(strategy) + .run() + } + + // sub_256_borrow_overflow - accepts custom inputs for the same operation. + // 1<=a) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::one(), U256::MAX - 1); + let b = U256::MAX; + + a.prop_map(move |a| { + let result = a + U256::one(); + + FuzzCase::new(a, b).expect(result).expect_bool(BORROW_TRUE) + }) + }; + + case_fuzz(Sub256Borrow, context, "u256 subtraction overflow") + .strategy(strategy) + .run() + } + + // mul_256_wide - accepts custom inputs for the same operation. + // Both u256 operands are nonzero; checks high and low product halves against U512. + #[simplex::fuzz] + fn mul_256(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + let b = arb_non_zero_u256(); + + (a, b).prop_map(|(a, b)| { + let (high, low) = split_u512(a.full_mul(b).to_big_endian()); + let high = U256::from_big_endian(&high); + let low = U256::from_big_endian(&low); + + FuzzCase::new(a, b).expect(high).second(low) + }) + }; + + case_fuzz(Mul256Wide, context, "u256 multiplication") + .strategy(strategy) + .run() + } + + // mul_256_64_wide - accepts custom inputs for the same operation. + // a is nonzero u256 and 1<=b<=u64::MAX; checks the widened product. + #[simplex::fuzz] + fn mul_256_64(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + let b = arb_non_zero_64bit_u256(); + + (a, b).prop_map(|(a, b)| { + let (high, low) = split_u512(a.full_mul(b).to_big_endian()); + let high = U256::from_big_endian(&high); + let low = U256::from_big_endian(&low); + + FuzzCase::new(a, b).expect(high).second(low) + }) + }; + + case_fuzz(Mul256_64Wide, context, "u256 by u64 multiplication") + .strategy(strategy) + .run() + } + + // mul_256_128_wide - accepts custom inputs for the same operation. + // a is nonzero u256 and 1<=b<=u128::MAX; checks the widened product. + #[simplex::fuzz] + fn mul_256_128(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + let b = arb_non_zero_128bit_u256(); + + (a, b).prop_map(|(a, b)| { + let (high, low) = split_u512(a.full_mul(b).to_big_endian()); + let high = U256::from_big_endian(&high); + let low = U256::from_big_endian(&low); + + FuzzCase::new(a, b).expect(high).second(low) + }) + }; + + case_fuzz(Mul256_128Wide, context, "u256 by u128 multiplication") + .strategy(strategy) + .run() + } + + // mul_512_128_wide - accepts custom inputs for the same operation. + // Both u256 dividend halves and the u128 multiplier are nonzero; checks all three result words. + #[simplex::fuzz] + fn mul_512_128(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a_1 = arb_non_zero_u256(); + let a_0 = arb_non_zero_u256(); + let b = 1..=u128::MAX; + + (a_1, a_0, b).prop_map(|(a_1, a_0, b)| { + let result_low = U512::from(a_0) * U512::from(b); + let result_high = U512::from(a_1) * U512::from(b); + + let (res_1, res_0) = split_u512(result_low.to_big_endian()); + let (res_3, res_2) = split_u512(result_high.to_big_endian()); + + let res_2_1 = U512::from_big_endian(&res_1) + U512::from_big_endian(&res_2); + let (res_3_1, res_2_1) = split_u512(res_2_1.to_big_endian()); + let res_3_final = U256::from_big_endian(&res_3_1) + U256::from_big_endian(&res_3); + let res_2_1 = U256::from_big_endian(&res_2_1); + let res_0 = U256::from_big_endian(&res_0); + + FuzzCase::new(a_1, a_0) + .third_arg(b) + .expect(res_3_final) + .second(res_2_1) + .third(res_0) + }) + }; + + case_fuzz(Mul512_128Wide, context, "u512 by u128 multiplication") + .strategy(strategy) + .run() + } + + // mul_256_128_fitting - accepts custom inputs for the same operation. + // 0<=a,b<=u128::MAX ensures the product fits u256. + #[simplex::fuzz] + fn safe_mul_256_128_fitting(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_128bit_u256(); + let b = arb_128bit_u256(); + + (a, b).prop_map(|(a, b)| FuzzCase::new(a, b).expect(a * b)) + }; + + case_fuzz( + Mul256_128, + context, + "safe u256 by u128 multiplication fitting", + ) + .strategy(strategy) + .run() + } + + // mul_256_128_overflow - accepts custom inputs for the same operation. + // b=u128::MAX and a>=MAX/b+1; tighter than the original lower bound, guaranteeing overflow. + #[simplex::fuzz] + fn safe_mul_256_128_overflow(context: TestContext) -> anyhow::Result<()> { + let b = U256::from(u128::MAX); + let first_overflowing_a = U256::MAX / b + U256::one(); + let strategy = arb_u256_in_range(first_overflowing_a, U256::MAX) + .prop_map(move |a| FuzzCase::new(a, b)); + + case_fuzz( + Mul256_128, + context, + "safe u256 by u128 multiplication overflow", + ) + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u256/mul_div.rs b/tests/stdlib/u256/mul_div.rs index 3727bbf..6df65f9 100644 --- a/tests/stdlib/u256/mul_div.rs +++ b/tests/stdlib/u256/mul_div.rs @@ -260,3 +260,410 @@ fn mul_div_256_normalize_to_threshold_512_127_norm_greater_than_1( .expect(res) .run(&context) } + +mod mul_div_tests_fuzz { + use primitive_types::U512; + + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + // (A, B, C, Result) + type MulDivInputs = (U256, U256, U256, U256); + + fn arb_u128() -> impl Strategy { + any::() + } + + fn arb_non_zero_u128() -> impl Strategy { + arb_u128().prop_map(|value| max(value, 1)) + } + + fn arb_128bit_u256() -> impl Strategy { + arb_u128().prop_map(U256::from) + } + + fn arb_non_zero_128bit_u256() -> impl Strategy { + arb_non_zero_u128().prop_map(U256::from) + } + + fn arb_u256() -> impl Strategy { + any::<[u8; 32]>().prop_map(|bytes| U256::from_big_endian(&bytes)) + } + + fn arb_non_zero_u256() -> impl Strategy { + arb_u256().prop_map(|value| max(value, U256::one())) + } + + fn arb_u256_in_range(low: U256, high: U256) -> impl Strategy { + assert!(low <= high); + + let range = high - low; + + arb_u256().prop_map(move |value| { + if range == U256::MAX { + value + } else { + low + value % (range + U256::one()) + } + }) + } + + #[inline] + fn safe_u512_to_u256_typed(a: U512) -> U256 { + let a = a.to_big_endian(); + let high = U256::from_big_endian(&a[0..32]); + let low = U256::from_big_endian(&a[32..64]); + + assert_eq!(high, U256::zero()); + + low + } + + fn product_high(a: U256, b: U256) -> U256 { + safe_u512_to_u256_typed(a.full_mul(b) >> 256) + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> CaseFuzz { + CaseFuzz { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + + let strategy = inputs + .prop_map(move |(a, b, c, expected)| { + ( + a.to_big_endian(), + b.to_big_endian(), + c.to_big_endian(), + expected.to_big_endian(), + ) + }) + .prop_map(move |(a, b, c, expected)| { + let arguments: Arguments = U256MulDivTestArguments {}.into(); + + let case = Case { + witness: witness.clone(), + } + .args(a, b, c); + + let case = if expect_failure { + case + } else { + case.expect(expected) + }; + + let witness: WitnessValues = case.witness.into(); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + // mul_div_256_product_fits_into_u256 - accepts custom inputs for the same operation. + // 0<=a,b<=u128::MAX and c>0; checks floor(a*b/c) with a U512 reference. + #[simplex::fuzz] + fn mul_div_256_product_fits_into_u256(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_128bit_u256(); + let b = arb_128bit_u256(); + let c = arb_non_zero_u256(); + + (a, b, c).prop_map(|(a, b, c)| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }) + }; + + case_fuzz(MulDiv, context, "mul_div_256 product fits into u256") + .strategy(strategy) + .run() + } + + // mul_div_256_intermediate_overflow - accepts custom inputs for the same operation. + // 2<=a<=MAX, b=MAX, a<=c<=MAX; product exceeds MAX but quotient fits. + #[simplex::fuzz] + fn mul_div_256_intermediate_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::from(2), U256::MAX); + let b = Just(U256::MAX); + + (a, b).prop_flat_map(|(a, b)| { + arb_u256_in_range(a, U256::MAX).prop_map(move |c| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }) + }) + }; + + case_fuzz(MulDiv, context, "mul_div_256 intermediate overflow") + .strategy(strategy) + .run() + } + + // mul_div_256_result_overflow - accepts custom inputs for the same operation. + // 2<=a<=MAX, b=MAX, 1<=c) -> anyhow::Result<()> { + let strategy = { + let a = arb_u256_in_range(U256::from(2), U256::MAX); + let b = Just(U256::MAX); + + (a, b).prop_flat_map(|(a, b)| { + arb_u256_in_range(U256::one(), a - U256::one()) + .prop_map(move |c| (a, b, c, U256::zero())) + }) + }; + + case_fuzz(MulDiv, context, "mul_div_256 result overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // mul_div_256_remainder_is_zero - accepts custom inputs for the same operation. + // 2^128<=a,b<=MAX and c=a; division is exact and expected result=b. + #[simplex::fuzz] + fn mul_div_256_remainder_is_zero(context: TestContext) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + let strategy = { + let a = arb_u256_in_range(pow2_128, U256::MAX); + let b = arb_u256_in_range(pow2_128, U256::MAX); + + (a, b).prop_map(|(a, b)| (a, b, a, b)) + }; + + case_fuzz(MulDiv, context, "mul_div_256 remainder is zero") + .strategy(strategy) + .run() + } + + // mul_div_256_denominator_is_u128 - accepts custom inputs for the same operation. + // a>0 and 1<=b<=c<=u128::MAX; quotient is guaranteed to fit u256. + #[simplex::fuzz] + fn mul_div_256_denominator_is_u128(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + let b = arb_non_zero_128bit_u256(); + + (a, b).prop_flat_map(|(a, b)| { + arb_u256_in_range(b, U256::from(u128::MAX)).prop_map(move |c| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }) + }) + }; + + case_fuzz(MulDiv, context, "mul_div_256 denominator is u128") + .strategy(strategy) + .run() + } + + // mul_div_256_min_denom_high - accepts custom inputs for the same operation. + // 2^128<=a,b<=2^129 and 2^128<=c<2^129; checks a divisor with the smallest nonzero high half. + #[simplex::fuzz] + fn mul_div_256_min_denom_high(context: TestContext) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + let pow2_129 = U256::one() << 129; + let strategy = { + let a = arb_u256_in_range(pow2_128, pow2_129); + let b = arb_u256_in_range(pow2_128, pow2_129); + let c = arb_u256_in_range(pow2_128, pow2_129 - U256::one()); + + (a, b, c).prop_map(|(a, b, c)| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }) + }; + + case_fuzz(MulDiv, context, "mul_div_256 minimal denominator high") + .strategy(strategy) + .run() + } + + // mul_div_256_div_by_zero - accepts custom inputs for the same operation. + // a,b>0 and c=0; expects an assertion failure; the failure witness has no expected result. + #[simplex::fuzz] + fn mul_div_256_div_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = { + let a = arb_non_zero_u256(); + let b = arb_non_zero_u256(); + + (a, b).prop_map(|(a, b)| (a, b, U256::zero(), U256::zero())) + }; + + case_fuzz(MulDiv, context, "mul_div_256 division by zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + // mul_div_256_algorithm_d_512_256_check - accepts custom inputs for the same operation. + // a,b>=2^128, b<=MAX-2^128, c>=max(2^128,product_high+1); quotient fits. + #[simplex::fuzz] + fn mul_div_256_algorithm_d_512_256_check(context: TestContext) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + let strategy = { + let a = arb_u256_in_range(pow2_128, U256::MAX); + let b = arb_u256_in_range(pow2_128, U256::MAX - pow2_128); + + (a, b).prop_flat_map(move |(a, b)| { + let min_c = max(pow2_128, product_high(a, b) + U256::one()); + + arb_u256_in_range(min_c, U256::MAX).prop_map(move |c| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }) + }) + }; + + case_fuzz(MulDiv, context, "mul_div_256 algorithm D") + .strategy(strategy) + .run() + } + + // mul_div_256_algorithm_d_512_256_c_is_res_high - accepts custom inputs for the same operation. + // a,b>=2^128, b<=MAX-2^128, c=product_high+1; tests the tight fitting-divisor boundary. + #[simplex::fuzz] + fn mul_div_256_algorithm_d_512_256_c_is_res_high( + context: TestContext, + ) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + + let strategy = { + let a = arb_u256_in_range(pow2_128, U256::MAX); + let b = arb_u256_in_range(pow2_128, U256::MAX - pow2_128); + + (a, b).prop_map(|(a, b)| { + let c = product_high(a, b) + U256::one(); + + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + + (a, b, c, expected) + }) + }; + + case_fuzz( + MulDiv, + context, + "mul_div_256 denominator just above product high", + ) + .strategy(strategy) + .run() + } + + // mul_div_256_normalize_to_threshold_512_127_norm_is_1 - accepts custom inputs for the same + // operation. + // a,b>=2^128, b<=MAX-2^128, c>=max(2^255,product_high+1); normalization factor is one. + #[simplex::fuzz] + fn mul_div_256_normalize_to_threshold_512_127_norm_is_1( + context: TestContext, + ) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + let pow2_255 = U256::one() << 255; + + let strategy = { + let a = arb_u256_in_range(pow2_128, U256::MAX); + let b = arb_u256_in_range(pow2_128, U256::MAX - pow2_128); + + (a, b).prop_flat_map(move |(a, b)| { + let min_c = max(pow2_255, product_high(a, b) + U256::one()); + + arb_u256_in_range(min_c, U256::MAX).prop_map(move |c| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + (a, b, c, expected) + }) + }) + }; + + case_fuzz(MulDiv, context, "mul_div_256 normalizer is one") + .strategy(strategy) + .run() + } + + // mul_div_256_normalize_to_threshold_512_127_norm_greater_than_1 - accepts custom inputs for + // the same operation. + // a>=2^128, 2^128<=b<=2^192, max(2^128,product_high+1)<=c<2^255; normalization factor exceeds + // one. + #[simplex::fuzz] + fn mul_div_256_normalize_to_threshold_512_127_norm_greater_than_1( + context: TestContext, + ) -> anyhow::Result<()> { + let pow2_128 = U256::one() << 128; + let pow2_192 = U256::one() << 192; + let pow2_255 = U256::one() << 255; + + let strategy = { + let a = arb_u256_in_range(pow2_128, U256::MAX); + let b = arb_u256_in_range(pow2_128, pow2_192); + + (a, b).prop_flat_map(move |(a, b)| { + let min_c = max(pow2_128, product_high(a, b) + U256::one()); + + arb_u256_in_range(min_c, pow2_255 - U256::one()).prop_map(move |c| { + let expected = safe_u512_to_u256_typed(a.full_mul(b).div(c)); + (a, b, c, expected) + }) + }) + }; + + case_fuzz( + MulDiv, + context, + "mul_div_256 normalizer is greater than one", + ) + .strategy(strategy) + .run() + } +} diff --git a/tests/stdlib/u32/convert.rs b/tests/stdlib/u32/convert.rs index ccf0cf9..d5275f4 100644 --- a/tests/stdlib/u32/convert.rs +++ b/tests/stdlib/u32/convert.rs @@ -173,3 +173,188 @@ fn safe_u32_to_u16_overflow(context: simplex::TestContext) -> anyhow::Result<()> .expect(U256::from(a).to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + // (first_arg) + type ConvertInputs = u32; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_u32() -> impl Strategy { + any::() + } + + fn arb_bool_u32() -> impl Strategy { + arb_bool().prop_map(u32::from) + } + + fn arb_u8_u32() -> impl Strategy { + arb_u8().prop_map(u32::from) + } + + fn arb_u16_u32() -> impl Strategy { + arb_u16().prop_map(u32::from) + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |a| { + let arguments: Arguments = U32ConvertTestArguments {}.into(); + + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u32_to_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U32ToU64, context, "u32_to_u64") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn u32_to_u128(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U32ToU128, context, "u32_to_u128") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn u32_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U32ToU256, context, "u32_to_u256") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn split_u32_into_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU32IntoU8, context, "split_u32_into_u8") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn split_u32_into_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU32IntoU16, context, "split_u32_into_u16") + .strategy(arb_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU1, context, "safe_u32_to_u1") + .strategy(arb_bool_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u1_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU1, context, "safe_u32_to_u1_overflow") + .strategy(2u32..=u32::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU8, context, "safe_u32_to_u8") + .strategy(arb_u8_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u8_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU8, context, "safe_u32_to_u8_overflow") + .strategy(u8::MAX as u32 + 1..=u32::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU16, context, "safe_u32_to_u16") + .strategy(arb_u16_u32()) + .run() + } + + #[simplex::fuzz] + fn safe_u32_to_u16_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU32ToU16, context, "safe_u32_to_u16_overflow") + .strategy(u16::MAX as u32 + 1..=u32::MAX) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u32/math.rs b/tests/stdlib/u32/math.rs index c25a816..af3d20c 100644 --- a/tests/stdlib/u32/math.rs +++ b/tests/stdlib/u32/math.rs @@ -32,3 +32,35 @@ impl TestUint for u32 { // Stamps the 22 `#[simplex::test]` entry points for u32. Logic lives in common::uint. crate::uint_tests!(u32); + +mod math_tests_fuzz { + use crate::common::uint_fuzz::TestUintFuzz; + + use super::*; + + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + impl TestUintFuzz for u32 { + type Arguments = U32MathTestArguments; + + fn arguments() -> Self::Arguments { + U32MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::().boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + any::().prop_map(|value| value.max(1)).boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + (low..=high).boxed() + } + } + + crate::uint_fuzz_tests!(u32); +} diff --git a/tests/stdlib/u32/mul_div.rs b/tests/stdlib/u32/mul_div.rs index ccec474..fb12494 100644 --- a/tests/stdlib/u32/mul_div.rs +++ b/tests/stdlib/u32/mul_div.rs @@ -108,3 +108,181 @@ fn mul_div_32_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod mul_div_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_non_zero_u32() -> impl Strategy { + any::().prop_filter("u32 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_numerator: Option, + second_numerator: Option, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn from_numerator(first_numerator: u32) -> Self { + let mut x = Self::default(); + let _ = x.first_numerator.insert(first_numerator); + x + } + + fn mul(mut self, second_numerator: u32) -> Self { + let _ = self.second_numerator.insert(second_numerator); + self + } + + fn div(mut self, denominator: u32) -> Self { + let _ = self.denominator.insert(denominator); + self + } + + fn expect(mut self, expected: u32) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness( + self, + witness: U32MulDivTestWitness, + expect_failure: bool, + ) -> WitnessValues { + let case = Case { witness }.args( + self.first_numerator.expect("no first numerator in witness"), + self.second_numerator + .expect("no second numerator in witness"), + self.denominator.expect("no denominator in witness"), + ); + + let case = if expect_failure { + case + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(context: TestContext, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U32MulDivTestArguments {}.into(); + let witness: WitnessValues = + fuzz_case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_32_product_is_u32(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_u16(), arb_u16(), arb_non_zero_u32()).prop_map(|(a, b, c)| { + let a = u32::from(a); + let b = u32::from(b); + + FuzzCase::from_numerator(a).mul(b).div(c).expect(a * b / c) + }); + + case_fuzz(context, "mul_div_32_product_is_u32") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_32_intermediate_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2u32..=u32::MAX).prop_flat_map(|a| { + (a..=u32::MAX).prop_map(move |c| { + let b = u32::MAX; + let expected = ((u64::from(a) * u64::from(b)) / u64::from(c)) as u32; + + FuzzCase::from_numerator(a).mul(b).div(c).expect(expected) + }) + }); + + case_fuzz(context, "mul_div_32_intermediate_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_32_result_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2u32..=u32::MAX).prop_flat_map(|a| { + (1u32..a).prop_map(move |c| FuzzCase::from_numerator(a).mul(u32::MAX).div(c)) + }); + + case_fuzz(context, "mul_div_32_result_overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn mul_div_32_div_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u32(), arb_non_zero_u32()) + .prop_map(|(a, b)| FuzzCase::from_numerator(a).mul(b).div(0).expect(0)); + + case_fuzz(context, "mul_div_32_div_by_zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u64/convert.rs b/tests/stdlib/u64/convert.rs index 78a6289..4c93c88 100644 --- a/tests/stdlib/u64/convert.rs +++ b/tests/stdlib/u64/convert.rs @@ -194,3 +194,208 @@ fn safe_u64_to_u32_overflow(context: simplex::TestContext) -> anyhow::Result<()> .expect(U256::from(a).to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |a| { + let arguments: Arguments = U64ConvertTestArguments {}.into(); + + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_u16() -> impl Strategy { + any::() + } + + fn arb_u32() -> impl Strategy { + any::() + } + + fn arb_u64() -> impl Strategy { + any::() + } + + fn arb_bool_u64() -> impl Strategy { + arb_bool().prop_map(u64::from) + } + + fn arb_u8_u64() -> impl Strategy { + arb_u8().prop_map(u64::from) + } + + fn arb_u16_u64() -> impl Strategy { + arb_u16().prop_map(u64::from) + } + + fn arb_u32_u64() -> impl Strategy { + arb_u32().prop_map(u64::from) + } + + #[simplex::fuzz] + fn u64_to_u128(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U64ToU128, context, "u64 to u128") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn u64_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U64ToU256, context, "u64 to u256") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn split_u64_into_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU64IntoU8, context, "split u64 into u8") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn split_u64_into_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU64IntoU16, context, "split u64 into u16") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn split_u64_into_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU64IntoU32, context, "split u64 into u32") + .strategy(arb_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU1, context, "safe u64 to u1") + .strategy(arb_bool_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u1_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU1, context, "safe u64 to u1 overflow") + .strategy(2u64..=u64::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u8(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU8, context, "safe u64 to u8") + .strategy(arb_u8_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u8_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU8, context, "safe u64 to u8 overflow") + .strategy((u64::from(u8::MAX) + 1)..=u64::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU16, context, "safe u64 to u16") + .strategy(arb_u16_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u16_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU16, context, "safe u64 to u16 overflow") + .strategy((u64::from(u16::MAX) + 1)..=u64::MAX) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU32, context, "safe u64 to u32") + .strategy(arb_u32_u64()) + .run() + } + + #[simplex::fuzz] + fn safe_u64_to_u32_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU64ToU32, context, "safe u64 to u32 overflow") + .strategy((u64::from(u32::MAX) + 1)..=u64::MAX) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u64/math.rs b/tests/stdlib/u64/math.rs index 327894e..4364c3b 100644 --- a/tests/stdlib/u64/math.rs +++ b/tests/stdlib/u64/math.rs @@ -32,3 +32,35 @@ impl TestUint for u64 { // Stamps the 22 `#[simplex::test]` entry points for u64. Logic lives in common::uint. crate::uint_tests!(u64); + +mod math_tests_fuzz { + use crate::common::uint_fuzz::TestUintFuzz; + + use super::*; + + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + impl TestUintFuzz for u64 { + type Arguments = U64MathTestArguments; + + fn arguments() -> Self::Arguments { + U64MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::().boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + any::().prop_map(|value| value.max(1)).boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + (low..=high).boxed() + } + } + + crate::uint_fuzz_tests!(u64); +} diff --git a/tests/stdlib/u64/mul_div.rs b/tests/stdlib/u64/mul_div.rs index 918661b..cc64f25 100644 --- a/tests/stdlib/u64/mul_div.rs +++ b/tests/stdlib/u64/mul_div.rs @@ -105,3 +105,182 @@ fn mul_div_64_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod mul_div_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::{Just, any}; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + fn arb_u32() -> impl Strategy { + any::() + } + + fn arb_non_zero_u64() -> impl Strategy { + any::().prop_filter("u64 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_numerator: Option, + second_numerator: Option, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn from_numerator(first_numerator: u64) -> Self { + let mut x = Self::default(); + let _ = x.first_numerator.insert(first_numerator); + x + } + + fn mul(mut self, second_numerator: u64) -> Self { + let _ = self.second_numerator.insert(second_numerator); + self + } + + fn div(mut self, denominator: u64) -> Self { + let _ = self.denominator.insert(denominator); + self + } + + fn expect(mut self, expected: u64) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness( + self, + witness: U64MulDivTestWitness, + expect_failure: bool, + ) -> WitnessValues { + let case = Case { witness }.args( + self.first_numerator.expect("no first numerator in witness"), + self.second_numerator + .expect("no second numerator in witness"), + self.denominator.expect("no denominator in witness"), + ); + + let case = if expect_failure { + case + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + + case.witness.into() + } + } + + struct CaseFuzz { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(context: TestContext, test_name: &'static str) -> CaseFuzz { + CaseFuzz { + case: case(MulDiv), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl CaseFuzz { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U64MulDivTestArguments {}.into(); + let witness: WitnessValues = + fuzz_case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_64_product_is_u64(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_u32(), arb_u32(), arb_non_zero_u64()).prop_map(|(a, b, c)| { + let (a, b) = (u64::from(a), u64::from(b)); + + FuzzCase::from_numerator(a).mul(b).div(c).expect(a * b / c) + }); + + case_fuzz(context, "mul_div_64_product_is_u64") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_64_intermediate_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2..=u64::MAX).prop_flat_map(|a| { + (a..=u64::MAX).prop_map(move |c| { + let expected = (u128::from(a) * u128::from(u64::MAX) / u128::from(c)) as u64; + + FuzzCase::from_numerator(a) + .mul(u64::MAX) + .div(c) + .expect(expected) + }) + }); + + case_fuzz(context, "mul_div_64_intermediate_overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_64_result_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2..=u64::MAX).prop_flat_map(|a| { + (1..a).prop_map(move |c| FuzzCase::from_numerator(a).mul(u64::MAX).div(c)) + }); + + case_fuzz(context, "mul_div_64_result_overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn mul_div_64_div_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u64(), arb_non_zero_u64(), Just(0u64)) + .prop_map(|(a, b, c)| FuzzCase::from_numerator(a).mul(b).div(c).expect(0)); + + case_fuzz(context, "mul_div_64_div_by_zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u8/convert.rs b/tests/stdlib/u8/convert.rs index d536dd8..5d3cbfc 100644 --- a/tests/stdlib/u8/convert.rs +++ b/tests/stdlib/u8/convert.rs @@ -142,3 +142,146 @@ fn safe_u8_to_u1_overflow(context: simplex::TestContext) -> anyhow::Result<()> { .expect(U256::from(a).to_big_endian()) .expecting(&context, Expect::AssertFailed) } + +mod convert_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz( + function: FunctionToTest, + context: TestContext, + test_name: &'static str, + ) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(function), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + fn arb_u8() -> impl Strategy { + any::() + } + + fn arb_bool() -> impl Strategy { + any::() + } + + fn arb_bool_u8() -> impl Strategy { + arb_bool().prop_map(u8::from) + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + + let strategy = inputs + .prop_map(move |a| { + let arguments: Arguments = U8ConvertTestArguments {}.into(); + + let witness: WitnessValues = Case { + witness: witness.clone(), + } + .arg(a) + .expect(U256::from(a).to_big_endian()) + .witness + .into(); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + #[simplex::fuzz] + fn u8_to_u16(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U8ToU16, context, "u8 to u16") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u32(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U8ToU32, context, "u8 to u32") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u64(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U8ToU64, context, "u8 to u64") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u128(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U8ToU128, context, "u8 to u128") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn u8_to_u256(context: TestContext) -> anyhow::Result<()> { + case_fuzz(U8ToU256, context, "u8 to u256") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn split_u8_into_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SplitU8IntoU1, context, "split u8 into u1") + .strategy(arb_u8()) + .run() + } + + #[simplex::fuzz] + fn safe_u8_to_u1(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU8ToU1, context, "safe u8 to u1") + .strategy(arb_bool_u8()) + .run() + } + + #[simplex::fuzz] + fn safe_u8_to_u1_overflow(context: TestContext) -> anyhow::Result<()> { + case_fuzz(SafeU8ToU1, context, "safe u8 to u1 overflow") + .strategy(2u8..=u8::MAX) + .expect(Expect::AssertFailed) + .run() + } +} diff --git a/tests/stdlib/u8/math.rs b/tests/stdlib/u8/math.rs index cefd1b8..e7378f5 100644 --- a/tests/stdlib/u8/math.rs +++ b/tests/stdlib/u8/math.rs @@ -32,3 +32,35 @@ impl TestUint for u8 { // Stamps the 22 `#[simplex::test]` entry points for u8. Logic lives in common::uint. crate::uint_tests!(u8); + +mod math_tests_fuzz { + use crate::common::uint_fuzz::TestUintFuzz; + + use super::*; + + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + + impl TestUintFuzz for u8 { + type Arguments = U8MathTestArguments; + + fn arguments() -> Self::Arguments { + U8MathTestArguments {} + } + + fn arb_any() -> BoxedStrategy { + any::().boxed() + } + + fn arb_non_zero() -> BoxedStrategy { + any::().prop_map(|value| value.max(1)).boxed() + } + + fn arb_fitting(low: Self, high: Self) -> BoxedStrategy { + assert!(low <= high); + (low..=high).boxed() + } + } + + crate::uint_fuzz_tests!(u8); +} diff --git a/tests/stdlib/u8/mul_div.rs b/tests/stdlib/u8/mul_div.rs index c01383b..c8d147d 100644 --- a/tests/stdlib/u8/mul_div.rs +++ b/tests/stdlib/u8/mul_div.rs @@ -105,3 +105,172 @@ fn mul_div_8_div_by_zero(context: simplex::TestContext) -> anyhow::Result<()> { .expect(0) .expecting(&context, Expect::AssertFailed) } + +mod mul_div_tests_fuzz { + use super::*; + + use simplex::simplicityhl::{Arguments, WitnessValues}; + + use simplex::fuzz::fuzz_check::{Expect, FuzzExecutionCheck}; + use simplex::fuzz::proptest::prelude::any; + use simplex::fuzz::proptest::strategy::{BoxedStrategy, Strategy}; + use simplex::{FuzzMode, TestContext}; + + fn arb_non_zero_u8() -> impl Strategy { + any::().prop_filter("u8 should not be zero", |index| *index != 0) + } + + #[derive(Debug, Default)] + struct FuzzCase { + first_numerator: Option, + second_numerator: Option, + denominator: Option, + expected: Option, + } + + impl FuzzCase { + fn from_numerator(first_numerator: u8) -> Self { + let mut x = Self::default(); + let _ = x.first_numerator.insert(first_numerator); + x + } + + fn mul(mut self, second_numerator: u8) -> Self { + let _ = self.second_numerator.insert(second_numerator); + self + } + + fn div(mut self, denominator: u8) -> Self { + let _ = self.denominator.insert(denominator); + self + } + + fn expect(mut self, expected: u8) -> Self { + let _ = self.expected.insert(expected); + self + } + + fn into_witness(self, witness: U8MulDivTestWitness, expect_failure: bool) -> WitnessValues { + let case = Case { witness }.args( + self.first_numerator.expect("no first numerator in witness"), + self.second_numerator + .expect("no second numerator in witness"), + self.denominator.expect("no denominator in witness"), + ); + + let case = if expect_failure { + case + } else { + case.expect(self.expected.expect("no expected arg in witness")) + }; + + case.witness.into() + } + } + + struct FuzzCaseBuilder { + case: Case, + context: TestContext, + inputs: Option>, + test_name: &'static str, + expect: Expect, + } + + fn case_fuzz(context: TestContext, test_name: &'static str) -> FuzzCaseBuilder { + FuzzCaseBuilder { + case: case(MulDiv), + context, + inputs: None, + test_name, + expect: Expect::Ok, + } + } + + impl FuzzCaseBuilder { + fn strategy(mut self, inputs: impl Strategy + 'static) -> Self { + self.inputs = Some(inputs.boxed()); + self + } + + fn expect(mut self, expect: Expect) -> Self { + self.expect = expect; + self + } + + fn run(self) -> anyhow::Result<()> { + let Case { witness } = self.case; + let inputs = self.inputs.expect("a fuzz strategy must be specified"); + let expect_failure = matches!(self.expect, Expect::AssertFailed); + + let strategy = inputs + .prop_map(move |fuzz_case| { + let arguments: Arguments = U8MulDivTestArguments {}.into(); + let witness: WitnessValues = + fuzz_case.into_witness(witness.clone(), expect_failure); + + (arguments, witness) + }) + .boxed(); + + self.context + .engine::() + .with_custom_strategy(strategy) + .with_custom_check(FuzzExecutionCheck::new(self.test_name, self.expect)) + .run(); + + Ok(()) + } + } + + #[simplex::fuzz] + fn mul_div_8_product_is_u8(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u8(), arb_non_zero_u8()).prop_map(|(a, c)| { + let b = u8::MAX / a; + + FuzzCase::from_numerator(a).mul(b).div(c).expect(a * b / c) + }); + + case_fuzz(context, "mul_div_8 product is u8") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_8_intermediate_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2u8..=u8::MAX).prop_flat_map(|a| { + (a..=u8::MAX).prop_map(move |c| { + let b = u8::MAX; + let expected = (u16::from(a) * u16::from(b) / u16::from(c)) as u8; + + FuzzCase::from_numerator(a).mul(b).div(c).expect(expected) + }) + }); + + case_fuzz(context, "mul_div_8 intermediate overflow") + .strategy(strategy) + .run() + } + + #[simplex::fuzz] + fn mul_div_8_result_overflow(context: TestContext) -> anyhow::Result<()> { + let strategy = (2u8..=u8::MAX).prop_flat_map(|a| { + (1u8..a).prop_map(move |c| FuzzCase::from_numerator(a).mul(u8::MAX).div(c)) + }); + + case_fuzz(context, "mul_div_8 result overflow") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } + + #[simplex::fuzz] + fn mul_div_8_div_by_zero(context: TestContext) -> anyhow::Result<()> { + let strategy = (arb_non_zero_u8(), arb_non_zero_u8()) + .prop_map(|(a, b)| FuzzCase::from_numerator(a).mul(b).div(0).expect(0)); + + case_fuzz(context, "mul_div_8 div by zero") + .strategy(strategy) + .expect(Expect::AssertFailed) + .run() + } +} From 4c37c1de04f8ef354ed782aa49ea23d9460e5b1b Mon Sep 17 00:00:00 2001 From: Illia Kripaka Date: Wed, 21 Oct 2026 14:44:30 +0300 Subject: [PATCH 4/4] add optimization --- simf/lib/u256/mul_div.simf | 294 ++++++++++++++++--------------------- 1 file changed, 123 insertions(+), 171 deletions(-) diff --git a/simf/lib/u256/mul_div.simf b/simf/lib/u256/mul_div.simf index 73a3efa..76a608e 100644 --- a/simf/lib/u256/mul_div.simf +++ b/simf/lib/u256/mul_div.simf @@ -1,201 +1,153 @@ use crate::lib::binary::not; - -use crate::lib::u128::math::{add_128_carry, sub_128_borrow, full_sub_128, mul_128_wide}; -use crate::lib::u128::bit::eq_128; -use crate::lib::u128::comparison::{is_zero_128, lt_128}; - -use crate::lib::u256::math::{ - algorithm_d_256_128, - mul_256_128_wide, - sub_256_borrow, - sub_256, - calculate_normalizer_base_128, - mul_512_128_wide, - mul_256_128, - mul_256_wide, - div_mod_256_128, - div_256 -}; -use crate::lib::u256::convert::split_u256_into_u128; +use crate::lib::u64::convert::u64_to_u128; +use crate::lib::u128::math::{calculate_normalizer_base_64, estimate_quotient_digit_base_64}; +use crate::lib::u128::convert::split_u128_into_u64; +use crate::lib::u256::math::{mul_256_wide, div_256}; +use crate::lib::u256::convert::split_u256_into_u64; use crate::lib::u256::comparison::{is_zero_256, lt_256}; - -/// Estimates and corrects the next quotient digit (q_hat) for Algorithm D. -/// Returns the quotient digit to use in the subsequent multiply-and-subtract step. -/// Expects result to fit into u128 -fn estimate_quotient_digit_base_128(u2: u128, u1: u128, u0: u128, v1: u128, v0: u128) -> u128 { - let ( - q_hat, - r_hat, - carry - ): (u128, u128, bool) = match lt_128(u2, v1) { - true => { - let ( - q_hat, - r_hat - ): (u256, u128) = algorithm_d_256_128(<(u128, u128)>::into((u2, u1)), v1); - let ( - _, - q_hat - ): (u128, u128) = split_u256_into_u128( - q_hat - ); // skip the check for optimization. u2 < v1, so q_hat fits into u128 - - (q_hat, r_hat, false) - }, - false => { - // This means u2 == v1, q_hat = 2^128, and r_hat = u1. - // Therefore, we need to decrement q and add v1 to r_hat. - // r_hat = u1 + v1 may overflow u128, which means that the estimate is exact - let (carry, r_hat): (bool, u128) = add_128_carry(u1, v1); - - let u128_max: u128 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - - (u128_max, r_hat, carry) - }, - }; - - match carry { - true => q_hat, - false => { - let r_hat_u0: u256 = <(u128, u128)>::into((r_hat, u0)); - - // correcting estimation: q_hat is off by at most 2. - match lt_256(r_hat_u0, mul_128_wide(q_hat, v0)) { - true => { - // can not overflow because r_hat_u0 < q_hat * v0, so q_hat is at least 1 - let (_, q_hat): (bool, u128) = sub_128_borrow(q_hat, 1); - let (carry, r_hat): (bool, u128) = add_128_carry(r_hat, v1); - - match carry { - true => q_hat, - false => { - let r_hat_u0: u256 = <(u128, u128)>::into((r_hat, u0)); - - match lt_256(r_hat_u0, mul_128_wide(q_hat, v0)) { - true => { - // can not overflow because r_hat_u0 < q_hat * v0, so q_hat is at least 1 - let (_, q_hat): (bool, u128) = sub_128_borrow(q_hat, 1); - - q_hat - }, - false => q_hat, - } - }, - } - }, - false => q_hat, - } +/// Shifts the 512-bit dividend and the divisor left by whole 64-bit words, +/// so that the most significant word of the divisor is non-zero. +/// Requires the divisor to be non-zero and `dividend_high < divisor`. +/// +/// The divisor fits into `4 - k` words, where `k` is the shift, +/// so `dividend_high < divisor` guarantees that the shifted out words of the dividend are zero +fn shift_to_top_word_512_256(dividend_high: u256, dividend_low: u256, divisor: u256) -> (u256, u256, u256) { + let (_, u6, u5, u4): (u64, u64, u64, u64) = split_u256_into_u64(dividend_high); + let (u3, u2, u1, u0): (u64, u64, u64, u64) = split_u256_into_u64(dividend_low); + let (v3, v2, v1, v0): (u64, u64, u64, u64) = split_u256_into_u64(divisor); + match jet::is_zero_64(v3) { + false => (dividend_high, dividend_low, divisor), + true => match jet::is_zero_64(v2) { + false => ( + <(u64, u64, u64, u64)>::into((u6, u5, u4, u3)), + <(u64, u64, u64, u64)>::into((u2, u1, u0, 0)), + <(u64, u64, u64, u64)>::into((v2, v1, v0, 0)) + ), + true => match jet::is_zero_64(v1) { + false => ( + <(u64, u64, u64, u64)>::into((u5, u4, u3, u2)), + <(u64, u64, u64, u64)>::into((u1, u0, 0, 0)), + <(u64, u64, u64, u64)>::into((v1, v0, 0, 0)) + ), + true => ( + <(u64, u64, u64, u64)>::into((u4, u3, u2, u1)), + <(u64, u64, u64, u64)>::into((u0, 0, 0, 0)), + <(u64, u64, u64, u64)>::into((v0, 0, 0, 0)) + ), + }, }, } } - -/// Normalizes the dividend and divisor for Algorithm D by multiplying -/// all values by the same factor, ensuring that -/// the most significant non-zero word of `b` is at least 2^127. -/// -/// The normalized dividend fits into 640 bits and is returned as a -/// `(u128, u256, u256)` tuple. The normalized divisor fits into a u256 -fn normalize_to_threshold_512_127(a_high: u256, a_low: u256, b: u256) -> (u128, u256, u256, u256) { - let norm: u128 = calculate_normalizer_base_128(b); - - match eq_128(norm, 1) { - true => (0, a_high, a_low, b), +/// Multiplies the 256-bit integer by the 64-bit integer. +/// Takes a carry-in word and returns a carry-out word +fn full_mul_256_64(carry_in: u64, a: u256, b: u64) -> (u64, u256) { + let (a3, a2, a1, a0): (u64, u64, u64, u64) = split_u256_into_u64(a); + let (c0, w0): (u64, u64) = split_u128_into_u64(jet::full_multiply_64(a0, b, carry_in, 0)); + let (c1, w1): (u64, u64) = split_u128_into_u64(jet::full_multiply_64(a1, b, c0, 0)); + let (c2, w2): (u64, u64) = split_u128_into_u64(jet::full_multiply_64(a2, b, c1, 0)); + let (c3, w3): (u64, u64) = split_u128_into_u64(jet::full_multiply_64(a3, b, c2, 0)); + (c3, <(u64, u64, u64, u64)>::into((w3, w2, w1, w0))) +} +/// Normalizes the dividend and divisor for Algorithm D in base 2^64, +/// ensuring that the most significant word of the divisor is at least 2^63. +/// Requires the divisor to be non-zero and `dividend_high < divisor`. +/// +/// The divisor is shifted left by whole words, and then both values are multiplied by +/// `norm = ceil(2^63 / v3) < 2^64`, where `v3` is the top word of the shifted divisor. +/// `(v3 + 1) * norm <= 2^64`, so the normalized divisor fits into u256, +/// and `dividend < divisor * 2^256`, so the normalized dividend fits into 512 bits +fn normalize_to_threshold_512_256_63(dividend_high: u256, dividend_low: u256, divisor: u256) -> (u256, u256, u256) { + let ( + dividend_high, + dividend_low, + divisor + ): (u256, u256, u256) = shift_to_top_word_512_256(dividend_high, dividend_low, divisor); + let (v3, _, _, _): (u64, u64, u64, u64) = split_u256_into_u64(divisor); + let norm: u64 = calculate_normalizer_base_64(u64_to_u128(v3), false); + match jet::eq_64(norm, 1) { + true => (dividend_high, dividend_low, divisor), false => { - let (a_2, a_1, a_0): (u128, u256, u256) = mul_512_128_wide(a_high, a_low, norm); - - (a_2, a_1, a_0, mul_256_128(b, norm)) + // the carry-outs below are zero, as the normalized values fit into 512 and 256 bits + let (carry, dividend_low): (u64, u256) = full_mul_256_64(0, dividend_low, norm); + let (_, dividend_high): (u64, u256) = full_mul_256_64(carry, dividend_high, norm); + let (_, divisor): (u64, u256) = full_mul_256_64(0, divisor, norm); + (dividend_high, dividend_low, divisor) }, } } - -/// Multiplies the divisor by the quotient digit and subtracts the result from -/// the corresponding dividend limbs. Returns the updated dividend segment, -/// which is used by Algorithm D -fn mul_and_sub_128(q: u128, u2: u128, u1: u128, u0: u128, v: u256) -> (u128, u128) { - let u1_0: u256 = <(u128, u128)>::into((u1, u0)); - - let (q_v_1, q_v_0): (u128, u256) = mul_256_128_wide(v, q); - - let (borrow_low, u_updated_1_0): (bool, u256) = sub_256_borrow(u1_0, q_v_0); - let (borrow, u_updated_2): (bool, u128) = full_sub_128(borrow_low, u2, q_v_1); - - assert!(not(borrow)); - assert!(is_zero_128(u_updated_2)); - - split_u256_into_u128(u_updated_1_0) +/// Performs one step of Algorithm D in base 2^64 with the normalized 4-word divisor `(v3, v2, v1, v0)`. +/// Divides the 5-word window `(u4, u3, u2, u1, u0)`, whose top four words are less than the divisor, +/// and returns the quotient word and the 4-word remainder +fn div_step_320_256( + u4: u64, + u3: u64, + u2: u64, + u1: u64, + u0: u64, + v3: u64, + v2: u64, + v1: u64, + v0: u64 +) -> (u64, u64, u64, u64, u64) { + // q_hat is either exact or one too large + let q_hat: u64 = estimate_quotient_digit_base_64(u4, u3, u2, v3, v2); + // multiply: q_hat * v fits into 5 words + let (c0, p0): (u64, u64) = split_u128_into_u64(jet::multiply_64(q_hat, v0)); + let (c1, p1): (u64, u64) = split_u128_into_u64(jet::full_multiply_64(q_hat, v1, c0, 0)); + let (c2, p2): (u64, u64) = split_u128_into_u64(jet::full_multiply_64(q_hat, v2, c1, 0)); + let (p4, p3): (u64, u64) = split_u128_into_u64(jet::full_multiply_64(q_hat, v3, c2, 0)); + // subtract + let (b0, r0): (bool, u64) = jet::subtract_64(u0, p0); + let (b1, r1): (bool, u64) = jet::full_subtract_64(b0, u1, p1); + let (b2, r2): (bool, u64) = jet::full_subtract_64(b1, u2, p2); + let (b3, r3): (bool, u64) = jet::full_subtract_64(b2, u3, p3); + let (borrow, _): (bool, u64) = jet::full_subtract_64(b3, u4, p4); + match borrow { + false => (q_hat, r3, r2, r1, r0), + true => { + // q_hat is one too large: decrement it and add the divisor back. + // q_hat is at least 1, because q_hat * v exceeds the window. + // The remainder is less than the divisor, so the carry-out and the top word are dropped + let (_, q): (bool, u64) = jet::subtract_64(q_hat, 1); + let (c0, r0): (bool, u64) = jet::add_64(r0, v0); + let (c1, r1): (bool, u64) = jet::full_add_64(c0, r1, v1); + let (c2, r2): (bool, u64) = jet::full_add_64(c1, r2, v2); + let (_, r3): (bool, u64) = jet::full_add_64(c2, r3, v3); + (q, r3, r2, r1, r0) + }, + } } - /// Divides a 512-bit integer by a 256-bit integer and returns a u256 quotient. -/// Implements Algorithm D by Donald Knuth. -/// Requires the upper half of the divisor to be non-zero (`divisor >= 2^128`) -/// and the quotient to fit into u256 +/// Implements Algorithm D by Donald Knuth in base 2^64. +/// Requires the divisor to be non-zero and `dividend_high < divisor`, +/// so the quotient fits into u256 fn algorithm_d_512_256(dividend_high: u256, dividend_low: u256, divisor: u256) -> u256 { let ( - _, norm_dividend_high, norm_dividend_low, norm_divisor - ): (u128, u256, u256, u256) = ( - normalize_to_threshold_512_127(dividend_high, dividend_low, divisor) - ); - - let (u3, u2): (u128, u128) = split_u256_into_u128(norm_dividend_high); - let (u1, u0): (u128, u128) = split_u256_into_u128(norm_dividend_low); - let (v1, v0): (u128, u128) = split_u256_into_u128(norm_divisor); - - let q1: u128 = estimate_quotient_digit_base_128(u3, u2, u1, v1, v0); - let (u2, u1): (u128, u128) = mul_and_sub_128(q1, u3, u2, u1, norm_divisor); - - let q0: u128 = estimate_quotient_digit_base_128(u2, u1, u0, v1, v0); - - <(u128, u128)>::into((q1, q0)) + ): (u256, u256, u256) = normalize_to_threshold_512_256_63(dividend_high, dividend_low, divisor); + let (u7, u6, u5, u4): (u64, u64, u64, u64) = split_u256_into_u64(norm_dividend_high); + let (u3, u2, u1, u0): (u64, u64, u64, u64) = split_u256_into_u64(norm_dividend_low); + let (v3, v2, v1, v0): (u64, u64, u64, u64) = split_u256_into_u64(norm_divisor); + // (u7, u6, u5, u4) < (v3, v2, v1, v0), and each remainder is less than the divisor + let (q3, r3, r2, r1, r0): (u64, u64, u64, u64, u64) = div_step_320_256(u7, u6, u5, u4, u3, v3, v2, v1, v0); + let (q2, r3, r2, r1, r0): (u64, u64, u64, u64, u64) = div_step_320_256(r3, r2, r1, r0, u2, v3, v2, v1, v0); + let (q1, r3, r2, r1, r0): (u64, u64, u64, u64, u64) = div_step_320_256(r3, r2, r1, r0, u1, v3, v2, v1, v0); + let (q0, _, _, _, _): (u64, u64, u64, u64, u64) = div_step_320_256(r3, r2, r1, r0, u0, v3, v2, v1, v0); + <(u64, u64, u64, u64)>::into((q3, q2, q1, q0)) } - /// Calculates `floor(a * b / denominator)` with full precision. /// Panics if the result overflows a u256 or if the denominator is zero pub fn mul_div_256(a: u256, b: u256, denominator: u256) -> u256 { assert!(not(is_zero_256(denominator))); - let (result_high, result_low): (u256, u256) = mul_256_wide(a, b); - match is_zero_256(result_high) { true => div_256(result_low, denominator), false => { // if result_high >= denominator, result will overflow u256 assert!(lt_256(result_high, denominator)); - let (denom_high, denom_low): (u128, u128) = split_u256_into_u128(denominator); - - match is_zero_128(denom_high) { - true => { - // result_high < denominator, denominator fits into u128, so result_high fits too. - // split_u256_into_u128 is used instead of safe_u256_to_u128 to avoid the unnecessary check - let (_, result_2): (u128, u128) = split_u256_into_u128(result_high); - let (result_1, result_0): (u128, u128) = split_u256_into_u128(result_low); - - // result_high < denominator, so result_2 < denom_low and q1 fits into u128 - let ( - q1, - r - ): (u256, u128) = div_mod_256_128( - <(u128, u128)>::into((result_2, result_1)), - denom_low - ); - let (_, q1): (u128, u128) = split_u256_into_u128(q1); - - // r < denom_low, so q0 fits into u128 - let ( - q0, - _ - ): (u256, u128) = div_mod_256_128( - <(u128, u128)>::into((r, result_0)), - denom_low - ); - let (_, q0): (u128, u128) = split_u256_into_u128(q0); - - <(u128, u128)>::into((q1, q0)) - }, - false => algorithm_d_512_256(result_high, result_low, denominator), - } + algorithm_d_512_256(result_high, result_low, denominator) }, } }