diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4afd90d2..2366f11b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,7 @@ name: Test env: RUSTFLAGS: "-Dwarnings --cfg tracing_unstable" + RUSTDOCFLAGS: "-Dwarnings" jobs: cargo-build: @@ -157,3 +158,19 @@ jobs: - name: Run Clippy run: cargo clippy --all-targets --all-features + + docs: + name: Build smirk docs + runs-on: ubuntu-latest-16-cores + needs: cargo-build + steps: + - uses: actions/checkout@v3 + + - uses: Swatinem/rust-cache@v2 + + - name: Install protoc + run: | + sudo apt-get install -y protobuf-compiler + + - name: Run Clippy + run: cargo doc --all-features -psmirk diff --git a/Cargo.lock b/Cargo.lock index 33ded033..842a9451 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -400,6 +400,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.3.2" @@ -748,9 +754,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.64.0" +version = "0.65.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" dependencies = [ "bitflags", "cexpr", @@ -758,12 +764,13 @@ dependencies = [ "lazy_static", "lazycell", "peeking_take_while", + "prettyplease 0.2.9", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 1.0.109", + "syn 2.0.16", ] [[package]] @@ -940,6 +947,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.0.79" @@ -1016,6 +1029,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cid" version = "0.10.1" @@ -1125,6 +1165,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -1222,6 +1274,76 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.9.0", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.15" @@ -1654,6 +1776,12 @@ dependencies = [ "log", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -1818,6 +1946,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "futures" version = "0.3.28" @@ -2050,6 +2184,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.12.3" @@ -2337,6 +2477,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "insta" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0770b0a3d4c70567f0d58331f3088b0e4c4f56c9b8d764efe654b4a5d46de3a" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", + "yaml-rust", +] + [[package]] name = "instant" version = "0.1.12" @@ -2832,9 +2985,9 @@ dependencies = [ [[package]] name = "librocksdb-sys" -version = "0.8.3+7.4.4" +version = "0.11.0+8.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557b255ff04123fcc176162f56ed0c9cd42d8f357cf55b3fabeb60f7413741b3" +checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" dependencies = [ "bindgen", "bzip2-sys", @@ -2842,6 +2995,7 @@ dependencies = [ "glob", "libc", "libz-sys", + "lz4-sys", "zstd-sys", ] @@ -2914,6 +3068,16 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "lz4-sys" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -2965,6 +3129,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "miden-air" version = "0.5.0" @@ -2988,7 +3161,7 @@ name = "miden-core" version = "0.5.0" source = "git+https://github.com/0xPolygonMiden/miden-vm?tag=v0.5.0#4195475d75ab2d586bdb01d1ff3ea2cd626eaf7b" dependencies = [ - "miden-crypto", + "miden-crypto 0.2.0", "winter-crypto 0.6.4", "winter-math 0.6.4", "winter-utils 0.6.4", @@ -3006,6 +3179,18 @@ dependencies = [ "winter-utils 0.6.4", ] +[[package]] +name = "miden-crypto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf95db953ee5fc8ef5a1df2d2e6e55e41587861c793fa26b45a214d3cc0f798" +dependencies = [ + "blake3", + "winter-crypto 0.6.4", + "winter-math 0.6.4", + "winter-utils 0.6.4", +] + [[package]] name = "miden-processor" version = "0.5.0" @@ -3293,7 +3478,7 @@ dependencies = [ "bitflags", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -3417,6 +3602,12 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -3657,6 +3848,34 @@ version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + [[package]] name = "polling" version = "2.8.0" @@ -3778,7 +3997,7 @@ dependencies = [ "serde", "serde_json", "wasm-bindgen", - "winter-math 0.4.2", + "winter-math 0.6.4", ] [[package]] @@ -3854,6 +4073,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.1.25" @@ -3864,6 +4093,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prettyplease" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9825a04601d60621feed79c4e6b56d65db77cdca55cef43b46b0de1096d1c282" +dependencies = [ + "proc-macro2", + "syn 2.0.16", +] + [[package]] name = "proc-macro-crate" version = "1.1.3" @@ -3950,7 +4189,7 @@ dependencies = [ "log", "multimap", "petgraph", - "prettyplease", + "prettyplease 0.1.25", "prost", "prost-types", "regex", @@ -4058,6 +4297,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.7.3" @@ -4102,6 +4354,21 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.5.1" @@ -4138,6 +4405,28 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "rcgen" version = "0.9.3" @@ -4163,6 +4452,15 @@ dependencies = [ "yasna", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -4224,6 +4522,15 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "reqwest" version = "0.11.18" @@ -4297,11 +4604,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rocksdb" -version = "0.19.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9562ea1d70c0cc63a34a22d977753b50cca91cc6b6527750463bd5dd8697bc" +checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" dependencies = [ "libc", "librocksdb-sys", @@ -4451,6 +4780,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.21" @@ -4818,6 +5156,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + [[package]] name = "siphasher" version = "0.3.10" @@ -4839,6 +5183,27 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smirk" +version = "0.1.0" +dependencies = [ + "criterion", + "hex", + "insta", + "miden-crypto 0.6.0", + "pretty_assertions", + "proptest", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rmp-serde", + "rocksdb", + "serde", + "tempdir", + "test-strategy", + "thiserror", + "traversal", +] + [[package]] name = "snow" version = "0.9.2" @@ -4876,7 +5241,6 @@ dependencies = [ "futures", "futures-timer", "hex", - "libp2p-core", "multihash 0.18.1", "parking_lot", "proptest", @@ -5053,6 +5417,16 @@ dependencies = [ "libc", ] +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + [[package]] name = "tempfile" version = "3.5.0" @@ -5348,7 +5722,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4" dependencies = [ - "prettyplease", + "prettyplease 0.1.25", "proc-macro2", "prost-build", "quote", @@ -5492,6 +5866,12 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "traversal" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ec9745d7517c8b8e8c0a65cba2d84e42f95fd348a01693c5e4da1bc6d00c99" + [[package]] name = "trust-dns-proto" version = "0.22.0" @@ -5790,6 +6170,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -6542,6 +6932,15 @@ dependencies = [ "time 0.3.21", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yamux" version = "0.10.2" @@ -6556,6 +6955,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yasna" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 30cf5ce0..f30777c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] -members = ["polybase", "indexer", "gateway", "solid"] +members = ["polybase", "indexer", "gateway", "solid", "smirk"] [profile.release] debug-assertions = true diff --git a/README.md b/README.md index 3fa2fb0e..e4c07b1f 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ cd polybase && cargo run -- generate_key cargo test ``` +Note, `smirk` contains some property tests which are relatively slow. +You may find that you have a faster dev cycle running tests with `--release`, since the test suite is relatively fast to compile but slow to run. + ## API API server runs on port 8080 by default: @@ -125,4 +128,4 @@ From variables in the web3.js example above, you can build a header like this: const headers = { "X--Signature": `${publicKey ? `pk=${publicKey},` : ""}sig=${signature},t=${timestamp},v0=1,h=eth-personal-sign`, }; -``` \ No newline at end of file +``` diff --git a/docker/Dockerfile b/docker/Dockerfile index 5ce203a1..395d06fe 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,6 +26,7 @@ COPY polybase ./polybase/ COPY indexer ./indexer/ COPY gateway ./gateway/ COPY solid ./solid/ +COPY smirk ./smirk/ RUN cargo chef prepare --recipe-path /recipe.json @@ -46,6 +47,7 @@ COPY polybase ./polybase/ COPY indexer ./indexer/ COPY gateway ./gateway/ COPY solid ./solid/ +COPY smirk ./smirk/ RUN --mount=type=cache,target=/usr/local/cargo/registry \ cargo build $(if [ "$RELEASE" = "1" ]; then echo "--release"; fi) diff --git a/flake.lock b/flake.lock index a1b3bafb..520c0027 100644 --- a/flake.lock +++ b/flake.lock @@ -36,39 +36,7 @@ "type": "github" } }, - "naersk": { - "inputs": { - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1679567394, - "narHash": "sha256-ZvLuzPeARDLiQUt6zSZFGOs+HZmE+3g4QURc8mkBsfM=", - "owner": "nix-community", - "repo": "naersk", - "rev": "88cd22380154a2c36799fe8098888f0f59861a15", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "naersk", - "type": "github" - } - }, "nixpkgs": { - "locked": { - "lastModified": 1685894048, - "narHash": "sha256-QKqv1QS+22k9oxncj1AnAxeqS5jGnQiUW3Jq3B+dI1w=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "2e56a850786211972d99d2bb39665a9b5a1801d6", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "type": "indirect" - } - }, - "nixpkgs_2": { "locked": { "lastModified": 1685947919, "narHash": "sha256-v282Pwz8tPwKqby4lQVGz3EwSXvYHAz4HXWTQcMGh04=", @@ -83,7 +51,7 @@ "type": "github" } }, - "nixpkgs_3": { + "nixpkgs_2": { "locked": { "lastModified": 1681358109, "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", @@ -102,15 +70,14 @@ "root": { "inputs": { "flake-utils": "flake-utils", - "naersk": "naersk", - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" } }, "rust-overlay": { "inputs": { "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_3" + "nixpkgs": "nixpkgs_2" }, "locked": { "lastModified": 1685932304, diff --git a/flake.nix b/flake.nix index d6efb3e8..8cbbe15c 100644 --- a/flake.nix +++ b/flake.nix @@ -4,10 +4,9 @@ nixpkgs.url = "github:NixOS/nixpkgs"; flake-utils.url = "github:numtide/flake-utils"; rust-overlay.url = "github:oxalica/rust-overlay"; - naersk.url = "github:nix-community/naersk"; }; - outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }@inputs: + outputs = { nixpkgs, flake-utils, rust-overlay, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { @@ -15,7 +14,7 @@ overlays = [ (import rust-overlay) ]; }; - rustToolchain = pkgs.rust-bin.stable.latest.default.override { + rustToolchain = pkgs.rust-bin.stable."1.68.0".default.override { extensions = [ "rust-src" ]; }; @@ -29,6 +28,9 @@ protobuf clang # required for rocksdb + + cargo-insta # snapshot testing for smirk + gnuplot # criterion graphs ]; LIBCLANG_PATH = "${pkgs.libclang.lib}/lib/"; diff --git a/gateway/src/lib.rs b/gateway/src/lib.rs index 04513c1a..e8d77197 100644 --- a/gateway/src/lib.rs +++ b/gateway/src/lib.rs @@ -305,10 +305,10 @@ fn reference_records( .ok_or(GatewayUserError::CollectionRecordIdNotFound)? .as_str() .ok_or(GatewayUserError::RecordIdNotString)?; - + let foreign_collection_id = collection_namespace.to_string() + "/" + &fr.collection; - + Ok( serde_json::json!({ "id": id, "collectionId": foreign_collection_id }), ) @@ -695,7 +695,8 @@ impl Gateway { instance = serde_json::to_string(&instance_json).unwrap_or_default(), args = serde_json::to_string(&args).unwrap_or_default(), auth = serde_json::to_string(&auth).unwrap_or_default(), - output = serde_json::to_string(&output.as_ref().map_err(|e| e.to_string())).unwrap_or_default(), + output = serde_json::to_string(&output.as_ref().map_err(|e| e.to_string())) + .unwrap_or_default(), "function output" ); diff --git a/indexer/Cargo.toml b/indexer/Cargo.toml index 0e060d75..e58f8a55 100644 --- a/indexer/Cargo.toml +++ b/indexer/Cargo.toml @@ -16,7 +16,7 @@ once_cell = "1.17.0" polylang = { git = "https://github.com/polybase/polylang", branch = "main", version = "0.1.0" } prost = "0.11" prost-types = "0.11" -rocksdb = "0.19" +rocksdb = "0.21" secp256k1 = { version = "0.26", features = ["rand-std"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/indexer/src/store.rs b/indexer/src/store.rs index 26158035..48cb9935 100644 --- a/indexer/src/store.rs +++ b/indexer/src/store.rs @@ -69,7 +69,7 @@ impl Store { pub fn open(path: impl AsRef) -> Result { let mut options = rocksdb::Options::default(); options.create_if_missing(true); - options.set_comparator("polybase", keys::comparator); + options.set_comparator("polybase", Box::new(keys::comparator)); let db = rocksdb::DB::open(&options, path)?; diff --git a/smirk/Cargo.toml b/smirk/Cargo.toml new file mode 100644 index 00000000..861e74fa --- /dev/null +++ b/smirk/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "smirk" +version = "0.1.0" +edition = "2021" + +[dependencies] +miden-crypto = "0.6" +rocksdb = "0.21" +thiserror = "1" +hex = "0.4" +traversal = "0.1" +serde = { version = "1", features = ["derive"] } +rmp-serde = "1" + +proptest = { version = "1", optional = true } + + +[dev-dependencies] +tempdir = "0.3" +proptest = "1" +test-strategy = "0.3" +pretty_assertions = "1" +insta = "1" +criterion = "0.5" +rand = "0.8" +rand_chacha = "0.3" + +[[bench]] +name = "tree_benchmark" +harness = false diff --git a/smirk/README.md b/smirk/README.md new file mode 100644 index 00000000..f13b1d35 --- /dev/null +++ b/smirk/README.md @@ -0,0 +1,92 @@ +# `smirk` - Persistent Merkle Tree + +`smirk` = "stable `merk`" + +This library provides `MerkleTree`, a Merkle tree that uses the [Rescue-Prime Optimized][rpo] +hash function, with a map-like API. There is also a [`Storage`] API for persisting the tree in +[rocksdb][db] + +```rust +# use smirk::{MerkleTree, smirk, storage::Storage}; +let mut tree = MerkleTree::new(); +tree.insert(1, "hello"); +tree.insert(2, "world"); + +// or you can use the macro to create a new tree +let tree = smirk! { + 1 => "hello", + 2 => "world", +}; + +assert_eq!(tree.get(&1), Some(&"hello")); +assert_eq!(tree.get(&2), Some(&"world")); +assert_eq!(tree.get(&3), None); +``` + +You can persist trees with the [`Storage`] API: +```rust,no_run +# use std::path::Path; +# use smirk::{smirk, storage::Storage}; +let path = Path::new("path/for/rocksdb"); +let storage = Storage::open(path).unwrap(); + +let tree = smirk! { + 1 => 123, + 2 => 234, +}; + +storage.store_tree(&tree).unwrap(); +let tree_again = storage.load_tree().unwrap().unwrap(); + +assert_eq!(tree, tree_again); +``` + +Any type that implements [`Serialize`] and [`Deserialize`] can be used + +```rust,no_run +# use std::path::Path; +# use serde::{Serialize, Deserialize}; +# use smirk::{smirk, storage::Storage, hash::{Hashable, Digest}}; +#[derive(Debug, Serialize, Deserialize)] +struct MyCoolType { + foo: i32, + bar: String, +} + +impl Hashable for MyCoolType { + fn hash(&self) -> Digest { + [self.foo.hash(), self.bar.hash()].into_iter().collect() + } +} + +let path = Path::new("path/for/rocksdb"); +let storage = Storage::open(path).unwrap(); + +let tree = smirk! { + 1 => MyCoolType { foo: 123, bar: "hello".to_string() }, + 2 => MyCoolType { foo: 234, bar: "world".to_string() }, +}; + +storage.store_tree(&tree).unwrap(); +let tree_again = storage.load_tree().unwrap().unwrap(); + +assert_eq!(tree, tree_again); +``` + +Types provided by this library implement [`Arbitrary`], for use with [`proptest`], gated behind +the `proptest` feature flag. + + +## Todo + + - benchmarks + - batch update API for storage + - use a slab allocator internally + +[rpo]: https://eprint.iacr.org/2022/1577.pdf +[db]: https://github.com/facebook/rocksdb + +[`Storage`]: storage::Storage +[`Arbitrary`]: proptest::prelude::Arbitrary +[`Serialize`]: serde::Serialize +[`Deserialize`]: serde::Deserialize diff --git a/smirk/benches/tree_benchmark.rs b/smirk/benches/tree_benchmark.rs new file mode 100644 index 00000000..4837e1da --- /dev/null +++ b/smirk/benches/tree_benchmark.rs @@ -0,0 +1,73 @@ +use std::hint::black_box; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaChaRng; +use smirk::{ + batch::{Batch, Operation}, + smirk, MerkleTree, +}; + +pub fn insert_benchmark(c: &mut Criterion) { + let mut rng = ChaChaRng::from_seed([0; 32]); + let mut nums = vec![0; 1000]; + rng.fill(nums.as_mut_slice()); + + c.bench_with_input( + BenchmarkId::new("insert", "1k random"), + &nums.as_slice(), + |bencher, nums| { + bencher.iter(|| { + let mut tree = smirk! {}; + for i in *nums { + tree.insert(i, i); + } + black_box(tree); + }); + }, + ); +} + +pub fn collect_benchmark(c: &mut Criterion) { + let mut rng = ChaChaRng::from_seed([0; 32]); + let mut nums = vec![0; 1000]; + rng.fill(nums.as_mut_slice()); + + c.bench_with_input( + BenchmarkId::new("collect", "1k random"), + &nums.as_slice(), + |bencher, nums| { + bencher.iter(|| { + let tree: MerkleTree<_, _> = nums.iter().copied().map(|i| (i, i)).collect(); + black_box(tree); + }); + }, + ); +} + +pub fn batch_insert_benchmark(c: &mut Criterion) { + let mut rng = ChaChaRng::from_seed([0; 32]); + let mut nums = vec![0; 1000]; + rng.fill(nums.as_mut_slice()); + + let batch = Batch::from_operations(nums.into_iter().map(|i| Operation::Insert(i, i)).collect()); + + c.bench_with_input( + BenchmarkId::new("batch insert", "1k random"), + &batch, + |bencher, batch| { + bencher.iter(|| { + let mut tree = smirk! {}; + tree.apply(batch.clone()); + black_box(tree); + }); + }, + ); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(10); + targets = insert_benchmark, collect_benchmark, batch_insert_benchmark +} +criterion_main!(benches); diff --git a/smirk/src/hash/from_iter.rs b/smirk/src/hash/from_iter.rs new file mode 100644 index 00000000..ca2eaa16 --- /dev/null +++ b/smirk/src/hash/from_iter.rs @@ -0,0 +1,78 @@ +use std::{backtrace::Backtrace, borrow::Borrow, fmt::Debug}; + +use super::Digest; + +impl FromIterator for Digest +where + H: Borrow + Debug, +{ + fn from_iter>(iter: T) -> Self { + let vec: Vec<_> = iter.into_iter().collect(); + let mut iter = vec.iter(); + + let Some(hash) = iter.next() else { return Digest::NULL }; + let mut hash = *hash.borrow(); + + for new_hash in iter { + hash.merge(new_hash.borrow()); + } + + if format!("{hash}") + .contains("e54944d3c80d00cc318e861d5d56c76a2b1bf9e7638422c0ec636e48ae8b4c0f") + { + let bt = Backtrace::capture(); + println!("{bt}"); + println!("{vec:?}"); + // panic!("uh oh"); + } + + hash + } +} + +#[cfg(test)] +mod tests { + use std::{borrow::Cow, rc::Rc, sync::Arc}; + + use crate::hash::Hashable; + + use super::*; + + #[test] + fn can_collect_various_types() { + let mut d = Digest::calculate(&[]); + + let _: Digest = [d].into_iter().collect(); + let _: Digest = [&d].into_iter().collect(); + let _: Digest = [&mut d].into_iter().collect(); + let _: Digest = [Box::new(d)].into_iter().collect(); + let _: Digest = [Cow::Owned(d)].into_iter().collect(); + let _: Digest = [Rc::new(d)].into_iter().collect(); + let _: Digest = [Arc::new(d)].into_iter().collect(); + } + + #[test] + fn collecting_empty_hash_is_null() { + let hash: Digest = Vec::::new().into_iter().collect(); + assert_eq!(hash, Digest::NULL); + } + + #[test] + fn collecting_single_hash_is_unchanged() { + let hash: Digest = vec![1.hash()].iter().collect(); + assert_eq!(hash, 1.hash()); + } + + #[test] + fn collecting_multiple_hashes() { + let hash: Digest = [1.hash(), "hello".hash(), [1u8, 2, 3].hash()] + .iter() + .collect(); + + let mut expected = 1.hash(); + expected.merge(&"hello".hash()); + expected.merge(&[1u8, 2, 3].hash()); + + assert_eq!(hash, expected); + } +} diff --git a/smirk/src/hash/hashable.rs b/smirk/src/hash/hashable.rs new file mode 100644 index 00000000..3373d529 --- /dev/null +++ b/smirk/src/hash/hashable.rs @@ -0,0 +1,125 @@ +use std::{borrow::Cow, rc::Rc, sync::Arc}; + +use miden_crypto::hash::rpo::Rpo256; + +use super::Digest; + +/// Types which can be hashed +/// +/// This trait is primarily used as a bound on the value type for most useful functions on +/// [`MerkleTree`] +/// +/// [`MerkleTree`]: crate::MerkleTree +pub trait Hashable { + /// Compute the hash of this object + /// + /// ```rust + /// # use smirk::hash::Hashable; + /// let digest = 1i32.hash(); + /// println!("the hash of 1 is {digest}"); + /// ``` + fn hash(&self) -> Digest; +} + +impl Hashable for &T +where + T: Hashable, +{ + fn hash(&self) -> Digest { + ::hash(self) + } +} + +impl Hashable for &mut T +where + T: Hashable, +{ + fn hash(&self) -> Digest { + ::hash(self) + } +} + +impl Hashable for Box +where + T: Hashable, +{ + fn hash(&self) -> Digest { + ::hash(self) + } +} + +impl<'a, T: ?Sized> Hashable for Cow<'a, T> +where + T: Hashable + Clone, +{ + fn hash(&self) -> Digest { + ::hash(self) + } +} + +impl Hashable for Rc +where + T: Hashable, +{ + fn hash(&self) -> Digest { + ::hash(self) + } +} + +impl Hashable for Arc +where + T: Hashable, +{ + fn hash(&self) -> Digest { + ::hash(self) + } +} + +macro_rules! int_impl { + ($int:ty) => { + impl Hashable for $int { + fn hash(&self) -> Digest { + Digest(Rpo256::hash(&self.to_be_bytes())) + } + } + }; +} + +int_impl!(i8); +int_impl!(i16); +int_impl!(i32); +int_impl!(i64); +int_impl!(i128); +int_impl!(isize); +int_impl!(u8); +int_impl!(u16); +int_impl!(u32); +int_impl!(u64); +int_impl!(u128); +int_impl!(usize); + +/// impl for any type that implements `AsRef<[u8]>` +macro_rules! as_ref_impl { + ($t:ty) => { + impl Hashable for $t { + fn hash(&self) -> Digest { + let bytes = <$t as AsRef<[u8]>>::as_ref(self); + Digest(Rpo256::hash(bytes)) + } + } + }; +} + +impl Hashable for [u8; N] { + fn hash(&self) -> Digest { + let bytes = <[u8; N] as AsRef<[u8]>>::as_ref(self); + Digest(Rpo256::hash(bytes)) + } +} + +// note, we implement the trait on `[u8]`, not `&[u8]` so it works with the above impls for types +// like `Arc<[u8]>` or `Box<[u8]>` - the same logic applies to `str` +as_ref_impl!([u8]); +as_ref_impl!(Vec); +as_ref_impl!(str); +as_ref_impl!(String); diff --git a/smirk/src/hash/mod.rs b/smirk/src/hash/mod.rs new file mode 100644 index 00000000..4b19c299 --- /dev/null +++ b/smirk/src/hash/mod.rs @@ -0,0 +1,147 @@ +//! Items relating to hashing +//! +//! In particular, the [`Digest`] type and the [`Hashable`] trait +//! +//! This module also contains [`MerklePath`], which can be used to verify the existance of a key in +//! a [`MerkleTree`] +//! +//! [`MerkleTree`]: crate::MerkleTree + +use std::fmt::{Debug, Display}; + +use miden_crypto::{ + hash::rpo::{Rpo256, RpoDigest}, + utils::{Deserializable, SliceReader}, + Felt, +}; + +mod from_iter; +mod hashable; +mod path; +mod serde_impls; + +pub use hashable::Hashable; +pub use path::MerklePath; +pub(crate) use path::Stage; + +#[cfg(any(test, feature = "proptest"))] +mod proptest_impls; + +/// A Rescue-Prime Optimized digest +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Digest(RpoDigest); + +impl Debug for Digest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Hash({})", hex::encode(self.0.as_bytes())) + } +} + +impl Display for Digest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Hash({})", hex::encode(self.0.as_bytes())) + } +} + +impl std::hash::Hash for Digest { + fn hash(&self, state: &mut H) { + <[u8; 32] as std::hash::Hash>::hash(&self.to_bytes(), state); + } +} + +impl Digest { + /// The null hash + /// + /// This represents the hash of "nothing" (for example, an empty Merkle tree will have this as + /// the root hash) + /// + /// ```rust + /// # use smirk::hash::Digest; + /// # use smirk::MerkleTree; + /// let empty_tree = MerkleTree::::new(); + /// assert_eq!(empty_tree.root_hash(), Digest::NULL); + /// ``` + pub const NULL: Digest = Digest(RpoDigest::new([Felt::new(0); 4])); + + /// The length of this hash in bytes + const LEN: usize = 32; + + /// Get the representation of this hash as a byte array + /// + /// These bytes can be converted back to a [`Digest`] using [`Digest::from_bytes`] (though this + /// function returns an `Option` since it can fail) + #[inline] + #[must_use] + pub fn to_bytes(&self) -> [u8; Self::LEN] { + self.0.as_bytes() + } + + /// Create a [`Digest`] from the byte array representation + /// + /// Note: this returns an `Option` because not all possible byte arrays are valid [`Digest`]s + /// + /// Any byte array returned from [`Digest::to_bytes`] will be valid for this function, and the + /// resulting hash will be equal to the hash that created the byte array + #[inline] + #[must_use] + pub fn from_bytes(bytes: [u8; 32]) -> Option { + let mut reader = SliceReader::new(&bytes); + RpoDigest::read_from(&mut reader).ok().map(Digest) + } + + /// Calculate the hash of the given bytes + #[inline] + #[must_use] + pub fn calculate(bytes: &[u8]) -> Self { + Self(Rpo256::hash(bytes)) + } + + /// Convert this [`Digest`] to its hex representation (i.e. the hex encoding of + /// [`Digest::to_bytes`]) + #[inline] + #[must_use] + pub fn to_hex(&self) -> String { + hex::encode(self.to_bytes()) + } + + /// Replace `self` with `rpo256(this + other)` + #[inline] + pub fn merge(&mut self, other: &Digest) { + self.0 = Rpo256::merge(&[self.0, other.0]); + } +} + +impl From for Digest { + fn from(value: RpoDigest) -> Self { + Self(value) + } +} + +#[cfg(test)] +mod tests { + use proptest::prop_assert_eq; + use test_strategy::proptest; + + use super::*; + + #[test] + fn null_hash_is_all_zeroes() { + assert_eq!(Digest::NULL.to_bytes(), [0; 32]); + } + + #[proptest] + fn digest_bytes_round_trip(digest: Digest) { + let bytes = digest.to_bytes(); + let digest_again = Digest::from_bytes(bytes).unwrap(); + + prop_assert_eq!(digest, digest_again); + } + + #[proptest] + fn digest_bytes_serde_round_trip(digest: Digest) { + let mp_bytes = rmp_serde::to_vec(&digest).unwrap(); + let digest_again: Digest = rmp_serde::from_slice(&mp_bytes).unwrap(); + + prop_assert_eq!(digest, digest_again); + } +} diff --git a/smirk/src/hash/path.rs b/smirk/src/hash/path.rs new file mode 100644 index 00000000..43703d14 --- /dev/null +++ b/smirk/src/hash/path.rs @@ -0,0 +1,89 @@ +use serde::{Deserialize, Serialize}; + +use super::Digest; + +/// A Merkle path that can be used to prove the existance of a value in the tree +/// +/// This type provides [`MerklePath::to_bytes`] and [`MerklePath::from_bytes`] for serialization +/// purposes. It also implements [`Serialize`] and [`Deserialize`], if more control over exact +/// serialization details is needed. +/// +/// Note: no [`Arbitrary`] implementation is provided for this type, since it has no public +/// constructors. The only way to create one is to prove the existance of a key-value pair in a +/// [`MerkleTree`]. +/// +/// Luckily, [`MerkleTree`] *does* implement [`Arbitrary`] +/// +/// [`Arbitrary`]: proptest::prelude::Arbitrary +/// [`MerkleTree`]: crate::MerkleTree +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MerklePath { + /// The intermediate stages between the root hash and the target node + pub(crate) stages: Vec, + + /// The root hash of the tree that generated this path + pub(crate) root_hash: Digest, + + /// The digest of the left sub-tree of the node that contained the target key-value pair + pub(crate) left: Option, + + /// The digest of the right sub-tree of the node that contained the target key-value pair + pub(crate) right: Option, +} + +impl MerklePath { + /// The root hash of the tree that generated this [`MerklePath`] + #[inline] + #[must_use] + pub fn root_hash(&self) -> Digest { + self.root_hash + } + + /// Convert this [`MerklePath`] to a canonical serialized representation. + /// + /// The exact details of the representation are not specified, other than that it can be + /// reversed with [`MerklePath::from_bytes`] + #[must_use] + pub fn to_bytes(&self) -> Vec { + rmp_serde::to_vec(&self).unwrap() + } + + /// Create a [`MerklePath`] from its canonical serialized representation + /// + /// The exact details of the representation are not specified, other than that it can be + /// reversed with [`MerklePath::to_bytes`] + #[must_use] + pub fn from_bytes(bytes: &[u8]) -> Option { + rmp_serde::from_slice(bytes).ok() + } +} + +/// A stage in a merkle proof (i.e. a single step in the binary search algorithm) +/// +/// - `this` is the hash of the key-value pair of the visited node in this stage +/// - `left`/`right` is the root hash of the "other side" of the tree +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum Stage { + Left { this: Digest, right: Option }, + Right { this: Digest, left: Option }, +} + +#[cfg(test)] +mod tests { + use test_strategy::proptest; + + use crate::MerkleTree; + + use super::*; + + #[proptest] + fn proof_serialization_round_trip(tree: MerkleTree) { + for node in tree.iter() { + let proof = tree.prove(node.key()).unwrap(); + let bytes = proof.to_bytes(); + let proof_again = MerklePath::from_bytes(&bytes).unwrap(); + + assert_eq!(proof, proof_again); + } + } +} diff --git a/smirk/src/hash/proptest_impls.rs b/smirk/src/hash/proptest_impls.rs new file mode 100644 index 00000000..058fbcf8 --- /dev/null +++ b/smirk/src/hash/proptest_impls.rs @@ -0,0 +1,13 @@ +use super::{Digest, RpoDigest}; + +use miden_crypto::Felt; +use proptest::{arbitrary::StrategyFor, prelude::*, strategy::Map}; + +impl Arbitrary for Digest { + type Parameters = (); + type Strategy = Map, fn([u64; 4]) -> Self>; + + fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { + any::<[u64; 4]>().prop_map(|nums| Digest(RpoDigest::new(nums.map(Felt::new)))) + } +} diff --git a/smirk/src/hash/serde_impls.rs b/smirk/src/hash/serde_impls.rs new file mode 100644 index 00000000..e16bab37 --- /dev/null +++ b/smirk/src/hash/serde_impls.rs @@ -0,0 +1,43 @@ +use serde::{de::Visitor, Deserializer, Serializer}; +use serde::{Deserialize, Serialize}; + +use super::Digest; + +impl Serialize for Digest { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let bytes = self.to_bytes(); + serializer.serialize_bytes(&bytes) + } +} + +impl<'de> Deserialize<'de> for Digest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct V; + impl Visitor<'_> for V { + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("bytes representing a rescue-prime optimized hash") + } + + type Value = Digest; + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + let bytes = v + .try_into() + .map_err(|_| E::custom(format!("incorrect number of bytes: {}", v.len())))?; + + Digest::from_bytes(bytes).ok_or(E::custom("deserialization error")) + } + } + + deserializer.deserialize_bytes(V) + } +} diff --git a/smirk/src/lib.rs b/smirk/src/lib.rs new file mode 100644 index 00000000..af531a38 --- /dev/null +++ b/smirk/src/lib.rs @@ -0,0 +1,23 @@ +#![doc = include_str!("../README.md")] +#![warn(clippy::pedantic)] +#![deny(missing_docs)] +#![deny(unsafe_code)] +#![deny(clippy::arithmetic_side_effects)] // explicitly choose wrapping/saturating/checked +#![allow( + clippy::module_name_repetitions, + clippy::match_bool, // overly restrictive style lint + clippy::bool_assert_comparison, // overly restrictive style lint + clippy::derive_partial_eq_without_eq, // semver hazard + clippy::missing_panics_doc, // implementation of lint is buggy + clippy::missing_errors_doc, // error is usually obvious from context, this forces useless docs +)] + +pub mod hash; +pub mod storage; + +mod tree; + +pub use tree::{batch, key_value_hash, visitor::Visitor, MerkleTree, TreeNode}; + +#[cfg(test)] +mod testing; diff --git a/smirk/src/snapshots/smirk__testing__root_hash_snapshot.snap b/smirk/src/snapshots/smirk__testing__root_hash_snapshot.snap new file mode 100644 index 00000000..eddfd3c7 --- /dev/null +++ b/smirk/src/snapshots/smirk__testing__root_hash_snapshot.snap @@ -0,0 +1,5 @@ +--- +source: smirk/src/testing.rs +expression: tree.root_hash().to_hex() +--- +f68a4fd087e98ee6af95ced5378e794cf85a68f58f46f9d2ccfc83c9872d8876 diff --git a/smirk/src/storage/codec.rs b/smirk/src/storage/codec.rs new file mode 100644 index 00000000..4499dedf --- /dev/null +++ b/smirk/src/storage/codec.rs @@ -0,0 +1,126 @@ +use rocksdb::{Transaction, TransactionDB}; +use serde::{Deserialize, Serialize}; + +use crate::{hash::Hashable, MerkleTree, TreeNode}; + +use super::{Error, Storage}; + +/// An error encountered when encoding data to its database format +#[derive(Debug, thiserror::Error)] +#[error("encode error: {0}")] +pub struct EncodeError(rmp_serde::encode::Error); + +/// An error encountered when decoding data in its database format +#[derive(Debug, thiserror::Error)] +#[error("decode error: {0}")] +pub struct DecodeError(rmp_serde::decode::Error); + +#[derive(Debug, Serialize, Deserialize)] +struct NodeFormat { + value: Vec, + left: Option>, + right: Option>, +} + +/// Note - this function doesn't actually write, the caller needs to call `tx.commit()` +pub(super) fn write_tree_to_tx( + tx: &Transaction, + tree: &MerkleTree, +) -> Result<(), Error> +where + K: Serialize, + V: Serialize, +{ + let root_value = tree + .inner + .as_deref() + .map(|node| encode(&node.key)) + .transpose()? + .unwrap_or(vec![]); + + tx.put(Storage::ROOT_KEY, &root_value)?; + + for node in tree.iter() { + let (key, value) = encode_single_node(node)?; + println!("writing to {}", hex::encode(&key)); + tx.put(&key, &value)?; + } + + Ok(()) +} + +pub(super) fn load_node( + tx: &Transaction, + key: &[u8], +) -> Result, Error> +where + K: for<'de> Deserialize<'de> + Hashable + Ord, + V: for<'de> Deserialize<'de> + Hashable, +{ + let value_bytes = tx + .get(key)? + .ok_or_else(|| Error::KeyMissing(key.to_vec()))?; + + let NodeFormat { value, left, right } = decode(&value_bytes)?; + + let value = decode(&value)?; + let key = decode(key)?; + + let left = left.map(|key| load_node(tx, &key)).transpose()?; + let right = right.map(|key| load_node(tx, &key)).transpose()?; + + Ok(TreeNode::new(key, value, left, right)) +} + +fn encode_single_node(node: &TreeNode) -> Result<(Vec, Vec), Error> +where + K: Serialize, + V: Serialize, +{ + let enc = |node: &TreeNode| encode(&node.key); + + let value = encode(&node.value)?; + let left = node.left.as_deref().map(enc).transpose()?; + let right = node.right.as_deref().map(enc).transpose()?; + + let value = NodeFormat { value, left, right }; + + let value_bytes = encode(&value)?; + let key_bytes = encode(&node.key)?; + + Ok((key_bytes, value_bytes)) +} + +fn encode(t: &T) -> Result, EncodeError> { + rmp_serde::encode::to_vec(t).map_err(EncodeError) +} + +fn decode<'de, 'a: 'de, T: Deserialize<'de>>(bytes: &'a [u8]) -> Result { + rmp_serde::decode::from_slice(bytes).map_err(DecodeError) +} + +#[cfg(test)] +mod tests { + use proptest::prop_assert_eq; + use test_strategy::{proptest, Arbitrary}; + + use crate::hash::Digest; + + use super::*; + + #[derive(Debug, Deserialize, Serialize, Arbitrary, PartialEq, Eq)] + struct CoolCustomType { + foo: String, + bar: Vec, + coords: [(i32, i32); 10], + } + + #[proptest] + fn encode_decode_bijective(key: Digest, value: CoolCustomType) { + let bytes = encode(&(&key, &value)).unwrap(); + let (key_again, value_again): (Digest, CoolCustomType) = decode(&bytes).unwrap(); + + prop_assert_eq!(key, key_again); + prop_assert_eq!(value, value_again); + } +} diff --git a/smirk/src/storage/error.rs b/smirk/src/storage/error.rs new file mode 100644 index 00000000..897821bb --- /dev/null +++ b/smirk/src/storage/error.rs @@ -0,0 +1,23 @@ +use super::{DecodeError, EncodeError}; + +/// An error encountered while persisting or restoring a [`MerkleTree`] +/// +/// [`MerkleTree`]: crate::MerkleTree +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Key didn't exist in the database + #[error("couldn't find key in database: 0x{}", hex::encode(.0))] + KeyMissing(Vec), + + /// Error encoding data to binary format + #[error("error encoding data to binary format: {0}")] + Encode(#[from] EncodeError), + + /// Error decoding data from binary format + #[error("error decoding data from binary format: {0}")] + Decode(#[from] DecodeError), + + /// Rocksdb error + #[error("rocksdb error: {0}")] + Unknown(#[from] rocksdb::Error), +} diff --git a/smirk/src/storage/mod.rs b/smirk/src/storage/mod.rs new file mode 100644 index 00000000..5b79adb6 --- /dev/null +++ b/smirk/src/storage/mod.rs @@ -0,0 +1,134 @@ +//! Persistence layer for [`MerkleTree`]s +use std::{fmt::Debug, path::Path}; + +use crate::{hash::Hashable, tree::MerkleTree}; +use rocksdb::{Transaction, TransactionDB}; +use serde::{Deserialize, Serialize}; + +mod codec; +mod error; + +#[cfg(test)] +mod tests; + +pub use codec::{DecodeError, EncodeError}; +pub use error::Error; + +/// A rocksdb-based storage mechanism for [`MerkleTree`]s +/// +/// ```rust,no_run +/// # use std::path::Path; +/// # use smirk::storage::Storage; +/// # use smirk::smirk; +/// let storage = Storage::open(Path::new("./db")).unwrap(); +/// +/// let tree = smirk! { +/// 1 => "hello".to_string(), +/// 2 => "world".to_string(), +/// }; +/// +/// storage.store_tree(&tree).unwrap(); +/// +/// // 2x .unwrap() because it returns `Ok(None)` if no tree has been stored yet +/// let tree_again = storage.load_tree().unwrap().unwrap(); +/// +/// // the root hashes are the same (since this is what the `Eq` impl for `MerkleTree` uses) +/// assert_eq!(tree, tree_again); +/// ``` +/// +/// This storage preserves the tree structure, meaning the root hash will not be changed by +/// loading it from storage. +pub struct Storage { + instance: TransactionDB, +} + +impl Debug for Storage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Storage") + } +} + +impl Storage { + /// Create a new [`Storage`] from an existing rocksdb instance + /// + /// This is useful if you want to create transactions that modify both data managed by smirk, + /// as well as data external to smirk + pub fn from_instance(instance: TransactionDB) -> Self { + Self { instance } + } + + /// Create a new [`Storage`] by opening a new rocksdb instance at the given path + pub fn open(path: &Path) -> Result { + let instance = TransactionDB::open_default(path)?; + Ok(Self { instance }) + } + + /// Key used to store the value of the root of the database + const ROOT_KEY: &[u8] = b"root"; +} + +impl Storage { + /// Store a tree + pub fn store_tree(&self, tree: &MerkleTree) -> Result<(), Error> + where + K: Serialize + 'static + Ord, + V: Serialize + 'static + Hashable, + { + let tx = self.instance.transaction(); + self.store_tree_with_tx(tree, &tx)?; + tx.commit()?; + + Ok(()) + } + + /// Store a tree with a given transaction + pub fn store_tree_with_tx( + &self, + tree: &MerkleTree, + tx: &Transaction, + ) -> Result<(), Error> + where + K: Serialize + 'static + Ord, + V: Serialize + 'static + Hashable, + { + codec::write_tree_to_tx(tx, tree) + } + + /// Load a tree from storage, if it is present + pub fn load_tree(&self) -> Result>, Error> + where + K: for<'a> Deserialize<'a> + 'static + Hashable + Ord, + V: for<'a> Deserialize<'a> + 'static + Hashable, + { + let tx = self.instance.transaction(); + let tree = self.load_tree_with_tx(&tx)?; + tx.commit()?; + + Ok(tree) + } + + /// Load a tree from storage, if it is present, using the given transaction + pub fn load_tree_with_tx( + &self, + tx: &Transaction, + ) -> Result>, Error> + where + K: for<'a> Deserialize<'a> + 'static + Hashable + Ord, + V: for<'a> Deserialize<'a> + 'static + Hashable, + { + let key = tx.get(Self::ROOT_KEY)?; + + let Some(key) = key else { return Ok(None) }; + + if key.is_empty() { + return Ok(Some(MerkleTree::new())); + } + + let node = codec::load_node(tx, &key)?; + let tree = MerkleTree { + inner: Some(Box::new(node)), + }; + + Ok(Some(tree)) + } +} diff --git a/smirk/src/storage/tests.rs b/smirk/src/storage/tests.rs new file mode 100644 index 00000000..ce56f91a --- /dev/null +++ b/smirk/src/storage/tests.rs @@ -0,0 +1,73 @@ +use super::*; + +use pretty_assertions::assert_eq; +use proptest::prop_assert_eq; +use test_strategy::proptest; + +use crate::{key_value_hash, smirk, testing::TestStorage}; + +#[test] +fn empty_db_returns_none() { + let db = TestStorage::new(); + + assert!(db.load_tree::().unwrap().is_none()); +} + +// test rocksdb behaviour, since we rely on this for storing empty trees +#[test] +fn store_empty_bytes_does_something() { + let db = TestStorage::new(); + assert_eq!(db.instance.get(b"hello").unwrap(), None); + db.instance.put(b"hello", []).unwrap(); + assert_eq!(db.instance.get(b"hello").unwrap(), Some(vec![])); +} + +#[test] +fn storing_empty_tree_returns_empty_tree() { + let db = TestStorage::new(); + let tree = MerkleTree::::new(); + + db.store_tree(&tree).unwrap(); + + assert_eq!(db.load_tree().unwrap(), Some(tree)); +} + +#[test] +fn storing_simple_tree() { + let db = TestStorage::new(); + let tree = smirk! { + 1 => "hello".to_string(), + 2 => "world".to_string(), + 3 => "foo".to_string(), + }; + + println!("hash: {}", tree.get_node(&3).unwrap().hash()); + + db.store_tree(&tree).unwrap(); + let mut tree_again = db.load_tree().unwrap().unwrap(); + + let changed = tree_again.recalculate_hash_recursive(); + dbg!(changed); + + assert_eq!( + tree_again.get_node(&1).unwrap().hash(), + key_value_hash(&1, "hello") + ); + + assert_eq!( + tree_again.get_node(&3).unwrap().hash(), + key_value_hash(&3, "foo") + ); + + assert_eq!(tree, tree_again); +} + +#[proptest] +fn storage_round_trip(tree: MerkleTree) { + let db = TestStorage::new(); + + db.store_tree(&tree).unwrap(); + let tree_again = db.load_tree::().unwrap().unwrap(); + + prop_assert_eq!(tree, tree_again); +} diff --git a/smirk/src/testing.rs b/smirk/src/testing.rs new file mode 100644 index 00000000..a1a17558 --- /dev/null +++ b/smirk/src/testing.rs @@ -0,0 +1,37 @@ +use std::ops::Deref; + +use tempdir::TempDir; + +use crate::{storage::Storage, MerkleTree}; + +/// Helper struct that makes it easier to test against a rocksdb instance +#[derive(Debug)] +pub struct TestStorage { + _dir: TempDir, + db: Storage, +} + +impl TestStorage { + pub fn new() -> Self { + let dir = TempDir::new("smirk").unwrap(); + let db = Storage::open(dir.path()).unwrap(); + + Self { _dir: dir, db } + } +} + +impl Deref for TestStorage { + type Target = Storage; + + fn deref(&self) -> &Self::Target { + &self.db + } +} + +// snapshot test for a well-known tree - if we accidentally change how the hash is calculated, this +// test will fail +#[test] +fn root_hash_snapshot() { + let tree: MerkleTree<_, _> = (0..100).map(|i| (i, format!("the value is {i}"))).collect(); + insta::assert_snapshot!(tree.root_hash().to_hex()); +} diff --git a/smirk/src/tree/batch.rs b/smirk/src/tree/batch.rs new file mode 100644 index 00000000..3bac6c09 --- /dev/null +++ b/smirk/src/tree/batch.rs @@ -0,0 +1,70 @@ +use crate::{hash::Hashable, MerkleTree}; + +/// An operation that represents an update to the tree +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum Operation { + /// Insert the following key-value pair + Insert(K, V), +} + +impl Operation { + fn key(&self) -> &K { + match self { + Operation::Insert(key, ..) => key, + } + } +} + +/// A batch of operations that can be applied to a [`MerkleTree`] +/// +/// If there are multiple operations +#[derive(Debug, Clone)] +pub struct Batch { + operations: Vec>, +} + +impl Batch { + /// Create a new [`Batch`] from a list of [`Operation`]s + /// + /// Note, if two operations reference the same key, they will be applied in the order they + /// exist in `operations`. No other guarantees about the order of execution are made + #[must_use] + pub fn from_operations(mut operations: Vec>) -> Self + where + K: Ord, + { + // preserve order of operations, so don't use sort_unstable + operations.sort_by(|a, b| a.key().cmp(b.key())); + Self { operations } + } +} + +impl FromIterator> for Batch +where + K: Ord, +{ + fn from_iter>>(iter: T) -> Self { + let vec = Vec::from_iter(iter); + Batch::from_operations(vec) + } +} + +impl MerkleTree +where + K: Hashable + Ord, + V: Hashable, +{ + /// Apply a [`Batch`] of operations to the tree + pub fn apply(&mut self, batch: Batch) { + for operation in batch.operations { + match operation { + Operation::Insert(key, value) => self.insert_without_update(key, value), + } + } + + if let Some(inner) = self.inner.as_mut() { + inner.recalculate_hash_recursive(); + } + } +} diff --git a/smirk/src/tree/hash.rs b/smirk/src/tree/hash.rs new file mode 100644 index 00000000..609d6299 --- /dev/null +++ b/smirk/src/tree/hash.rs @@ -0,0 +1,130 @@ +use std::iter::once; + +use crate::{ + hash::{Digest, Hashable}, + MerkleTree, TreeNode, +}; + +impl MerkleTree { + pub(crate) fn recalculate_hash_recursive(&mut self) -> bool { + match self.inner.as_mut() { + Some(inner) => inner.recalculate_hash_recursive(), + None => false, + } + } +} + +impl TreeNode { + /// The hash of the left subtree (if it exists) + pub(crate) fn left_hash(&self) -> Option { + self.left.as_ref().map(|node| node.hash) + } + + /// The hash of the right subtree (if it exists) + pub(crate) fn right_hash(&self) -> Option { + self.right.as_ref().map(|node| node.hash) + } + + /// Update the `hash` field of this node, and all child nodes + pub(crate) fn recalculate_hash_recursive(&mut self) -> bool { + if let Some(left) = &mut self.left { + left.recalculate_hash_recursive(); + } + + if let Some(right) = &mut self.right { + right.recalculate_hash_recursive(); + } + + let this = key_value_hash(self.key(), self.value()); + let left = self.left.as_ref().map(|node| node.hash); + let right = self.right.as_ref().map(|node| node.hash); + let new_hash = hash_left_right_this(this, left, right); + + let changed = self.hash != new_hash; + + self.hash = new_hash; + + changed + } +} + +/// Compute the hash of a pair of values (i.e. a key-value pair) +/// +/// The hash will change if either input changes: +/// +/// ```rust +/// # use smirk::key_value_hash; +/// let hash1 = key_value_hash(&1, "hello"); +/// let hash2 = key_value_hash(&2, "hello"); +/// let hash3 = key_value_hash(&1, "world"); +/// +/// assert_ne!(hash1, hash2); +/// assert_ne!(hash1, hash3); +/// assert_ne!(hash2, hash3); +/// ``` +/// +/// This is guaranteed to be the root hash of a tree with a single entry (with the same key + value) +/// +/// ```rust +/// # use smirk::{key_value_hash, smirk}; +/// let tree = smirk! { 1 => "hello" }; +/// let root_hash = key_value_hash(&1, &"hello"); +/// +/// assert_eq!(root_hash, tree.root_hash()); +/// ``` +#[must_use] +pub fn key_value_hash(key: &K, value: &V) -> Digest { + [key.hash(), value.hash()].into_iter().collect() +} + +/// Helper to a +pub(crate) fn hash_left_right_this( + this: Digest, + left: Option, + right: Option, +) -> Digest { + once(this).chain(left).chain(right).collect() +} + +#[cfg(test)] +mod tests { + use proptest::prop_assert_eq; + use test_strategy::proptest; + + use crate::{key_value_hash, smirk, MerkleTree}; + + #[test] + fn root_hash_is_probably_deterministic() { + let make = || { + smirk! { + 1 => "hello", + 2 => "world", + 3 => "foo", + } + }; + + let root_hash = make().root_hash(); + + for _ in 0..1000 { + let root_hash_again = make().root_hash(); + assert_eq!(root_hash, root_hash_again); + } + } + + #[proptest] + fn root_hash_doesnt_change_when_recalculating(mut tree: MerkleTree) { + let hash_before = tree.root_hash(); + tree.recalculate_hash_recursive(); + let hash_after = tree.root_hash(); + + assert_eq!(hash_before, hash_after); + } + + #[proptest] + fn single_element_tree_root_hash_is_kv_hash(key: i32, value: String) { + let hash = key_value_hash(&key, &value); + let tree = smirk! { key => value }; + + prop_assert_eq!(hash, tree.root_hash()); + } +} diff --git a/smirk/src/tree/impls.rs b/smirk/src/tree/impls.rs new file mode 100644 index 00000000..bc330676 --- /dev/null +++ b/smirk/src/tree/impls.rs @@ -0,0 +1,153 @@ +use std::{iter::Chain, option}; + +use traversal::{Bft, DftPre}; + +use super::{MerkleTree, TreeNode}; + +impl MerkleTree { + /// Returns an iterator over the keys and values in depth-first order + #[allow(clippy::must_use_candidate)] + pub fn depth_first(&self) -> DepthFirstIter { + match &self.inner { + None => DepthFirstIter { inner: None }, + Some(node) => node.depth_first(), + } + } + + /// Returns an iterator over the keys and values in breadth-first order + #[allow(clippy::must_use_candidate)] + pub fn breadth_first(&self) -> BreadthFirstIter { + match &self.inner { + None => BreadthFirstIter { inner: None }, + Some(node) => node.breadth_first(), + } + } +} + +impl TreeNode { + /// Get an iterator over the values in this node in depth-first order + fn depth_first(&self) -> DepthFirstIter { + let inner = DftPre::new(self, children); + let inner = Box::new(inner.map(|(_, node)| (&node.key, &node.value))); + + DepthFirstIter { inner: Some(inner) } + } + + /// Get an iterator over the values in this node in breadth-first order + fn breadth_first(&self) -> BreadthFirstIter<'_, K, V> { + let inner = Bft::new(self, children); + let inner = Box::new(inner.map(|(_, node)| (&node.key, &node.value))); + + BreadthFirstIter { inner: Some(inner) } + } +} + +fn children(node: &TreeNode) -> ChildIter { + node.left + .as_deref() + .into_iter() + .chain(node.right.as_deref().into_iter()) +} + +type ChildIter<'a, K, V> = + Chain>, option::IntoIter<&'a TreeNode>>; + +pub struct DepthFirstIter<'a, K, V> { + inner: Option + 'a>>, +} + +impl<'a, K, V> Iterator for DepthFirstIter<'a, K, V> { + type Item = (&'a K, &'a V); + + fn next(&mut self) -> Option { + self.inner.as_mut().and_then(Iterator::next) + } +} + +pub struct BreadthFirstIter<'a, K, V> { + inner: Option + 'a>>, +} + +impl<'a, K, V> Iterator for BreadthFirstIter<'a, K, V> { + type Item = (&'a K, &'a V); + + fn next(&mut self) -> Option { + self.inner.as_mut().and_then(Iterator::next) + } +} + +#[cfg(any(test, feature = "proptest"))] +mod proptest_impls { + use std::fmt::Debug; + + use crate::hash::Hashable; + + use super::MerkleTree; + + use proptest::{arbitrary::StrategyFor, prelude::*, strategy::Map}; + + impl Arbitrary for MerkleTree + where + K: Debug + Arbitrary + Hashable + Ord, + V: Debug + Arbitrary + Hashable, + { + type Parameters = (); + type Strategy = Map>, fn(Vec<(K, V)>) -> Self>; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + any::>().prop_map(|v| v.into_iter().collect()) + } + } +} + +#[cfg(test)] +mod tests { + use crate::{tree::MerkleTree, TreeNode}; + + // 1 + // |\ + // 2 5 + // |\ + // 3 4 + fn example_node() -> TreeNode { + TreeNode::new( + 1, + 1, + Some(TreeNode::new( + 2, + 2, + Some(TreeNode::new(3, 3, None, None)), + Some(TreeNode::new(4, 4, None, None)), + )), + Some(TreeNode::new(5, 5, None, None)), + ) + } + + #[test] + fn depth_first_test() { + let tree = example_node(); + let items: Vec<_> = tree.depth_first().map(|(k, v)| (*k, *v)).collect(); + assert_eq!(items, vec![(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]); + + assert_eq!( + MerkleTree::from_iter::<[(i32, i32); 0]>([]) + .depth_first() + .count(), + 0 + ); + } + + #[test] + fn breadth_first_test() { + let tree = example_node(); + let items: Vec<_> = tree.breadth_first().map(|(k, v)| (*k, *v)).collect(); + assert_eq!(items, vec![(1, 1), (2, 2), (5, 5), (3, 3), (4, 4)]); + + assert_eq!( + MerkleTree::from_iter::<[(i32, i32); 0]>([]) + .breadth_first() + .count(), + 0 + ); + } +} diff --git a/smirk/src/tree/iterator.rs b/smirk/src/tree/iterator.rs new file mode 100644 index 00000000..4b057a6c --- /dev/null +++ b/smirk/src/tree/iterator.rs @@ -0,0 +1,82 @@ +use std::iter::empty; + +use crate::{batch::Operation, hash::Hashable, MerkleTree, TreeNode}; + +impl FromIterator<(K, V)> for MerkleTree { + fn from_iter>(iter: T) -> Self { + let mut tree = MerkleTree::new(); + + let batch = iter + .into_iter() + .map(|(key, value)| Operation::Insert(key, value)) + .collect(); + + tree.apply(batch); + tree + } +} + +impl<'a, K, V> MerkleTree { + /// Create an [`Iterator`] over the nodes in key order (i.e. the order specified by the `Ord` + /// impl for `K`) + /// + /// ```rust + /// # use smirk::{smirk, MerkleTree}; + /// let tree = smirk! { + /// 1 => "hello", + /// 2 => "world", + /// 3 => "foo", + /// }; + /// + /// let keys: Vec<_> = tree.iter().map(|node| *node.key()).collect(); + /// + /// assert_eq!(keys, vec![1, 2, 3]); + /// ``` + #[allow(clippy::must_use_candidate)] + pub fn iter(&'a self) -> Iter<'a, K, V> { + match &self.inner { + None => Iter(Box::new(empty())), + Some(node) => Iter(Box::new(iter(node))), + } + } +} + +fn iter<'a, K, V>(node: &'a TreeNode) -> Box> + 'a> { + let left_iter = node.left.iter().flat_map(|node| iter(node)); + let right_iter = node.right.iter().flat_map(|node| iter(node)); + + Box::new(left_iter.chain(Some(node)).chain(right_iter)) +} + +pub struct Iter<'a, K, V>(Box> + 'a>); + +impl<'a, K, V> Iterator for Iter<'a, K, V> { + type Item = &'a TreeNode; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +#[cfg(test)] +mod tests { + use proptest::prop_assert_eq; + use test_strategy::proptest; + + use super::*; + + #[proptest(cases = 100)] + fn iter_order_is_correct(mut vec: Vec) { + vec.sort_unstable(); + + let mut tree = MerkleTree::new(); + + for elem in &vec { + tree.insert(*elem, *elem); + } + + let vec_again: Vec<_> = tree.iter().map(|node| *node.key()).collect(); + + prop_assert_eq!(vec, vec_again); + } +} diff --git a/smirk/src/tree/macros.rs b/smirk/src/tree/macros.rs new file mode 100644 index 00000000..a4571bfd --- /dev/null +++ b/smirk/src/tree/macros.rs @@ -0,0 +1,75 @@ +/// Macro to generate a [`MerkleTree`] with a more convenient syntax +/// +/// ```rust +/// # use smirk::smirk; +/// let tree = smirk! { +/// 1 => "hello".to_string(), +/// 2 => "world".to_string(), +/// }; +/// +/// assert_eq!(tree.get(&1).unwrap(), "hello"); +/// ``` +/// +/// [`MerkleTree`]: crate::MerkleTree +#[macro_export] +macro_rules! smirk { + {} => {{ $crate::MerkleTree::new() }}; + { $key:expr => $value:expr $(,)? } => {{ + let mut tree = $crate::MerkleTree::new(); + tree.insert($key, $value); + tree + }}; + + { $key:expr => $value:expr, $($t:tt)* } => {{ + let mut tree = smirk!{ $($t)* }; + tree.insert($key, $value); + tree + }}; +} + +#[cfg(test)] +mod tests { + use crate::MerkleTree; + + #[test] + fn simple_syntax_test() { + let tree = smirk! { + 1 => "hello", + 2 => "world" // without trailing comma + }; + + let other_tree = smirk! { + 1 => "hello", + 2 => "world", // with trailing comma + }; + + assert_eq!(tree.root_hash(), other_tree.root_hash()); + + assert_eq!(*tree.get(&1).unwrap(), "hello"); + assert_eq!(*tree.get(&2).unwrap(), "world"); + assert_eq!(tree.get(&3), None); + + let _many_items = smirk! { + 1 => "hello", + 2 => "world", + 3 => "foo", + 4 => "bar", + }; + let _many_items_no_trailing = smirk! { + 1 => "hello", + 2 => "world", + 3 => "foo", + 4 => "bar" + }; + + let _single_item = smirk! { + 1 => "hello", + }; + + let _single_item_no_trailing = smirk! { + 1 => "hello" + }; + + let _empty: MerkleTree = smirk! {}; + } +} diff --git a/smirk/src/tree/mod.rs b/smirk/src/tree/mod.rs new file mode 100644 index 00000000..dbc40b9e --- /dev/null +++ b/smirk/src/tree/mod.rs @@ -0,0 +1,447 @@ +use std::{borrow::Borrow, cmp::Ordering}; + +use crate::hash::{Digest, Hashable}; + +/// Batch API for performing many operations on a [`MerkleTree`] at once +pub mod batch; +mod iterator; +pub use iterator::*; + +mod impls; +pub use impls::*; + +mod macros; +pub mod visitor; + +mod proof; + +mod hash; +pub use hash::key_value_hash; + +#[cfg(test)] +mod tests; + +/// A Merkle tree with a map-like API +/// +/// ```rust +/// # use smirk::{MerkleTree, smirk}; +/// let mut tree = MerkleTree::new(); +/// tree.insert(123, "hello"); +/// +/// // or you can use the macro to create a tree +/// let tree = smirk! { +/// 123 => "hello", +/// }; +/// +/// assert_eq!(tree.size(), 1); +/// ``` +/// You can use [`MerkleTree::iter`] to get an iterator over tuples of key-value pairs +/// +/// The order will be the order specified by the [`Ord`] implementation for the key type +/// ```rust +/// # use smirk::smirk; +/// let tree = smirk! { +/// 1 => 123, +/// 2 => 234, +/// 3 => 345, +/// }; +/// +/// let pairs: Vec<(i32, i32)> = tree +/// .iter() +/// .map(|node| (*node.key(), *node.value())) +/// .collect(); +/// +/// assert_eq!(pairs, vec![ +/// (1, 123), +/// (2, 234), +/// (3, 345), +/// ]); +/// ``` +/// You can also go the other way via [`FromIterator`], just like you would for a [`HashMap`]: +/// ```rust +/// # use smirk::MerkleTree; +/// let pairs = vec![ +/// (1, 123), +/// (2, 234), +/// (3, 345), +/// ]; +/// let tree: MerkleTree<_, _> = pairs.into_iter().collect(); +/// +/// assert_eq!(tree.size(), 3); +/// ``` +/// +/// Broadly speaking, to do anything useful with a Merkle tree, the key type must implement +/// [`Ord`] and [`Hashable`], and the value type must implement [`Hashable`] +/// +/// Warning: *DO NOT* use types with interior mutability as either the +/// key or value in this tree, since it can potentially invalidate hashes/ordering guarantees that +/// the tree otherwise maintains. +/// +/// If this happens, behaviour of the tree is unspecified, but not +/// undefined. In other words, the usual soundness rules will be upheld, but any function performed +/// on the tree itself may give incorrect results +/// +/// [`HashMap`]: std::collections::HashMap +/// +#[derive(Debug, Clone)] +pub struct MerkleTree { + pub(crate) inner: Option>>, +} + +impl PartialEq for MerkleTree { + fn eq(&self, other: &Self) -> bool { + self.root_hash() == other.root_hash() + } +} + +impl Hashable for MerkleTree +where + K: Hashable, + V: Hashable, +{ + fn hash(&self) -> Digest { + self.root_hash() + } +} + +impl Default for MerkleTree { + fn default() -> Self { + Self::new() + } +} + +impl MerkleTree { + /// Create a new, empty [`MerkleTree`] + /// + /// ```rust + /// # use smirk::MerkleTree; + /// let tree = MerkleTree::::new(); + /// ``` + #[inline] + #[must_use] + pub fn new() -> Self { + Self { inner: None } + } + + /// Insert a new key-value pair into the tree + /// + /// ```rust + /// # use smirk::MerkleTree; + /// let mut tree = MerkleTree::new(); + /// tree.insert(1, "hello".to_string()); + /// + /// assert_eq!(tree.get(&1).unwrap(), "hello"); + /// ``` + /// If the key is already present in the tree, the tree is left unchanged + /// + /// Note: inserting a single value will potentially rebalance the tree, and also recompute hash + /// values, which can be expensive. If you are inserting many items, consider using + /// [`MerkleTree::apply`] + pub fn insert(&mut self, key: K, value: V) + where + K: Hashable + Ord, + V: Hashable, + { + self.insert_without_update(key, value); + self.recalculate_hash_recursive(); + } + + /// Basically [`MerkleTree::insert`] but without updating the hashes - performance optimization + /// for batch API + pub(crate) fn insert_without_update(&mut self, key: K, value: V) + where + K: Hashable + Ord, + V: Hashable, + { + self.inner = Some(Self::insert_node(self.inner.take(), key, value)); + } + + #[allow(clippy::unnecesary_box_returns)] + fn insert_node(node: Option>>, key: K, value: V) -> Box> + where + K: Hashable + Ord, + V: Hashable, + { + let Some(mut node) = node else { return Box::new(TreeNode::new(key, value, None, None)) }; + + match key.cmp(&node.key) { + Ordering::Equal => { + node.value = value; + } + Ordering::Less => { + node.left = Some(Self::insert_node(node.left.take(), key, value)); + } + Ordering::Greater => { + node.right = Some(Self::insert_node(node.right.take(), key, value)); + } + } + + node.update_height(); + Self::balance(node) + } + + fn balance(mut node: Box>) -> Box> { + let balance = node.balance_factor(); + + if balance > 1 { + if node.left.as_ref().unwrap().balance_factor() < 0 { + node.left = Some(Self::rotate_left(node.left.unwrap())); + } + node = Self::rotate_right(node); + } else if balance < -1 { + if node.right.as_ref().unwrap().balance_factor() > 0 { + node.right = Some(Self::rotate_right(node.right.unwrap())); + } + node = Self::rotate_left(node); + } + + node + } + + fn rotate_left(mut root: Box>) -> Box> { + let mut new_root = root.right.take().unwrap(); + root.right = new_root.left.take(); + new_root.left = Some(root); + + new_root.left.as_mut().unwrap().update_height(); + new_root.update_height(); + + new_root + } + + fn rotate_right(mut root: Box>) -> Box> { + let mut new_root = root.left.take().unwrap(); + root.left = new_root.right.take(); + new_root.right = Some(root); + new_root.right.as_mut().unwrap().update_height(); + new_root.update_height(); + + new_root + } + + /// The number of elements in the tree + /// + /// ```rust + /// # use smirk::smirk; + /// let tree = smirk! { + /// 1 => "hello", + /// 2 => "world", + /// 3 => "foo", + /// }; + /// + /// assert_eq!(tree.size(), 3); + /// ``` + #[must_use] + pub fn size(&self) -> usize { + struct Counter(usize); + impl visitor::Visitor for Counter { + fn visit(&mut self, _: &K, _: &V) { + self.0 = self + .0 + .checked_add(1) + .expect("this is never going to overflow"); + } + } + + let mut counter = Counter(0); + self.visit(&mut counter); + + counter.0 + } + + /// Returns true if and only if the tree contains no elements + #[must_use] + pub fn is_empty(&self) -> bool { + self.size() == 0 + } + + /// Returns `true` if and only if `key` is present in the tree + /// + /// ```rust + /// # use smirk::smirk; + /// let tree = smirk! { + /// 1 => "hello", + /// }; + /// + /// assert!(tree.contains(&1)); + /// assert!(!tree.contains(&2)); + /// ``` + pub fn contains(&self, key: &Q) -> bool + where + Q: Borrow + ?Sized, + K: Ord, + { + self.get(key).is_some() + } + + /// The height of this tree + #[inline] + #[must_use] + pub fn height(&self) -> usize { + match &self.inner { + None => 0, + Some(node) => node.height(), + } + } + + /// Get the value associated with the given key + /// + /// If you need access to the node itself, consider using [`MerkleTree::get_node`] + /// ```rust + /// # use smirk::smirk; + /// let tree = smirk! { + /// 1 => "hello".to_string(), + /// }; + /// + /// assert_eq!(tree.get(&1).unwrap(), "hello"); + /// assert!(tree.get(&2).is_none()); + /// ``` + pub fn get(&self, key: &Q) -> Option<&V> + where + Q: Borrow + ?Sized, + K: Ord, + { + self.inner.as_ref().and_then(|node| node.get(key)) + } + + /// Get the node associated with the given key + /// + /// If you only need access to the value stored in this node, consider using [`MerkleTree::get`] + /// ```rust + /// # use smirk::smirk; + /// # use smirk::hash::Digest; + /// let tree = smirk! { + /// 1 => "hello".to_string(), + /// }; + /// + /// let node = tree.get_node(&1).unwrap(); + /// + /// assert_eq!(*node.key(), 1); + /// assert_eq!(*node.value(), "hello"); + /// let _hash = node.hash(); // the hash of this node plus all the children + /// ``` + pub fn get_node(&self, key: &Q) -> Option<&TreeNode> + where + Q: Borrow + ?Sized, + K: Ord, + { + self.inner.as_ref().and_then(|node| node.get_node(key)) + } +} + +/// An individual node in a Merkle tree +#[derive(Debug, Clone)] +pub struct TreeNode { + pub(crate) key: K, + pub(crate) value: V, + pub(crate) hash: Digest, + pub(crate) left: Option>>, + pub(crate) right: Option>>, + pub(crate) height: usize, +} + +impl TreeNode { + /// The height of the tree + /// + /// For example, in a tree with 3 nodes A, B, C, where A is the parent of B and C: + /// - A has height 1 + /// - B has height 0 + /// - C has height 0 + #[must_use] + pub fn height(&self) -> usize { + self.height + } + + // pub(crate) for testing only + pub(crate) fn update_height(&mut self) { + let left_height = self.left.as_ref().map_or(0, |x| x.height()); + let right_height = self.right.as_ref().map_or(0, |x| x.height()); + self.height = std::cmp::max(left_height, right_height) + .checked_add(1) + .expect("this is never going to overflow"); + } + + fn balance_factor(&self) -> isize { + let left_height = self.left.as_ref().map_or(0, |x| x.height()); + let right_height = self.right.as_ref().map_or(0, |x| x.height()); + + let left_height = isize::try_from(left_height).expect("height never overflows"); + let right_height = isize::try_from(right_height).expect("height never overflows"); + + left_height + .checked_sub(right_height) + .expect("this is never going to over/underflow") + } + + fn get(&self, key: &Q) -> Option<&V> + where + Q: Borrow + ?Sized, + K: Ord, + { + let node = self.get_node(key)?; + Some(&node.value) + } + + fn get_node(&self, key: &Q) -> Option<&TreeNode> + where + Q: Borrow + ?Sized, + K: Ord, + { + match key.borrow().cmp(&self.key) { + Ordering::Less => self.left.as_ref().and_then(|node| node.get_node(key)), + Ordering::Greater => self.right.as_ref().and_then(|node| node.get_node(key)), + Ordering::Equal => Some(self), + } + } +} + +impl TreeNode { + pub(crate) fn new( + key: K, + value: V, + left: Option>, + right: Option>, + ) -> Self { + let hash = Digest::NULL; + let left = left.map(Box::new); + let right = right.map(Box::new); + + let mut node = Self { + key, + value, + hash, + left, + right, + height: 0, + }; + + node.update_height(); + node.recalculate_hash_recursive(); + + node + } + + /// The key associated with this node + pub fn key(&self) -> &K { + &self.key + } + + /// The value associated with this node + pub fn value(&self) -> &V { + &self.value + } + + /// The hash of this node and all child nodes + #[inline] + pub fn hash(&self) -> Digest { + self.hash + } + + /// The hash of the value contained in this node + /// + /// Note: this is unaffected by the value of child nodes + #[inline] + pub fn hash_of_value(&self) -> Digest { + self.value.hash() + } +} diff --git a/smirk/src/tree/proof.rs b/smirk/src/tree/proof.rs new file mode 100644 index 00000000..04a4d4cf --- /dev/null +++ b/smirk/src/tree/proof.rs @@ -0,0 +1,163 @@ +use std::{borrow::Borrow, cmp::Ordering}; + +use crate::{ + hash::{Digest, Hashable, MerklePath, Stage}, + key_value_hash, MerkleTree, +}; + +use super::hash::hash_left_right_this; + +impl MerkleTree { + /// Generate a [`MerklePath`] that proves that a given key exists in the tree + /// + /// ```rust + /// # use smirk::{smirk, hash::MerklePath}; + /// let tree = smirk! { + /// 1 => "hello", + /// 2 => "world", + /// }; + /// + /// assert!(tree.prove(&1).is_some()); + /// assert!(tree.prove(&2).is_some()); + /// assert!(tree.prove(&3).is_none()); + /// ``` + pub fn prove(&self, key: &Q) -> Option + where + Q: Borrow + ?Sized, + K: Ord, + { + let Some(mut node) = self.inner.as_deref() else { return None }; + let mut stages = Vec::with_capacity(node.height()); + + loop { + match key.borrow().cmp(&node.key) { + Ordering::Less => { + let this = key_value_hash(&node.key, &node.value); + let right = node.right_hash(); + let stage = Stage::Left { this, right }; + stages.push(stage); + + node = node.left.as_deref()?; + } + Ordering::Greater => { + let this = key_value_hash(&node.key, &node.value); + let left = node.left_hash(); + let stage = Stage::Right { this, left }; + stages.push(stage); + + node = node.right.as_deref()?; + } + Ordering::Equal => { + let left = node.left_hash(); + let right = node.right_hash(); + let root_hash = self.root_hash(); + + let path = MerklePath { + stages, + root_hash, + left, + right, + }; + + return Some(path); + } + } + } + } + + /// Get the root hash of the Merkle tree + /// + /// ```rust + /// # use smirk::smirk; + /// let mut tree = smirk! { 1 => "hello" }; + /// let hash = tree.root_hash(); + /// + /// tree.insert(2, "world"); + /// let new_hash = tree.root_hash(); + /// + /// assert_ne!(hash, new_hash); + /// ``` + /// + /// The root hash can be viewed as a "summary" of the whole tree - any change to any key or + /// value will change the root hash. Changing the "layout" of the tree will also change the + /// root hash + #[must_use] + pub fn root_hash(&self) -> Digest { + match &self.inner { + None => Digest::NULL, // should this function return an option? + Some(node) => node.hash, + } + } +} + +impl MerklePath { + /// Verify that the given key-value pair exists in the tree that generated this [`MerklePath`] + #[must_use = "this function indicates a verification failure by returning false"] + pub fn verify(&self, key: &K, value: &V) -> bool { + let mut hash = key_value_hash(key, value); + hash = hash_left_right_this(hash, self.left, self.right); + + for stage in self.stages.iter().rev() { + match *stage { + Stage::Left { this, right } => hash = hash_left_right_this(this, Some(hash), right), + Stage::Right { this, left } => hash = hash_left_right_this(this, left, Some(hash)), + } + } + + hash == self.root_hash + } +} + +#[cfg(test)] +mod tests { + use test_strategy::proptest; + + use crate::{smirk, MerkleTree}; + + #[test] + fn simple_proof_example() { + let tree = smirk! { + 1 => "hello", + 2 => "world", + 3 => "foo", + }; + + let path = tree.prove(&1).unwrap(); + + assert!(path.verify(&1, &"hello")); + assert!(!path.verify(&2, &"hello")); + assert!(!path.verify(&1, &"world")); + + assert!(tree.prove(&4).is_none()); + } + + #[proptest] + fn all_proof_root_hash_match(tree: MerkleTree) { + for node in tree.iter() { + let proof = tree.prove(node.key()).unwrap(); + assert_eq!(proof.root_hash(), tree.root_hash()); + } + } + + // we use u8 as the key type to improve the chances of it being in the tree + #[proptest] + fn proof_succeeds_iff_key_contained(tree: MerkleTree, key: u8) { + let tree_contains_key = tree.contains(&key); + let proof_valid = tree.prove(&key).is_some(); + + assert_eq!(tree_contains_key, proof_valid); + } + + #[proptest] + fn proof_is_valid(tree: MerkleTree, key: u8) { + let proof = tree.prove(&key); + + let Some(value) = tree.get(&key) else { return Ok(()); }; + let proof = proof.unwrap(); + + let valid = proof.verify(&key, value); + assert!(valid); + + assert_eq!(tree.root_hash(), proof.root_hash()); + } +} diff --git a/smirk/src/tree/tests.rs b/smirk/src/tree/tests.rs new file mode 100644 index 00000000..d812d92d --- /dev/null +++ b/smirk/src/tree/tests.rs @@ -0,0 +1,91 @@ +use test_strategy::proptest; + +use crate::{ + hash::{Digest, Hashable}, + smirk, MerkleTree, +}; + +#[test] +fn simple_example() { + let mut tree = smirk! { + 1 => 1, + 2 => 2, + 3 => 3, + }; + + assert_eq!(tree.size(), 3); + + tree.insert(4, 4); + assert_eq!(tree.size(), 4); + + println!("{tree:#?}"); + + let _items: Vec<_> = tree.depth_first().collect(); +} + +#[test] +fn insert_already_exists() { + let mut tree = smirk! { 1 => "hello" }; + + tree.insert(1, "world"); + + assert_eq!(*tree.get(&1).unwrap(), "world"); +} + +#[test] +fn new_tree_is_empty() { + let tree = MerkleTree::::new(); + assert!(tree.is_empty()); +} + +#[proptest(cases = 100)] +fn collecting_tree_has_same_length(items: Vec) { + let len = items.len(); + let tree: MerkleTree<_, _> = items.into_iter().map(|i| (i, i)).collect(); + + assert_eq!(tree.size(), len); +} + +#[test] +fn hash_includes_key_and_value() { + let tree = smirk! { 1 => "hello" }; + let different_key = smirk! { 2 => "hello" }; + let different_value = smirk! { 1 => "world" }; + + let hash = |tree: &MerkleTree| tree.inner.as_ref().unwrap().hash; + + assert_ne!(hash(&tree), hash(&different_key)); + assert_ne!(hash(&tree), hash(&different_value)); +} + +#[test] +fn hash_of_leaf_is_correct() { + let tree = smirk! { 1 => "hello" }; + let hash = tree.inner.as_ref().unwrap().hash; + + let expected: Digest = [1.hash(), "hello".hash()].iter().collect(); + + assert_eq!(hash, expected); +} + +#[test] +fn stays_balanced_in_order_inserts() { + let values = (0..1000).map(|i| (i, i)).collect(); + stays_balanced(values); +} + +#[proptest] +fn tree_stays_balanced(values: Vec<(i32, i32)>) { + stays_balanced(values); +} + +fn stays_balanced(values: Vec<(i32, i32)>) { + let mut tree = smirk! {}; + + for (key, value) in values { + tree.insert(key, value); + let balance = tree.inner.as_ref().unwrap().balance_factor(); + assert!(balance <= 1); + assert!(balance >= -1); + } +} diff --git a/smirk/src/tree/visitor.rs b/smirk/src/tree/visitor.rs new file mode 100644 index 00000000..06bb84df --- /dev/null +++ b/smirk/src/tree/visitor.rs @@ -0,0 +1,82 @@ +use super::{MerkleTree, TreeNode}; + +/// A trait for types which can visit nodes in a Merkle tree +/// +/// Note: currently only immutable access is given to prevent invalidating memoized hashes +pub trait Visitor { + /// The function to be called on each node + fn visit(&mut self, key: &K, value: &V); +} + +impl Visitor for &mut Vis +where + Vis: Visitor, +{ + fn visit(&mut self, key: &K, value: &V) { + Vis::visit(self, key, value); + } +} + +impl MerkleTree { + /// Apply a visitor to all the nodes in a tree + /// + /// The visitor will run on `self`, then `left` (if it is `Some`), then `right` (if it is `Some`) + pub fn visit>(&self, mut visitor: Vis) { + if let Some(inner) = &self.inner { + inner.visit(&mut visitor); + } + } +} + +impl TreeNode { + fn visit>(&self, visitor: &mut Vis) { + visitor.visit(&self.key, &self.value); + + if let Some(left) = &self.left { + left.visit(visitor); + } + + if let Some(right) = &self.right { + right.visit(visitor); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn counter_example() { + struct Counter(usize); + + impl Visitor for Counter { + fn visit(&mut self, _: &K, _: &V) { + self.0 += 1; + } + } + + let tree = MerkleTree::from_iter([(1, 1), (2, 2), (3, 3)]); + let mut counter = Counter(0); + tree.visit(&mut counter); + + assert_eq!(counter.0, 3); + } + + #[test] + fn sum_example() { + struct Sum(i32); + + impl Visitor for Sum { + fn visit(&mut self, key: &i32, _value: &i32) { + self.0 += *key; + } + } + + let tree = MerkleTree::from_iter([(1, 1), (2, 2), (3, 3)]); + let mut sum = Sum(0); + tree.visit(&mut sum); + + assert_eq!(sum.0, 6); + } +} diff --git a/solid/Cargo.toml b/solid/Cargo.toml index 0753a01c..d4a4efbe 100644 --- a/solid/Cargo.toml +++ b/solid/Cargo.toml @@ -10,7 +10,7 @@ tokio = { version = "1", features = ["full"] } serde = { version = "1.0", features = ["derive"] } sha2 = "0.10.6" uint = "0.9.5" -libp2p-core = { versin = "0.39.0" } +# libp2p-core = { versin = "0.39.0" } futures-timer = "3.0.2" futures = "0.3.26" prost = "0.11"