From b769ee77befbd23c39b48343d008b272c79c45d3 Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Mon, 11 Mar 2024 15:19:54 -0700 Subject: [PATCH 01/29] chore: update crate versions to v0.2.0 --- Cargo.lock | 12 ++++++------ block-producer/Cargo.toml | 8 ++++---- node/Cargo.toml | 12 ++++++------ proto/Cargo.toml | 4 ++-- rpc/Cargo.toml | 12 ++++++------ store/Cargo.toml | 8 ++++---- utils/Cargo.toml | 2 +- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7973814ba..91dd8340b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1119,7 +1119,7 @@ dependencies = [ [[package]] name = "miden-node" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", @@ -1138,7 +1138,7 @@ dependencies = [ [[package]] name = "miden-node-block-producer" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "async-trait", @@ -1167,7 +1167,7 @@ dependencies = [ [[package]] name = "miden-node-proto" -version = "0.1.0" +version = "0.2.0" dependencies = [ "hex", "miden-node-utils", @@ -1184,7 +1184,7 @@ dependencies = [ [[package]] name = "miden-node-rpc" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", @@ -1208,7 +1208,7 @@ dependencies = [ [[package]] name = "miden-node-store" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", @@ -1243,7 +1243,7 @@ dependencies = [ [[package]] name = "miden-node-utils" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "figment", diff --git a/block-producer/Cargo.toml b/block-producer/Cargo.toml index 64593d389..0a99e2fe8 100644 --- a/block-producer/Cargo.toml +++ b/block-producer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miden-node-block-producer" -version = "0.1.0" +version = "0.2.0" description = "Miden node's block producer component" authors = ["miden contributors"] readme = "README.md" @@ -25,9 +25,9 @@ async-trait = { version = "0.1" } clap = { version = "4.3", features = ["derive"] } figment = { version = "0.10", features = ["toml", "env"] } itertools = { version = "0.12" } -miden-node-proto = { path = "../proto", version = "0.1" } -miden-node-store = { path = "../store", version = "0.1" } -miden-node-utils = { path = "../utils", version = "0.1" } +miden-node-proto = { path = "../proto", version = "0.2" } +miden-node-store = { path = "../store", version = "0.2" } +miden-node-utils = { path = "../utils", version = "0.2" } miden-objects = { workspace = true } miden-processor = { workspace = true } miden-stdlib = { workspace = true } diff --git a/node/Cargo.toml b/node/Cargo.toml index 1e0bfcf8a..26eb2ad83 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miden-node" -version = "0.1.0" +version = "0.2.0" description = "Miden node single binary" authors = ["miden contributors"] readme = "README.md" @@ -19,10 +19,10 @@ tracing-forest = ["miden-node-block-producer/tracing-forest"] anyhow = { version = "1.0" } clap = { version = "4.3", features = ["derive"] } miden-lib = { workspace = true, features = ["concurrent"] } -miden-node-block-producer = { path = "../block-producer", version = "0.1" } -miden-node-rpc = { path = "../rpc", version = "0.1" } -miden-node-store = { path = "../store", version = "0.1" } -miden-node-utils = { path = "../utils", version = "0.1" } +miden-node-block-producer = { path = "../block-producer", version = "0.2" } +miden-node-rpc = { path = "../rpc", version = "0.2" } +miden-node-store = { path = "../store", version = "0.2" } +miden-node-utils = { path = "../utils", version = "0.2" } miden-objects = { workspace = true } serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.29", features = ["rt-multi-thread", "net", "macros"] } @@ -31,4 +31,4 @@ tracing-subscriber = { workspace = true } [dev-dependencies] figment = { version = "0.10", features = ["toml", "env", "test"] } -miden-node-utils = { path = "../utils", version = "0.1", features = ["tracing-forest"] } +miden-node-utils = { path = "../utils", version = "0.2", features = ["tracing-forest"] } diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 7d68981fb..4a89426e4 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miden-node-proto" -version = "0.1.0" +version = "0.2.0" description = "Miden RPC message definitions" authors = ["miden contributors"] readme = "README.md" @@ -12,7 +12,7 @@ rust-version = "1.75" [dependencies] hex = { version = "0.4" } -miden-node-utils = { path = "../utils", version = "0.1" } +miden-node-utils = { path = "../utils", version = "0.2" } miden-objects = { workspace = true } prost = { version = "0.12" } thiserror = { workspace = true } diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 6137abbaf..336a70e5e 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miden-node-rpc" -version = "0.1.0" +version = "0.2.0" description = "Miden node's front-end RPC server" authors = ["miden contributors"] readme = "README.md" @@ -16,10 +16,10 @@ clap = { version = "4.3", features = ["derive"] } directories = { version = "5.0" } figment = { version = "0.10", features = ["toml", "env"] } hex = { version = "0.4" } -miden-node-block-producer = { path = "../block-producer", version = "0.1" } -miden-node-proto = { path = "../proto", version = "0.1" } -miden-node-store = { path = "../store", version = "0.1" } -miden-node-utils = { path = "../utils", version = "0.1" } +miden-node-block-producer = { path = "../block-producer", version = "0.2" } +miden-node-proto = { path = "../proto", version = "0.2" } +miden-node-store = { path = "../store", version = "0.2" } +miden-node-utils = { path = "../utils", version = "0.2" } miden-objects = { workspace = true } miden-tx = { workspace = true } prost = { version = "0.12" } @@ -32,4 +32,4 @@ tracing-subscriber = { workspace = true } [dev-dependencies] figment = { version = "0.10", features = ["toml", "env", "test"] } -miden-node-utils = { path = "../utils", version = "0.1", features = ["tracing-forest"] } +miden-node-utils = { path = "../utils", version = "0.2", features = ["tracing-forest"] } diff --git a/store/Cargo.toml b/store/Cargo.toml index 35f50508d..3d9929eaf 100644 --- a/store/Cargo.toml +++ b/store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miden-node-store" -version = "0.1.0" +version = "0.2.0" description = "Miden node's state store component" authors = ["miden contributors"] readme = "README.md" @@ -24,8 +24,8 @@ directories = { version = "5.0" } figment = { version = "0.10", features = ["toml", "env"] } hex = { version = "0.4" } miden-lib = { workspace = true } -miden-node-proto = { path = "../proto", version = "0.1" } -miden-node-utils = { path = "../utils", version = "0.1" } +miden-node-proto = { path = "../proto", version = "0.2" } +miden-node-utils = { path = "../utils", version = "0.2" } miden-objects = { workspace = true } once_cell = { version = "1.18.0" } prost = { version = "0.12" } @@ -41,4 +41,4 @@ tracing-subscriber = { workspace = true } [dev-dependencies] figment = { version = "0.10", features = ["toml", "env", "test"] } -miden-node-utils = { path = "../utils", version = "0.1", features = ["tracing-forest"] } +miden-node-utils = { path = "../utils", version = "0.2", features = ["tracing-forest"] } diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 7365ccfd2..442af69df 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miden-node-utils" -version = "0.1.0" +version = "0.2.0" description = "Miden node's shared utilities" authors = ["miden contributors"] readme = "README.md" From 2a8f37ad4ec4f434747bd2d12cfec7c8506ee280 Mon Sep 17 00:00:00 2001 From: Paul-Henry Kajfasz <42912740+phklive@users.noreply.github.com> Date: Thu, 14 Mar 2024 10:45:16 +0100 Subject: [PATCH 02/29] Add script to check rust versions + cleanup (#272) * ci: turn doc warnings into errors (#259) * remove unnecessary comments * Add script to check rust versions * removed versions and editions except from root, updated script * add back cargo make doc to ci * Added section for testing * Fixed typo * Set versions to 0.1.0 + inherit all infos from workspace * Fix use of versioning * Set individual versions for crates * Fixed nits, code organization and updated links * Improve naming of ci job * Fix formatting in contributing.md * Add task to format not only check * Reduced indent --------- Co-authored-by: Augusto Hack --- .github/workflows/ci.yml | 11 +++++++++++ .gitignore | 5 +---- CONTRIBUTING.md | 11 +++++++++-- Cargo.toml | 11 +++++++++++ Makefile.toml | 6 ++++++ block-producer/Cargo.toml | 15 ++++++++------- node/Cargo.toml | 15 ++++++++------- proto/Cargo.toml | 13 +++++++------ rpc/Cargo.toml | 13 +++++++------ scripts/check-rust-version.sh | 13 +++++++++++++ store/Cargo.toml | 13 +++++++------ test-macro/Cargo.toml | 15 ++++++++------- utils/Cargo.toml | 13 +++++++------ 13 files changed, 103 insertions(+), 51 deletions(-) create mode 100755 scripts/check-rust-version.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e68234e5d..ad8e18276 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,17 @@ on: types: [opened, reopened, synchronize] jobs: + version: + name: check rust version consistency + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + profile: minimal + override: true + - name: check rust versions + run: ./scripts/check-rust-version.sh + rustfmt: name: rustfmt nightly on ubuntu-latest runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index f01a5aff5..30edfb6b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Generated by Cargo + # will have compiled files and executables debug/ target/ @@ -6,10 +7,6 @@ target/ # Generated by protox `file_descriptor_set.bin` *.bin -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -# Cargo.lock - # These are backup files generated by rustfmt **/*.rs.bk diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd9ada360..1a8ef1707 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,10 +4,10 @@ We want to make contributing to this project as easy and transparent as possible, whether it's: -- Reporting a [bug](https://github.com/0xPolygonMiden/miden-node/issues/new) +- Reporting a [bug](https://github.com/0xPolygonMiden/miden-node/issues/new?assignees=&labels=bug&projects=&template=1-bugreport.yml&title=%5BBug%5D%3A+) - Taking part in [discussions](https://github.com/0xPolygonMiden/miden-node/discussions) - Submitting a [fix](https://github.com/0xPolygonMiden/miden-node/pulls) -- Proposing new [features](https://github.com/0xPolygonMiden/miden-node/issues/new) +- Proposing new [features](https://github.com/0xPolygonMiden/miden-node/issues/new?assignees=&labels=enhancement&projects=&template=2-feature-request.yml&title=%5BFeature%5D%3A+)   @@ -80,6 +80,13 @@ For example, a new change to the `miden-node-store` crate might have the followi You can find more information about the `cargo make` commands in the [Makefile](Makefile.toml) +### Testing +After writing code different types of tests (unit, integration, end-to-end) are required to make sure that the correct behavior has been achieved and that no bugs have been introduced. You can run tests using the following command: + +``` +cargo make test +``` + ### Versioning We use [semver](https://semver.org/) naming convention. diff --git a/Cargo.toml b/Cargo.toml index 788b34cbb..81310bd4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,19 @@ members = [ "utils", "test-macro", ] + resolver = "2" +[workspace.package] +edition = "2021" +rust-version = "1.75" +license = "MIT" +authors = ["Miden contributors"] +readme = "README.md" +homepage = "https://polygon.technology/polygon-miden" +repository = "https://github.com/0xPolygonMiden/miden-node" +exclude = [".github/"] + [workspace.dependencies] miden-air = { version = "0.8", default-features = false } miden-lib = { version = "0.1" } diff --git a/Makefile.toml b/Makefile.toml index 4df41d232..3843976f7 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -6,6 +6,11 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true [tasks.format] toolchain = "nightly" command = "cargo" +args = ["fmt", "--all"] + +[tasks.format-check] +toolchain = "nightly" +command = "cargo" args = ["fmt", "--all", "--", "--check"] [tasks.clippy-default] @@ -34,6 +39,7 @@ args = ["test", "--all-features", "--workspace", "--", "--nocapture"] [tasks.lint] dependencies = [ "format", + "format-check", "clippy", "docs" ] diff --git a/block-producer/Cargo.toml b/block-producer/Cargo.toml index 0a99e2fe8..864dd9012 100644 --- a/block-producer/Cargo.toml +++ b/block-producer/Cargo.toml @@ -2,13 +2,14 @@ name = "miden-node-block-producer" version = "0.2.0" description = "Miden node's block producer component" -authors = ["miden contributors"] -readme = "README.md" -license = "MIT" -repository = "https://github.com/0xPolygonMiden/miden-node" -keywords = ["miden", "node", "store"] -edition = "2021" -rust-version = "1.75" +keywords = ["miden", "node", "block-producer"] +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true [[bin]] name = "miden-node-block-producer" diff --git a/node/Cargo.toml b/node/Cargo.toml index 26eb2ad83..e9fdcc236 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "miden-node" version = "0.2.0" -description = "Miden node single binary" -authors = ["miden contributors"] -readme = "README.md" -license = "MIT" -repository = "https://github.com/0xPolygonMiden/miden-node" +description = "Miden node binary" keywords = ["miden", "node"] -edition = "2021" -rust-version = "1.75" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true [features] # Makes `make-genesis` subcommand run faster. Is only suitable for testing. diff --git a/proto/Cargo.toml b/proto/Cargo.toml index 4a89426e4..2080e7687 100644 --- a/proto/Cargo.toml +++ b/proto/Cargo.toml @@ -2,13 +2,14 @@ name = "miden-node-proto" version = "0.2.0" description = "Miden RPC message definitions" -authors = ["miden contributors"] -readme = "README.md" -license = "MIT" -repository = "https://github.com/0xPolygonMiden/miden-node" keywords = ["miden", "node", "protobuf", "rpc"] -edition = "2021" -rust-version = "1.75" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true [dependencies] hex = { version = "0.4" } diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 336a70e5e..48c216c76 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -2,13 +2,14 @@ name = "miden-node-rpc" version = "0.2.0" description = "Miden node's front-end RPC server" -authors = ["miden contributors"] -readme = "README.md" -license = "MIT" -repository = "https://github.com/0xPolygonMiden/miden-node" keywords = ["miden", "node", "rpc"] -edition = "2021" -rust-version = "1.75" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true [dependencies] anyhow = { version = "1.0" } diff --git a/scripts/check-rust-version.sh b/scripts/check-rust-version.sh new file mode 100755 index 000000000..a98dd8bf3 --- /dev/null +++ b/scripts/check-rust-version.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Check rust-toolchain file +TOOLCHAIN_VERSION=$(cat rust-toolchain) + +# Check workspace Cargo.toml file +CARGO_VERSION=$(cat Cargo.toml | grep "rust-version" | cut -d '"' -f 2) +if [ "$CARGO_VERSION" != "$TOOLCHAIN_VERSION" ]; then + echo "Mismatch in $file. Expected $TOOLCHAIN_VERSION, found $CARGO_VERSION" + exit 1 +fi + +echo "Rust versions match ✅" diff --git a/store/Cargo.toml b/store/Cargo.toml index 3d9929eaf..a485300df 100644 --- a/store/Cargo.toml +++ b/store/Cargo.toml @@ -2,13 +2,14 @@ name = "miden-node-store" version = "0.2.0" description = "Miden node's state store component" -authors = ["miden contributors"] -readme = "README.md" -license = "MIT" -repository = "https://github.com/0xPolygonMiden/miden-node" keywords = ["miden", "node", "store"] -edition = "2021" -rust-version = "1.75" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true [[bin]] name = "miden-node-store" diff --git a/test-macro/Cargo.toml b/test-macro/Cargo.toml index dfd5ad2f3..a02c66c9c 100644 --- a/test-macro/Cargo.toml +++ b/test-macro/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "miden-node-test-macro" version = "0.1.0" -description = "Miden's test macro" -authors = ["miden contributors"] -readme = "README.md" -license = "MIT" -repository = "https://github.com/0xPolygonMiden/miden-node" +description = "Miden node's test macro" keywords = ["miden", "node", "utils", "macro"] -edition = "2021" -rust-version = "1.75" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true [dependencies] quote = { version = "1.0" } diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 442af69df..39f2d4486 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -2,13 +2,14 @@ name = "miden-node-utils" version = "0.2.0" description = "Miden node's shared utilities" -authors = ["miden contributors"] -readme = "README.md" -license = "MIT" -repository = "https://github.com/0xPolygonMiden/miden-node" keywords = ["miden", "node", "utils"] -edition = "2021" -rust-version = "1.75" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true [dependencies] anyhow = { version = "1.0" } From 2e2813ccc466ca062bf94e465a275cfead599cee Mon Sep 17 00:00:00 2001 From: Paul-Henry Kajfasz <42912740+phklive@users.noreply.github.com> Date: Thu, 14 Mar 2024 16:23:29 +0100 Subject: [PATCH 03/29] Dockerize node (#257) * Working initial docker * fmt * added script * Removed docker script + updated integration * script works * Need to install grpcurl * Docker works * Working Dockerfile * ci: turn doc warnings into errors (#259) * Removed makefile, removing start.sh file moving in Dockerfile * Added bookworm + alpine + removed gcc + added caching * Moved Dockerfile to node and added Makefile.toml * cargo make works + builds dockerfile for node * remove start script * added comment regarding PID1 * Set labels at top of Dockerfile + added comments * Added correct dependencies and explanation * Volume works persisting db files between runs * Added documentation * Moved labels + enable mounting of miden-node.toml * Added docker-run-node command + added build arguments * Add git commit as arg to docker container * Added spacing --------- Co-authored-by: Augusto Hack --- .dockerignore | 38 +++++++++++++++++++++++++++ .github/workflows/ci.yml | 3 +++ Makefile.toml | 43 ++++++++++++++++++++++++++---- README.md | 31 ++++++++++++++++++++-- miden-node.toml | 21 +++++++++++++++ node/Dockerfile | 56 ++++++++++++++++++++++++++++++++++++++++ node/miden-node.toml | 4 +-- 7 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 .dockerignore create mode 100644 miden-node.toml create mode 100644 node/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..b5dbe9424 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,38 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/secrets.dev.yaml +**/values.dev.yaml +./bin +./target +/bin +/target +LICENSE +README.md +accounts +genesis.dat +miden-store.* +store.* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad8e18276..af5cc2278 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,7 @@ +# Runs the CI + name: CI + on: push: branches: diff --git a/Makefile.toml b/Makefile.toml index 3843976f7..2266170e7 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,8 +1,11 @@ # Cargo Makefile +# If running cargo-make in a workspace you need to add this env variable to make sure it function correctly. +# See docs: https://github.com/sagiegurari/cargo-make?tab=readme-ov-file#usage [env] CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true +# linting [tasks.format] toolchain = "nightly" command = "cargo" @@ -30,11 +33,7 @@ dependencies = [ [tasks.doc] env = { "RUSTDOCFLAGS" = "-D warnings" } command = "cargo" -args = ["doc", "--all-features", "--keep-going", "--release"] - -[tasks.test] -command = "cargo" -args = ["test", "--all-features", "--workspace", "--", "--nocapture"] +args = ["doc", "--verbose", "--all-features", "--keep-going", "--release"] [tasks.lint] dependencies = [ @@ -43,3 +42,37 @@ dependencies = [ "clippy", "docs" ] + +# testing +[tasks.test] +command = "cargo" +args = ["test", "--all-features", "--workspace", "--", "--nocapture"] + +# docker +[tasks.docker-build-node] +workspace = false +script = ''' +CREATED=$(date) +VERSION=$(cat node/Cargo.toml | grep -m 1 '^version' | cut -d '"' -f 2) +COMMIT=$(git rev-parse HEAD) + +docker build --build-arg CREATED="$CREATED" \ + --build-arg VERSION="$VERSION" \ + --build-arg COMMIT="$COMMIT" \ + -f node/Dockerfile \ + -t miden-node-image . +''' + +[tasks.docker-run-node] +workspace = false +script = ''' +docker volume create miden-db + +ABSOLUTE_PATH="$(pwd)/node/miden-node.toml" + +docker run --name miden-node \ + -p 57291:57291 \ + -v miden-db:/db \ + -v "${ABSOLUTE_PATH}:/miden-node.toml" \ + -d miden-node-image +''' diff --git a/README.md b/README.md index 58fee8cef..a90d2b879 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Before you can build and run the Miden node or any of its components, you'll nee Depending on the platform, you may need to install additional libraries. For example, on Ubuntu 22.04 the following command ensures that all required libraries are installed. ```sh -sudo apt install gcc llvm clang bindgen pkg-config libssl-dev libsqlite3-dev +sudo apt install llvm clang bindgen pkg-config libssl-dev libsqlite3-dev ``` ### Installing the node @@ -91,7 +91,6 @@ Please, refer to each component's documentation: Each directory containing the executables also contains an example configuration file. Make sure that the configuration files are mutually consistent. That is, make sure that the URLs are valid and point to the right endpoint. - ### Debian Packages The debian packages allow for easy install for miden on debian based systems. Note that there are checksums available for the package. @@ -114,5 +113,33 @@ Please make sure you have the sha256sum program installed, for most linux operat brew install coreutils ``` +### Running the node using Docker + +If you intend on running the node inside a Docker container, you will need to follow these steps: + +1. Build the docker image from source + + ```sh + cargo make docker-build-node + ``` + + This command will build the docker image for the Miden node and save it locally. + +2. Run the Docker container + + ```sh + docker run --name miden-node -p 57291:57291 -d miden-node-image + ``` + + This command will run the node as a container named `miden-node` using the `miden-node-image` and make port `57291` available (rpc endpoint). + +3. Monitor container + + ```sh + docker ps + ``` + + After running this command you should see the name of the container `miden-node` being outputed and marked as `Up`. + ## License This project is [MIT licensed](./LICENSE). diff --git a/miden-node.toml b/miden-node.toml new file mode 100644 index 000000000..04f582981 --- /dev/null +++ b/miden-node.toml @@ -0,0 +1,21 @@ +# This is an example configuration file for the Miden node. + +[block_producer] +# port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-block-producer', 1)) % 2**16 +endpoint = { host = "localhost", port = 48046 } +store_url = "http://localhost:28943" +# enables or disables the verification of transaction proofs before they are accepted into the +# transaction queue. +verify_tx_proofs = true + +[rpc] +# port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-rpc', 1)) % 2**16 +endpoint = { host = "0.0.0.0", port = 57291 } +block_producer_url = "http://localhost:48046" +store_url = "http://localhost:28943" + +[store] +# port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-store', 1)) % 2**16 +endpoint = { host = "localhost", port = 28943 } +database_filepath = "db/miden-store.sqlite3" +genesis_filepath = "genesis.dat" diff --git a/node/Dockerfile b/node/Dockerfile new file mode 100644 index 000000000..6fc95e69c --- /dev/null +++ b/node/Dockerfile @@ -0,0 +1,56 @@ +# Miden node Dockerfile + +# Setup image builder +FROM rust:1.76-slim-bookworm AS builder + +# Install dependencies +RUN apt-get update && \ + apt-get -y upgrade && \ + apt-get install -y llvm clang bindgen pkg-config libssl-dev libsqlite3-dev && \ + rm -rf /var/lib/apt/lists/* + +# Copy source code +WORKDIR /app +COPY . . + +# Build the node crate +RUN cargo install --features testing --path node +RUN miden-node make-genesis --inputs-path node/genesis.toml + +# Run Miden node +FROM debian:bookworm-slim + +# Update machine & install required packages +# The instalation of sqlite3 is needed for correct function of the SQLite database +RUN apt-get update && \ + apt-get -y upgrade && \ + apt-get install -y --no-install-recommends \ + sqlite3 \ + && rm -rf /var/lib/apt/lists/* + +# Copy artifacts from the builder stage +COPY --from=builder /app/genesis.dat genesis.dat +COPY --from=builder /app/accounts accounts +COPY --from=builder /usr/local/cargo/bin/miden-node /usr/local/bin/miden-node + +# Set labels +LABEL org.opencontainers.image.authors=miden@polygon.io \ + org.opencontainers.image.url=https://0xpolygonmiden.github.io/ \ + org.opencontainers.image.documentation=https://github.com/0xPolygonMiden/miden-node \ + org.opencontainers.image.source=https://github.com/0xPolygonMiden/miden-node \ + org.opencontainers.image.vendor=Polygon \ + org.opencontainers.image.licenses=MIT + +ARG CREATED +ARG VERSION +ARG COMMIT +LABEL org.opencontainers.image.created=$CREATED \ + org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.revision=$COMMIT + +# Expose RPC port +EXPOSE 57291 + +# Start the Miden node +# Miden node does not spawn sub-processes, so it can be used as the PID1 +CMD miden-node start --config miden-node.toml diff --git a/node/miden-node.toml b/node/miden-node.toml index f0811acd7..04f582981 100644 --- a/node/miden-node.toml +++ b/node/miden-node.toml @@ -10,12 +10,12 @@ verify_tx_proofs = true [rpc] # port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-rpc', 1)) % 2**16 -endpoint = { host = "localhost", port = 57291 } +endpoint = { host = "0.0.0.0", port = 57291 } block_producer_url = "http://localhost:48046" store_url = "http://localhost:28943" [store] # port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-store', 1)) % 2**16 endpoint = { host = "localhost", port = 28943 } -database_filepath = "miden-store.sqlite3" +database_filepath = "db/miden-store.sqlite3" genesis_filepath = "genesis.dat" From 8d4390db36e37530e0de2a7d5d975323e2729074 Mon Sep 17 00:00:00 2001 From: polydez <155382956+polydez@users.noreply.github.com> Date: Thu, 14 Mar 2024 22:47:08 +0500 Subject: [PATCH 04/29] Implemented nullifier tree wrapper over `Smt` (#275) * feat: implement nullifier tree wrapper over `Smt` * refactor: move `NullifierTree` to separated file, renames ans small fixes * fix: address review comments --- store/src/db/mod.rs | 4 +- store/src/db/sql.rs | 11 ++-- store/src/db/tests.rs | 26 +++++---- store/src/errors.rs | 27 +++++++-- store/src/lib.rs | 1 + store/src/nullifier_tree.rs | 113 ++++++++++++++++++++++++++++++++++++ store/src/server/api.rs | 7 ++- store/src/state.rs | 86 +++++++-------------------- 8 files changed, 185 insertions(+), 90 deletions(-) create mode 100644 store/src/nullifier_tree.rs diff --git a/store/src/db/mod.rs b/store/src/db/mod.rs index c743a81be..e6ea9ee76 100644 --- a/store/src/db/mod.rs +++ b/store/src/db/mod.rs @@ -133,7 +133,7 @@ impl Db { /// Loads all the nullifiers from the DB. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] - pub async fn select_nullifiers(&self) -> Result> { + pub async fn select_nullifiers(&self) -> Result> { self.pool.get().await?.interact(sql::select_nullifiers).await.map_err(|err| { DatabaseError::InteractError(format!("Select nullifiers task failed: {err}")) })? @@ -242,7 +242,7 @@ impl Db { acquire_done: oneshot::Receiver<()>, block_header: BlockHeader, notes: Vec, - nullifiers: Vec, + nullifiers: Vec, accounts: Vec<(AccountId, RpoDigest)>, ) -> Result<()> { self.pool diff --git a/store/src/db/sql.rs b/store/src/db/sql.rs index 1b59415f7..55d2ddf65 100644 --- a/store/src/db/sql.rs +++ b/store/src/db/sql.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use miden_objects::{ crypto::hash::rpo::RpoDigest, + notes::Nullifier, utils::serde::{Deserializable, Serializable}, BlockHeader, }; @@ -152,7 +153,7 @@ pub fn upsert_accounts( /// transaction. pub fn insert_nullifiers_for_block( transaction: &Transaction, - nullifiers: &[RpoDigest], + nullifiers: &[Nullifier], block_num: BlockNumber, ) -> Result { let mut stmt = transaction.prepare( @@ -172,7 +173,7 @@ pub fn insert_nullifiers_for_block( /// # Returns /// /// A vector with nullifiers and the block height at which they were created, or an error. -pub fn select_nullifiers(conn: &mut Connection) -> Result> { +pub fn select_nullifiers(conn: &mut Connection) -> Result> { let mut stmt = conn.prepare("SELECT nullifier, block_number FROM nullifiers ORDER BY block_number ASC;")?; let mut rows = stmt.query([])?; @@ -536,7 +537,7 @@ pub fn apply_block( transaction: &Transaction, block_header: &BlockHeader, notes: &[Note], - nullifiers: &[RpoDigest], + nullifiers: &[Nullifier], accounts: &[(AccountId, RpoDigest)], ) -> Result { let mut count = 0; @@ -556,8 +557,8 @@ fn deserialize(data: &[u8]) -> Result { } /// Returns the high 16 bits of the provided nullifier. -pub(crate) fn get_nullifier_prefix(nullifier: &RpoDigest) -> u32 { - (nullifier[3].as_int() >> 48) as u32 +pub(crate) fn get_nullifier_prefix(nullifier: &Nullifier) -> u32 { + (nullifier.most_significant_felt().as_int() >> 48) as u32 } /// Converts a `u64` into a [Value]. diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index 25fd21341..7abcf5da9 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -3,7 +3,7 @@ use miden_objects::{ hash::rpo::RpoDigest, merkle::{LeafIndex, MerklePath, SimpleSmt}, }, - notes::NOTE_LEAF_DEPTH, + notes::{Nullifier, NOTE_LEAF_DEPTH}, BlockHeader, Felt, FieldElement, }; use rusqlite::{vtab::array, Connection}; @@ -22,7 +22,7 @@ fn create_db() -> Connection { fn test_sql_insert_nullifiers_for_block() { let mut conn = create_db(); - let nullifiers = [num_to_rpo_digest(1 << 48)]; + let nullifiers = [num_to_nullifier(1 << 48)]; let block_num = 1; // Insert a new nullifier succeeds @@ -53,7 +53,7 @@ fn test_sql_insert_nullifiers_for_block() { // test inserting multiple nullifiers { - let nullifiers: Vec<_> = (0..10).map(num_to_rpo_digest).collect(); + let nullifiers: Vec<_> = (0..10).map(num_to_nullifier).collect(); let block_num = 1; let transaction = conn.transaction().unwrap(); let res = sql::insert_nullifiers_for_block(&transaction, &nullifiers, block_num); @@ -74,7 +74,7 @@ fn test_sql_select_nullifiers() { let block_num = 1; let mut state = vec![]; for i in 0..10 { - let nullifier = num_to_rpo_digest(i); + let nullifier = num_to_nullifier(i); state.push((nullifier, block_num)); let transaction = conn.transaction().unwrap(); @@ -157,7 +157,7 @@ fn test_sql_select_nullifiers_by_block_range() { assert!(nullifiers.is_empty()); // test single item - let nullifier1 = num_to_rpo_digest(1 << 48); + let nullifier1 = num_to_nullifier(1 << 48); let block_number1 = 1; let transaction = conn.transaction().unwrap(); @@ -174,13 +174,13 @@ fn test_sql_select_nullifiers_by_block_range() { assert_eq!( nullifiers, vec![NullifierInfo { - nullifier: nullifier1.into(), + nullifier: nullifier1, block_num: block_number1 }] ); // test two elements - let nullifier2 = num_to_rpo_digest(2 << 48); + let nullifier2 = num_to_nullifier(2 << 48); let block_number2 = 2; let transaction = conn.transaction().unwrap(); @@ -201,7 +201,7 @@ fn test_sql_select_nullifiers_by_block_range() { assert_eq!( nullifiers, vec![NullifierInfo { - nullifier: nullifier1.into(), + nullifier: nullifier1, block_num: block_number1 }] ); @@ -215,7 +215,7 @@ fn test_sql_select_nullifiers_by_block_range() { assert_eq!( nullifiers, vec![NullifierInfo { - nullifier: nullifier2.into(), + nullifier: nullifier2, block_num: block_number2 }] ); @@ -231,7 +231,7 @@ fn test_sql_select_nullifiers_by_block_range() { assert_eq!( nullifiers, vec![NullifierInfo { - nullifier: nullifier1.into(), + nullifier: nullifier1, block_num: block_number1 }] ); @@ -247,7 +247,7 @@ fn test_sql_select_nullifiers_by_block_range() { assert_eq!( nullifiers, vec![NullifierInfo { - nullifier: nullifier2.into(), + nullifier: nullifier2, block_num: block_number2 }] ); @@ -481,3 +481,7 @@ fn test_notes() { fn num_to_rpo_digest(n: u64) -> RpoDigest { RpoDigest::new([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(n)]) } + +fn num_to_nullifier(n: u64) -> Nullifier { + Nullifier::from(num_to_rpo_digest(n)) +} diff --git a/store/src/errors.rs b/store/src/errors.rs index 2c046e750..2856b5f96 100644 --- a/store/src/errors.rs +++ b/store/src/errors.rs @@ -6,12 +6,29 @@ use miden_objects::{ merkle::{MerkleError, MmrError}, utils::DeserializationError, }, - AccountError, BlockHeader, Digest, + notes::Nullifier, + AccountError, BlockHeader, }; use rusqlite::types::FromSqlError; use thiserror::Error; use tokio::sync::oneshot::error::RecvError; +use crate::types::BlockNumber; + +// INTERNAL ERRORS +// ================================================================================================= + +#[derive(Debug, Error)] +pub enum NullifierTreeError { + #[error("Merkle error: {0}")] + MerkleError(#[from] MerkleError), + #[error("Nullifier {nullifier} for block #{block_num} already exists in the nullifier tree")] + NullifierAlreadyExists { + nullifier: Nullifier, + block_num: BlockNumber, + }, +} + // DATABASE ERRORS // ================================================================================================= @@ -40,8 +57,8 @@ pub enum DatabaseError { pub enum StateInitializationError { #[error("Database error: {0}")] DatabaseError(#[from] DatabaseError), - #[error("Failed to create nullifiers tree: {0}")] - FailedToCreateNullifiersTree(MerkleError), + #[error("Failed to create nullifier tree: {0}")] + FailedToCreateNullifierTree(NullifierTreeError), #[error("Failed to create accounts tree: {0}")] FailedToCreateAccountsTree(MerkleError), } @@ -106,7 +123,7 @@ pub enum ApplyBlockError { #[error("Received invalid nullifier root")] NewBlockInvalidNullifierRoot, #[error("Duplicated nullifiers {0:?}")] - DuplicatedNullifiers(Vec), + DuplicatedNullifiers(Vec), #[error("Unable to create proof for note: {0}")] UnableToCreateProofForNote(MerkleError), #[error("Block applying was broken because of closed channel on database side: {0}")] @@ -117,6 +134,8 @@ pub enum ApplyBlockError { DbBlockHeaderEmpty, #[error("Failed to get MMR peaks for forest ({forest}): {error}")] FailedToGetMmrPeaksForForest { forest: usize, error: MmrError }, + #[error("Failed to update nullifier tree: {0}")] + FailedToUpdateNullifierTree(NullifierTreeError), } #[derive(Error, Debug)] diff --git a/store/src/lib.rs b/store/src/lib.rs index 19bceb9c6..a98c4764c 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -2,6 +2,7 @@ pub mod config; pub mod db; pub mod errors; pub mod genesis; +mod nullifier_tree; pub mod server; pub mod state; pub mod types; diff --git a/store/src/nullifier_tree.rs b/store/src/nullifier_tree.rs new file mode 100644 index 000000000..e4ef902b1 --- /dev/null +++ b/store/src/nullifier_tree.rs @@ -0,0 +1,113 @@ +use miden_objects::{ + crypto::{ + hash::rpo::RpoDigest, + merkle::{Smt, SmtProof}, + }, + notes::Nullifier, + Felt, FieldElement, Word, +}; + +use crate::{errors::NullifierTreeError, types::BlockNumber}; + +/// Nullifier SMT. +#[derive(Debug, Clone)] +pub struct NullifierTree(Smt); + +impl NullifierTree { + /// Construct new nullifier tree from list of items. + pub fn with_entries( + entries: impl IntoIterator + ) -> Result { + let leaves = entries.into_iter().map(|(nullifier, block_num)| { + (nullifier.inner(), Self::block_num_to_leaf_value(block_num)) + }); + + let inner = Smt::with_entries(leaves)?; + + Ok(Self(inner)) + } + + /// Get SMT root. + pub fn root(&self) -> RpoDigest { + self.0.root() + } + + /// Returns an opening of the leaf associated with the given nullifier. + pub fn open( + &self, + nullifier: &Nullifier, + ) -> SmtProof { + self.0.open(&nullifier.inner()) + } + + /// Inserts block number in which nullifier was consumed. + pub fn insert( + &mut self, + nullifier: &Nullifier, + block_num: BlockNumber, + ) -> Result<(), NullifierTreeError> { + let key = nullifier.inner(); + let prev_value = self.0.get_value(&key); + if prev_value != Smt::EMPTY_VALUE { + return Err(NullifierTreeError::NullifierAlreadyExists { + nullifier: *nullifier, + block_num: Self::leaf_value_to_block_num(prev_value), + }); + } + + self.0.insert(key, Self::block_num_to_leaf_value(block_num)); + + Ok(()) + } + + /// Returns block number stored for the given nullifier or `None` if the nullifier wasn't + /// consumed. + pub fn get_block_num( + &self, + nullifier: &Nullifier, + ) -> Option { + let value = self.0.get_value(&nullifier.inner()); + if value == Smt::EMPTY_VALUE { + return None; + } + + Some(Self::leaf_value_to_block_num(value)) + } + + /// Returns the nullifier's leaf value in the SMT by its block number. + fn block_num_to_leaf_value(block: BlockNumber) -> Word { + [Felt::from(block), Felt::ZERO, Felt::ZERO, Felt::ZERO] + } + + /// Given the leaf value of the nullifier SMT, returns the nullifier's block number. + /// + /// There are no nullifiers in the genesis block. The value zero is instead used to signal + /// absence of a value. + fn leaf_value_to_block_num(value: Word) -> BlockNumber { + value[0].as_int().try_into().expect("invalid block number found in store") + } +} + +#[cfg(test)] +mod tests { + use miden_objects::{Felt, ZERO}; + + use super::NullifierTree; + + #[test] + fn test_leaf_value_encoding() { + let block_num = 123; + let nullifier_value = NullifierTree::block_num_to_leaf_value(block_num); + + assert_eq!(nullifier_value, [Felt::from(block_num), ZERO, ZERO, ZERO]) + } + + #[test] + fn test_leaf_value_decoding() { + let block_num = 123; + let nullifier_value = [Felt::from(block_num), ZERO, ZERO, ZERO]; + let decoded_block_num = NullifierTree::leaf_value_to_block_num(nullifier_value); + + assert_eq!(decoded_block_num, block_num); + } +} diff --git a/store/src/server/api.rs b/store/src/server/api.rs index aaaad56a8..dc682bfb4 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -23,7 +23,7 @@ use miden_node_proto::{ }, AccountState, }; -use miden_objects::{crypto::hash::rpo::RpoDigest, BlockHeader, Felt, ZERO}; +use miden_objects::{notes::Nullifier, BlockHeader, Felt, ZERO}; use tonic::{Response, Status}; use tracing::{debug, info, instrument}; @@ -284,7 +284,7 @@ impl api_server::Api for StoreApi { .nullifiers .into_iter() .map(|nullifier| NullifierTransactionInputRecord { - nullifier: Some(nullifier.nullifier.inner().into()), + nullifier: Some(nullifier.nullifier.into()), block_num: nullifier.block_num, }) .collect(), @@ -388,9 +388,10 @@ fn invalid_argument(err: E) -> Status { } #[instrument(target = "miden-store", skip_all, err)] -fn validate_nullifiers(nullifiers: &[generated::digest::Digest]) -> Result, Status> { +fn validate_nullifiers(nullifiers: &[generated::digest::Digest]) -> Result, Status> { nullifiers .iter() + .cloned() .map(TryInto::try_into) .collect::>() .map_err(|_| invalid_argument("Digest field is not in the modulus range")) diff --git a/store/src/state.rs b/store/src/state.rs index a42b32487..b34a93026 100644 --- a/store/src/state.rs +++ b/store/src/state.rs @@ -9,10 +9,10 @@ use miden_node_utils::formatting::{format_account_id, format_array}; use miden_objects::{ crypto::{ hash::rpo::RpoDigest, - merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, SimpleSmt, Smt, SmtProof, ValuePath}, + merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, SimpleSmt, SmtProof, ValuePath}, }, - notes::{NoteMetadata, NOTE_LEAF_DEPTH}, - AccountError, BlockHeader, Felt, FieldElement, Word, ACCOUNT_TREE_DEPTH, EMPTY_WORD, + notes::{NoteMetadata, Nullifier, NOTE_LEAF_DEPTH}, + AccountError, BlockHeader, Word, ACCOUNT_TREE_DEPTH, }; use tokio::{ sync::{oneshot, Mutex, RwLock}, @@ -26,6 +26,7 @@ use crate::{ ApplyBlockError, DatabaseError, GetBlockInputsError, StateInitializationError, StateSyncError, }, + nullifier_tree::NullifierTree, types::{AccountId, BlockNumber}, COMPONENT, }; @@ -41,7 +42,7 @@ pub struct TransactionInputs { /// Container for state that needs to be updated atomically. struct InnerState { - nullifier_tree: Smt, + nullifier_tree: NullifierTree, chain_mmr: Mmr, account_tree: SimpleSmt, } @@ -103,7 +104,7 @@ impl State { pub async fn apply_block( &self, block_header: BlockHeader, - nullifiers: Vec, + nullifiers: Vec, accounts: Vec<(AccountId, RpoDigest)>, notes: Vec, ) -> Result<(), ApplyBlockError> { @@ -132,7 +133,7 @@ impl State { // nullifiers can be produced only once let duplicate_nullifiers: Vec<_> = nullifiers .iter() - .filter(|&n| inner.nullifier_tree.get_value(n) != EMPTY_WORD) + .filter(|&n| inner.nullifier_tree.get_block_num(n).is_some()) .cloned() .collect(); if !duplicate_nullifiers.is_empty() { @@ -164,9 +165,10 @@ impl State { // update nullifier tree let nullifier_tree = { let mut nullifier_tree = inner.nullifier_tree.clone(); - let nullifier_data = block_num_to_nullifier_value(block_header.block_num()); for nullifier in nullifiers.iter() { - nullifier_tree.insert(*nullifier, nullifier_data); + nullifier_tree + .insert(nullifier, block_header.block_num()) + .map_err(ApplyBlockError::FailedToUpdateNullifierTree)?; } if nullifier_tree.root() != block_header.nullifier_root() { @@ -296,7 +298,7 @@ impl State { #[instrument(target = "miden-store", skip_all, ret(level = "debug"))] pub async fn check_nullifiers( &self, - nullifiers: &[RpoDigest], + nullifiers: &[Nullifier], ) -> Vec { let inner = self.inner.read().await; nullifiers.iter().map(|n| inner.nullifier_tree.open(n)).collect() @@ -363,7 +365,7 @@ impl State { pub async fn get_block_inputs( &self, account_ids: &[AccountId], - nullifiers: &[RpoDigest], + nullifiers: &[Nullifier], ) -> Result< (BlockHeader, MmrPeaks, Vec, Vec), GetBlockInputsError, @@ -414,7 +416,7 @@ impl State { let proof = inner.nullifier_tree.open(nullifier); NullifierWitness { - nullifier: (*nullifier).into(), + nullifier: *nullifier, proof, } }) @@ -428,7 +430,7 @@ impl State { pub async fn get_transaction_inputs( &self, account_id: AccountId, - nullifiers: &[RpoDigest], + nullifiers: &[Nullifier], ) -> TransactionInputs { info!(target: COMPONENT, account_id = %format_account_id(account_id), nullifiers = %format_array(nullifiers)); @@ -438,15 +440,9 @@ impl State { let nullifiers = nullifiers .iter() - .cloned() - .map(|nullifier| { - let value = inner.nullifier_tree.get_value(&nullifier); - let block_num = nullifier_value_to_block_num(value); - - NullifierInfo { - nullifier: nullifier.into(), - block_num, - } + .map(|nullifier| NullifierInfo { + nullifier: *nullifier, + block_num: inner.nullifier_tree.get_block_num(nullifier).unwrap_or_default(), }) .collect(); @@ -457,7 +453,7 @@ impl State { } /// Lists all known nullifiers with their inclusion blocks, intended for testing. - pub async fn list_nullifiers(&self) -> Result, DatabaseError> { + pub async fn list_nullifiers(&self) -> Result, DatabaseError> { self.db.select_nullifiers().await } @@ -476,19 +472,6 @@ impl State { // UTILITIES // ================================================================================================ -/// Returns the nullifier's leaf value in the SMT by its block number. -fn block_num_to_nullifier_value(block: BlockNumber) -> Word { - [Felt::from(block), Felt::ZERO, Felt::ZERO, Felt::ZERO] -} - -/// Given the leaf value of the nullifier SMT, returns the nullifier's block number. -/// -/// There are no nullifiers in the genesis block. The value zero is instead used to signal absence -/// of a value. -fn nullifier_value_to_block_num(value: Word) -> BlockNumber { - value[0].as_int().try_into().expect("invalid block number found in store") -} - /// Creates a [SimpleSmt] tree from the `notes`. #[instrument(target = "miden-store", skip_all)] pub fn build_notes_tree( @@ -513,16 +496,13 @@ pub fn build_notes_tree( } #[instrument(target = "miden-store", skip_all)] -async fn load_nullifier_tree(db: &mut Db) -> Result { +async fn load_nullifier_tree(db: &mut Db) -> Result { let nullifiers = db.select_nullifiers().await?; let len = nullifiers.len(); - let leaves = nullifiers - .into_iter() - .map(|(nullifier, block)| (nullifier, block_num_to_nullifier_value(block))); let now = Instant::now(); - let nullifier_tree = Smt::with_entries(leaves) - .map_err(StateInitializationError::FailedToCreateNullifiersTree)?; + let nullifier_tree = NullifierTree::with_entries(nullifiers) + .map_err(StateInitializationError::FailedToCreateNullifierTree)?; let elapsed = now.elapsed().as_secs(); info!( @@ -556,27 +536,3 @@ async fn load_accounts( SimpleSmt::with_leaves(account_data) .map_err(StateInitializationError::FailedToCreateAccountsTree) } - -#[cfg(test)] -mod tests { - use miden_objects::{Felt, ZERO}; - - use super::{block_num_to_nullifier_value, nullifier_value_to_block_num}; - - #[test] - fn test_nullifier_data_encoding() { - let block_num = 123; - let nullifier_value = block_num_to_nullifier_value(block_num); - - assert_eq!(nullifier_value, [Felt::from(block_num), ZERO, ZERO, ZERO]) - } - - #[test] - fn test_nullifier_data_decoding() { - let block_num = 123; - let nullifier_value = [Felt::from(block_num), ZERO, ZERO, ZERO]; - let decoded_block_num = nullifier_value_to_block_num(nullifier_value); - - assert_eq!(decoded_block_num, block_num); - } -} From 0a4fa39303a937f04064e4d24d51f35d5efe3a7f Mon Sep 17 00:00:00 2001 From: Paul-Henry Kajfasz <42912740+phklive@users.noreply.github.com> Date: Fri, 15 Mar 2024 18:04:17 +0100 Subject: [PATCH 05/29] Separate CI into multiple files (#278) * ci: turn doc warnings into errors (#259) * Separated different CI jobs in their respective files * Add next branch in ci * Fix wrong naming in CI + use cargo make for doc * Fixed badge + removed unnecessary additional file * Change naming of ci and jobs * Fix naming * Fix comment --------- Co-authored-by: Augusto Hack --- .github/workflows/doc.yml | 25 +++++++++++++++ .github/workflows/{ci.yml => lint.yml} | 43 +++----------------------- .github/workflows/test.yml | 26 ++++++++++++++++ README.md | 2 +- miden-node.toml | 21 ------------- 5 files changed, 57 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/doc.yml rename .github/workflows/{ci.yml => lint.yml} (56%) create mode 100644 .github/workflows/test.yml delete mode 100644 miden-node.toml diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml new file mode 100644 index 000000000..a849cc96f --- /dev/null +++ b/.github/workflows/doc.yml @@ -0,0 +1,25 @@ +# Runs documentation related jobs. + +name: doc + +on: + push: + branches: [main, next] + pull_request: + types: [opened, reopened, synchronize] + +jobs: + doc: + name: doc stable on ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + override: true + - name: Install cargo make + run: cargo install cargo-make + - name: cargo make - doc + run: cargo make doc diff --git a/.github/workflows/ci.yml b/.github/workflows/lint.yml similarity index 56% rename from .github/workflows/ci.yml rename to .github/workflows/lint.yml index af5cc2278..7a4c33576 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/lint.yml @@ -1,11 +1,10 @@ -# Runs the CI +# Runs linting related jobs. -name: CI +name: lint on: push: - branches: - - main + branches: [main, next] pull_request: types: [opened, reopened, synchronize] @@ -35,8 +34,8 @@ jobs: override: true - name: Install cargo make run: cargo install cargo-make - - name: cargo make - format - run: cargo make format + - name: cargo make - format-check + run: cargo make format-check clippy: name: clippy stable on ubuntu-latest @@ -53,35 +52,3 @@ jobs: run: cargo install cargo-make - name: cargo make - clippy run: cargo make clippy - - doc: - name: doc stable on ubuntu-latest - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - override: true - - name: Install cargo make - run: cargo install cargo-make - - name: cargo make - format - run: cargo make doc - - test: - name: test stable on ubuntu-latest - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - override: true - - name: Install cargo make - run: cargo install cargo-make - - name: cargo make - format - run: cargo make test - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..fd2778950 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +# Runs testing related jobs. + +name: test + +on: + push: + branches: [main, next] + pull_request: + types: [opened, reopened, synchronize] + +jobs: + unit-and-integration: + name: test stable on ubuntu-latest + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + override: true + - name: Install cargo make + run: cargo install cargo-make + - name: cargo make - test + run: cargo make test diff --git a/README.md b/README.md index a90d2b879..d21ba47c0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Miden node - + This repository holds the Miden node; that is, the software which processes transactions and creates blocks for the Miden rollup. diff --git a/miden-node.toml b/miden-node.toml deleted file mode 100644 index 04f582981..000000000 --- a/miden-node.toml +++ /dev/null @@ -1,21 +0,0 @@ -# This is an example configuration file for the Miden node. - -[block_producer] -# port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-block-producer', 1)) % 2**16 -endpoint = { host = "localhost", port = 48046 } -store_url = "http://localhost:28943" -# enables or disables the verification of transaction proofs before they are accepted into the -# transaction queue. -verify_tx_proofs = true - -[rpc] -# port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-rpc', 1)) % 2**16 -endpoint = { host = "0.0.0.0", port = 57291 } -block_producer_url = "http://localhost:48046" -store_url = "http://localhost:28943" - -[store] -# port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-store', 1)) % 2**16 -endpoint = { host = "localhost", port = 28943 } -database_filepath = "db/miden-store.sqlite3" -genesis_filepath = "genesis.dat" From e22e685afa217c9c7c38c18f536201e51e6ec0ee Mon Sep 17 00:00:00 2001 From: polydez <155382956+polydez@users.noreply.github.com> Date: Wed, 20 Mar 2024 17:23:35 +0500 Subject: [PATCH 06/29] refactor: small error refactoring (using `'static str` instead of `String`) (#281) --- block-producer/src/block_builder/prover/mod.rs | 8 ++++---- block-producer/src/errors.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/block-producer/src/block_builder/prover/mod.rs b/block-producer/src/block_builder/prover/mod.rs index 426908e8d..eb3696f53 100644 --- a/block-producer/src/block_builder/prover/mod.rs +++ b/block-producer/src/block_builder/prover/mod.rs @@ -260,22 +260,22 @@ impl BlockProver { let new_account_root = execution_output .stack_outputs() .get_stack_word(ACCOUNT_ROOT_WORD_IDX) - .ok_or(BlockProverError::InvalidRootOutput("account".to_string()))?; + .ok_or(BlockProverError::InvalidRootOutput("account"))?; let new_note_root = execution_output .stack_outputs() .get_stack_word(NOTE_ROOT_WORD_IDX) - .ok_or(BlockProverError::InvalidRootOutput("note".to_string()))?; + .ok_or(BlockProverError::InvalidRootOutput("note"))?; let new_nullifier_root = execution_output .stack_outputs() .get_stack_word(NULLIFIER_ROOT_WORD_IDX) - .ok_or(BlockProverError::InvalidRootOutput("nullifier".to_string()))?; + .ok_or(BlockProverError::InvalidRootOutput("nullifier"))?; let new_chain_mmr_root = execution_output .stack_outputs() .get_stack_word(CHAIN_MMR_ROOT_WORD_IDX) - .ok_or(BlockProverError::InvalidRootOutput("chain mmr".to_string()))?; + .ok_or(BlockProverError::InvalidRootOutput("chain mmr"))?; Ok(( new_account_root.into(), diff --git a/block-producer/src/errors.rs b/block-producer/src/errors.rs index f7635495c..a597714a1 100644 --- a/block-producer/src/errors.rs +++ b/block-producer/src/errors.rs @@ -91,7 +91,7 @@ pub enum BlockProverError { #[error("program execution failed")] ProgramExecutionFailed(ExecutionError), #[error("failed to retrieve {0} root from stack outputs")] - InvalidRootOutput(String), + InvalidRootOutput(&'static str), } // Block inputs errors From bcbe4b4e3a24baa330fef2bab96050882c2bcb9a Mon Sep 17 00:00:00 2001 From: Augusto Hack Date: Thu, 21 Mar 2024 13:43:57 +0100 Subject: [PATCH 07/29] bugfix: table name is block_headers instead of block_header (#283) --- store/src/db/migrations.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/store/src/db/migrations.rs b/store/src/db/migrations.rs index d98be6422..a4faa9a7e 100644 --- a/store/src/db/migrations.rs +++ b/store/src/db/migrations.rs @@ -27,7 +27,7 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { PRIMARY KEY (block_num, note_index), CONSTRAINT notes_block_number_is_u32 CHECK (block_num >= 0 AND block_num < 4294967296), CONSTRAINT notes_note_index_is_u32 CHECK (note_index >= 0 AND note_index < 4294967296), - FOREIGN KEY (block_num) REFERENCES block_header (block_num) + FOREIGN KEY (block_num) REFERENCES block_headers (block_num) ) STRICT, WITHOUT ROWID; CREATE TABLE @@ -38,7 +38,7 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { block_num INTEGER NOT NULL, PRIMARY KEY (account_id), - FOREIGN KEY (block_num) REFERENCES block_header (block_num), + FOREIGN KEY (block_num) REFERENCES block_headers (block_num), CONSTRAINT accounts_block_num_is_u32 CHECK (block_num >= 0 AND block_num < 4294967296) ) STRICT, WITHOUT ROWID; @@ -53,7 +53,7 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { CONSTRAINT nullifiers_nullifier_is_digest CHECK (length(nullifier) = 32), CONSTRAINT nullifiers_nullifier_prefix_is_u16 CHECK (nullifier_prefix >= 0 AND nullifier_prefix < 65536), CONSTRAINT nullifiers_block_number_is_u32 CHECK (block_number >= 0 AND block_number < 4294967296), - FOREIGN KEY (block_number) REFERENCES block_header (block_num) + FOREIGN KEY (block_number) REFERENCES block_headers (block_num) ) STRICT, WITHOUT ROWID; ", )]) From a1508709f3ba720e796e06a0c7c03c34dcb73f6e Mon Sep 17 00:00:00 2001 From: Augusto Hack Date: Thu, 21 Mar 2024 20:31:58 +0100 Subject: [PATCH 08/29] sqlite: use bundled version to enable FK checks (#284) --- Cargo.lock | 1 + store/Cargo.toml | 2 +- store/src/db/migrations.rs | 12 ++++---- store/src/db/tests.rs | 61 +++++++++++++++++++++++++++++++------- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91dd8340b..8ca119d6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -979,6 +979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "bindgen", + "cc", "pkg-config", "vcpkg", ] diff --git a/store/Cargo.toml b/store/Cargo.toml index a485300df..db5f8b0a3 100644 --- a/store/Cargo.toml +++ b/store/Cargo.toml @@ -30,7 +30,7 @@ miden-node-utils = { path = "../utils", version = "0.2" } miden-objects = { workspace = true } once_cell = { version = "1.18.0" } prost = { version = "0.12" } -rusqlite = { version = "0.30", features = ["array", "buildtime_bindgen"] } +rusqlite = { version = "0.30", features = ["array", "buildtime_bindgen", "bundled"] } rusqlite_migration = { version = "1.0" } serde = { version = "1.0", features = ["derive"] } thiserror = { workspace = true } diff --git a/store/src/db/migrations.rs b/store/src/db/migrations.rs index a4faa9a7e..3eec2bc0b 100644 --- a/store/src/db/migrations.rs +++ b/store/src/db/migrations.rs @@ -25,9 +25,9 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { merkle_path BLOB NOT NULL, PRIMARY KEY (block_num, note_index), + CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers (block_num), CONSTRAINT notes_block_number_is_u32 CHECK (block_num >= 0 AND block_num < 4294967296), - CONSTRAINT notes_note_index_is_u32 CHECK (note_index >= 0 AND note_index < 4294967296), - FOREIGN KEY (block_num) REFERENCES block_headers (block_num) + CONSTRAINT notes_note_index_is_u32 CHECK (note_index >= 0 AND note_index < 4294967296) ) STRICT, WITHOUT ROWID; CREATE TABLE @@ -38,7 +38,7 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { block_num INTEGER NOT NULL, PRIMARY KEY (account_id), - FOREIGN KEY (block_num) REFERENCES block_headers (block_num), + CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers (block_num), CONSTRAINT accounts_block_num_is_u32 CHECK (block_num >= 0 AND block_num < 4294967296) ) STRICT, WITHOUT ROWID; @@ -50,10 +50,10 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { block_number INTEGER NOT NULL, PRIMARY KEY (nullifier), + CONSTRAINT fk_block_num FOREIGN KEY (block_number) REFERENCES block_headers (block_num), CONSTRAINT nullifiers_nullifier_is_digest CHECK (length(nullifier) = 32), CONSTRAINT nullifiers_nullifier_prefix_is_u16 CHECK (nullifier_prefix >= 0 AND nullifier_prefix < 65536), - CONSTRAINT nullifiers_block_number_is_u32 CHECK (block_number >= 0 AND block_number < 4294967296), - FOREIGN KEY (block_number) REFERENCES block_headers (block_num) + CONSTRAINT nullifiers_block_number_is_u32 CHECK (block_number >= 0 AND block_number < 4294967296) ) STRICT, WITHOUT ROWID; ", )]) @@ -61,5 +61,5 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { #[test] fn migrations_test() { - assert!(MIGRATIONS.validate().is_ok()); + assert_eq!(MIGRATIONS.validate(), Ok(())); } diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index 7abcf5da9..57af54176 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -18,12 +18,36 @@ fn create_db() -> Connection { conn } +fn create_block( + conn: &mut Connection, + block_num: u32, +) { + let block_header = BlockHeader::new( + num_to_rpo_digest(1), + block_num, + num_to_rpo_digest(3), + num_to_rpo_digest(4), + num_to_rpo_digest(5), + num_to_rpo_digest(6), + num_to_rpo_digest(7), + num_to_rpo_digest(8), + 9_u8.into(), + 10_u8.into(), + ); + + let transaction = conn.transaction().unwrap(); + sql::insert_block_header(&transaction, &block_header).unwrap(); + transaction.commit().unwrap(); +} + #[test] fn test_sql_insert_nullifiers_for_block() { let mut conn = create_db(); let nullifiers = [num_to_nullifier(1 << 48)]; + let block_num = 1; + create_block(&mut conn, block_num); // Insert a new nullifier succeeds { @@ -66,12 +90,14 @@ fn test_sql_insert_nullifiers_for_block() { fn test_sql_select_nullifiers() { let mut conn = create_db(); + let block_num = 1; + create_block(&mut conn, block_num); + // test querying empty table let nullifiers = sql::select_nullifiers(&mut conn).unwrap(); assert!(nullifiers.is_empty()); // test multiple entries - let block_num = 1; let mut state = vec![]; for i in 0..10 { let nullifier = num_to_nullifier(i); @@ -90,12 +116,14 @@ fn test_sql_select_nullifiers() { fn test_sql_select_notes() { let mut conn = create_db(); + let block_num = 1; + create_block(&mut conn, block_num); + // test querying empty table let notes = sql::select_notes(&mut conn).unwrap(); assert!(notes.is_empty()); // test multiple entries - let block_num = 1; let mut state = vec![]; for i in 0..10 { let note = Note { @@ -123,12 +151,14 @@ fn test_sql_select_notes() { fn test_sql_select_accounts() { let mut conn = create_db(); + let block_num = 1; + create_block(&mut conn, block_num); + // test querying empty table let accounts = sql::select_accounts(&mut conn).unwrap(); assert!(accounts.is_empty()); // test multiple entries - let block_num = 1; let mut state = vec![]; for i in 0..10 { let account_id = i; @@ -159,6 +189,7 @@ fn test_sql_select_nullifiers_by_block_range() { // test single item let nullifier1 = num_to_nullifier(1 << 48); let block_number1 = 1; + create_block(&mut conn, block_number1); let transaction = conn.transaction().unwrap(); sql::insert_nullifiers_for_block(&transaction, &[nullifier1], block_number1).unwrap(); @@ -182,6 +213,7 @@ fn test_sql_select_nullifiers_by_block_range() { // test two elements let nullifier2 = num_to_nullifier(2 << 48); let block_number2 = 2; + create_block(&mut conn, block_number2); let transaction = conn.transaction().unwrap(); sql::insert_nullifiers_for_block(&transaction, &[nullifier2], block_number2).unwrap(); @@ -339,13 +371,15 @@ fn test_db_block_header() { fn test_db_account() { let mut conn = create_db(); + let block_num = 1; + create_block(&mut conn, block_num); + // test empty table let account_ids = vec![0, 1, 2, 3, 4, 5]; let res = sql::select_accounts_by_block_range(&mut conn, 0, u32::MAX, &account_ids).unwrap(); assert!(res.is_empty()); // test insertion - let block_num = 1; let account_id = 0; let account_hash = num_to_rpo_digest(0); @@ -382,6 +416,9 @@ fn test_db_account() { fn test_notes() { let mut conn = create_db(); + let block_num_1 = 1; + create_block(&mut conn, block_num_1); + // test empty table let res = sql::select_notes_since_block_by_tag_and_sender(&mut conn, &[], &[], 0).unwrap(); assert!(res.is_empty()); @@ -391,7 +428,6 @@ fn test_notes() { assert!(res.is_empty()); // test insertion - let block_num = 1; let note_index = 2u32; let tag = 5; let note_hash = num_to_rpo_digest(3); @@ -401,7 +437,7 @@ fn test_notes() { let merkle_path = MerklePath::new(notes_db.open(&idx).path.nodes().to_vec()); let note = Note { - block_num, + block_num: block_num_1, note_created: NoteCreated { note_index, note_id: num_to_rpo_digest(3), @@ -424,7 +460,7 @@ fn test_notes() { &mut conn, &[(tag >> 48) as u32], &[], - block_num, + block_num_1, ) .unwrap(); assert!(res.is_empty()); @@ -434,14 +470,17 @@ fn test_notes() { &mut conn, &[(tag >> 48) as u32], &[], - block_num - 1, + block_num_1 - 1, ) .unwrap(); assert_eq!(res, vec![note.clone()]); + let block_num_2 = note.block_num + 1; + create_block(&mut conn, block_num_2); + // insertion second note with same tag, but on higher block let note2 = Note { - block_num: note.block_num + 1, + block_num: block_num_2, note_created: NoteCreated { note_index: note.note_created.note_index, note_id: num_to_rpo_digest(3), @@ -460,7 +499,7 @@ fn test_notes() { &mut conn, &[(tag >> 48) as u32], &[], - block_num - 1, + block_num_1 - 1, ) .unwrap(); assert_eq!(res, vec![note.clone()]); @@ -470,7 +509,7 @@ fn test_notes() { &mut conn, &[(tag >> 48) as u32], &[], - block_num, + block_num_1, ) .unwrap(); assert_eq!(res, vec![note2.clone()]); From 184978781a2d95e389787a6d210f394d9f649339 Mon Sep 17 00:00:00 2001 From: Martin Fraga Date: Mon, 25 Mar 2024 14:03:57 -0300 Subject: [PATCH 09/29] chore: update CI action for rust install (#280) --- .github/workflows/doc.yml | 5 +---- .github/workflows/lint.yml | 9 +++------ .github/workflows/test.yml | 5 +---- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index a849cc96f..4fe3d9eb4 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -15,10 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - override: true + uses: dtolnay/rust-toolchain@stable - name: Install cargo make run: cargo install cargo-make - name: cargo make - doc diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7a4c33576..752e5985e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -26,12 +26,10 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install minimal Rust with rustfmt - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal toolchain: nightly components: rustfmt - override: true - name: Install cargo make run: cargo install cargo-make - name: cargo make - format-check @@ -43,11 +41,10 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install minimal Rust with clippy - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal + toolchain: stable components: clippy - override: true - name: Install cargo make run: cargo install cargo-make - name: cargo make - clippy diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd2778950..625bd9487 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,10 +16,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - override: true + uses: dtolnay/rust-toolchain@stable - name: Install cargo make run: cargo install cargo-make - name: cargo make - test From 51fe84d7624f1d6e9ab966e50c5342b00e569461 Mon Sep 17 00:00:00 2001 From: Paul-Henry Kajfasz <42912740+phklive@users.noreply.github.com> Date: Fri, 29 Mar 2024 10:55:46 +0100 Subject: [PATCH 10/29] Add faucet enabling testing (#270) * ci: turn doc warnings into errors (#259) * Boilerplate done * pushed fix for miden-client * Fix formatting * Pulled after client fix * Fixed typos + added build_client fn * Added configs + figment * Fix client implementation in faucet + use released client * updated cargo * Fixed Miden node when importing Miden client, db errors * Format files * Added proper support of configuration file + logging * Improved config file handling * Removed superfluous file * First pass at improvements * Change naming of note * Fixed html + added metadata endpoint * Want to implement Display for NoteId * Upgraded js to use async + NoteId does not impl Display in main * cargo updated --------- Co-authored-by: Augusto Hack --- .gitignore | 3 +- Cargo.lock | 1060 ++++++++++++++++++++++++++---- Cargo.toml | 1 + faucet/Cargo.toml | 27 + faucet/miden-faucet.toml | 3 + faucet/src/cli.rs | 54 ++ faucet/src/config.rs | 37 ++ faucet/src/errors.rs | 32 + faucet/src/handlers.rs | 102 +++ faucet/src/main.rs | 140 ++++ faucet/src/static/background.png | Bin 0 -> 165941 bytes faucet/src/static/favicon.ico | Bin 0 -> 1150 bytes faucet/src/static/index.css | 90 +++ faucet/src/static/index.html | 25 + faucet/src/static/index.js | 70 ++ faucet/src/utils.rs | 102 +++ 16 files changed, 1608 insertions(+), 138 deletions(-) create mode 100644 faucet/Cargo.toml create mode 100644 faucet/miden-faucet.toml create mode 100644 faucet/src/cli.rs create mode 100644 faucet/src/config.rs create mode 100644 faucet/src/errors.rs create mode 100644 faucet/src/handlers.rs create mode 100644 faucet/src/main.rs create mode 100644 faucet/src/static/background.png create mode 100644 faucet/src/static/favicon.ico create mode 100644 faucet/src/static/index.css create mode 100644 faucet/src/static/index.html create mode 100644 faucet/src/static/index.js create mode 100644 faucet/src/utils.rs diff --git a/.gitignore b/.gitignore index 30edfb6b0..4cc60b600 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ genesis.dat *.sqlite3-shm *.sqlite3-wal - # Docs ignore .code .idea @@ -35,4 +34,4 @@ env/ *.out node_modules/ *DS_Store -*.iml \ No newline at end of file +*.iml diff --git a/Cargo.lock b/Cargo.lock index 8ca119d6d..e873dd380 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,223 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.5.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-cors" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e772b3bcafe335042b5db010ab7c09013dad6eac4915c91d8d50902769f331" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + +[[package]] +name = "actix-files" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0bdd6ff79de7c9a021f5d9ea79ce23e108d8bfc9b49b5b4a2cf6fad5a35212" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "bitflags 2.5.0", + "bytes", + "derive_more", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "v_htmlescape", +] + +[[package]] +name = "actix-http" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d223b13fd481fc0d1f83bb12659ae774d9e3601814c68a0bc539731698cca743" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64", + "bitflags 2.5.0", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.55", +] + +[[package]] +name = "actix-router" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a6556ddebb638c2358714d853257ed226ece6023ef9364f23f0c70737ea984" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1f50ebbb30eca122b188319a4398b3f7bb4a8cdf50ecfb73bfc6a3c3ce54f5" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -24,6 +241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -31,13 +249,28 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -109,9 +342,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "arrayref" @@ -125,6 +358,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -144,18 +386,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -169,9 +411,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "axum" @@ -220,9 +462,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -260,7 +502,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -271,7 +513,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn", + "syn 2.0.55", ] [[package]] @@ -297,15 +539,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "blake3" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" +checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" dependencies = [ "arrayref", "arrayvec", @@ -323,6 +565,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.15.4" @@ -331,15 +594,24 @@ checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytemuck" -version = "1.14.3" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "bytestring" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +dependencies = [ + "bytes", +] [[package]] name = "cc" @@ -368,9 +640,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", @@ -393,9 +665,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.2" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -415,14 +687,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -437,12 +709,41 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "comfy-table" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" +dependencies = [ + "crossterm", + "strum", + "strum_macros", + "unicode-width", +] + [[package]] name = "constant_time_eq" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -458,6 +759,62 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "parking_lot", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -509,6 +866,28 @@ dependencies = [ "deadpool-runtime", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.7" @@ -546,6 +925,15 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -562,6 +950,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -576,15 +970,15 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "figment" -version = "0.10.14" +version = "0.10.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b6e5bc7bd59d60d0d45a6ccab6cf0f4ce28698fb4e81e750ddf229c9b824026" +checksum = "7270677e7067213e04f323b55084586195f18308cd7546cfac9f873344ccceb6" dependencies = [ "atomic", "parking_lot", @@ -602,12 +996,31 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -645,6 +1058,7 @@ dependencies = [ "futures-task", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -682,9 +1096,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", @@ -692,7 +1106,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.5", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -730,6 +1144,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -773,6 +1193,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.8.0" @@ -844,6 +1270,16 @@ dependencies = [ "cc", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -856,9 +1292,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -896,9 +1332,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" @@ -927,6 +1363,12 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.4.0" @@ -967,7 +1409,7 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "libc", "redox_syscall", ] @@ -990,6 +1432,23 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + [[package]] name = "lock_api" version = "0.4.11" @@ -1026,7 +1485,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.6.29", - "syn", + "syn 2.0.55", ] [[package]] @@ -1055,9 +1514,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "miden-air" @@ -1081,6 +1540,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "miden-client" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cd9db9681b986edd6775559ebb79f90fb124af68dc9ccc69f3f2a88d61b960" +dependencies = [ + "async-trait", + "clap", + "comfy-table", + "figment", + "lazy_static", + "miden-lib", + "miden-node-proto 0.1.0", + "miden-objects", + "miden-tx", + "rand", + "rusqlite", + "rusqlite_migration", + "serde", + "serde_json", + "tokio", + "tonic", + "tracing", + "tracing-subscriber", +] + [[package]] name = "miden-core" version = "0.8.0" @@ -1094,14 +1579,14 @@ dependencies = [ [[package]] name = "miden-crypto" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f42129adccef0debcdfcadb1a34323b765fe5895303a71bad820cc67ad2a1db7" +checksum = "a186bed6fd782e0af049164f7e3b565aabf30227ce75e3405550930eebf14627" dependencies = [ "blake3", "cc", "glob", - "libc", + "serde", "winter-crypto", "winter-math", "winter-utils", @@ -1129,7 +1614,7 @@ dependencies = [ "miden-node-block-producer", "miden-node-rpc", "miden-node-store", - "miden-node-utils", + "miden-node-utils 0.2.0", "miden-objects", "serde", "tokio", @@ -1147,10 +1632,10 @@ dependencies = [ "figment", "itertools 0.12.1", "miden-air", - "miden-node-proto", + "miden-node-proto 0.2.0", "miden-node-store", "miden-node-test-macro", - "miden-node-utils", + "miden-node-utils 0.2.0", "miden-objects", "miden-processor", "miden-stdlib", @@ -1166,12 +1651,51 @@ dependencies = [ "winterfell", ] +[[package]] +name = "miden-node-faucet" +version = "0.1.0" +dependencies = [ + "actix-cors", + "actix-files", + "actix-web", + "async-mutex", + "clap", + "derive_more", + "figment", + "miden-client", + "miden-lib", + "miden-node-proto 0.2.0", + "miden-node-utils 0.2.0", + "miden-objects", + "serde", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "miden-node-proto" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdcb2af46af307aaf5d80e940200c08e86d7d7491854f5a9cc33bb3ada7e757" +dependencies = [ + "hex", + "miden-node-utils 0.1.0", + "miden-objects", + "miette", + "prost", + "prost-build", + "protox", + "thiserror", + "tonic", + "tonic-build", +] + [[package]] name = "miden-node-proto" version = "0.2.0" dependencies = [ "hex", - "miden-node-utils", + "miden-node-utils 0.2.0", "miden-objects", "miette", "proptest", @@ -1193,9 +1717,9 @@ dependencies = [ "figment", "hex", "miden-node-block-producer", - "miden-node-proto", + "miden-node-proto 0.2.0", "miden-node-store", - "miden-node-utils", + "miden-node-utils 0.2.0", "miden-objects", "miden-tx", "prost", @@ -1218,8 +1742,8 @@ dependencies = [ "figment", "hex", "miden-lib", - "miden-node-proto", - "miden-node-utils", + "miden-node-proto 0.2.0", + "miden-node-utils 0.2.0", "miden-objects", "once_cell", "prost", @@ -1239,7 +1763,22 @@ name = "miden-node-test-macro" version = "0.1.0" dependencies = [ "quote", - "syn", + "syn 2.0.55", +] + +[[package]] +name = "miden-node-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd92792c7a90a2ea6e7e7e2f1c84d675b9ba84385cbf95ce04ab1f04cf46a1f" +dependencies = [ + "anyhow", + "figment", + "itertools 0.12.1", + "miden-objects", + "serde", + "tracing", + "tracing-subscriber", ] [[package]] @@ -1267,6 +1806,7 @@ dependencies = [ "miden-crypto", "miden-processor", "miden-verifier", + "serde", "winter-rand-utils", ] @@ -1356,7 +1896,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -1365,6 +1905,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1387,6 +1937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -1417,6 +1968,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.18" @@ -1455,7 +2012,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -1514,11 +2071,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pear" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ccca0f6c17acc81df8e242ed473ec144cbf5c98037e69aa6d144780aad103c8" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" dependencies = [ "inlinable_string", "pear_codegen", @@ -1527,14 +2090,14 @@ dependencies = [ [[package]] name = "pear_codegen" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e22670e8eb757cff11d6c199ca7b987f352f0346e0be4dd23869ec72cb53c77" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -1550,7 +2113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.2.5", + "indexmap 2.2.6", ] [[package]] @@ -1570,7 +2133,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -1591,6 +2154,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1599,12 +2168,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.55", ] [[package]] @@ -1618,9 +2187,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -1633,7 +2202,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", "version_check", "yansi", ] @@ -1646,13 +2215,13 @@ checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.4.2", + "bitflags 2.5.0", "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", "rusty-fork", "tempfile", "unarray", @@ -1675,7 +2244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", - "heck", + "heck 0.4.1", "itertools 0.11.0", "log", "multimap", @@ -1685,7 +2254,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn", + "syn 2.0.55", "tempfile", "which", ] @@ -1700,7 +2269,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -1806,6 +2375,26 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1828,14 +2417,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -1855,7 +2444,7 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -1866,9 +2455,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rusqlite" @@ -1876,7 +2465,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1906,13 +2495,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -1949,6 +2547,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + [[package]] name = "serde" version = "1.0.197" @@ -1966,14 +2570,14 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -1989,6 +2593,29 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha3" version = "0.10.8" @@ -2014,6 +2641,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.9" @@ -2025,9 +2661,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smawk" @@ -2051,6 +2687,25 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.55", +] + [[package]] name = "supports-color" version = "3.0.0" @@ -2074,9 +2729,20 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.52" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" dependencies = [ "proc-macro2", "quote", @@ -2124,22 +2790,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -2152,18 +2818,66 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", @@ -2187,14 +2901,14 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -2217,14 +2931,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.7", + "toml_edit 0.22.9", ] [[package]] @@ -2242,18 +2956,18 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.7" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -2297,7 +3011,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -2338,6 +3052,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2351,7 +3066,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", ] [[package]] @@ -2446,6 +3161,21 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -2458,18 +3188,44 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + [[package]] name = "valuable" version = "0.1.0" @@ -2533,7 +3289,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.55", "wasm-bindgen-shared", ] @@ -2555,7 +3311,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2761,9 +3517,9 @@ dependencies = [ [[package]] name = "winter-air" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89fcd28faa34e8e4d1e03ee60b6ac6629cc01b344093cb3a4d45d84ef9bb3832" +checksum = "07390d3217bdd6410c1ef43f3d06510d3a424e7259b371fccbc7cd79a9c00a15" dependencies = [ "libm", "winter-crypto", @@ -2774,9 +3530,9 @@ dependencies = [ [[package]] name = "winter-crypto" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "118a3228b6b2424df48d25941a85ad4b7a69fe877d31d8b5ed57935720ebd152" +checksum = "6aea508aa819e934c837f24bb706e69d890b9be2db82da39cde887e6f0a37246" dependencies = [ "blake3", "sha3", @@ -2786,9 +3542,9 @@ dependencies = [ [[package]] name = "winter-fri" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644b37fc9093fec78c272f3f594dd205ec21dd35d5722001b1d24383693bb6c6" +checksum = "660f47c5c9f5872940ac07a724b1df426590dcffad26776e0528466f2e3095f8" dependencies = [ "winter-crypto", "winter-math", @@ -2797,18 +3553,19 @@ dependencies = [ [[package]] name = "winter-math" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0dff7a27296595fbdc653751b3718acd9518288df646e860ecb48915ff0d6c3" +checksum = "a0c91111b368b08c5a76009514e9b6d26af41fbb28604ea77a249282323b64d5" dependencies = [ + "serde", "winter-utils", ] [[package]] name = "winter-prover" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ec0e854d35d5d35ae778ea2e5178f81507806071294104f3e1a807beb2554c" +checksum = "170c1ef487df609625580156ea0350c500aeabb3f429dc88cfe800c4b7893edf" dependencies = [ "tracing", "winter-air", @@ -2820,9 +3577,9 @@ dependencies = [ [[package]] name = "winter-rand-utils" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "042a5754bc7bbd2d0741f6658885c7567a4fbab52ad2efec9b797a82bfca7300" +checksum = "8b19ce50e688442052e957a69d72b8057d72ae8f03a7aea7c2538e11c76b2583" dependencies = [ "rand", "winter-utils", @@ -2830,15 +3587,18 @@ dependencies = [ [[package]] name = "winter-utils" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefd8f1108fd48c2516c316e0b42b78e7070e116d1373322bf58993c68cf036" +checksum = "ab6efccf6efa6fd0a80784f3894bc372ada67cc30d9c017fc907d4c0cdce86e7" +dependencies = [ + "rayon", +] [[package]] name = "winter-verifier" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4135f28670385a66d2c80943d7998d383660ff63f8c9f544555d9849eb621ba0" +checksum = "6f817a425ca4aa7acefb356601798d0b86b9c0e383b397af69e7e5e97f5b4008" dependencies = [ "winter-air", "winter-crypto", @@ -2849,9 +3609,9 @@ dependencies = [ [[package]] name = "winterfell" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4f653cb8281d965ec77b87354f1a87da3bc0e15b3e40262a1e64fa93a947b3" +checksum = "be0a6acdc1fd126ba950b0e5bfaafd9f294fc157b285f35541603164054e9358" dependencies = [ "winter-prover", "winter-verifier", @@ -2859,9 +3619,9 @@ dependencies = [ [[package]] name = "yansi" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2861d76f58ec8fc95708b9b1e417f7b12fd72ad33c01fa6886707092dea0d3" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" @@ -2880,5 +3640,33 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.55", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.10+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +dependencies = [ + "cc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 81310bd4a..8c7fc933a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "store", "utils", "test-macro", + "faucet" ] resolver = "2" diff --git a/faucet/Cargo.toml b/faucet/Cargo.toml new file mode 100644 index 000000000..50fc69be6 --- /dev/null +++ b/faucet/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "miden-node-faucet" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +# Makes `make-genesis` subcommand run faster. Is only suitable for testing. +testing = ["miden-client/testing"] + +[dependencies] +actix-web = "4" +actix-files = "0.6.5" +actix-cors = "0.7.0" +derive_more = "0.99.17" +figment = { version = "0.10", features = ["toml", "env"] } +miden-lib = { workspace = true } +miden-client = { version = "0.1.0", features = ["concurrent"] } +miden-node-proto = { path = "../proto", version = "0.2" } +miden-node-utils = { path = "../utils", version = "0.2" } +miden-objects = { workspace = true } +serde = { version = "1.0", features = ["derive"] } +clap = { version = "4.5.1", features = ["derive"] } +async-mutex = "1.4.0" +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/faucet/miden-faucet.toml b/faucet/miden-faucet.toml new file mode 100644 index 000000000..de62c291b --- /dev/null +++ b/faucet/miden-faucet.toml @@ -0,0 +1,3 @@ +endpoint = { protocol = "http", host = "localhost", port = 8080 } +rpc_url = "http://localhost:57291" +database_filepath = "miden-faucet.sqlite3" diff --git a/faucet/src/cli.rs b/faucet/src/cli.rs new file mode 100644 index 000000000..eeeae7252 --- /dev/null +++ b/faucet/src/cli.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; + +use crate::config; + +#[derive(Parser, Debug)] +#[clap(name = "Miden Faucet")] +#[clap(about = "A command line tool for the Miden faucet", long_about = None)] +pub struct Cli { + #[clap(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand, Debug)] +pub enum Commands { + /// Initialise a new Miden faucet from arguments + Init(InitArgs), + + /// Imports an existing Miden faucet from specified file + Import(ImportArgs), +} + +#[derive(Parser, Debug)] +pub struct InitArgs { + #[clap(short, long)] + pub token_symbol: String, + + #[clap(short, long)] + pub decimals: u8, + + #[clap(short, long)] + pub max_supply: u64, + + /// Amount of assets to be dispersed by the faucet on each request + #[clap(short, long)] + pub asset_amount: u64, + + #[clap(short, long, value_name = "FILE", default_value = config::CONFIG_FILENAME)] + pub config: PathBuf, +} + +#[derive(Parser, Debug)] +pub struct ImportArgs { + #[clap(short, long)] + pub faucet_path: PathBuf, + + /// Amount of assets to be dispersed by the faucet on each request + #[clap(short, long)] + pub asset_amount: u64, + + #[clap(short, long, value_name = "FILE", default_value = config::CONFIG_FILENAME)] + pub config: PathBuf, +} diff --git a/faucet/src/config.rs b/faucet/src/config.rs new file mode 100644 index 000000000..3479fc124 --- /dev/null +++ b/faucet/src/config.rs @@ -0,0 +1,37 @@ +use std::fmt::{Display, Formatter}; + +use miden_node_utils::config::Endpoint; +use serde::{Deserialize, Serialize}; + +pub const CONFIG_FILENAME: &str = "miden-faucet.toml"; + +// Faucet config +// ================================================================================================ + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)] +pub struct FaucetConfig { + /// Endpoint of the faucet + pub endpoint: Endpoint, + /// rpc gRPC endpoint in the format `http://[:]`. + pub rpc_url: String, + /// Location to store database files + pub database_filepath: String, +} + +impl FaucetConfig { + pub fn as_url(&self) -> String { + self.endpoint.to_string() + } +} + +impl Display for FaucetConfig { + fn fmt( + &self, + f: &mut Formatter<'_>, + ) -> std::fmt::Result { + f.write_fmt(format_args!( + "{{ endpoint: \"{}\", store_url: \"{}\", block_producer_url: \"{}\" }}", + self.endpoint, self.database_filepath, self.rpc_url + )) + } +} diff --git a/faucet/src/errors.rs b/faucet/src/errors.rs new file mode 100644 index 000000000..248b38a97 --- /dev/null +++ b/faucet/src/errors.rs @@ -0,0 +1,32 @@ +use actix_web::{ + error, + http::{header::ContentType, StatusCode}, + HttpResponse, +}; +use derive_more::Display; + +#[derive(Debug, Display)] +pub enum FaucetError { + BadRequest(String), + InternalServerError(String), +} + +impl error::ResponseError for FaucetError { + fn error_response(&self) -> HttpResponse { + let message = match self { + FaucetError::BadRequest(msg) => msg, + FaucetError::InternalServerError(msg) => msg, + }; + + HttpResponse::build(self.status_code()) + .insert_header(ContentType::html()) + .body(message.to_owned()) + } + + fn status_code(&self) -> actix_web::http::StatusCode { + match *self { + FaucetError::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR, + FaucetError::BadRequest(_) => StatusCode::BAD_REQUEST, + } + } +} diff --git a/faucet/src/handlers.rs b/faucet/src/handlers.rs new file mode 100644 index 000000000..f97047ca0 --- /dev/null +++ b/faucet/src/handlers.rs @@ -0,0 +1,102 @@ +use actix_web::{get, http::header, post, web, HttpResponse, Result}; +use miden_client::client::transactions::TransactionTemplate; +use miden_objects::{ + accounts::AccountId, assets::FungibleAsset, notes::NoteId, utils::serde::Serializable, +}; +use serde::{Deserialize, Serialize}; +use tracing::info; + +use crate::{errors::FaucetError, FaucetState}; + +#[derive(Deserialize)] +struct FaucetRequest { + account_id: String, +} + +#[derive(Serialize)] +struct FaucetMetadataReponse { + id: String, + asset_amount: u64, +} + +#[get("/get_metadata")] +pub async fn get_metadata(state: web::Data) -> HttpResponse { + let response = FaucetMetadataReponse { + id: state.id.to_string(), + asset_amount: state.asset_amount, + }; + + HttpResponse::Ok().json(response) +} + +#[post("/get_tokens")] +pub async fn get_tokens( + req: web::Json, + state: web::Data, +) -> Result { + info!("Received a request with account_id: {}", req.account_id); + + let client = state.client.clone(); + + // Receive and hex user account id + let target_account_id = AccountId::from_hex(req.account_id.as_str()) + .map_err(|err| FaucetError::BadRequest(err.to_string()))?; + + // Instantiate asset + let asset = + FungibleAsset::new(state.id, state.asset_amount).expect("Failed to instantiate asset."); + + // Instantiate transaction template + let tx_template = TransactionTemplate::MintFungibleAsset { + asset, + target_account_id, + }; + + // Run transaction executor & execute transaction + let tx_result = client + .lock() + .await + .new_transaction(tx_template) + .map_err(|err| FaucetError::InternalServerError(err.to_string()))?; + + // Get note id + let note_id: NoteId = tx_result + .created_notes() + .first() + .ok_or_else(|| { + FaucetError::InternalServerError("Failed to access generated note.".to_string()) + })? + .id(); + + // Run transaction prover & send transaction to node + { + let mut client_guard = client.lock().await; + client_guard + .send_transaction(tx_result) + .await + .map_err(|err| FaucetError::InternalServerError(err.to_string()))?; + } + + // Get note from client store + let input_note = state + .client + .clone() + .lock() + .await + .get_input_note(note_id) + .map_err(|err| FaucetError::InternalServerError(err.to_string()))?; + + // Serialize note for transport + let bytes = input_note.to_bytes(); + + // Send generated note to user + Ok(HttpResponse::Ok() + .content_type("application/octet-stream") + .append_header(header::ContentDisposition { + disposition: actix_web::http::header::DispositionType::Attachment, + parameters: vec![actix_web::http::header::DispositionParam::Filename( + "note.mno".to_string(), + )], + }) + .body(bytes)) +} diff --git a/faucet/src/main.rs b/faucet/src/main.rs new file mode 100644 index 000000000..68293c888 --- /dev/null +++ b/faucet/src/main.rs @@ -0,0 +1,140 @@ +use std::{io, sync::Arc}; + +use actix_cors::Cors; +use actix_files::Files; +use actix_web::{ + middleware::{DefaultHeaders, Logger}, + web, App, HttpServer, +}; +use async_mutex::Mutex; +use clap::Parser; +use cli::Cli; +use config::FaucetConfig; +use handlers::{get_metadata, get_tokens}; +use miden_client::{ + client::{rpc::TonicRpcClient, Client}, + store::sqlite_store::SqliteStore, +}; +use miden_node_utils::config::load_config; +use miden_objects::accounts::AccountId; +use tracing::info; + +use crate::cli::{ImportArgs, InitArgs}; + +mod cli; +mod config; +mod errors; +mod handlers; +mod utils; + +pub type FaucetClient = Client; + +#[derive(Clone)] +pub struct FaucetState { + id: AccountId, + asset_amount: u64, + client: Arc>, +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let cli = Cli::parse(); + + miden_node_utils::logging::setup_logging().map_err(|e| { + io::Error::new(io::ErrorKind::Other, format!("Failed to load logging: {}", e)) + })?; + + let mut client: FaucetClient; + let config: FaucetConfig; + let amount: u64; + + // Create faucet account + let faucet_account = match &cli.command { + cli::Commands::Init(InitArgs { + asset_amount, + token_symbol, + decimals, + max_supply, + config: faucet_config, + }) => { + config = load_config(faucet_config.as_path()).extract().map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Failed to load configuration file: {}", e), + ) + })?; + + client = utils::build_client(config.database_filepath.clone()); + + amount = *asset_amount; + utils::create_fungible_faucet(token_symbol, decimals, max_supply, &mut client).map_err( + |e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Failed to create faucet account: {}", e), + ) + }, + ) + }, + cli::Commands::Import(ImportArgs { + asset_amount, + faucet_path, + config: faucet_config, + }) => { + config = load_config(faucet_config.as_path()).extract().map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Failed to load configuration file: {}", e), + ) + })?; + + client = utils::build_client(config.database_filepath.clone()); + + amount = *asset_amount; + utils::import_fungible_faucet(faucet_path, &mut client).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Failed to import faucet account: {}", e), + ) + }) + }, + }?; + + // Sync client + client.sync_state().await.map_err(|e| { + io::Error::new(io::ErrorKind::NotConnected, format!("Failed to sync state: {e:?}")) + })?; + + info!("✅ Faucet setup successful, account id: {}", faucet_account.id()); + + info!("🚀 Starting server on: {}", config.as_url()); + + // Instantiate faucet state + let faucet_state = FaucetState { + id: faucet_account.id(), + asset_amount: amount, + client: Arc::new(Mutex::new(client)), + }; + + HttpServer::new(move || { + let cors = Cors::default().allow_any_origin().allow_any_method(); + App::new() + .app_data(web::Data::new(faucet_state.clone())) + .wrap(cors) + .wrap(Logger::default()) + .wrap(DefaultHeaders::new().add(("Cache-Control", "no-cache"))) + .service(get_metadata) + .service(get_tokens) + .service( + Files::new("/", "faucet/src/static") + .use_etag(false) + .use_last_modified(false) + .index_file("index.html"), + ) + }) + .bind((config.endpoint.host, config.endpoint.port))? + .run() + .await?; + + Ok(()) +} diff --git a/faucet/src/static/background.png b/faucet/src/static/background.png new file mode 100644 index 0000000000000000000000000000000000000000..dbf2e4269db132405efd58c7abd635d3e6294657 GIT binary patch literal 165941 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

2Cbw{3=b0$9<*(m)s}T5dsQ`eDMT-E+k*- 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%0Ae08oe{xVQW-;WL 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 MAfqZ>DQW2cKSe{fS^xk5 literal 0 HcmV?d00001 diff --git a/faucet/src/static/favicon.ico b/faucet/src/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..056183ba17cb94907209bed91384cc10088e7d51 GIT binary patch literal 1150 zcma)*OGwmF6voduKCp)ws3C}on~XM5NsEv$HFch9T11+-&`Qd2oEc}-#?*>}5~EE) z5JeBFNl@seZ7L`#%`(eZW~}_6B#PbPn|sc==R1#k&tJsAFFIQIPnLb5 zB5@*;3NT@rv9pPA_CM>TsYagV9yE;D>Jmon-jp{Mm+`IDy(slz_I{Zv^vkfVE_TfB zt$lCzb%HK%gjmVbd)h@~&s{y46~eg;THKLic3%Y#N0b<^F$-d zqit2eFt5$E!Tv{V-ZbnNK_3_xwRyKr{`2hvY=5u7Bt9szOS0Tn!Xjc1o_BiLpIN`ujt(KQgYOjvl!kr)!}9!-#}gl z7k+)^kk!-tbLY|7{4@N|+x_y&?2LsQz&8cX1-M1e^Qu=8GXN6Cxx0nt{iU3p(0)D@ z9uoK!@HAfrax1d-9#2gndf&vTBIn`nJ6iGe>Kp3Y-$%|OXDGhm$R%(ugDadpLAQ5j zRvM{mJ$e^<;Y5Ce-naCdoFV8I?49_x^PZ{Q%;bx_>aYh{l}2hy=j<`inYfE@Idc-h zn+0dE*W?h5MNK;Q&KVz{8$Aw0ssAkhII)tZ%g+e&H}^9To#pH4CECww{7(V>emcN; zYTC#39(NFV3OsnL>Sk ItPoM|A1wr`X8-^I literal 0 HcmV?d00001 diff --git a/faucet/src/static/index.css b/faucet/src/static/index.css new file mode 100644 index 000000000..4c9a7d2d7 --- /dev/null +++ b/faucet/src/static/index.css @@ -0,0 +1,90 @@ +input:focus { + outline: none; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background-image: url(./background.png); + background-repeat: repeat; +} + +#error-message { + display: none; + color: red; + text-align: center; + margin-bottom: 5px; +} + +#navbar { + position: fixed; + top: 0; + left: 0; + background-color: rgb(17, 24, 39); + width: 100%; + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; +} + +#title { + font-size: x-large; + text-align: center; + font-weight: bold; + color: white; + margin: 0; +} + +#center-container { + background-color: rgb(17, 24, 39); + border-radius: 10px; + display: flex; + flex-direction: column; + padding: 30px; +} + +#subtitle { + font-size: large; + text-align: center; + font-weight: bold; + color: white; + margin: 0; + margin-bottom: 20px; +} + +#account-id { + padding: 10px; + border-radius: 10px; + border: 1px solid #ccc; + margin-bottom: 30px; + width: 300px; +} + +#faucetId { + color: white; + margin-top: 4px; +} + +#button { + color: white; + border-radius: 10px; + font-weight: bold; + padding: 10px 20px; + background-color: rgb(124, 58, 237); + width: 300px; +} diff --git a/faucet/src/static/index.html b/faucet/src/static/index.html new file mode 100644 index 000000000..aee693612 --- /dev/null +++ b/faucet/src/static/index.html @@ -0,0 +1,25 @@ + + + + + + + Miden Faucet + + + + +

+
+

Request test tokens

+ + + +
+ + + + diff --git a/faucet/src/static/index.js b/faucet/src/static/index.js new file mode 100644 index 000000000..1dc7b81b2 --- /dev/null +++ b/faucet/src/static/index.js @@ -0,0 +1,70 @@ +document.addEventListener('DOMContentLoaded', function () { + const faucetIdElem = document.getElementById('faucetId'); + const button = document.getElementById('button'); + const accountIdInput = document.getElementById('account-id'); + const errorMessage = document.getElementById('error-message'); + + fetchMetadata(); + + button.addEventListener('click', handleButtonClick); + + function fetchMetadata() { + fetch('http://localhost:8080/get_metadata') + .then(response => response.json()) + .then(data => { + faucetIdElem.textContent = data.id; + button.textContent = `Send me ${data.asset_amount} tokens!`; + button.dataset.originalText = button.textContent; + }) + .catch(error => { + console.error('Error fetching metadata:', error); + faucetIdElem.textContent = 'Error loading Faucet ID.'; + button.textContent = 'Error retrieving Faucet asset amount.'; + }); + } + + async function handleButtonClick() { + let accountId = accountIdInput.value.trim(); + errorMessage.style.display = 'none'; + + if (!accountId || !/^0x[0-9a-fA-F]{16}$/i.test(accountId)) { + errorMessage.textContent = !accountId ? "Account ID is required." : "Invalid Account ID."; + errorMessage.style.display = 'block'; + return; + } + + button.textContent = 'Loading...'; + try { + const response = await fetch('http://localhost:8080/get_tokens', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ account_id: accountId }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const blob = await response.blob(); + downloadBlob(blob, 'note.mno'); + } catch (error) { + console.error('Error:', error); + errorMessage.textContent = 'Failed to receive tokens. Please try again.'; + errorMessage.style.display = 'block'; + } finally { + button.textContent = button.dataset.originalText; + } + } + + function downloadBlob(blob, filename) { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + } +}); diff --git a/faucet/src/utils.rs b/faucet/src/utils.rs new file mode 100644 index 000000000..05a2288e3 --- /dev/null +++ b/faucet/src/utils.rs @@ -0,0 +1,102 @@ +use std::{ + fs::File, + io::{self, Read}, + path::{Path, PathBuf}, +}; + +use miden_client::{ + client::{rpc::TonicRpcClient, Client}, + config::{RpcConfig, StoreConfig}, + store::{sqlite_store::SqliteStore, AuthInfo}, +}; +use miden_lib::{accounts::faucets::create_basic_fungible_faucet, AuthScheme}; +use miden_objects::{ + accounts::{Account, AccountData}, + assets::TokenSymbol, + crypto::dsa::rpo_falcon512::KeyPair, + utils::serde::Deserializable, + Felt, +}; + +use crate::FaucetClient; + +/// Instantiates the Miden client +pub fn build_client(database_filepath: String) -> FaucetClient { + // Setup store + let store_config = StoreConfig { + database_filepath: database_filepath.clone(), + }; + let store = SqliteStore::new(store_config).expect("Failed to instantiate store."); + + // Setup the executor store + let executor_store_config = StoreConfig { + database_filepath: database_filepath.clone(), + }; + let executor_store = + SqliteStore::new(executor_store_config).expect("Failed to instantiate datastore store"); + + // Setup the tonic rpc client + let rpc_config = RpcConfig::default(); + let api = TonicRpcClient::new(&rpc_config.endpoint.to_string()); + + // Setup the client + Client::new(api, store, executor_store).expect("Failed to instantiate client.") +} + +/// Creates a Miden fungible faucet from arguments +pub fn create_fungible_faucet( + token_symbol: &str, + decimals: &u8, + max_supply: &u64, + client: &mut FaucetClient, +) -> Result { + let token_symbol = TokenSymbol::new(token_symbol).expect("Failed to parse token_symbol."); + + // Instantiate init_seed + let init_seed: [u8; 32] = [0; 32]; + + // Instantiate keypair and authscheme + let auth_seed: [u8; 40] = [0; 40]; + let keypair = KeyPair::from_seed(&auth_seed).expect("Failed to generate keypair."); + let auth_scheme = AuthScheme::RpoFalcon512 { + pub_key: keypair.public_key(), + }; + + let (account, account_seed) = create_basic_fungible_faucet( + init_seed, + token_symbol, + *decimals, + Felt::try_from(*max_supply).expect("Max_supply is outside of the possible range."), + auth_scheme, + ) + .expect("Failed to generate faucet account."); + + client + .insert_account(&account, Some(account_seed), &AuthInfo::RpoFalcon512(keypair)) + .map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "Failed to insert account into client.") + })?; + + Ok(account) +} + +/// Imports a Miden fungible faucet from a file +pub fn import_fungible_faucet( + faucet_path: &PathBuf, + client: &mut FaucetClient, +) -> Result { + let path = Path::new(faucet_path); + let mut file = File::open(path).expect("Failed to open file."); + + let mut contents = Vec::new(); + let _ = file.read_to_end(&mut contents); + + let account_data = + AccountData::read_from_bytes(&contents).expect("Failed to deserialize faucet from file."); + + client.import_account(account_data.clone()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "Failed to import account into client.") + })?; + + Ok(account_data.account) +} From ebdfa6c13c5ec8f22d459ad8a5159770724572a2 Mon Sep 17 00:00:00 2001 From: polydez <155382956+polydez@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:20:06 +0500 Subject: [PATCH 11/29] Migration to the `next` version of Miden dependencies (#297) * feat: migration to the `next` versions of Miden dependencies * fix: `parse_auth_inputs` * chore: use dependencies from crates.io * chore: update deps * fix: compilation errors * fix: address review comments * fix: genesis test --- Cargo.lock | 285 +++++++++++++++--- Cargo.toml | 12 +- .../src/block_builder/prover/block_witness.rs | 4 +- .../src/block_builder/prover/tests.rs | 7 +- block-producer/src/errors.rs | 18 +- block-producer/src/test_utils/proven_tx.rs | 18 +- faucet/Cargo.toml | 4 +- node/Cargo.toml | 1 + node/genesis.toml | 4 +- node/src/commands/genesis/mod.rs | 14 +- proto/src/errors.rs | 4 +- store/src/errors.rs | 4 +- store/src/state.rs | 11 +- 13 files changed, 302 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e873dd380..0b91e7717 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1524,7 +1524,18 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74fe3da3cee72178aa6d8cdd0e88a4d11a983f6c79a0257abfb6f04056177bee" dependencies = [ - "miden-core", + "miden-core 0.8.0", + "winter-air", + "winter-prover", +] + +[[package]] +name = "miden-air" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c695ee3a51649711a6c202e4c7280222b66e987c8abefce9090fdc49412bbcbe" +dependencies = [ + "miden-core 0.9.1", "winter-air", "winter-prover", ] @@ -1535,7 +1546,18 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4261c0b2593e540004ddff8eb4e04419cf06d4c3794dbaa03bac6b51d09bf5" dependencies = [ - "miden-core", + "miden-core 0.8.0", + "num_enum", + "tracing", +] + +[[package]] +name = "miden-assembly" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "015abf93a083b1255a45c5bde02479e5203468c662fd33dd092b6daf1e4a3988" +dependencies = [ + "miden-core 0.9.1", "num_enum", "tracing", ] @@ -1551,10 +1573,10 @@ dependencies = [ "comfy-table", "figment", "lazy_static", - "miden-lib", + "miden-lib 0.1.0", "miden-node-proto 0.1.0", - "miden-objects", - "miden-tx", + "miden-objects 0.1.1", + "miden-tx 0.1.0", "rand", "rusqlite", "rusqlite_migration", @@ -1572,7 +1594,18 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bb50725295351f6f01e68a03b6f43724a727c3545ddd849a94e21290fbae2ce" dependencies = [ - "miden-crypto", + "miden-crypto 0.8.4", + "winter-math", + "winter-utils", +] + +[[package]] +name = "miden-core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc7219af4b6c5fb6a01d80b9dfdad68962157ccf986f7a923df01103e853e74" +dependencies = [ + "miden-crypto 0.9.1", "winter-math", "winter-utils", ] @@ -1592,15 +1625,44 @@ dependencies = [ "winter-utils", ] +[[package]] +name = "miden-crypto" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e339568704e3135360528e65edde5a47e585c007b7d14b55d010ba44c735b2" +dependencies = [ + "blake3", + "cc", + "glob", + "num", + "num-complex", + "rand", + "rand_core", + "sha3", + "winter-crypto", + "winter-math", + "winter-utils", +] + [[package]] name = "miden-lib" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "825bc94780153bd3783d9adbdf276ea38fab2ff3ce0815067badaa4d404c5fae" dependencies = [ - "miden-assembly", - "miden-objects", - "miden-stdlib", + "miden-assembly 0.8.0", + "miden-objects 0.1.1", + "miden-stdlib 0.8.0", +] + +[[package]] +name = "miden-lib" +version = "0.2.0" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#d6af2ae8ce3da19e9dcd66ce3ad81a724a8d8a7a" +dependencies = [ + "miden-assembly 0.9.1", + "miden-objects 0.2.0", + "miden-stdlib 0.9.1", ] [[package]] @@ -1610,12 +1672,13 @@ dependencies = [ "anyhow", "clap", "figment", - "miden-lib", + "miden-lib 0.2.0", "miden-node-block-producer", "miden-node-rpc", "miden-node-store", "miden-node-utils 0.2.0", - "miden-objects", + "miden-objects 0.2.0", + "rand_chacha", "serde", "tokio", "tracing", @@ -1631,15 +1694,15 @@ dependencies = [ "clap", "figment", "itertools 0.12.1", - "miden-air", + "miden-air 0.9.1", "miden-node-proto 0.2.0", "miden-node-store", "miden-node-test-macro", "miden-node-utils 0.2.0", - "miden-objects", - "miden-processor", - "miden-stdlib", - "miden-tx", + "miden-objects 0.2.0", + "miden-processor 0.9.1", + "miden-stdlib 0.9.1", + "miden-tx 0.2.0", "once_cell", "serde", "thiserror", @@ -1663,10 +1726,10 @@ dependencies = [ "derive_more", "figment", "miden-client", - "miden-lib", + "miden-lib 0.1.0", "miden-node-proto 0.2.0", "miden-node-utils 0.2.0", - "miden-objects", + "miden-objects 0.1.1", "serde", "tracing", "tracing-subscriber", @@ -1680,7 +1743,7 @@ checksum = "3fdcb2af46af307aaf5d80e940200c08e86d7d7491854f5a9cc33bb3ada7e757" dependencies = [ "hex", "miden-node-utils 0.1.0", - "miden-objects", + "miden-objects 0.1.1", "miette", "prost", "prost-build", @@ -1696,7 +1759,7 @@ version = "0.2.0" dependencies = [ "hex", "miden-node-utils 0.2.0", - "miden-objects", + "miden-objects 0.2.0", "miette", "proptest", "prost", @@ -1720,8 +1783,8 @@ dependencies = [ "miden-node-proto 0.2.0", "miden-node-store", "miden-node-utils 0.2.0", - "miden-objects", - "miden-tx", + "miden-objects 0.2.0", + "miden-tx 0.2.0", "prost", "serde", "tokio", @@ -1741,10 +1804,10 @@ dependencies = [ "directories", "figment", "hex", - "miden-lib", + "miden-lib 0.2.0", "miden-node-proto 0.2.0", "miden-node-utils 0.2.0", - "miden-objects", + "miden-objects 0.2.0", "once_cell", "prost", "rusqlite", @@ -1775,7 +1838,7 @@ dependencies = [ "anyhow", "figment", "itertools 0.12.1", - "miden-objects", + "miden-objects 0.1.1", "serde", "tracing", "tracing-subscriber", @@ -1788,7 +1851,7 @@ dependencies = [ "anyhow", "figment", "itertools 0.12.1", - "miden-objects", + "miden-objects 0.2.0", "serde", "tracing", "tracing-forest", @@ -1801,23 +1864,48 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7379df161104e3ba97812a89e33b4cff8183964af848074d62dd49f34f00aeed" dependencies = [ - "miden-assembly", - "miden-core", - "miden-crypto", - "miden-processor", - "miden-verifier", + "miden-assembly 0.8.0", + "miden-core 0.8.0", + "miden-crypto 0.8.4", + "miden-processor 0.8.0", + "miden-verifier 0.8.0", "serde", "winter-rand-utils", ] +[[package]] +name = "miden-objects" +version = "0.2.0" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#d6af2ae8ce3da19e9dcd66ce3ad81a724a8d8a7a" +dependencies = [ + "miden-assembly 0.9.1", + "miden-core 0.9.1", + "miden-crypto 0.9.1", + "miden-processor 0.9.1", + "miden-verifier 0.9.1", + "winter-rand-utils", +] + [[package]] name = "miden-processor" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e355306e8495f40685aac012d7ec62f86c42b89dcd8d88d26a507b363d4e5de5" dependencies = [ - "miden-air", - "miden-core", + "miden-air 0.8.0", + "miden-core 0.8.0", + "tracing", + "winter-prover", +] + +[[package]] +name = "miden-processor" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "682f881050c637bbf01bee5970d5f13d818b544b7f0ea3ecb1b624aa54ec04e3" +dependencies = [ + "miden-air 0.9.1", + "miden-core 0.9.1", "tracing", "winter-prover", ] @@ -1828,8 +1916,20 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "757e94100fd703d9b76ef0b4772a6e983e895454fd43da8ba8145b57976e9f69" dependencies = [ - "miden-air", - "miden-processor", + "miden-air 0.8.0", + "miden-processor 0.8.0", + "tracing", + "winter-prover", +] + +[[package]] +name = "miden-prover" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9a685fc0e9ba2b5c4b7203b759ee2dea469600813f213de381461996c6775c8" +dependencies = [ + "miden-air 0.9.1", + "miden-processor 0.9.1", "tracing", "winter-prover", ] @@ -1840,7 +1940,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a560cf74e712c121b4f531aa6df2de5f9daef29dfc328a0ee91064ce6720d5d" dependencies = [ - "miden-assembly", + "miden-assembly 0.8.0", +] + +[[package]] +name = "miden-stdlib" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60994609096a8cb1e32f1fcf1c8ed63bed9b24b32730658609bad841e8caeaf9" +dependencies = [ + "miden-assembly 0.9.1", ] [[package]] @@ -1849,11 +1958,23 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d59fa14ff23132a14fd3cf215e681d99aad83d819500319f0b20c251179c671" dependencies = [ - "miden-lib", - "miden-objects", - "miden-processor", - "miden-prover", - "miden-verifier", + "miden-lib 0.1.0", + "miden-objects 0.1.1", + "miden-processor 0.8.0", + "miden-prover 0.8.0", + "miden-verifier 0.8.0", +] + +[[package]] +name = "miden-tx" +version = "0.2.0" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#d6af2ae8ce3da19e9dcd66ce3ad81a724a8d8a7a" +dependencies = [ + "miden-lib 0.2.0", + "miden-objects 0.2.0", + "miden-processor 0.9.1", + "miden-prover 0.9.1", + "miden-verifier 0.9.1", ] [[package]] @@ -1862,8 +1983,20 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a54fb6a52f8a3a0874718bf2cb41526e4a59972e8918db9a7796a7a73ab37f" dependencies = [ - "miden-air", - "miden-core", + "miden-air 0.8.0", + "miden-core 0.8.0", + "tracing", + "winter-verifier", +] + +[[package]] +name = "miden-verifier" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8af794328085d13d11bd0ff009464b0af74206cd43f51c8aa3b1c42f17993b62" +dependencies = [ + "miden-air 0.9.1", + "miden-core 0.9.1", "tracing", "winter-verifier", ] @@ -1968,12 +2101,78 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.18" diff --git a/Cargo.toml b/Cargo.toml index 8c7fc933a..02576560d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,12 +23,12 @@ repository = "https://github.com/0xPolygonMiden/miden-node" exclude = [".github/"] [workspace.dependencies] -miden-air = { version = "0.8", default-features = false } -miden-lib = { version = "0.1" } -miden-objects = { version = "0.1" } -miden-processor = { version = "0.8" } -miden-stdlib = { version = "0.8", default-features = false } -miden-tx = { version = "0.1" } +miden-air = { version = "0.9", default-features = false } +miden-lib = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } +miden-objects = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } +miden-processor = { version = "0.9" } +miden-stdlib = { version = "0.9", default-features = false } +miden-tx = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } thiserror = { version = "1.0" } tracing = { version = "0.1" } tracing-subscriber = { version = "0.3", features = [ diff --git a/block-producer/src/block_builder/prover/block_witness.rs b/block-producer/src/block_builder/prover/block_witness.rs index 87bcbcd0e..ba7e7f6a5 100644 --- a/block-producer/src/block_builder/prover/block_witness.rs +++ b/block-producer/src/block_builder/prover/block_witness.rs @@ -261,7 +261,9 @@ impl BlockWitness { .expect("updated accounts number is greater than or equal to the field modulus"), ); - StackInputs::new(stack_inputs) + // TODO: We need provide produced nullifier different way, because such big stack inputs + // will cause problem in recursive proofs + StackInputs::new(stack_inputs).expect("Stack inputs count extends max limit") } /// Builds the advice inputs to the block kernel diff --git a/block-producer/src/block_builder/prover/tests.rs b/block-producer/src/block_builder/prover/tests.rs index 11f0324eb..5c698cfa0 100644 --- a/block-producer/src/block_builder/prover/tests.rs +++ b/block-producer/src/block_builder/prover/tests.rs @@ -6,7 +6,7 @@ use miden_objects::{ EmptySubtreeRoots, LeafIndex, MerklePath, Mmr, MmrPeaks, SimpleSmt, Smt, SmtLeaf, SmtProof, SMT_DEPTH, }, - notes::{NoteEnvelope, NoteMetadata}, + notes::{NoteEnvelope, NoteMetadata, NoteType}, BLOCK_OUTPUT_NOTES_TREE_DEPTH, ONE, ZERO, }; @@ -381,7 +381,10 @@ async fn test_compute_note_root_success() { .into_iter() .zip(account_ids.iter()) .map(|(note_digest, &account_id)| { - NoteEnvelope::new(note_digest.into(), NoteMetadata::new(account_id, Felt::new(1u64))) + NoteEnvelope::new( + note_digest.into(), + NoteMetadata::new(account_id, NoteType::OffChain, 0.into(), ONE).unwrap(), + ) }) .collect(); diff --git a/block-producer/src/errors.rs b/block-producer/src/errors.rs index a597714a1..e8cc22dd2 100644 --- a/block-producer/src/errors.rs +++ b/block-producer/src/errors.rs @@ -13,7 +13,7 @@ use thiserror::Error; // Transaction verification errors // ================================================================================================= -#[derive(Error, Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq, Error)] pub enum VerifyTxError { /// The account that the transaction modifies has already been modified and isn't yet committed /// to a block @@ -49,7 +49,7 @@ pub enum VerifyTxError { // Transaction adding errors // ================================================================================================= -#[derive(Error, Debug)] +#[derive(Debug, PartialEq, Eq, Error)] pub enum AddTransactionError { #[error("Transaction verification failed: {0}")] VerificationFailed(#[from] VerifyTxError), @@ -63,7 +63,7 @@ pub enum AddTransactionError { /// These errors are returned from the batch builder to the transaction queue, instead of /// dropping the transactions, they are included into the error values, so that the transaction /// queue can re-queue them. -#[derive(Error, Debug)] +#[derive(Debug, PartialEq, Eq, Error)] pub enum BuildBatchError { #[error("Too many notes in the batch. Got: {0}, max: {}", MAX_NOTES_PER_BATCH)] TooManyNotesCreated(usize, Vec), @@ -84,13 +84,13 @@ impl BuildBatchError { // Block prover errors // ================================================================================================= -#[derive(Error, Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq, Error)] pub enum BlockProverError { #[error("Received invalid merkle path")] InvalidMerklePaths(MerkleError), - #[error("program execution failed")] + #[error("Program execution failed")] ProgramExecutionFailed(ExecutionError), - #[error("failed to retrieve {0} root from stack outputs")] + #[error("Failed to retrieve {0} root from stack outputs")] InvalidRootOutput(&'static str), } @@ -98,7 +98,7 @@ pub enum BlockProverError { // ================================================================================================= #[allow(clippy::enum_variant_names)] -#[derive(Debug, PartialEq, Error)] +#[derive(Debug, PartialEq, Eq, Error)] pub enum BlockInputsError { #[error("failed to parse protobuf message: {0}")] ParseError(#[from] ParseError), @@ -122,7 +122,7 @@ pub enum ApplyBlockError { // Block building errors // ================================================================================================= -#[derive(Debug, Error, PartialEq)] +#[derive(Debug, PartialEq, Eq, Error)] pub enum BuildBlockError { #[error("failed to compute new block: {0}")] BlockProverFailed(#[from] BlockProverError), @@ -146,7 +146,7 @@ pub enum BuildBlockError { // Transaction inputs errors // ================================================================================================= -#[derive(Debug, PartialEq, Error)] +#[derive(Debug, PartialEq, Eq, Error)] pub enum TxInputsError { #[error("gRPC client failed with error: {0}")] GrpcClientError(String), diff --git a/block-producer/src/test_utils/proven_tx.rs b/block-producer/src/test_utils/proven_tx.rs index 1f8f11b53..51d1813a0 100644 --- a/block-producer/src/test_utils/proven_tx.rs +++ b/block-producer/src/test_utils/proven_tx.rs @@ -3,8 +3,8 @@ use std::ops::Range; use miden_air::HashFunction; use miden_objects::{ accounts::AccountId, - notes::{NoteEnvelope, NoteMetadata, Nullifier}, - transaction::{InputNotes, OutputNotes, ProvenTransaction}, + notes::{NoteEnvelope, NoteMetadata, NoteType, Nullifier}, + transaction::{ProvenTransaction, ProvenTransactionBuilder}, vm::ExecutionProof, Digest, Felt, Hasher, ONE, }; @@ -82,7 +82,10 @@ impl MockProvenTxBuilder { .map(|note_index| { let note_hash = Hasher::hash(¬e_index.to_be_bytes()); - NoteEnvelope::new(note_hash.into(), NoteMetadata::new(self.account_id, ONE)) + NoteEnvelope::new( + note_hash.into(), + NoteMetadata::new(self.account_id, NoteType::OffChain, 0.into(), ONE).unwrap(), + ) }) .collect(); @@ -90,15 +93,16 @@ impl MockProvenTxBuilder { } pub fn build(self) -> ProvenTransaction { - ProvenTransaction::new( + ProvenTransactionBuilder::new( self.account_id, self.initial_account_hash, self.final_account_hash, - InputNotes::new(self.nullifiers.unwrap_or_default()).unwrap(), - OutputNotes::new(self.notes_created.unwrap_or_default()).unwrap(), - None, Digest::default(), ExecutionProof::new(StarkProof::new_dummy(), HashFunction::Blake3_192), ) + .add_input_notes(self.nullifiers.unwrap_or_default()) + .add_output_notes(self.notes_created.unwrap_or_default()) + .build() + .unwrap() } } diff --git a/faucet/Cargo.toml b/faucet/Cargo.toml index 50fc69be6..d7815297f 100644 --- a/faucet/Cargo.toml +++ b/faucet/Cargo.toml @@ -15,11 +15,11 @@ actix-files = "0.6.5" actix-cors = "0.7.0" derive_more = "0.99.17" figment = { version = "0.10", features = ["toml", "env"] } -miden-lib = { workspace = true } +miden-lib = { version = "0.1.0" } # Version of miden-base is pinned due to client requirement miden-client = { version = "0.1.0", features = ["concurrent"] } miden-node-proto = { path = "../proto", version = "0.2" } miden-node-utils = { path = "../utils", version = "0.2" } -miden-objects = { workspace = true } +miden-objects = { version = "0.1.0" } # Version of miden-base is pinned due to client requirement serde = { version = "1.0", features = ["derive"] } clap = { version = "4.5.1", features = ["derive"] } async-mutex = "1.4.0" diff --git a/node/Cargo.toml b/node/Cargo.toml index e9fdcc236..b61fe5af9 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -25,6 +25,7 @@ miden-node-rpc = { path = "../rpc", version = "0.2" } miden-node-store = { path = "../store", version = "0.2" } miden-node-utils = { path = "../utils", version = "0.2" } miden-objects = { workspace = true } +rand_chacha = "0.3" serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.29", features = ["rt-multi-thread", "net", "macros"] } tracing = { workspace = true } diff --git a/node/genesis.toml b/node/genesis.toml index 4e864d46c..a0afd9189 100644 --- a/node/genesis.toml +++ b/node/genesis.toml @@ -7,13 +7,13 @@ timestamp = 1672531200 type = "BasicWallet" init_seed = "0xa123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" auth_scheme = "RpoFalcon512" -auth_seed = "0xb123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +auth_seed = "0xb123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" [[accounts]] type = "BasicFungibleFaucet" init_seed = "0xc123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" auth_scheme = "RpoFalcon512" -auth_seed = "0xd123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +auth_seed = "0xd123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" token_symbol = "POL" decimals = 12 max_supply = 1000000 diff --git a/node/src/commands/genesis/mod.rs b/node/src/commands/genesis/mod.rs index eb4a9d0bc..f05fb9fb2 100644 --- a/node/src/commands/genesis/mod.rs +++ b/node/src/commands/genesis/mod.rs @@ -15,11 +15,12 @@ use miden_objects::{ accounts::{Account, AccountData, AccountType, AuthData}, assets::TokenSymbol, crypto::{ - dsa::rpo_falcon512::KeyPair, + dsa::rpo_falcon512::SecretKey, utils::{hex_to_bytes, Serializable}, }, Felt, ONE, }; +use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng}; mod inputs; @@ -183,11 +184,12 @@ fn parse_auth_inputs( ) -> Result<(AuthScheme, AuthData)> { match auth_scheme_input { AuthSchemeInput::RpoFalcon512 => { - let auth_seed = hex_to_bytes(auth_seed)?; - let keypair = KeyPair::from_seed(&auth_seed)?; + let auth_seed: [u8; 32] = hex_to_bytes(auth_seed)?; + let mut rng = ChaCha20Rng::from_seed(auth_seed); + let secret = SecretKey::with_rng(&mut rng); let auth_scheme = AuthScheme::RpoFalcon512 { - pub_key: keypair.public_key(), + pub_key: secret.public_key(), }; let auth_info = AuthData::RpoFalcon512Seed(auth_seed); @@ -226,13 +228,13 @@ mod tests { type = "BasicWallet" init_seed = "0xa123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" auth_scheme = "RpoFalcon512" - auth_seed = "0xb123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + auth_seed = "0xb123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" [[accounts]] type = "BasicFungibleFaucet" init_seed = "0xc123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" auth_scheme = "RpoFalcon512" - auth_seed = "0xd123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + auth_seed = "0xd123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" token_symbol = "POL" decimals = 12 max_supply = 1000000 diff --git a/proto/src/errors.rs b/proto/src/errors.rs index 5b432c661..da0e29577 100644 --- a/proto/src/errors.rs +++ b/proto/src/errors.rs @@ -3,7 +3,7 @@ use std::any::type_name; use miden_objects::crypto::merkle::{SmtLeafError, SmtProofError}; use thiserror::Error; -#[derive(Error, Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Error)] pub enum ParseError { #[error("Hex error: {0}")] HexError(#[from] hex::FromHexError), @@ -26,6 +26,8 @@ pub enum ParseError { }, } +impl Eq for ParseError {} + pub trait MissingFieldHelper { fn missing_field(field_name: &'static str) -> ParseError; } diff --git a/store/src/errors.rs b/store/src/errors.rs index 2856b5f96..9cc54c3dd 100644 --- a/store/src/errors.rs +++ b/store/src/errors.rs @@ -7,7 +7,7 @@ use miden_objects::{ utils::DeserializationError, }, notes::Nullifier, - AccountError, BlockHeader, + AccountError, BlockHeader, NoteError, }; use rusqlite::types::FromSqlError; use thiserror::Error; @@ -108,6 +108,8 @@ pub enum ApplyBlockError { DatabaseError(#[from] DatabaseError), #[error("Account error: {0}")] AccountError(#[from] AccountError), + #[error("Note error: {0}")] + NoteError(#[from] NoteError), #[error("Concurrent write detected")] ConcurrentWrite, #[error("New block number must be 1 greater than the current block number")] diff --git a/store/src/state.rs b/store/src/state.rs index b34a93026..06246d958 100644 --- a/store/src/state.rs +++ b/store/src/state.rs @@ -11,8 +11,8 @@ use miden_objects::{ hash::rpo::RpoDigest, merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, SimpleSmt, SmtProof, ValuePath}, }, - notes::{NoteMetadata, Nullifier, NOTE_LEAF_DEPTH}, - AccountError, BlockHeader, Word, ACCOUNT_TREE_DEPTH, + notes::{NoteMetadata, NoteType, Nullifier, NOTE_LEAF_DEPTH}, + AccountError, BlockHeader, NoteError, Word, ACCOUNT_TREE_DEPTH, ZERO, }; use tokio::{ sync::{oneshot, Mutex, RwLock}, @@ -481,12 +481,15 @@ pub fn build_notes_tree( let mut entries: Vec<(u64, Word)> = Vec::with_capacity(notes.len() * 2); for note in notes.iter() { + let note_type = NoteType::OffChain; // TODO: provide correct note type let note_metadata = NoteMetadata::new( note.sender.try_into()?, + note_type, note.tag .try_into() - .expect("tag value is greater than or equal to the field modulus"), - ); + .map_err(|_| NoteError::InconsistentNoteTag(note_type, note.tag))?, + ZERO, + )?; let index = note.note_index as u64; entries.push((index, note.note_id.into())); entries.push((index + 1, note_metadata.into())); From 3b42e8d024d3e2d5236a28008e257b0f4e067bd9 Mon Sep 17 00:00:00 2001 From: polydez <155382956+polydez@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:34:39 +0500 Subject: [PATCH 12/29] Implemented support for note trees in Miden node (#295) * feat: migration to the `next` versions of Miden dependencies * fix: address review comments * fix: update to the latest `next` * feat: support of `BatchNoteTree` and `BlockNoteTree` * feat: support of `BatchNoteTree` and `BlockNoteTree`, use `batch_index` instead of encoding it into the note index * docs: add comments in SQL for batch and note indexes --- block-producer/src/batch_builder/batch.rs | 18 ++++----- block-producer/src/block.rs | 2 +- block-producer/src/block_builder/mod.rs | 10 ++--- block-producer/src/test_utils/block.rs | 39 ++++++++----------- block-producer/src/test_utils/store.rs | 11 +++--- proto/proto/note.proto | 9 +++-- proto/src/domain/notes.rs | 7 ++-- proto/src/generated/note.rs | 8 ++-- store/src/db/migrations.rs | 4 +- store/src/db/mod.rs | 1 + store/src/db/sql.rs | 45 ++++++++++++++++------ store/src/db/tests.rs | 41 +++++++++++++------- store/src/errors.rs | 2 +- store/src/server/api.rs | 1 + store/src/state.rs | 47 +++++++++++------------ 15 files changed, 138 insertions(+), 107 deletions(-) diff --git a/block-producer/src/batch_builder/batch.rs b/block-producer/src/batch_builder/batch.rs index 2f4752f1d..864948cec 100644 --- a/block-producer/src/batch_builder/batch.rs +++ b/block-producer/src/batch_builder/batch.rs @@ -2,12 +2,10 @@ use std::collections::BTreeMap; use miden_objects::{ accounts::AccountId, - crypto::{ - hash::blake::{Blake3Digest, Blake3_256}, - merkle::SimpleSmt, - }, + batches::BatchNoteTree, + crypto::hash::blake::{Blake3Digest, Blake3_256}, notes::{NoteEnvelope, Nullifier}, - Digest, BATCH_OUTPUT_NOTES_TREE_DEPTH, MAX_NOTES_PER_BATCH, + Digest, MAX_NOTES_PER_BATCH, }; use tracing::instrument; @@ -27,7 +25,7 @@ pub struct TransactionBatch { id: BatchId, updated_accounts: BTreeMap, produced_nullifiers: Vec, - created_notes_smt: SimpleSmt, + created_notes_smt: BatchNoteTree, /// The notes stored `created_notes_smt` created_notes: Vec, } @@ -74,10 +72,10 @@ impl TransactionBatch { // TODO: document under what circumstances SMT creating can fail ( created_notes.clone(), - SimpleSmt::::with_contiguous_leaves( - created_notes.into_iter().flat_map(|note_envelope| { - [note_envelope.note_id().into(), note_envelope.metadata().into()] - }), + BatchNoteTree::with_contiguous_leaves( + created_notes + .iter() + .map(|note_envelope| (note_envelope.note_id(), note_envelope.metadata())), ) .map_err(|e| BuildBatchError::NotesSmtError(e, txs))?, ) diff --git a/block-producer/src/block.rs b/block-producer/src/block.rs index e2f818cf7..cfc026aee 100644 --- a/block-producer/src/block.rs +++ b/block-producer/src/block.rs @@ -18,7 +18,7 @@ use crate::store::BlockInputsError; pub struct Block { pub header: BlockHeader, pub updated_accounts: Vec<(AccountId, Digest)>, - pub created_notes: BTreeMap, + pub created_notes: Vec<(usize, usize, NoteEnvelope)>, pub produced_nullifiers: Vec, // TODO: // - full states for updated public accounts diff --git a/block-producer/src/block_builder/mod.rs b/block-producer/src/block_builder/mod.rs index d92bd7943..615b687e3 100644 --- a/block-producer/src/block_builder/mod.rs +++ b/block-producer/src/block_builder/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use async_trait::async_trait; use miden_node_utils::formatting::{format_array, format_blake3_digest}; -use miden_objects::{accounts::AccountId, notes::Nullifier, Digest, MAX_NOTES_PER_BATCH}; +use miden_objects::{accounts::AccountId, notes::Nullifier, Digest}; use tracing::{debug, info, instrument}; use crate::{ @@ -83,10 +83,10 @@ where .iter() .enumerate() .flat_map(|(batch_idx, batch)| { - batch.created_notes().enumerate().map(move |(note_idx_in_batch, note)| { - let note_idx_in_block = batch_idx * MAX_NOTES_PER_BATCH + note_idx_in_batch; - (note_idx_in_block as u64, *note) - }) + batch + .created_notes() + .enumerate() + .map(move |(note_idx_in_batch, note)| (batch_idx, note_idx_in_batch, *note)) }) .collect(); let produced_nullifiers: Vec = diff --git a/block-producer/src/test_utils/block.rs b/block-producer/src/test_utils/block.rs index a158a8d72..5bfd2c00f 100644 --- a/block-producer/src/test_utils/block.rs +++ b/block-producer/src/test_utils/block.rs @@ -1,11 +1,9 @@ -use std::collections::BTreeMap; - use miden_objects::{ accounts::AccountId, + block::BlockNoteTree, crypto::merkle::{Mmr, SimpleSmt}, notes::{NoteEnvelope, Nullifier}, - BlockHeader, Digest, ACCOUNT_TREE_DEPTH, BLOCK_OUTPUT_NOTES_TREE_DEPTH, MAX_NOTES_PER_BATCH, - ONE, ZERO, + BlockHeader, Digest, ACCOUNT_TREE_DEPTH, ONE, ZERO, }; use super::MockStoreSuccess; @@ -92,7 +90,7 @@ pub struct MockBlockBuilder { last_block_header: BlockHeader, updated_accounts: Option>, - created_notes: Option>, + created_notes: Option>, produced_nullifiers: Option>, } @@ -124,7 +122,7 @@ impl MockBlockBuilder { pub fn created_notes( mut self, - created_notes: BTreeMap, + created_notes: Vec<(usize, usize, NoteEnvelope)>, ) -> Self { self.created_notes = Some(created_notes); @@ -149,7 +147,7 @@ impl MockBlockBuilder { self.store_chain_mmr.peaks(self.store_chain_mmr.forest()).unwrap().hash_peaks(), self.store_accounts.root(), Digest::default(), - note_created_smt_from_envelopes(created_notes.iter()).root(), + note_created_smt_from_envelopes(created_notes.iter().cloned()).root(), Digest::default(), Digest::default(), ZERO, @@ -165,28 +163,23 @@ impl MockBlockBuilder { } } -pub(crate) fn note_created_smt_from_envelopes<'a>( - note_iterator: impl Iterator -) -> SimpleSmt { - SimpleSmt::::with_leaves(note_iterator.flat_map( - |(note_idx_in_block, note)| { - let index = note_idx_in_block * 2; - [(index, note.note_id().into()), (index + 1, note.metadata().into())] - }, - )) +pub(crate) fn note_created_smt_from_envelopes( + note_iterator: impl Iterator +) -> BlockNoteTree { + BlockNoteTree::with_entries(note_iterator.map(|(batch_idx, note_idx_in_batch, note)| { + (batch_idx, note_idx_in_batch, (note.note_id().into(), *note.metadata())) + })) .unwrap() } pub(crate) fn note_created_smt_from_batches<'a>( batches: impl Iterator -) -> SimpleSmt { - let note_leaf_iterator = batches.enumerate().flat_map(|(batch_index, batch)| { - let subtree_index = batch_index * MAX_NOTES_PER_BATCH * 2; - batch.created_notes().enumerate().flat_map(move |(note_index, note)| { - let index = (subtree_index + note_index * 2) as u64; - [(index, note.note_id().into()), (index + 1, note.metadata().into())] +) -> BlockNoteTree { + let note_leaf_iterator = batches.enumerate().flat_map(|(batch_idx, batch)| { + batch.created_notes().enumerate().map(move |(note_idx_in_batch, note)| { + (batch_idx, note_idx_in_batch, (note.note_id().into(), *note.metadata())) }) }); - SimpleSmt::::with_leaves(note_leaf_iterator).unwrap() + BlockNoteTree::with_entries(note_leaf_iterator).unwrap() } diff --git a/block-producer/src/test_utils/store.rs b/block-producer/src/test_utils/store.rs index 00f26a2a1..1fbb675e4 100644 --- a/block-producer/src/test_utils/store.rs +++ b/block-producer/src/test_utils/store.rs @@ -2,9 +2,10 @@ use std::collections::BTreeSet; use async_trait::async_trait; use miden_objects::{ + block::BlockNoteTree, crypto::merkle::{Mmr, SimpleSmt, Smt, ValuePath}, notes::{NoteEnvelope, Nullifier}, - BlockHeader, ACCOUNT_TREE_DEPTH, BLOCK_OUTPUT_NOTES_TREE_DEPTH, EMPTY_WORD, ONE, ZERO, + BlockHeader, ACCOUNT_TREE_DEPTH, EMPTY_WORD, ONE, ZERO, }; use super::*; @@ -22,7 +23,7 @@ use crate::{ #[derive(Debug)] pub struct MockStoreSuccessBuilder { accounts: Option>, - notes: Option>, + notes: Option, produced_nullifiers: Option>, chain_mmr: Option, block_num: Option, @@ -68,9 +69,9 @@ impl MockStoreSuccessBuilder { } } - pub fn initial_notes<'a>( + pub fn initial_notes( mut self, - notes: impl Iterator, + notes: impl Iterator, ) -> Self { self.notes = Some(note_created_smt_from_envelopes(notes)); @@ -107,7 +108,7 @@ impl MockStoreSuccessBuilder { pub fn build(self) -> MockStoreSuccess { let block_num = self.block_num.unwrap_or(1); let accounts_smt = self.accounts.unwrap_or(SimpleSmt::new().unwrap()); - let notes_smt = self.notes.unwrap_or(SimpleSmt::new().unwrap()); + let notes_smt = self.notes.unwrap_or_default(); let chain_mmr = self.chain_mmr.unwrap_or_default(); let nullifiers_smt = self .produced_nullifiers diff --git a/proto/proto/note.proto b/proto/proto/note.proto index 5fbc0784a..fed3ea755 100644 --- a/proto/proto/note.proto +++ b/proto/proto/note.proto @@ -23,8 +23,9 @@ message NoteSyncRecord { } message NoteCreated { - uint32 note_index = 1; - digest.Digest note_id = 2; - account.AccountId sender = 3; - fixed64 tag = 4; + uint32 batch_index = 1; + uint32 note_index = 2; + digest.Digest note_id = 3; + account.AccountId sender = 4; + fixed64 tag = 5; } \ No newline at end of file diff --git a/proto/src/domain/notes.rs b/proto/src/domain/notes.rs index 8e03252e6..d27d6e95a 100644 --- a/proto/src/domain/notes.rs +++ b/proto/src/domain/notes.rs @@ -20,13 +20,14 @@ impl From for note::NoteSyncRecord { // NoteCreated // ================================================================================================ -impl From<(u64, NoteEnvelope)> for note::NoteCreated { - fn from((note_idx, note): (u64, NoteEnvelope)) -> Self { +impl From<(usize, usize, NoteEnvelope)> for note::NoteCreated { + fn from((batch_idx, note_idx, note): (usize, usize, NoteEnvelope)) -> Self { Self { + batch_index: batch_idx as u32, + note_index: note_idx as u32, note_id: Some(note.note_id().into()), sender: Some(note.metadata().sender().into()), tag: note.metadata().tag().into(), - note_index: note_idx as u32, } } } diff --git a/proto/src/generated/note.rs b/proto/src/generated/note.rs index b6c5849ed..05d5ef286 100644 --- a/proto/src/generated/note.rs +++ b/proto/src/generated/note.rs @@ -35,11 +35,13 @@ pub struct NoteSyncRecord { #[derive(Clone, PartialEq, ::prost::Message)] pub struct NoteCreated { #[prost(uint32, tag = "1")] + pub batch_index: u32, + #[prost(uint32, tag = "2")] pub note_index: u32, - #[prost(message, optional, tag = "2")] - pub note_id: ::core::option::Option, #[prost(message, optional, tag = "3")] + pub note_id: ::core::option::Option, + #[prost(message, optional, tag = "4")] pub sender: ::core::option::Option, - #[prost(fixed64, tag = "4")] + #[prost(fixed64, tag = "5")] pub tag: u64, } diff --git a/store/src/db/migrations.rs b/store/src/db/migrations.rs index 3eec2bc0b..48905976f 100644 --- a/store/src/db/migrations.rs +++ b/store/src/db/migrations.rs @@ -18,7 +18,8 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { notes ( block_num INTEGER NOT NULL, - note_index INTEGER NOT NULL, + batch_index INTEGER NOT NULL, -- Index of batch in block, starting from 0 + note_index INTEGER NOT NULL, -- Index of note in batch, starting from 0 note_hash BLOB NOT NULL, sender INTEGER NOT NULL, tag INTEGER NOT NULL, @@ -27,6 +28,7 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { PRIMARY KEY (block_num, note_index), CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers (block_num), CONSTRAINT notes_block_number_is_u32 CHECK (block_num >= 0 AND block_num < 4294967296), + CONSTRAINT notes_batch_index_is_u32 CHECK (batch_index BETWEEN 0 AND 0xFFFFFFFF) CONSTRAINT notes_note_index_is_u32 CHECK (note_index >= 0 AND note_index < 4294967296) ) STRICT, WITHOUT ROWID; diff --git a/store/src/db/mod.rs b/store/src/db/mod.rs index e6ea9ee76..493d08731 100644 --- a/store/src/db/mod.rs +++ b/store/src/db/mod.rs @@ -45,6 +45,7 @@ pub struct NullifierInfo { #[derive(Debug, Clone, PartialEq)] pub struct NoteCreated { + pub batch_index: u32, pub note_index: u32, pub note_id: RpoDigest, pub sender: AccountId, diff --git a/store/src/db/sql.rs b/store/src/db/sql.rs index 55d2ddf65..ff39c06bb 100644 --- a/store/src/db/sql.rs +++ b/store/src/db/sql.rs @@ -248,24 +248,40 @@ pub fn select_nullifiers_by_block_range( /// /// A vector with notes, or an error. pub fn select_notes(conn: &mut Connection) -> Result> { - let mut stmt = conn.prepare("SELECT * FROM notes ORDER BY block_num ASC;")?; + let mut stmt = conn.prepare( + " + SELECT + block_num, + batch_index, + note_index, + note_hash, + sender, + tag, + merkle_path + FROM + notes + ORDER BY + block_num ASC; + ", + )?; let mut rows = stmt.query([])?; let mut notes = vec![]; while let Some(row) = rows.next()? { - let note_id_data = row.get_ref(2)?.as_blob()?; + let note_id_data = row.get_ref(3)?.as_blob()?; let note_id = deserialize(note_id_data)?; - let merkle_path_data = row.get_ref(5)?.as_blob()?; + let merkle_path_data = row.get_ref(6)?.as_blob()?; let merkle_path = deserialize(merkle_path_data)?; notes.push(Note { block_num: row.get(0)?, note_created: NoteCreated { - note_index: row.get(1)?, + batch_index: row.get(1)?, + note_index: row.get(2)?, note_id, - sender: column_value_as_u64(row, 3)?, - tag: column_value_as_u64(row, 4)?, + sender: column_value_as_u64(row, 4)?, + tag: column_value_as_u64(row, 5)?, }, merkle_path, }) @@ -293,6 +309,7 @@ pub fn insert_notes( notes ( block_num, + batch_index, note_index, note_hash, sender, @@ -301,7 +318,7 @@ pub fn insert_notes( ) VALUES ( - ?1, ?2, ?3, ?4, ?5, ?6 + ?1, ?2, ?3, ?4, ?5, ?6, ?7 );", )?; @@ -309,6 +326,7 @@ pub fn insert_notes( for note in notes.iter() { count += stmt.execute(params![ note.block_num, + note.note_created.batch_index, note.note_created.note_index, note.note_created.note_id.to_bytes(), u64_to_value(note.note_created.sender), @@ -345,6 +363,7 @@ pub fn select_notes_since_block_by_tag_and_sender( " SELECT block_num, + batch_index, note_index, note_hash, sender, @@ -376,17 +395,19 @@ pub fn select_notes_since_block_by_tag_and_sender( let mut res = Vec::new(); while let Some(row) = rows.next()? { let block_num = row.get(0)?; - let note_index = row.get(1)?; - let note_id_data = row.get_ref(2)?.as_blob()?; + let batch_index = row.get(1)?; + let note_index = row.get(2)?; + let note_id_data = row.get_ref(3)?.as_blob()?; let note_id = deserialize(note_id_data)?; - let sender = column_value_as_u64(row, 3)?; - let tag = column_value_as_u64(row, 4)?; - let merkle_path_data = row.get_ref(5)?.as_blob()?; + let sender = column_value_as_u64(row, 4)?; + let tag = column_value_as_u64(row, 5)?; + let merkle_path_data = row.get_ref(6)?.as_blob()?; let merkle_path = deserialize(merkle_path_data)?; let note = Note { block_num, note_created: NoteCreated { + batch_index, note_index, note_id, sender, diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index 57af54176..ae7edcffd 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -1,10 +1,9 @@ use miden_objects::{ - crypto::{ - hash::rpo::RpoDigest, - merkle::{LeafIndex, MerklePath, SimpleSmt}, - }, - notes::{Nullifier, NOTE_LEAF_DEPTH}, - BlockHeader, Felt, FieldElement, + accounts::AccountId, + block::BlockNoteTree, + crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, + notes::{NoteMetadata, NoteType, Nullifier}, + BlockHeader, Digest, Felt, FieldElement, ZERO, }; use rusqlite::{vtab::array, Connection}; @@ -129,6 +128,7 @@ fn test_sql_select_notes() { let note = Note { block_num, note_created: NoteCreated { + batch_index: 0, note_index: i, note_id: num_to_rpo_digest(i as u64), sender: i as u64, @@ -428,20 +428,32 @@ fn test_notes() { assert!(res.is_empty()); // test insertion + let batch_index = 0u32; let note_index = 2u32; - let tag = 5; - let note_hash = num_to_rpo_digest(3); - let values = [(note_index as u64, *note_hash)]; - let notes_db = SimpleSmt::::with_leaves(values.iter().cloned()).unwrap(); - let idx = LeafIndex::::new(note_index as u64).unwrap(); - let merkle_path = MerklePath::new(notes_db.open(&idx).path.nodes().to_vec()); + let note_id = num_to_rpo_digest(3); + let tag = 5u64; + // Precomputed seed for regular off-chain account for zeroed initial seed: + let seed = [ + Felt::new(9826372627067279707), + Felt::new(8305692282416592320), + Felt::new(2014458279716538454), + Felt::new(11038932562555857644), + ]; + let sender = AccountId::new(seed, Digest::default(), Digest::default()).unwrap(); + let note_metadata = + NoteMetadata::new(sender, NoteType::OffChain, (tag as u32).into(), ZERO).unwrap(); + + let values = [(batch_index as usize, note_index as usize, (note_id, note_metadata))]; + let notes_db = BlockNoteTree::with_entries(values.iter().cloned()).unwrap(); + let merkle_path = notes_db.merkle_path(batch_index as usize, note_index as usize).unwrap(); let note = Note { block_num: block_num_1, note_created: NoteCreated { + batch_index, note_index, - note_id: num_to_rpo_digest(3), - sender: 4, + note_id, + sender: sender.into(), tag, }, merkle_path: merkle_path.clone(), @@ -482,6 +494,7 @@ fn test_notes() { let note2 = Note { block_num: block_num_2, note_created: NoteCreated { + batch_index: note.note_created.batch_index, note_index: note.note_created.note_index, note_id: num_to_rpo_digest(3), sender: note.note_created.sender, diff --git a/store/src/errors.rs b/store/src/errors.rs index 9cc54c3dd..3263333e5 100644 --- a/store/src/errors.rs +++ b/store/src/errors.rs @@ -131,7 +131,7 @@ pub enum ApplyBlockError { #[error("Block applying was broken because of closed channel on database side: {0}")] BlockApplyingBrokenBecauseOfClosedChannel(RecvError), #[error("Failed to create notes tree: {0}")] - FailedToCreateNotesTree(MerkleError), + FailedToCreateNoteTree(MerkleError), #[error("Database doesn't have any block header data")] DbBlockHeaderEmpty, #[error("Failed to get MMR peaks for forest ({forest}): {error}")] diff --git a/store/src/server/api.rs b/store/src/server/api.rs index dc682bfb4..abfbb84d4 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -207,6 +207,7 @@ impl api_server::Api for StoreApi { .into_iter() .map(|note| { Ok(NoteCreated { + batch_index: note.batch_index, note_index: note.note_index, note_id: note .note_id diff --git a/store/src/state.rs b/store/src/state.rs index 06246d958..4f9065815 100644 --- a/store/src/state.rs +++ b/store/src/state.rs @@ -7,12 +7,13 @@ use std::{mem, sync::Arc}; use miden_node_proto::{AccountInputRecord, NullifierWitness}; use miden_node_utils::formatting::{format_account_id, format_array}; use miden_objects::{ + block::BlockNoteTree, crypto::{ hash::rpo::RpoDigest, merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, SimpleSmt, SmtProof, ValuePath}, }, - notes::{NoteMetadata, NoteType, Nullifier, NOTE_LEAF_DEPTH}, - AccountError, BlockHeader, NoteError, Word, ACCOUNT_TREE_DEPTH, ZERO, + notes::{NoteMetadata, NoteType, Nullifier}, + AccountError, BlockHeader, NoteError, ACCOUNT_TREE_DEPTH, ZERO, }; use tokio::{ sync::{oneshot, Mutex, RwLock}, @@ -188,7 +189,7 @@ impl State { } // build notes tree - let note_tree = build_notes_tree(¬es)?; + let note_tree = build_note_tree(¬es)?; if note_tree.root() != block_header.note_root() { return Err(ApplyBlockError::NewBlockInvalidNoteRoot); } @@ -197,22 +198,17 @@ impl State { let notes = notes .into_iter() - .map(|note| { - // Safety: This should never happen, the note_tree is created directly form - // this list of notes - let leaf_index = LeafIndex::::new(note.note_index as u64) + .map(|note_created| { + let merkle_path = note_tree + .merkle_path( + note_created.batch_index as usize, + note_created.note_index as usize, + ) .map_err(ApplyBlockError::UnableToCreateProofForNote)?; - let merkle_path = note_tree.open(&leaf_index).path; - Ok(Note { block_num: block_header.block_num(), - note_created: NoteCreated { - note_id: note.note_id, - sender: note.sender, - note_index: note.note_index, - tag: note.tag, - }, + note_created, merkle_path, }) }) @@ -472,16 +468,15 @@ impl State { // UTILITIES // ================================================================================================ -/// Creates a [SimpleSmt] tree from the `notes`. +/// Creates a [BlockNoteTree] from the `notes`. #[instrument(target = "miden-store", skip_all)] -pub fn build_notes_tree( - notes: &[NoteCreated] -) -> Result, ApplyBlockError> { +pub fn build_note_tree(notes: &[NoteCreated]) -> Result { // TODO: create SimpleSmt without this allocation - let mut entries: Vec<(u64, Word)> = Vec::with_capacity(notes.len() * 2); + let mut entries: Vec<(usize, usize, (RpoDigest, NoteMetadata))> = + Vec::with_capacity(notes.len() * 2); for note in notes.iter() { - let note_type = NoteType::OffChain; // TODO: provide correct note type + let note_type = NoteType::OffChain; // TODO: Provide correct note type let note_metadata = NoteMetadata::new( note.sender.try_into()?, note_type, @@ -490,12 +485,14 @@ pub fn build_notes_tree( .map_err(|_| NoteError::InconsistentNoteTag(note_type, note.tag))?, ZERO, )?; - let index = note.note_index as u64; - entries.push((index, note.note_id.into())); - entries.push((index + 1, note_metadata.into())); + entries.push(( + note.batch_index as usize, + note.note_index as usize, + (note.note_id, note_metadata), + )); } - SimpleSmt::with_leaves(entries).map_err(ApplyBlockError::FailedToCreateNotesTree) + BlockNoteTree::with_entries(entries).map_err(ApplyBlockError::FailedToCreateNoteTree) } #[instrument(target = "miden-store", skip_all)] From 12701209f149638ff91371f3b0f679f874f37364 Mon Sep 17 00:00:00 2001 From: polydez <155382956+polydez@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:12:01 +0500 Subject: [PATCH 13/29] On-chain accounts protobuf & domain objects (#287) * feat: add `account_details` table to the DB * refactor: rename `block_number` column in nullifiers table to `block_num` * refactor: use `BETWEEN` in interval comparison checks * feat: implement account details protobuf messages, domain objects and conversions * feat: (WIP) implement account details support * feat: (WIP) implement account details support * feat: (WIP) implement account details support * feat: (WIP) implement account details support * fix: db creation * docs: remove TODO * refactor: apply formatting * feat: implement endpoint for getting public account details * tests: add test for storage * feat: add rpc endpoint for getting account details * refactor: keep only domain object changes * fix: compilation errors * fix: use note tag conversion from `u64` * refactor: remove account details protobuf messages * fix: remove unused error invariants * refactor: introduce `UpdatedAccount` struct * fix: rollback details conversion * fix: compilation error * format: reformat using rustfmt --- .gitignore | 9 ++- block-producer/src/batch_builder/batch.rs | 9 ++- block-producer/src/block.rs | 9 +-- block-producer/src/block_builder/mod.rs | 6 +- .../src/block_builder/prover/block_witness.rs | 41 ++++++++------ .../src/block_builder/prover/tests.rs | 7 ++- block-producer/src/block_builder/tests.rs | 12 +++- block-producer/src/errors.rs | 8 +-- block-producer/src/state_view/mod.rs | 11 +--- .../src/state_view/tests/apply_block.rs | 20 ++++++- block-producer/src/store/mod.rs | 6 +- block-producer/src/test_utils/block.rs | 23 ++++---- block-producer/src/test_utils/store.rs | 4 +- proto/src/domain/accounts.rs | 56 +++++++++++-------- proto/src/domain/blocks.rs | 6 +- proto/src/domain/digest.rs | 18 +++--- proto/src/domain/merkle.rs | 14 ++--- proto/src/domain/nullifiers.rs | 6 +- proto/src/errors.rs | 12 ++-- store/src/server/api.rs | 12 ++-- 20 files changed, 167 insertions(+), 122 deletions(-) diff --git a/.gitignore b/.gitignore index 4cc60b600..b3359a675 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,11 @@ target/ # VS Code .vscode/ +# JetBrains IDEs +.idea/ +.code/ +.fleet/ + # Genesis files /accounts genesis.dat @@ -26,12 +31,10 @@ genesis.dat *.sqlite3-wal # Docs ignore -.code -.idea site/ venv/ env/ *.out node_modules/ *DS_Store -*.iml +*.iml \ No newline at end of file diff --git a/block-producer/src/batch_builder/batch.rs b/block-producer/src/batch_builder/batch.rs index 864948cec..b5784c433 100644 --- a/block-producer/src/batch_builder/batch.rs +++ b/block-producer/src/batch_builder/batch.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; +use miden_node_proto::domain::accounts::UpdatedAccount; use miden_objects::{ accounts::AccountId, batches::BatchNoteTree, @@ -108,10 +109,14 @@ impl TransactionBatch { /// Returns an iterator over (account_id, new_state_hash) tuples for accounts that were /// modified in this transaction batch. - pub fn updated_accounts(&self) -> impl Iterator + '_ { + pub fn updated_accounts(&self) -> impl Iterator + '_ { self.updated_accounts .iter() - .map(|(account_id, account_states)| (*account_id, account_states.final_state)) + .map(|(&account_id, account_states)| UpdatedAccount { + account_id, + final_state_hash: account_states.final_state, + details: None, // TODO: In the next PR: account_states.details.clone(), + }) } /// Returns an iterator over produced nullifiers for all consumed notes. diff --git a/block-producer/src/block.rs b/block-producer/src/block.rs index cfc026aee..6e43674c5 100644 --- a/block-producer/src/block.rs +++ b/block-producer/src/block.rs @@ -1,7 +1,8 @@ use std::collections::BTreeMap; use miden_node_proto::{ - errors::{MissingFieldHelper, ParseError}, + domain::accounts::UpdatedAccount, + errors::{ConversionError, MissingFieldHelper}, generated::responses::GetBlockInputsResponse, AccountInputRecord, NullifierWitness, }; @@ -17,7 +18,7 @@ use crate::store::BlockInputsError; #[derive(Debug, Clone)] pub struct Block { pub header: BlockHeader, - pub updated_accounts: Vec<(AccountId, Digest)>, + pub updated_accounts: Vec, pub created_notes: Vec<(usize, usize, NoteEnvelope)>, pub produced_nullifiers: Vec, // TODO: @@ -94,7 +95,7 @@ impl TryFrom for BlockInputs { }; Ok((domain.account_id, witness)) }) - .collect::, ParseError>>()?; + .collect::, ConversionError>>()?; let nullifiers = get_block_inputs .nullifiers @@ -103,7 +104,7 @@ impl TryFrom for BlockInputs { let witness: NullifierWitness = entry.try_into()?; Ok((witness.nullifier, witness.proof)) }) - .collect::, ParseError>>()?; + .collect::, ConversionError>>()?; Ok(Self { block_header, diff --git a/block-producer/src/block_builder/mod.rs b/block-producer/src/block_builder/mod.rs index 615b687e3..cb0502528 100644 --- a/block-producer/src/block_builder/mod.rs +++ b/block-producer/src/block_builder/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use async_trait::async_trait; use miden_node_utils::formatting::{format_array, format_blake3_digest}; -use miden_objects::{accounts::AccountId, notes::Nullifier, Digest}; +use miden_objects::notes::Nullifier; use tracing::{debug, info, instrument}; use crate::{ @@ -77,7 +77,7 @@ where batches = %format_array(batches.iter().map(|batch| format_blake3_digest(batch.id()))), ); - let updated_accounts: Vec<(AccountId, Digest)> = + let updated_accounts: Vec<_> = batches.iter().flat_map(TransactionBatch::updated_accounts).collect(); let created_notes = batches .iter() @@ -95,7 +95,7 @@ where let block_inputs = self .store .get_block_inputs( - updated_accounts.iter().map(|(account_id, _)| account_id), + updated_accounts.iter().map(|update| &update.account_id), produced_nullifiers.iter(), ) .await?; diff --git a/block-producer/src/block_builder/prover/block_witness.rs b/block-producer/src/block_builder/prover/block_witness.rs index ba7e7f6a5..c72c84ddc 100644 --- a/block-producer/src/block_builder/prover/block_witness.rs +++ b/block-producer/src/block_builder/prover/block_witness.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; +use miden_node_proto::domain::accounts::UpdatedAccount; use miden_objects::{ accounts::AccountId, crypto::merkle::{EmptySubtreeRoots, MerklePath, MerkleStore, MmrPeaks, SmtProof}, @@ -48,23 +49,29 @@ impl BlockWitness { batches .iter() .flat_map(|batch| batch.updated_accounts()) - .map(|(account_id, final_state_hash)| { - let initial_state_hash = account_initial_states - .remove(&account_id) - .expect("already validated that key exists"); - let proof = account_merkle_proofs - .remove(&account_id) - .expect("already validated that key exists"); - - ( - account_id, - AccountUpdate { - initial_state_hash, - final_state_hash, - proof, - }, - ) - }) + .map( + |UpdatedAccount { + account_id, + final_state_hash, + .. + }| { + let initial_state_hash = account_initial_states + .remove(&account_id) + .expect("already validated that key exists"); + let proof = account_merkle_proofs + .remove(&account_id) + .expect("already validated that key exists"); + + ( + account_id, + AccountUpdate { + initial_state_hash, + final_state_hash, + proof, + }, + ) + }, + ) .collect() }; diff --git a/block-producer/src/block_builder/prover/tests.rs b/block-producer/src/block_builder/prover/tests.rs index 5c698cfa0..3869064d1 100644 --- a/block-producer/src/block_builder/prover/tests.rs +++ b/block-producer/src/block_builder/prover/tests.rs @@ -1,5 +1,6 @@ use std::{collections::BTreeMap, iter}; +use miden_node_proto::domain::accounts::UpdatedAccount; use miden_objects::{ accounts::AccountId, crypto::merkle::{ @@ -235,7 +236,11 @@ async fn test_compute_account_root_success() { account_ids .iter() .zip(account_final_states.iter()) - .map(|(&account_id, &account_hash)| (account_id, account_hash.into())) + .map(|(&account_id, &account_hash)| UpdatedAccount { + account_id, + final_state_hash: account_hash.into(), + details: None, + }) .collect(), ) .build(); diff --git a/block-producer/src/block_builder/tests.rs b/block-producer/src/block_builder/tests.rs index bb2e4b985..df3ce2f2b 100644 --- a/block-producer/src/block_builder/tests.rs +++ b/block-producer/src/block_builder/tests.rs @@ -1,9 +1,15 @@ // block builder tests (higher level) // `apply_block()` is called -use miden_objects::Felt; -use super::*; -use crate::test_utils::{MockProvenTxBuilder, MockStoreFailure, MockStoreSuccessBuilder}; +use std::sync::Arc; + +use miden_objects::{accounts::AccountId, Digest, Felt}; + +use crate::{ + batch_builder::TransactionBatch, + block_builder::{BlockBuilder, BuildBlockError, DefaultBlockBuilder}, + test_utils::{MockProvenTxBuilder, MockStoreFailure, MockStoreSuccessBuilder}, +}; /// Tests that `build_block()` succeeds when the transaction batches are not empty #[tokio::test] diff --git a/block-producer/src/errors.rs b/block-producer/src/errors.rs index e8cc22dd2..8242f2b92 100644 --- a/block-producer/src/errors.rs +++ b/block-producer/src/errors.rs @@ -1,4 +1,4 @@ -use miden_node_proto::errors::ParseError; +use miden_node_proto::errors::ConversionError; use miden_node_utils::formatting::format_opt; use miden_objects::{ accounts::AccountId, @@ -101,7 +101,7 @@ pub enum BlockProverError { #[derive(Debug, PartialEq, Eq, Error)] pub enum BlockInputsError { #[error("failed to parse protobuf message: {0}")] - ParseError(#[from] ParseError), + ConversionError(#[from] ConversionError), #[error("MmrPeaks error: {0}")] MmrPeaksError(#[from] MmrError), #[error("gRPC client failed with error: {0}")] @@ -113,8 +113,6 @@ pub enum BlockInputsError { #[derive(Debug, PartialEq, Eq, Error)] pub enum ApplyBlockError { - #[error("Merkle error: {0}")] - MerkleError(#[from] MerkleError), #[error("gRPC client failed with error: {0}")] GrpcClientError(String), } @@ -153,7 +151,7 @@ pub enum TxInputsError { #[error("malformed response from store: {0}")] MalformedResponse(String), #[error("failed to parse protobuf message: {0}")] - ParseError(#[from] ParseError), + ConversionError(#[from] ConversionError), #[error("dummy")] Dummy, } diff --git a/block-producer/src/state_view/mod.rs b/block-producer/src/state_view/mod.rs index bafbf4e2d..8af9ebff6 100644 --- a/block-producer/src/state_view/mod.rs +++ b/block-producer/src/state_view/mod.rs @@ -127,18 +127,13 @@ where let mut locked_nullifiers_in_flight = self.nullifiers_in_flight.write().await; // Remove account ids of transactions in block - let account_ids_in_block = block - .updated_accounts - .iter() - .map(|(account_id, _final_account_hash)| account_id); - - for account_id in account_ids_in_block { - let was_in_flight = locked_accounts_in_flight.remove(account_id); + for update in &block.updated_accounts { + let was_in_flight = locked_accounts_in_flight.remove(&update.account_id); debug_assert!(was_in_flight); } // Remove new nullifiers of transactions in block - for nullifier in block.produced_nullifiers.iter() { + for nullifier in &block.produced_nullifiers { let was_in_flight = locked_nullifiers_in_flight.remove(nullifier); debug_assert!(was_in_flight); } diff --git a/block-producer/src/state_view/tests/apply_block.rs b/block-producer/src/state_view/tests/apply_block.rs index ac4a52348..0db199277 100644 --- a/block-producer/src/state_view/tests/apply_block.rs +++ b/block-producer/src/state_view/tests/apply_block.rs @@ -6,6 +6,8 @@ use std::iter; +use miden_node_proto::domain::accounts::UpdatedAccount; + use super::*; use crate::test_utils::{block::MockBlockBuilder, MockStoreSuccessBuilder}; @@ -32,7 +34,11 @@ async fn test_apply_block_ab1() { .await .account_updates( std::iter::once(account) - .map(|mock_account| (mock_account.id, mock_account.states[1])) + .map(|mock_account| UpdatedAccount { + account_id: mock_account.id, + final_state_hash: mock_account.states[1], + details: None, + }) .collect(), ) .build(); @@ -75,7 +81,11 @@ async fn test_apply_block_ab2() { .account_updates( accounts_in_block .into_iter() - .map(|mock_account| (mock_account.id, mock_account.states[1])) + .map(|mock_account| UpdatedAccount { + account_id: mock_account.id, + final_state_hash: mock_account.states[1], + details: None, + }) .collect(), ) .build(); @@ -120,7 +130,11 @@ async fn test_apply_block_ab3() { accounts .clone() .into_iter() - .map(|mock_account| (mock_account.id, mock_account.states[1])) + .map(|mock_account| UpdatedAccount { + account_id: mock_account.id, + final_state_hash: mock_account.states[1], + details: None, + }) .collect(), ) .build(); diff --git a/block-producer/src/store/mod.rs b/block-producer/src/store/mod.rs index 542c47d31..b02f9fa41 100644 --- a/block-producer/src/store/mod.rs +++ b/block-producer/src/store/mod.rs @@ -6,7 +6,7 @@ use std::{ use async_trait::async_trait; use miden_node_proto::{ convert, - errors::{MissingFieldHelper, ParseError}, + errors::{ConversionError, MissingFieldHelper}, generated::{ account, digest, requests::{ApplyBlockRequest, GetBlockInputsRequest, GetTransactionInputsRequest}, @@ -83,7 +83,7 @@ impl Display for TransactionInputs { } impl TryFrom for TransactionInputs { - type Error = ParseError; + type Error = ConversionError; fn try_from(response: GetTransactionInputsResponse) -> Result { let AccountState { @@ -135,7 +135,7 @@ impl ApplyBlock for DefaultStore { ) -> Result<(), ApplyBlockError> { let request = tonic::Request::new(ApplyBlockRequest { block: Some(block.header.into()), - accounts: convert(block.updated_accounts), + accounts: convert(&block.updated_accounts), nullifiers: convert(block.produced_nullifiers), notes: convert(block.created_notes), }); diff --git a/block-producer/src/test_utils/block.rs b/block-producer/src/test_utils/block.rs index 5bfd2c00f..02c8a28cd 100644 --- a/block-producer/src/test_utils/block.rs +++ b/block-producer/src/test_utils/block.rs @@ -1,5 +1,5 @@ +use miden_node_proto::domain::accounts::UpdatedAccount; use miden_objects::{ - accounts::AccountId, block::BlockNoteTree, crypto::merkle::{Mmr, SimpleSmt}, notes::{NoteEnvelope, Nullifier}, @@ -23,12 +23,12 @@ pub async fn build_expected_block_header( let last_block_header = *store.last_block_header.read().await; // Compute new account root - let updated_accounts: Vec<(AccountId, Digest)> = + let updated_accounts: Vec<_> = batches.iter().flat_map(TransactionBatch::updated_accounts).collect(); let new_account_root = { let mut store_accounts = store.accounts.read().await.clone(); - for (account_id, new_account_state) in updated_accounts { - store_accounts.insert(account_id.into(), new_account_state.into()); + for update in updated_accounts { + store_accounts.insert(update.account_id.into(), update.final_state_hash.into()); } store_accounts.root() @@ -65,14 +65,14 @@ pub async fn build_actual_block_header( store: &MockStoreSuccess, batches: Vec, ) -> BlockHeader { - let updated_accounts: Vec<(AccountId, Digest)> = - batches.iter().flat_map(|batch| batch.updated_accounts()).collect(); + let updated_accounts: Vec<_> = + batches.iter().flat_map(TransactionBatch::updated_accounts).collect(); let produced_nullifiers: Vec = batches.iter().flat_map(|batch| batch.produced_nullifiers()).collect(); let block_inputs_from_store: BlockInputs = store .get_block_inputs( - updated_accounts.iter().map(|(account_id, _)| account_id), + updated_accounts.iter().map(|update| &update.account_id), produced_nullifiers.iter(), ) .await @@ -89,7 +89,7 @@ pub struct MockBlockBuilder { store_chain_mmr: Mmr, last_block_header: BlockHeader, - updated_accounts: Option>, + updated_accounts: Option>, created_notes: Option>, produced_nullifiers: Option>, } @@ -109,10 +109,11 @@ impl MockBlockBuilder { pub fn account_updates( mut self, - updated_accounts: Vec<(AccountId, Digest)>, + updated_accounts: Vec, ) -> Self { - for &(account_id, new_account_state) in updated_accounts.iter() { - self.store_accounts.insert(account_id.into(), new_account_state.into()); + for update in &updated_accounts { + self.store_accounts + .insert(update.account_id.into(), update.final_state_hash.into()); } self.updated_accounts = Some(updated_accounts); diff --git a/block-producer/src/test_utils/store.rs b/block-producer/src/test_utils/store.rs index 1fbb675e4..abd6cba22 100644 --- a/block-producer/src/test_utils/store.rs +++ b/block-producer/src/test_utils/store.rs @@ -181,8 +181,8 @@ impl ApplyBlock for MockStoreSuccess { let mut locked_produced_nullifiers = self.produced_nullifiers.write().await; // update accounts - for &(account_id, account_hash) in block.updated_accounts.iter() { - locked_accounts.insert(account_id.into(), account_hash.into()); + for update in &block.updated_accounts { + locked_accounts.insert(update.account_id.into(), update.final_state_hash.into()); } debug_assert_eq!(locked_accounts.root(), block.header.account_root()); diff --git a/proto/src/domain/accounts.rs b/proto/src/domain/accounts.rs index 2cc367018..9669fea75 100644 --- a/proto/src/domain/accounts.rs +++ b/proto/src/domain/accounts.rs @@ -1,12 +1,14 @@ use std::fmt::{Debug, Display, Formatter}; use miden_node_utils::formatting::format_opt; -use miden_objects::{accounts::AccountId, crypto::merkle::MerklePath, Digest}; +use miden_objects::{ + accounts::AccountId, crypto::merkle::MerklePath, transaction::AccountDetails, Digest, +}; use crate::{ - errors::{MissingFieldHelper, ParseError}, + errors::{ConversionError, MissingFieldHelper}, generated::{ - self, + account::AccountId as AccountIdPb, requests::AccountUpdate, responses::{AccountBlockInputRecord, AccountTransactionInputRecord}, }, @@ -15,7 +17,7 @@ use crate::{ // ACCOUNT ID // ================================================================================================ -impl Display for generated::account::AccountId { +impl Display for AccountIdPb { fn fmt( &self, f: &mut Formatter<'_>, @@ -24,7 +26,7 @@ impl Display for generated::account::AccountId { } } -impl Debug for generated::account::AccountId { +impl Debug for AccountIdPb { fn fmt( &self, f: &mut Formatter<'_>, @@ -36,13 +38,13 @@ impl Debug for generated::account::AccountId { // INTO PROTO ACCOUNT ID // ------------------------------------------------------------------------------------------------ -impl From for generated::account::AccountId { +impl From for AccountIdPb { fn from(value: u64) -> Self { - generated::account::AccountId { id: value } + AccountIdPb { id: value } } } -impl From for generated::account::AccountId { +impl From for AccountIdPb { fn from(account_id: AccountId) -> Self { Self { id: account_id.into(), @@ -53,28 +55,36 @@ impl From for generated::account::AccountId { // FROM PROTO ACCOUNT ID // ------------------------------------------------------------------------------------------------ -impl From for u64 { - fn from(value: generated::account::AccountId) -> Self { +impl From for u64 { + fn from(value: AccountIdPb) -> Self { value.id } } -impl TryFrom for AccountId { - type Error = ParseError; +impl TryFrom for AccountId { + type Error = ConversionError; - fn try_from(account_id: generated::account::AccountId) -> Result { - account_id.id.try_into().map_err(|_| ParseError::NotAValidFelt) + fn try_from(account_id: AccountIdPb) -> Result { + account_id.id.try_into().map_err(|_| ConversionError::NotAValidFelt) } } -// INTO ACCOUNT UPDATE +// ACCOUNT UPDATE // ================================================================================================ -impl From<(AccountId, Digest)> for AccountUpdate { - fn from((account_id, account_hash): (AccountId, Digest)) -> Self { +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UpdatedAccount { + pub account_id: AccountId, + pub final_state_hash: Digest, + pub details: Option, +} + +impl From<&UpdatedAccount> for AccountUpdate { + fn from(update: &UpdatedAccount) -> Self { Self { - account_id: Some(account_id.into()), - account_hash: Some(account_hash.into()), + account_id: Some(update.account_id.into()), + account_hash: Some(update.final_state_hash.into()), + // details: update.details.to_bytes(), } } } @@ -100,7 +110,7 @@ impl From for AccountBlockInputRecord { } impl TryFrom for AccountInputRecord { - type Error = ParseError; + type Error = ConversionError; fn try_from(account_input_record: AccountBlockInputRecord) -> Result { Ok(Self { @@ -155,7 +165,7 @@ impl From for AccountTransactionInputRecord { } impl TryFrom for AccountState { - type Error = ParseError; + type Error = ConversionError; fn try_from(from: AccountTransactionInputRecord) -> Result { let account_id = from @@ -185,7 +195,7 @@ impl TryFrom for AccountState { } impl TryFrom for AccountState { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: AccountUpdate) -> Result { Ok(Self { @@ -199,7 +209,7 @@ impl TryFrom for AccountState { } impl TryFrom<&AccountUpdate> for AccountState { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: &AccountUpdate) -> Result { value.clone().try_into() diff --git a/proto/src/domain/blocks.rs b/proto/src/domain/blocks.rs index 3916308d1..c33ace75f 100644 --- a/proto/src/domain/blocks.rs +++ b/proto/src/domain/blocks.rs @@ -1,7 +1,7 @@ use miden_objects::BlockHeader; use crate::{ - errors::{MissingFieldHelper, ParseError}, + errors::{ConversionError, MissingFieldHelper}, generated::block_header, }; @@ -28,7 +28,7 @@ impl From for block_header::BlockHeader { } impl TryFrom<&block_header::BlockHeader> for BlockHeader { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: &block_header::BlockHeader) -> Result { value.clone().try_into() @@ -36,7 +36,7 @@ impl TryFrom<&block_header::BlockHeader> for BlockHeader { } impl TryFrom for BlockHeader { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: block_header::BlockHeader) -> Result { Ok(BlockHeader::new( diff --git a/proto/src/domain/digest.rs b/proto/src/domain/digest.rs index 8b2d14a66..e9bfb00a8 100644 --- a/proto/src/domain/digest.rs +++ b/proto/src/domain/digest.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Display, Formatter}; use hex::{FromHex, ToHex}; use miden_objects::{notes::NoteId, Digest, Felt, StarkField}; -use crate::{errors::ParseError, generated::digest}; +use crate::{errors::ConversionError, generated::digest}; // CONSTANTS // ================================================================================================ @@ -62,17 +62,17 @@ impl ToHex for digest::Digest { } impl FromHex for digest::Digest { - type Error = ParseError; + type Error = ConversionError; fn from_hex>(hex: T) -> Result { let data = hex::decode(hex)?; match data.len() { - size if size < DIGEST_DATA_SIZE => Err(ParseError::InsufficientData { + size if size < DIGEST_DATA_SIZE => Err(ConversionError::InsufficientData { expected: DIGEST_DATA_SIZE, got: size, }), - size if size > DIGEST_DATA_SIZE => Err(ParseError::TooMuchData { + size if size > DIGEST_DATA_SIZE => Err(ConversionError::TooMuchData { expected: DIGEST_DATA_SIZE, got: size, }), @@ -164,14 +164,14 @@ impl From for [u64; 4] { } impl TryFrom for [Felt; 4] { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: digest::Digest) -> Result { if ![value.d0, value.d1, value.d2, value.d3] .iter() .all(|v| *v < ::MODULUS) { - Err(ParseError::NotAValidFelt) + Err(ConversionError::NotAValidFelt) } else { Ok([ Felt::new(value.d0), @@ -184,7 +184,7 @@ impl TryFrom for [Felt; 4] { } impl TryFrom for Digest { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: digest::Digest) -> Result { Ok(Self::new(value.try_into()?)) @@ -192,7 +192,7 @@ impl TryFrom for Digest { } impl TryFrom<&digest::Digest> for [Felt; 4] { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: &digest::Digest) -> Result { value.clone().try_into() @@ -200,7 +200,7 @@ impl TryFrom<&digest::Digest> for [Felt; 4] { } impl TryFrom<&digest::Digest> for Digest { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: &digest::Digest) -> Result { value.clone().try_into() diff --git a/proto/src/domain/merkle.rs b/proto/src/domain/merkle.rs index a038ef033..04d890247 100644 --- a/proto/src/domain/merkle.rs +++ b/proto/src/domain/merkle.rs @@ -5,7 +5,7 @@ use miden_objects::{ use super::{convert, try_convert}; use crate::{ - errors::{MissingFieldHelper, ParseError}, + errors::{ConversionError, MissingFieldHelper}, generated, }; @@ -20,7 +20,7 @@ impl From for generated::merkle::MerklePath { } impl TryFrom for MerklePath { - type Error = ParseError; + type Error = ConversionError; fn try_from(merkle_path: generated::merkle::MerklePath) -> Result { merkle_path.siblings.into_iter().map(Digest::try_from).collect() @@ -41,10 +41,10 @@ impl From for generated::mmr::MmrDelta { } impl TryFrom for MmrDelta { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: generated::mmr::MmrDelta) -> Result { - let data: Result, ParseError> = + let data: Result, ConversionError> = value.data.into_iter().map(Digest::try_from).collect(); Ok(MmrDelta { @@ -61,7 +61,7 @@ impl TryFrom for MmrDelta { // ------------------------------------------------------------------------------------------------ impl TryFrom for SmtLeaf { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: generated::smt::SmtLeaf) -> Result { let leaf = value.leaf.ok_or(generated::smt::SmtLeaf::missing_field(stringify!(leaf)))?; @@ -104,7 +104,7 @@ impl From for generated::smt::SmtLeaf { // ------------------------------------------------------------------------------------------------ impl TryFrom for (Digest, Word) { - type Error = ParseError; + type Error = ConversionError; fn try_from(entry: generated::smt::SmtLeafEntry) -> Result { let key: Digest = entry @@ -133,7 +133,7 @@ impl From<(Digest, Word)> for generated::smt::SmtLeafEntry { // ------------------------------------------------------------------------------------------------ impl TryFrom for SmtProof { - type Error = ParseError; + type Error = ConversionError; fn try_from(opening: generated::smt::SmtOpening) -> Result { let path: MerklePath = opening diff --git a/proto/src/domain/nullifiers.rs b/proto/src/domain/nullifiers.rs index ef0179b6e..0b3cc0d0c 100644 --- a/proto/src/domain/nullifiers.rs +++ b/proto/src/domain/nullifiers.rs @@ -4,7 +4,7 @@ use miden_objects::{ }; use crate::{ - errors::{MissingFieldHelper, ParseError}, + errors::{ConversionError, MissingFieldHelper}, generated::{digest::Digest, responses::NullifierBlockInputRecord}, }; @@ -27,7 +27,7 @@ impl From for Digest { // ================================================================================================ impl TryFrom for Nullifier { - type Error = ParseError; + type Error = ConversionError; fn try_from(value: Digest) -> Result { let digest: RpoDigest = value.try_into()?; @@ -45,7 +45,7 @@ pub struct NullifierWitness { } impl TryFrom for NullifierWitness { - type Error = ParseError; + type Error = ConversionError; fn try_from(nullifier_input_record: NullifierBlockInputRecord) -> Result { Ok(Self { diff --git a/proto/src/errors.rs b/proto/src/errors.rs index da0e29577..e8bf4c598 100644 --- a/proto/src/errors.rs +++ b/proto/src/errors.rs @@ -4,7 +4,7 @@ use miden_objects::crypto::merkle::{SmtLeafError, SmtProofError}; use thiserror::Error; #[derive(Debug, Clone, PartialEq, Error)] -pub enum ParseError { +pub enum ConversionError { #[error("Hex error: {0}")] HexError(#[from] hex::FromHexError), #[error("SMT leaf error: {0}")] @@ -15,8 +15,6 @@ pub enum ParseError { TooMuchData { expected: usize, got: usize }, #[error("Not enough data, expected {expected}, got {got}")] InsufficientData { expected: usize, got: usize }, - #[error("Number of MmrPeaks doesn't fit into memory")] - TooManyMmrPeaks, #[error("Value is not in the range 0..MODULUS")] NotAValidFelt, #[error("Field `{field_name}` required to be filled in protobuf representation of {entity}")] @@ -26,15 +24,15 @@ pub enum ParseError { }, } -impl Eq for ParseError {} +impl Eq for ConversionError {} pub trait MissingFieldHelper { - fn missing_field(field_name: &'static str) -> ParseError; + fn missing_field(field_name: &'static str) -> ConversionError; } impl MissingFieldHelper for T { - fn missing_field(field_name: &'static str) -> ParseError { - ParseError::MissingFieldInProtobufRepresentation { + fn missing_field(field_name: &'static str) -> ConversionError { + ConversionError::MissingFieldInProtobufRepresentation { entity: type_name::(), field_name, } diff --git a/store/src/server/api.rs b/store/src/server/api.rs index abfbb84d4..5e48207da 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use miden_node_proto::{ convert, - errors::ParseError, + errors::ConversionError, generated::{ self, note::NoteSyncRecord, @@ -181,7 +181,7 @@ impl api_server::Api for StoreApi { .block .ok_or(invalid_argument("Apply block missing block header"))? .try_into() - .map_err(|err: ParseError| Status::invalid_argument(err.to_string()))?; + .map_err(|err: ConversionError| Status::invalid_argument(err.to_string()))?; info!(target: COMPONENT, block_num = block_header.block_num(), block_hash = %block_header.hash()); @@ -192,7 +192,7 @@ impl api_server::Api for StoreApi { .map(|account_update| { let account_state: AccountState = account_update .try_into() - .map_err(|err: ParseError| Status::invalid_argument(err.to_string()))?; + .map_err(|err: ConversionError| Status::invalid_argument(err.to_string()))?; Ok(( account_state.account_id.into(), account_state @@ -213,7 +213,9 @@ impl api_server::Api for StoreApi { .note_id .ok_or(invalid_argument("Note missing id"))? .try_into() - .map_err(|err: ParseError| Status::invalid_argument(err.to_string()))?, + .map_err(|err: ConversionError| { + Status::invalid_argument(err.to_string()) + })?, sender: note.sender.ok_or(invalid_argument("Note missing sender"))?.into(), tag: note.tag, }) @@ -394,6 +396,6 @@ fn validate_nullifiers(nullifiers: &[generated::digest::Digest]) -> Result>() + .collect::>() .map_err(|_| invalid_argument("Digest field is not in the modulus range")) } From 9adf0cd63570b12edaca978abae9751eb2ac36e6 Mon Sep 17 00:00:00 2001 From: Martin Fraga Date: Fri, 5 Apr 2024 18:20:26 -0300 Subject: [PATCH 14/29] fix: include batch index in db constraint for notes (#302) --- store/src/db/migrations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/src/db/migrations.rs b/store/src/db/migrations.rs index 48905976f..d48b40d7e 100644 --- a/store/src/db/migrations.rs +++ b/store/src/db/migrations.rs @@ -25,7 +25,7 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { tag INTEGER NOT NULL, merkle_path BLOB NOT NULL, - PRIMARY KEY (block_num, note_index), + PRIMARY KEY (block_num, batch_index, note_index), CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers (block_num), CONSTRAINT notes_block_number_is_u32 CHECK (block_num >= 0 AND block_num < 4294967296), CONSTRAINT notes_batch_index_is_u32 CHECK (batch_index BETWEEN 0 AND 0xFFFFFFFF) From 2b609bd5199fb6beabf13f8ecc4de47dac1fddfb Mon Sep 17 00:00:00 2001 From: igamigo Date: Sat, 6 Apr 2024 01:04:00 -0300 Subject: [PATCH 15/29] fix: Send correct note index on sync response (#304) * Try fix * Remove line * Update mod.rs * Remove unused conversion * revert cargo toml --- Cargo.lock | 211 +++++++++--------- .../src/block_builder/prover/tests.rs | 31 +-- block-producer/src/block_builder/tests.rs | 9 +- block-producer/src/test_utils/account.rs | 2 +- proto/src/domain/notes.rs | 15 -- store/src/db/mod.rs | 9 + store/src/db/tests.rs | 14 +- store/src/server/api.rs | 4 +- store/src/state.rs | 2 +- 9 files changed, 146 insertions(+), 151 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b91e7717..5346f51d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -216,7 +216,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -386,7 +386,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -397,7 +397,7 @@ checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -505,7 +505,7 @@ dependencies = [ "bitflags 2.5.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools", "lazy_static", "lazycell", "proc-macro2", @@ -513,7 +513,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -694,7 +694,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -711,9 +711,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "comfy-table" -version = "7.1.0" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" +checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" dependencies = [ "crossterm", "strum", @@ -1073,9 +1073,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "a06fddc2749e0528d2813f95e050e87e52c8cbbae56223b9babf73b3e53b0cc6" dependencies = [ "cfg-if", "libc", @@ -1096,9 +1096,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1162,15 +1162,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "http" version = "0.2.12" @@ -1312,15 +1303,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -1405,13 +1387,12 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.5.0", "libc", - "redox_syscall", ] [[package]] @@ -1471,7 +1452,16 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c000ca4d908ff18ac99b93a062cb8958d331c3220719c52e77cb19cc6ac5d2c1" dependencies = [ - "logos-derive", + "logos-derive 0.13.0", +] + +[[package]] +name = "logos" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161971eb88a0da7ae0c333e1063467c5b5727e7fb6b710b8db4814eade3a42e8" +dependencies = [ + "logos-derive 0.14.0", ] [[package]] @@ -1485,7 +1475,22 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.6.29", - "syn 2.0.55", + "syn 2.0.58", +] + +[[package]] +name = "logos-codegen" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e31badd9de5131fdf4921f6473d457e3dd85b11b7f091ceb50e4df7c3eeb12a" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax 0.8.3", + "syn 2.0.58", ] [[package]] @@ -1494,7 +1499,16 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbfc0d229f1f42d790440136d941afd806bc9e949e2bcb8faa813b0f00d1267e" dependencies = [ - "logos-codegen", + "logos-codegen 0.13.0", +] + +[[package]] +name = "logos-derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2a69b3eb68d5bd595107c9ee58d7e07fe2bb5e360cc85b0f084dedac80de0a" +dependencies = [ + "logos-codegen 0.14.0", ] [[package]] @@ -1658,7 +1672,7 @@ dependencies = [ [[package]] name = "miden-lib" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#d6af2ae8ce3da19e9dcd66ce3ad81a724a8d8a7a" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=bobbon-block-note-tree#e7f22c9a348b2a878ec52efd89575a2c989bd75b" dependencies = [ "miden-assembly 0.9.1", "miden-objects 0.2.0", @@ -1693,7 +1707,7 @@ dependencies = [ "async-trait", "clap", "figment", - "itertools 0.12.1", + "itertools", "miden-air 0.9.1", "miden-node-proto 0.2.0", "miden-node-store", @@ -1826,7 +1840,7 @@ name = "miden-node-test-macro" version = "0.1.0" dependencies = [ "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1837,7 +1851,7 @@ checksum = "7dd92792c7a90a2ea6e7e7e2f1c84d675b9ba84385cbf95ce04ab1f04cf46a1f" dependencies = [ "anyhow", "figment", - "itertools 0.12.1", + "itertools", "miden-objects 0.1.1", "serde", "tracing", @@ -1850,7 +1864,7 @@ version = "0.2.0" dependencies = [ "anyhow", "figment", - "itertools 0.12.1", + "itertools", "miden-objects 0.2.0", "serde", "tracing", @@ -1876,7 +1890,7 @@ dependencies = [ [[package]] name = "miden-objects" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#d6af2ae8ce3da19e9dcd66ce3ad81a724a8d8a7a" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=bobbon-block-note-tree#e7f22c9a348b2a878ec52efd89575a2c989bd75b" dependencies = [ "miden-assembly 0.9.1", "miden-core 0.9.1", @@ -1968,7 +1982,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#d6af2ae8ce3da19e9dcd66ce3ad81a724a8d8a7a" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=bobbon-block-note-tree#e7f22c9a348b2a878ec52efd89575a2c989bd75b" dependencies = [ "miden-lib 0.2.0", "miden-objects 0.2.0", @@ -2029,7 +2043,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2077,9 +2091,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.8.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "nom" @@ -2211,7 +2225,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2296,7 +2310,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2332,14 +2346,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2372,7 +2386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" dependencies = [ "proc-macro2", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2401,7 +2415,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "version_check", "yansi", ] @@ -2428,9 +2442,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" dependencies = [ "bytes", "prost-derive", @@ -2438,13 +2452,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +checksum = "80b776a1b2dc779f5ee0641f8ade0125bc1298dd41a9a0c16d8bd57b42d222b1" dependencies = [ "bytes", - "heck 0.4.1", - "itertools 0.11.0", + "heck 0.5.0", + "itertools", "log", "multimap", "once_cell", @@ -2453,31 +2467,30 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.55", + "syn 2.0.58", "tempfile", - "which", ] [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] name = "prost-reflect" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9372e3227f3685376a0836e5c248611eafc95a0be900d44bc6cdf225b700f" +checksum = "6f5eec97d5d34bdd17ad2db2219aabf46b054c6c41bd5529767c9ce55be5898f" dependencies = [ - "logos", + "logos 0.14.0", "miette", "once_cell", "prost", @@ -2486,9 +2499,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" dependencies = [ "prost", ] @@ -2514,7 +2527,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "033b939d76d358f7c32120c86c71f515bae45e64f2bde455200356557276276c" dependencies = [ - "logos", + "logos 0.13.0", "miette", "prost-types", "thiserror", @@ -2605,9 +2618,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -2769,7 +2782,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2882,27 +2895,27 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.25.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", "rustversion", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2939,9 +2952,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -3004,7 +3017,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3100,7 +3113,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3210,7 +3223,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3265,7 +3278,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3488,7 +3501,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "wasm-bindgen-shared", ] @@ -3510,7 +3523,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3521,18 +3534,6 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - [[package]] name = "winapi" version = "0.3.9" @@ -3752,9 +3753,9 @@ dependencies = [ [[package]] name = "winter-math" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c91111b368b08c5a76009514e9b6d26af41fbb28604ea77a249282323b64d5" +checksum = "9c36d2a04b4f79f2c8c6945aab6545b7310a0cd6ae47b9210750400df6775a04" dependencies = [ "serde", "winter-utils", @@ -3839,7 +3840,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] diff --git a/block-producer/src/block_builder/prover/tests.rs b/block-producer/src/block_builder/prover/tests.rs index 3869064d1..1c1c38ab9 100644 --- a/block-producer/src/block_builder/prover/tests.rs +++ b/block-producer/src/block_builder/prover/tests.rs @@ -2,7 +2,9 @@ use std::{collections::BTreeMap, iter}; use miden_node_proto::domain::accounts::UpdatedAccount; use miden_objects::{ - accounts::AccountId, + accounts::{ + AccountId, ACCOUNT_ID_OFF_CHAIN_SENDER, ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + }, crypto::merkle::{ EmptySubtreeRoots, LeafIndex, MerklePath, Mmr, MmrPeaks, SimpleSmt, Smt, SmtLeaf, SmtProof, SMT_DEPTH, @@ -31,9 +33,9 @@ use crate::{ /// The store will contain accounts 1 & 2, while the transaction batches will contain 2 & 3. #[test] fn test_block_witness_validation_inconsistent_account_ids() { - let account_id_1 = AccountId::new_unchecked(ZERO); - let account_id_2 = AccountId::new_unchecked(ONE); - let account_id_3 = AccountId::new_unchecked(Felt::new(42)); + let account_id_1 = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER)); + let account_id_2 = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER + 1)); + let account_id_3 = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER + 2)); let block_inputs_from_store: BlockInputs = { let block_header = BlockHeader::mock(0, None, None, &[]); @@ -92,8 +94,9 @@ fn test_block_witness_validation_inconsistent_account_ids() { /// Only account 1 will have a different state hash #[test] fn test_block_witness_validation_inconsistent_account_hashes() { - let account_id_1 = AccountId::new_unchecked(ZERO); - let account_id_2 = AccountId::new_unchecked(ONE); + let account_id_1 = + AccountId::new_unchecked(Felt::new(ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN)); + let account_id_2 = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER)); let account_1_hash_store = Digest::new([Felt::new(1u64), Felt::new(2u64), Felt::new(3u64), Felt::new(4u64)]); @@ -162,11 +165,11 @@ async fn test_compute_account_root_success() { // Set up account states // --------------------------------------------------------------------------------------------- let account_ids = [ - AccountId::new_unchecked(Felt::new(0b0000_0000_0000_0000u64)), - AccountId::new_unchecked(Felt::new(0b1111_0000_0000_0000u64)), - AccountId::new_unchecked(Felt::new(0b1111_1111_0000_0000u64)), - AccountId::new_unchecked(Felt::new(0b1111_1111_1111_0000u64)), - AccountId::new_unchecked(Felt::new(0b1111_1111_1111_1111u64)), + AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER)), + AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER + 1)), + AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER + 2)), + AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER + 3)), + AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER + 4)), ]; let account_initial_states = [ @@ -373,9 +376,9 @@ async fn test_compute_note_root_empty_notes_success() { #[miden_node_test_macro::enable_logging] async fn test_compute_note_root_success() { let account_ids = [ - AccountId::new_unchecked(Felt::new(0u64)), - AccountId::new_unchecked(Felt::new(1u64)), - AccountId::new_unchecked(Felt::new(2u64)), + AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER)), + AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER + 1)), + AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER + 2)), ]; let notes_created: Vec = [ diff --git a/block-producer/src/block_builder/tests.rs b/block-producer/src/block_builder/tests.rs index df3ce2f2b..668c4e2a1 100644 --- a/block-producer/src/block_builder/tests.rs +++ b/block-producer/src/block_builder/tests.rs @@ -3,7 +3,10 @@ use std::sync::Arc; -use miden_objects::{accounts::AccountId, Digest, Felt}; +use miden_objects::{ + accounts::{AccountId, ACCOUNT_ID_OFF_CHAIN_SENDER}, + Digest, Felt, +}; use crate::{ batch_builder::TransactionBatch, @@ -15,7 +18,7 @@ use crate::{ #[tokio::test] #[miden_node_test_macro::enable_logging] async fn test_apply_block_called_nonempty_batches() { - let account_id = AccountId::new_unchecked(42u32.into()); + let account_id = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER)); let account_initial_hash: Digest = [Felt::new(1u64), Felt::new(1u64), Felt::new(1u64), Felt::new(1u64)].into(); let store = Arc::new( @@ -49,7 +52,7 @@ async fn test_apply_block_called_nonempty_batches() { #[tokio::test] #[miden_node_test_macro::enable_logging] async fn test_apply_block_called_empty_batches() { - let account_id = AccountId::new_unchecked(42u32.into()); + let account_id = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER)); let account_hash: Digest = [Felt::new(1u64), Felt::new(1u64), Felt::new(1u64), Felt::new(1u64)].into(); let store = Arc::new( diff --git a/block-producer/src/test_utils/account.rs b/block-producer/src/test_utils/account.rs index 0513d282e..009cbf958 100644 --- a/block-producer/src/test_utils/account.rs +++ b/block-producer/src/test_utils/account.rs @@ -20,7 +20,7 @@ impl MockPrivateAccount { let account_seed = get_account_seed( init_seed, miden_objects::accounts::AccountType::RegularAccountUpdatableCode, - false, + miden_objects::accounts::AccountStorageType::OffChain, Digest::default(), Digest::default(), ) diff --git a/proto/src/domain/notes.rs b/proto/src/domain/notes.rs index d27d6e95a..1ed040cef 100644 --- a/proto/src/domain/notes.rs +++ b/proto/src/domain/notes.rs @@ -2,21 +2,6 @@ use miden_objects::notes::NoteEnvelope; use crate::generated::note; -// Note -// ================================================================================================ - -impl From for note::NoteSyncRecord { - fn from(value: note::Note) -> Self { - Self { - note_index: value.note_index, - note_id: value.note_id, - sender: value.sender, - tag: value.tag, - merkle_path: value.merkle_path, - } - } -} - // NoteCreated // ================================================================================================ diff --git a/store/src/db/mod.rs b/store/src/db/mod.rs index 493d08731..e06873e39 100644 --- a/store/src/db/mod.rs +++ b/store/src/db/mod.rs @@ -2,6 +2,7 @@ use std::fs::{self, create_dir_all}; use deadpool_sqlite::{Config as SqliteConfig, Hook, HookError, Pool, Runtime}; use miden_objects::{ + block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath, utils::Deserializable}, notes::Nullifier, BlockHeader, GENESIS_BLOCK, @@ -52,6 +53,14 @@ pub struct NoteCreated { pub tag: u64, } +impl NoteCreated { + /// Returns the absolute position on the note tree based on the batch index + /// and local-to-the-subtree index + pub fn absolute_note_index(&self) -> u32 { + BlockNoteTree::note_index(self.batch_index as usize, self.note_index as usize) as u32 + } +} + #[derive(Debug, Clone, PartialEq)] pub struct Note { pub block_num: BlockNumber, diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index ae7edcffd..4658ef70c 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -1,9 +1,9 @@ use miden_objects::{ - accounts::AccountId, + accounts::{AccountId, ACCOUNT_ID_OFF_CHAIN_SENDER}, block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, notes::{NoteMetadata, NoteType, Nullifier}, - BlockHeader, Digest, Felt, FieldElement, ZERO, + BlockHeader, Felt, FieldElement, ZERO, }; use rusqlite::{vtab::array, Connection}; @@ -433,19 +433,13 @@ fn test_notes() { let note_id = num_to_rpo_digest(3); let tag = 5u64; // Precomputed seed for regular off-chain account for zeroed initial seed: - let seed = [ - Felt::new(9826372627067279707), - Felt::new(8305692282416592320), - Felt::new(2014458279716538454), - Felt::new(11038932562555857644), - ]; - let sender = AccountId::new(seed, Digest::default(), Digest::default()).unwrap(); + let sender = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER)); let note_metadata = NoteMetadata::new(sender, NoteType::OffChain, (tag as u32).into(), ZERO).unwrap(); let values = [(batch_index as usize, note_index as usize, (note_id, note_metadata))]; let notes_db = BlockNoteTree::with_entries(values.iter().cloned()).unwrap(); - let merkle_path = notes_db.merkle_path(batch_index as usize, note_index as usize).unwrap(); + let merkle_path = notes_db.get_note_path(batch_index as usize, note_index as usize).unwrap(); let note = Note { block_num: block_num_1, diff --git a/store/src/server/api.rs b/store/src/server/api.rs index 5e48207da..f0f24ac59 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -132,7 +132,7 @@ impl api_server::Api for StoreApi { .notes .into_iter() .map(|note| NoteSyncRecord { - note_index: note.note_created.note_index, + note_index: note.note_created.absolute_note_index(), note_id: Some(note.note_created.note_id.into()), sender: Some(note.note_created.sender.into()), tag: note.note_created.tag, @@ -340,7 +340,7 @@ impl api_server::Api for StoreApi { .into_iter() .map(|note| generated::note::Note { block_num: note.block_num, - note_index: note.note_created.note_index, + note_index: note.note_created.absolute_note_index(), note_id: Some(note.note_created.note_id.into()), sender: Some(note.note_created.sender.into()), tag: note.note_created.tag, diff --git a/store/src/state.rs b/store/src/state.rs index 4f9065815..85099f7f5 100644 --- a/store/src/state.rs +++ b/store/src/state.rs @@ -200,7 +200,7 @@ impl State { .into_iter() .map(|note_created| { let merkle_path = note_tree - .merkle_path( + .get_note_path( note_created.batch_index as usize, note_created.note_index as usize, ) From 3765aa6d5998c805174a164da566ede570a65c24 Mon Sep 17 00:00:00 2001 From: polydez <155382956+polydez@users.noreply.github.com> Date: Sat, 6 Apr 2024 12:46:03 +0500 Subject: [PATCH 16/29] Added support for on-chain (public) accounts in store (#293) * feat: add `account_details` table to the DB * refactor: rename `block_number` column in nullifiers table to `block_num` * refactor: use `BETWEEN` in interval comparison checks * feat: implement account details protobuf messages, domain objects and conversions * feat: (WIP) implement account details support * feat: (WIP) implement account details support * feat: (WIP) implement account details support * feat: (WIP) implement account details support * fix: db creation * docs: remove TODO * refactor: apply formatting * feat: implement endpoint for getting public account details * tests: add test for storage * feat: add rpc endpoint for getting account details * refactor: keep only domain object changes * fix: compilation errors * fix: use note tag conversion from `u64` * refactor: remove account details protobuf messages * fix: remove unused error invariants * refactor: introduce `UpdatedAccount` struct * fix: rollback details conversion * fix: compilation error * feat: account details in store * refactor: add constraint name for foreign key * refactor: small code improvement Co-authored-by: Augusto Hack * feat: account id validation * refactor: rename `get_account_details` to `select_*` * feat: return serialized account details * feat: add requirement of account id to be public in RPC * fix: remove error message used in different PR * fix: union account details with account and process them together * docs: remove `GetAccountDetails` from README.md * fix: remove unused error invariants * fix: use `Account` instead of `AccountDetails` in store * wip * feat: implement `GetAccountDetails` endpoint * docs: document `GetAccountDetails` endpoint * refactor: simplify code, make account details optional * fix: clippy warning * fix: address review comments * fix: update code to the latest miden-base * refactor: little code improvement --------- Co-authored-by: Augusto Hack --- Cargo.lock | 211 +++++++++++------------ block-producer/src/test_utils/account.rs | 9 +- proto/proto/account.proto | 11 +- proto/proto/requests.proto | 6 + proto/proto/responses.proto | 15 +- proto/proto/rpc.proto | 1 + proto/proto/store.proto | 1 + proto/src/domain/accounts.rs | 43 ++++- proto/src/generated/account.rs | 13 +- proto/src/generated/requests.rs | 9 + proto/src/generated/responses.rs | 23 ++- proto/src/generated/rpc.rs | 82 +++++++++ proto/src/generated/store.rs | 83 +++++++++ rpc/README.md | 13 ++ rpc/src/server/api.rs | 37 +++- store/README.md | 13 ++ store/src/db/migrations.rs | 17 +- store/src/db/mod.rs | 31 ++-- store/src/db/sql.rs | 161 +++++++++++------ store/src/db/tests.rs | 33 ++-- store/src/errors.rs | 12 +- store/src/server/api.rs | 46 +++-- store/src/state.rs | 17 +- 23 files changed, 649 insertions(+), 238 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5346f51d3..1014985bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -216,7 +216,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -386,7 +386,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -397,7 +397,7 @@ checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -505,7 +505,7 @@ dependencies = [ "bitflags 2.5.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "proc-macro2", @@ -513,7 +513,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -694,7 +694,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -711,9 +711,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "comfy-table" -version = "7.1.1" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" +checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" dependencies = [ "crossterm", "strum", @@ -1073,9 +1073,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.13" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06fddc2749e0528d2813f95e050e87e52c8cbbae56223b9babf73b3e53b0cc6" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -1096,9 +1096,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.26" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", @@ -1162,6 +1162,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -1303,6 +1312,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1387,12 +1405,13 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" -version = "0.1.3" +version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ "bitflags 2.5.0", "libc", + "redox_syscall", ] [[package]] @@ -1452,16 +1471,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c000ca4d908ff18ac99b93a062cb8958d331c3220719c52e77cb19cc6ac5d2c1" dependencies = [ - "logos-derive 0.13.0", -] - -[[package]] -name = "logos" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161971eb88a0da7ae0c333e1063467c5b5727e7fb6b710b8db4814eade3a42e8" -dependencies = [ - "logos-derive 0.14.0", + "logos-derive", ] [[package]] @@ -1475,22 +1485,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.6.29", - "syn 2.0.58", -] - -[[package]] -name = "logos-codegen" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e31badd9de5131fdf4921f6473d457e3dd85b11b7f091ceb50e4df7c3eeb12a" -dependencies = [ - "beef", - "fnv", - "lazy_static", - "proc-macro2", - "quote", - "regex-syntax 0.8.3", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -1499,16 +1494,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbfc0d229f1f42d790440136d941afd806bc9e949e2bcb8faa813b0f00d1267e" dependencies = [ - "logos-codegen 0.13.0", -] - -[[package]] -name = "logos-derive" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c2a69b3eb68d5bd595107c9ee58d7e07fe2bb5e360cc85b0f084dedac80de0a" -dependencies = [ - "logos-codegen 0.14.0", + "logos-codegen", ] [[package]] @@ -1672,7 +1658,7 @@ dependencies = [ [[package]] name = "miden-lib" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=bobbon-block-note-tree#e7f22c9a348b2a878ec52efd89575a2c989bd75b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#1d32f951e29c2e12526520f734aaba02555e8f79" dependencies = [ "miden-assembly 0.9.1", "miden-objects 0.2.0", @@ -1707,7 +1693,7 @@ dependencies = [ "async-trait", "clap", "figment", - "itertools", + "itertools 0.12.1", "miden-air 0.9.1", "miden-node-proto 0.2.0", "miden-node-store", @@ -1840,7 +1826,7 @@ name = "miden-node-test-macro" version = "0.1.0" dependencies = [ "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -1851,7 +1837,7 @@ checksum = "7dd92792c7a90a2ea6e7e7e2f1c84d675b9ba84385cbf95ce04ab1f04cf46a1f" dependencies = [ "anyhow", "figment", - "itertools", + "itertools 0.12.1", "miden-objects 0.1.1", "serde", "tracing", @@ -1864,7 +1850,7 @@ version = "0.2.0" dependencies = [ "anyhow", "figment", - "itertools", + "itertools 0.12.1", "miden-objects 0.2.0", "serde", "tracing", @@ -1890,7 +1876,7 @@ dependencies = [ [[package]] name = "miden-objects" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=bobbon-block-note-tree#e7f22c9a348b2a878ec52efd89575a2c989bd75b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#1d32f951e29c2e12526520f734aaba02555e8f79" dependencies = [ "miden-assembly 0.9.1", "miden-core 0.9.1", @@ -1982,7 +1968,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=bobbon-block-note-tree#e7f22c9a348b2a878ec52efd89575a2c989bd75b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#1d32f951e29c2e12526520f734aaba02555e8f79" dependencies = [ "miden-lib 0.2.0", "miden-objects 0.2.0", @@ -2043,7 +2029,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -2091,9 +2077,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.10.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "nom" @@ -2225,7 +2211,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -2310,7 +2296,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -2346,14 +2332,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2386,7 +2372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" dependencies = [ "proc-macro2", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -2415,7 +2401,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", "version_check", "yansi", ] @@ -2442,9 +2428,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.4" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", "prost-derive", @@ -2452,13 +2438,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.4" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80b776a1b2dc779f5ee0641f8ade0125bc1298dd41a9a0c16d8bd57b42d222b1" +checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", - "heck 0.5.0", - "itertools", + "heck 0.4.1", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -2467,30 +2453,31 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.58", + "syn 2.0.55", "tempfile", + "which", ] [[package]] name = "prost-derive" -version = "0.12.4" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools", + "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] name = "prost-reflect" -version = "0.13.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5eec97d5d34bdd17ad2db2219aabf46b054c6c41bd5529767c9ce55be5898f" +checksum = "9ae9372e3227f3685376a0836e5c248611eafc95a0be900d44bc6cdf225b700f" dependencies = [ - "logos 0.14.0", + "logos", "miette", "once_cell", "prost", @@ -2499,9 +2486,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.4" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ "prost", ] @@ -2527,7 +2514,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "033b939d76d358f7c32120c86c71f515bae45e64f2bde455200356557276276c" dependencies = [ - "logos 0.13.0", + "logos", "miette", "prost-types", "thiserror", @@ -2618,9 +2605,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ "getrandom", "libredox", @@ -2782,7 +2769,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -2895,27 +2882,27 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "strum" -version = "0.26.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" [[package]] name = "strum_macros" -version = "0.26.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", "rustversion", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -2952,9 +2939,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" dependencies = [ "proc-macro2", "quote", @@ -3017,7 +3004,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -3113,7 +3100,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -3223,7 +3210,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -3278,7 +3265,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] @@ -3501,7 +3488,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", "wasm-bindgen-shared", ] @@ -3523,7 +3510,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3534,6 +3521,18 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3753,9 +3752,9 @@ dependencies = [ [[package]] name = "winter-math" -version = "0.8.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c36d2a04b4f79f2c8c6945aab6545b7310a0cd6ae47b9210750400df6775a04" +checksum = "a0c91111b368b08c5a76009514e9b6d26af41fbb28604ea77a249282323b64d5" dependencies = [ "serde", "winter-utils", @@ -3840,7 +3839,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.55", ] [[package]] diff --git a/block-producer/src/test_utils/account.rs b/block-producer/src/test_utils/account.rs index 009cbf958..f375427e2 100644 --- a/block-producer/src/test_utils/account.rs +++ b/block-producer/src/test_utils/account.rs @@ -1,4 +1,7 @@ -use miden_objects::{accounts::get_account_seed, Hasher}; +use miden_objects::{ + accounts::{get_account_seed, AccountStorageType, AccountType}, + Hasher, +}; use super::*; @@ -19,8 +22,8 @@ impl MockPrivateAccount { ) -> Self { let account_seed = get_account_seed( init_seed, - miden_objects::accounts::AccountType::RegularAccountUpdatableCode, - miden_objects::accounts::AccountStorageType::OffChain, + AccountType::RegularAccountUpdatableCode, + AccountStorageType::OffChain, Digest::default(), Digest::default(), ) diff --git a/proto/proto/account.proto b/proto/proto/account.proto index 25d63bf73..ec4b5b7eb 100644 --- a/proto/proto/account.proto +++ b/proto/proto/account.proto @@ -10,8 +10,13 @@ message AccountId { fixed64 id = 1; } -message AccountInfo { - AccountId account_id = 1; +message AccountHashUpdate { + account.AccountId account_id = 1; digest.Digest account_hash = 2; - fixed32 block_num = 3; + uint32 block_num = 3; +} + +message AccountInfo { + AccountHashUpdate update = 1; + optional bytes details = 2; } diff --git a/proto/proto/requests.proto b/proto/proto/requests.proto index d872b3d58..bf0082de0 100644 --- a/proto/proto/requests.proto +++ b/proto/proto/requests.proto @@ -84,3 +84,9 @@ message ListNullifiersRequest {} message ListAccountsRequest {} message ListNotesRequest {} + +// Returns the latest state of an account with the specified ID. +message GetAccountDetailsRequest { + // Account ID to get details. + account.AccountId account_id = 1; +} diff --git a/proto/proto/responses.proto b/proto/proto/responses.proto index d7b8f487b..239d7a7a1 100644 --- a/proto/proto/responses.proto +++ b/proto/proto/responses.proto @@ -20,12 +20,6 @@ message GetBlockHeaderByNumberResponse { block_header.BlockHeader block_header = 1; } -message AccountHashUpdate { - account.AccountId account_id = 1; - digest.Digest account_hash = 2; - uint32 block_num = 3; -} - message NullifierUpdate { digest.Digest nullifier = 1; fixed32 block_num = 2; @@ -42,7 +36,7 @@ message SyncStateResponse { mmr.MmrDelta mmr_delta = 3; // a list of account hashes updated after `block_num + 1` but not after `block_header.block_num` - repeated AccountHashUpdate accounts = 5; + repeated account.AccountHashUpdate accounts = 5; // a list of all notes together with the Merkle paths from `block_header.note_root` repeated note.NoteSyncRecord notes = 6; @@ -71,7 +65,7 @@ message GetBlockInputsResponse { // Peaks of the above block's mmr, The `forest` value is equal to the block number. repeated digest.Digest mmr_peaks = 2; - // The hashes of the requested accouts and their authentication paths + // The hashes of the requested accounts and their authentication paths repeated AccountBlockInputRecord account_states = 3; // The requested nullifiers and their authentication paths @@ -113,3 +107,8 @@ message ListNotesResponse { // Lists all notes of the current chain repeated note.Note notes = 1; } + +message GetAccountDetailsResponse { + // Account info (with details for on-chain accounts) + account.AccountInfo account = 1; +} diff --git a/proto/proto/rpc.proto b/proto/proto/rpc.proto index 015b0a9d3..10a61108a 100644 --- a/proto/proto/rpc.proto +++ b/proto/proto/rpc.proto @@ -10,4 +10,5 @@ service Api { rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} rpc SyncState(requests.SyncStateRequest) returns (responses.SyncStateResponse) {} rpc SubmitProvenTransaction(requests.SubmitProvenTransactionRequest) returns (responses.SubmitProvenTransactionResponse) {} + rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} } diff --git a/proto/proto/store.proto b/proto/proto/store.proto index 4c5557755..b307f2bcc 100644 --- a/proto/proto/store.proto +++ b/proto/proto/store.proto @@ -17,4 +17,5 @@ service Api { rpc ListNullifiers(requests.ListNullifiersRequest) returns (responses.ListNullifiersResponse) {} rpc ListAccounts(requests.ListAccountsRequest) returns (responses.ListAccountsResponse) {} rpc ListNotes(requests.ListNotesRequest) returns (responses.ListNotesResponse) {} + rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} } diff --git a/proto/src/domain/accounts.rs b/proto/src/domain/accounts.rs index 9669fea75..d8a1e383a 100644 --- a/proto/src/domain/accounts.rs +++ b/proto/src/domain/accounts.rs @@ -2,13 +2,20 @@ use std::fmt::{Debug, Display, Formatter}; use miden_node_utils::formatting::format_opt; use miden_objects::{ - accounts::AccountId, crypto::merkle::MerklePath, transaction::AccountDetails, Digest, + accounts::{Account, AccountId}, + crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, + transaction::AccountDetails, + utils::Serializable, + Digest, }; use crate::{ errors::{ConversionError, MissingFieldHelper}, generated::{ - account::AccountId as AccountIdPb, + account::{ + AccountHashUpdate as AccountHashUpdatePb, AccountId as AccountIdPb, + AccountInfo as AccountInfoPb, + }, requests::AccountUpdate, responses::{AccountBlockInputRecord, AccountTransactionInputRecord}, }, @@ -72,6 +79,38 @@ impl TryFrom for AccountId { // ACCOUNT UPDATE // ================================================================================================ +#[derive(Debug, PartialEq)] +pub struct AccountHashUpdate { + pub account_id: AccountId, + pub account_hash: RpoDigest, + pub block_num: u32, +} + +impl From<&AccountHashUpdate> for AccountHashUpdatePb { + fn from(update: &AccountHashUpdate) -> Self { + Self { + account_id: Some(update.account_id.into()), + account_hash: Some(update.account_hash.into()), + block_num: update.block_num, + } + } +} + +#[derive(Debug, PartialEq)] +pub struct AccountInfo { + pub update: AccountHashUpdate, + pub details: Option, +} + +impl From<&AccountInfo> for AccountInfoPb { + fn from(AccountInfo { update, details }: &AccountInfo) -> Self { + Self { + update: Some(update.into()), + details: details.as_ref().map(|account| account.to_bytes()), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdatedAccount { pub account_id: AccountId, diff --git a/proto/src/generated/account.rs b/proto/src/generated/account.rs index 923c1d896..72fd8afbf 100644 --- a/proto/src/generated/account.rs +++ b/proto/src/generated/account.rs @@ -12,11 +12,20 @@ pub struct AccountId { #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct AccountInfo { +pub struct AccountHashUpdate { #[prost(message, optional, tag = "1")] pub account_id: ::core::option::Option, #[prost(message, optional, tag = "2")] pub account_hash: ::core::option::Option, - #[prost(fixed32, tag = "3")] + #[prost(uint32, tag = "3")] pub block_num: u32, } +#[derive(Eq, PartialOrd, Ord, Hash)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AccountInfo { + #[prost(message, optional, tag = "1")] + pub update: ::core::option::Option, + #[prost(bytes = "vec", optional, tag = "2")] + pub details: ::core::option::Option<::prost::alloc::vec::Vec>, +} diff --git a/proto/src/generated/requests.rs b/proto/src/generated/requests.rs index 799551253..7bf353f9f 100644 --- a/proto/src/generated/requests.rs +++ b/proto/src/generated/requests.rs @@ -112,3 +112,12 @@ pub struct ListAccountsRequest {} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListNotesRequest {} +/// Returns the latest state of an account with the specified ID. +#[derive(Eq, PartialOrd, Ord, Hash)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAccountDetailsRequest { + /// Account ID to get details. + #[prost(message, optional, tag = "1")] + pub account_id: ::core::option::Option, +} diff --git a/proto/src/generated/responses.rs b/proto/src/generated/responses.rs index a0d00f301..553cbbd5a 100644 --- a/proto/src/generated/responses.rs +++ b/proto/src/generated/responses.rs @@ -20,17 +20,6 @@ pub struct GetBlockHeaderByNumberResponse { #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct AccountHashUpdate { - #[prost(message, optional, tag = "1")] - pub account_id: ::core::option::Option, - #[prost(message, optional, tag = "2")] - pub account_hash: ::core::option::Option, - #[prost(uint32, tag = "3")] - pub block_num: u32, -} -#[derive(Eq, PartialOrd, Ord, Hash)] -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] pub struct NullifierUpdate { #[prost(message, optional, tag = "1")] pub nullifier: ::core::option::Option, @@ -52,7 +41,7 @@ pub struct SyncStateResponse { pub mmr_delta: ::core::option::Option, /// a list of account hashes updated after `block_num + 1` but not after `block_header.block_num` #[prost(message, repeated, tag = "5")] - pub accounts: ::prost::alloc::vec::Vec, + pub accounts: ::prost::alloc::vec::Vec, /// a list of all notes together with the Merkle paths from `block_header.note_root` #[prost(message, repeated, tag = "6")] pub notes: ::prost::alloc::vec::Vec, @@ -92,7 +81,7 @@ pub struct GetBlockInputsResponse { /// Peaks of the above block's mmr, The `forest` value is equal to the block number. #[prost(message, repeated, tag = "2")] pub mmr_peaks: ::prost::alloc::vec::Vec, - /// The hashes of the requested accouts and their authentication paths + /// The hashes of the requested accounts and their authentication paths #[prost(message, repeated, tag = "3")] pub account_states: ::prost::alloc::vec::Vec, /// The requested nullifiers and their authentication paths @@ -158,3 +147,11 @@ pub struct ListNotesResponse { #[prost(message, repeated, tag = "1")] pub notes: ::prost::alloc::vec::Vec, } +#[derive(Eq, PartialOrd, Ord, Hash)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetAccountDetailsResponse { + /// Account info (with details for on-chain accounts) + #[prost(message, optional, tag = "1")] + pub account: ::core::option::Option, +} diff --git a/proto/src/generated/rpc.rs b/proto/src/generated/rpc.rs index 75d47961e..403d5ea57 100644 --- a/proto/src/generated/rpc.rs +++ b/proto/src/generated/rpc.rs @@ -183,6 +183,32 @@ pub mod api_client { .insert(GrpcMethod::new("rpc.Api", "SubmitProvenTransaction")); self.inner.unary(req, path, codec).await } + pub async fn get_account_details( + &mut self, + request: impl tonic::IntoRequest< + super::super::requests::GetAccountDetailsRequest, + >, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/rpc.Api/GetAccountDetails", + ); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("rpc.Api", "GetAccountDetails")); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -224,6 +250,13 @@ pub mod api_server { tonic::Response, tonic::Status, >; + async fn get_account_details( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } #[derive(Debug)] pub struct ApiServer { @@ -501,6 +534,55 @@ pub mod api_server { }; Box::pin(fut) } + "/rpc.Api/GetAccountDetails" => { + #[allow(non_camel_case_types)] + struct GetAccountDetailsSvc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService< + super::super::requests::GetAccountDetailsRequest, + > for GetAccountDetailsSvc { + type Response = super::super::responses::GetAccountDetailsResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::requests::GetAccountDetailsRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_account_details(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetAccountDetailsSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } _ => { Box::pin(async move { Ok( diff --git a/proto/src/generated/store.rs b/proto/src/generated/store.rs index d2f62f986..1820970bc 100644 --- a/proto/src/generated/store.rs +++ b/proto/src/generated/store.rs @@ -299,6 +299,33 @@ pub mod api_client { req.extensions_mut().insert(GrpcMethod::new("store.Api", "ListNotes")); self.inner.unary(req, path, codec).await } + pub async fn get_account_details( + &mut self, + request: impl tonic::IntoRequest< + super::super::requests::GetAccountDetailsRequest, + >, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/store.Api/GetAccountDetails", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("store.Api", "GetAccountDetails")); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -373,6 +400,13 @@ pub mod api_server { tonic::Response, tonic::Status, >; + async fn get_account_details( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } #[derive(Debug)] pub struct ApiServer { @@ -895,6 +929,55 @@ pub mod api_server { }; Box::pin(fut) } + "/store.Api/GetAccountDetails" => { + #[allow(non_camel_case_types)] + struct GetAccountDetailsSvc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService< + super::super::requests::GetAccountDetailsRequest, + > for GetAccountDetailsSvc { + type Response = super::super::responses::GetAccountDetailsResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::requests::GetAccountDetailsRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_account_details(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetAccountDetailsSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } _ => { Box::pin(async move { Ok( diff --git a/rpc/README.md b/rpc/README.md index 04fef8444..e356c0a98 100644 --- a/rpc/README.md +++ b/rpc/README.md @@ -60,6 +60,19 @@ Retrieves block header by given block number. * `block_header`: `BlockHeader` – block header. +### GetAccountDetails + +Returns the latest state of an account with the specified ID. + +**Parameters** + +* `account_id`: `AccountId` – account ID. + +**Returns** + +* `account`: `AccountInfo` – account state information. For public accounts there is also details describing current state, stored on-chain; + for private accounts only hash of the latest known state is returned. + ### SyncState Returns info which can be used by the client to sync up to the latest state of the chain diff --git a/rpc/src/server/api.rs b/rpc/src/server/api.rs index 9b98f1dd1..60b9b3b8c 100644 --- a/rpc/src/server/api.rs +++ b/rpc/src/server/api.rs @@ -2,18 +2,19 @@ use anyhow::Result; use miden_node_proto::generated::{ block_producer::api_client as block_producer_client, requests::{ - CheckNullifiersRequest, GetBlockHeaderByNumberRequest, SubmitProvenTransactionRequest, - SyncStateRequest, + CheckNullifiersRequest, GetAccountDetailsRequest, GetBlockHeaderByNumberRequest, + SubmitProvenTransactionRequest, SyncStateRequest, }, responses::{ - CheckNullifiersResponse, GetBlockHeaderByNumberResponse, SubmitProvenTransactionResponse, - SyncStateResponse, + CheckNullifiersResponse, GetAccountDetailsResponse, GetBlockHeaderByNumberResponse, + SubmitProvenTransactionResponse, SyncStateResponse, }, rpc::api_server, store::api_client as store_client, }; use miden_objects::{ - transaction::ProvenTransaction, utils::serde::Deserializable, Digest, MIN_PROOF_SECURITY_LEVEL, + accounts::AccountId, transaction::ProvenTransaction, utils::serde::Deserializable, Digest, + MIN_PROOF_SECURITY_LEVEL, }; use miden_tx::TransactionVerifier; use tonic::{ @@ -132,4 +133,30 @@ impl api_server::Api for RpcApi { self.block_producer.clone().submit_proven_transaction(request).await } + + /// Returns details for public (on-chain) account by id. + #[instrument( + target = "miden-rpc", + name = "rpc:get_account_details", + skip_all, + ret(level = "debug"), + err + )] + async fn get_account_details( + &self, + request: Request, + ) -> std::result::Result, Status> { + debug!(target: COMPONENT, request = ?request.get_ref()); + + // Validating account using conversion: + let _account_id: AccountId = request + .get_ref() + .account_id + .clone() + .ok_or(Status::invalid_argument("account_id is missing"))? + .try_into() + .map_err(|err| Status::invalid_argument(format!("Invalid account id: {err}")))?; + + self.store.clone().get_account_details(request).await + } } diff --git a/store/README.md b/store/README.md index a3a95fc69..e0969dae7 100644 --- a/store/README.md +++ b/store/README.md @@ -106,6 +106,19 @@ Returns the data needed by the block producer to check validity of an incoming t * `account_state`: `AccountTransactionInputRecord` – account's descriptors. * `nullifiers`: `[NullifierTransactionInputRecord]` – the block numbers at which corresponding nullifiers have been consumed, zero if not consumed. +### GetAccountDetails + +Returns the latest state of an account with the specified ID. + +**Parameters** + +* `account_id`: `AccountId` – account ID. + +**Returns** + +* `account`: `AccountInfo` – account state information. For public accounts there is also details describing current state, stored on-chain; +for private accounts only hash of the latest known state is returned. + ### SyncState Returns info which can be used by the client to sync up to the latest state of the chain diff --git a/store/src/db/migrations.rs b/store/src/db/migrations.rs index d48b40d7e..1f8d1226d 100644 --- a/store/src/db/migrations.rs +++ b/store/src/db/migrations.rs @@ -11,7 +11,7 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { block_header BLOB NOT NULL, PRIMARY KEY (block_num), - CONSTRAINT block_header_block_num_is_u32 CHECK (block_num >= 0 AND block_num < 4294967296) + CONSTRAINT block_header_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF) ) STRICT, WITHOUT ROWID; CREATE TABLE @@ -27,9 +27,9 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { PRIMARY KEY (block_num, batch_index, note_index), CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers (block_num), - CONSTRAINT notes_block_number_is_u32 CHECK (block_num >= 0 AND block_num < 4294967296), + CONSTRAINT notes_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF), CONSTRAINT notes_batch_index_is_u32 CHECK (batch_index BETWEEN 0 AND 0xFFFFFFFF) - CONSTRAINT notes_note_index_is_u32 CHECK (note_index >= 0 AND note_index < 4294967296) + CONSTRAINT notes_note_index_is_u32 CHECK (note_index BETWEEN 0 AND 0xFFFFFFFF) ) STRICT, WITHOUT ROWID; CREATE TABLE @@ -38,10 +38,11 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { account_id INTEGER NOT NULL, account_hash BLOB NOT NULL, block_num INTEGER NOT NULL, + details BLOB, PRIMARY KEY (account_id), CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers (block_num), - CONSTRAINT accounts_block_num_is_u32 CHECK (block_num >= 0 AND block_num < 4294967296) + CONSTRAINT accounts_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF) ) STRICT, WITHOUT ROWID; CREATE TABLE @@ -49,13 +50,13 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { ( nullifier BLOB NOT NULL, nullifier_prefix INTEGER NOT NULL, - block_number INTEGER NOT NULL, + block_num INTEGER NOT NULL, PRIMARY KEY (nullifier), - CONSTRAINT fk_block_num FOREIGN KEY (block_number) REFERENCES block_headers (block_num), + CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers (block_num), CONSTRAINT nullifiers_nullifier_is_digest CHECK (length(nullifier) = 32), - CONSTRAINT nullifiers_nullifier_prefix_is_u16 CHECK (nullifier_prefix >= 0 AND nullifier_prefix < 65536), - CONSTRAINT nullifiers_block_number_is_u32 CHECK (block_number >= 0 AND block_number < 4294967296) + CONSTRAINT nullifiers_nullifier_prefix_is_u16 CHECK (nullifier_prefix BETWEEN 0 AND 0xFFFF), + CONSTRAINT nullifiers_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF) ) STRICT, WITHOUT ROWID; ", )]) diff --git a/store/src/db/mod.rs b/store/src/db/mod.rs index e06873e39..b448e9093 100644 --- a/store/src/db/mod.rs +++ b/store/src/db/mod.rs @@ -1,10 +1,12 @@ use std::fs::{self, create_dir_all}; use deadpool_sqlite::{Config as SqliteConfig, Hook, HookError, Pool, Runtime}; +use miden_node_proto::domain::accounts::{AccountHashUpdate, AccountInfo}; use miden_objects::{ block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath, utils::Deserializable}, notes::Nullifier, + transaction::AccountDetails, BlockHeader, GENESIS_BLOCK, }; use rusqlite::vtab::array; @@ -31,13 +33,6 @@ pub struct Db { pool: Pool, } -#[derive(Debug, PartialEq)] -pub struct AccountInfo { - pub account_id: AccountId, - pub account_hash: RpoDigest, - pub block_num: BlockNumber, -} - #[derive(Debug, PartialEq)] pub struct NullifierInfo { pub nullifier: Nullifier, @@ -73,7 +68,7 @@ pub struct StateSyncUpdate { pub notes: Vec, pub block_header: BlockHeader, pub chain_tip: BlockNumber, - pub account_updates: Vec, + pub account_updates: Vec, pub nullifiers: Vec, } @@ -209,6 +204,22 @@ impl Db { })? } + /// Loads public account details from the DB. + #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] + pub async fn select_account( + &self, + id: AccountId, + ) -> Result { + self.pool + .get() + .await? + .interact(move |conn| sql::select_account(conn, id)) + .await + .map_err(|err| { + DatabaseError::InteractError(format!("Get account details task failed: {err}")) + })? + } + #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] pub async fn get_state_sync( &self, @@ -253,7 +264,7 @@ impl Db { block_header: BlockHeader, notes: Vec, nullifiers: Vec, - accounts: Vec<(AccountId, RpoDigest)>, + accounts: Vec<(AccountId, Option, RpoDigest)>, ) -> Result<()> { self.pool .get() @@ -336,7 +347,7 @@ impl Db { let transaction = conn.transaction()?; let accounts: Vec<_> = account_smt .leaves() - .map(|(account_id, state_hash)| (account_id, state_hash.into())) + .map(|(account_id, state_hash)| (account_id, None, state_hash.into())) .collect(); sql::apply_block( &transaction, diff --git a/store/src/db/sql.rs b/store/src/db/sql.rs index ff39c06bb..2088f1382 100644 --- a/store/src/db/sql.rs +++ b/store/src/db/sql.rs @@ -1,15 +1,18 @@ //! Wrapper functions for SQL statements. use std::rc::Rc; +use miden_node_proto::domain::accounts::{AccountHashUpdate, AccountInfo}; use miden_objects::{ - crypto::hash::rpo::RpoDigest, + accounts::Account, + crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, notes::Nullifier, + transaction::AccountDetails, utils::serde::{Deserializable, Serializable}, BlockHeader, }; use rusqlite::{params, types::Value, Connection, Transaction}; -use super::{AccountInfo, Note, NoteCreated, NullifierInfo, Result, StateSyncUpdate}; +use super::{Note, NoteCreated, NullifierInfo, Result, StateSyncUpdate}; use crate::{ errors::{DatabaseError, StateSyncError}, types::{AccountId, BlockNumber}, @@ -24,21 +27,24 @@ use crate::{ /// /// A vector with accounts, or an error. pub fn select_accounts(conn: &mut Connection) -> Result> { - let mut stmt = conn.prepare("SELECT * FROM accounts ORDER BY block_num ASC;")?; + let mut stmt = conn.prepare( + " + SELECT + account_id, + account_hash, + block_num, + details + FROM + accounts + ORDER BY + block_num ASC; + ", + )?; let mut rows = stmt.query([])?; let mut accounts = vec![]; while let Some(row) = rows.next()? { - let account_hash_data = row.get_ref(1)?.as_blob()?; - let account_hash = deserialize(account_hash_data)?; - let account_id = column_value_as_u64(row, 0)?; - let block_num = row.get(2)?; - - accounts.push(AccountInfo { - account_id, - account_hash, - block_num, - }) + accounts.push(account_info_from_row(row)?) } Ok(accounts) } @@ -57,7 +63,7 @@ pub fn select_account_hashes(conn: &mut Connection) -> Result Result> { +) -> Result> { let account_ids: Vec = account_ids.iter().copied().map(u64_to_value).collect(); let mut stmt = conn.prepare( " SELECT - account_id, account_hash, block_num + account_id, + account_hash, + block_num FROM accounts WHERE @@ -98,19 +106,39 @@ pub fn select_accounts_by_block_range( let mut result = Vec::new(); while let Some(row) = rows.next()? { - let account_id = column_value_as_u64(row, 0)?; - let account_hash_data = row.get_ref(1)?.as_blob()?; - let account_hash = deserialize(account_hash_data)?; - let block_num = row.get(2)?; + result.push(account_hash_update_from_row(row)?) + } - result.push(AccountInfo { + Ok(result) +} + +/// Select the latest account details by account id from the DB using the given [Connection]. +/// +/// # Returns +/// +/// The latest account details, or an error. +pub fn select_account( + conn: &mut Connection, + account_id: AccountId, +) -> Result { + let mut stmt = conn.prepare( + " + SELECT account_id, account_hash, block_num, - }); - } + details + FROM + accounts + WHERE + account_id = ?1; + ", + )?; - Ok(result) + let mut rows = stmt.query(params![u64_to_value(account_id)])?; + let row = rows.next()?.ok_or(DatabaseError::AccountNotFoundInDb(account_id))?; + + account_info_from_row(row) } /// Inserts or updates accounts to the DB using the given [Transaction]. @@ -125,15 +153,27 @@ pub fn select_accounts_by_block_range( /// transaction. pub fn upsert_accounts( transaction: &Transaction, - accounts: &[(AccountId, RpoDigest)], + accounts: &[(AccountId, Option, RpoDigest)], block_num: BlockNumber, ) -> Result { - let mut stmt = transaction.prepare("INSERT OR REPLACE INTO accounts (account_id, account_hash, block_num) VALUES (?1, ?2, ?3);")?; + let mut stmt = transaction.prepare( + " + INSERT OR REPLACE INTO + accounts (account_id, account_hash, block_num, details) + VALUES (?1, ?2, ?3, ?4); + ", + )?; let mut count = 0; - for (account_id, account_hash) in accounts.iter() { - count += - stmt.execute(params![u64_to_value(*account_id), account_hash.to_bytes(), block_num])? + for (account_id, details, account_hash) in accounts.iter() { + // TODO: Process account details/delta (in the next PR) + + count += stmt.execute(params![ + u64_to_value(*account_id), + account_hash.to_bytes(), + block_num, + details.as_ref().map(|details| details.to_bytes()), + ])? } Ok(count) } @@ -157,7 +197,7 @@ pub fn insert_nullifiers_for_block( block_num: BlockNumber, ) -> Result { let mut stmt = transaction.prepare( - "INSERT INTO nullifiers (nullifier, nullifier_prefix, block_number) VALUES (?1, ?2, ?3);", + "INSERT INTO nullifiers (nullifier, nullifier_prefix, block_num) VALUES (?1, ?2, ?3);", )?; let mut count = 0; @@ -175,13 +215,13 @@ pub fn insert_nullifiers_for_block( /// A vector with nullifiers and the block height at which they were created, or an error. pub fn select_nullifiers(conn: &mut Connection) -> Result> { let mut stmt = - conn.prepare("SELECT nullifier, block_number FROM nullifiers ORDER BY block_number ASC;")?; + conn.prepare("SELECT nullifier, block_num FROM nullifiers ORDER BY block_num ASC;")?; let mut rows = stmt.query([])?; let mut result = vec![]; while let Some(row) = rows.next()? { let nullifier_data = row.get_ref(0)?.as_blob()?; - let nullifier = deserialize(nullifier_data)?; + let nullifier = Nullifier::read_from_bytes(nullifier_data)?; let block_number = row.get(1)?; result.push((nullifier, block_number)); } @@ -211,15 +251,15 @@ pub fn select_nullifiers_by_block_range( " SELECT nullifier, - block_number + block_num FROM nullifiers WHERE - block_number > ?1 AND - block_number <= ?2 AND + block_num > ?1 AND + block_num <= ?2 AND nullifier_prefix IN rarray(?3) ORDER BY - block_number ASC + block_num ASC ", )?; @@ -228,7 +268,7 @@ pub fn select_nullifiers_by_block_range( let mut result = Vec::new(); while let Some(row) = rows.next()? { let nullifier_data = row.get_ref(0)?.as_blob()?; - let nullifier = deserialize(nullifier_data)?; + let nullifier = Nullifier::read_from_bytes(nullifier_data)?; let block_num = row.get(1)?; result.push(NullifierInfo { nullifier, @@ -269,10 +309,10 @@ pub fn select_notes(conn: &mut Connection) -> Result> { let mut notes = vec![]; while let Some(row) = rows.next()? { let note_id_data = row.get_ref(3)?.as_blob()?; - let note_id = deserialize(note_id_data)?; + let note_id = RpoDigest::read_from_bytes(note_id_data)?; let merkle_path_data = row.get_ref(6)?.as_blob()?; - let merkle_path = deserialize(merkle_path_data)?; + let merkle_path = MerklePath::read_from_bytes(merkle_path_data)?; notes.push(Note { block_num: row.get(0)?, @@ -398,11 +438,11 @@ pub fn select_notes_since_block_by_tag_and_sender( let batch_index = row.get(1)?; let note_index = row.get(2)?; let note_id_data = row.get_ref(3)?.as_blob()?; - let note_id = deserialize(note_id_data)?; + let note_id = RpoDigest::read_from_bytes(note_id_data)?; let sender = column_value_as_u64(row, 4)?; let tag = column_value_as_u64(row, 5)?; let merkle_path_data = row.get_ref(6)?.as_blob()?; - let merkle_path = deserialize(merkle_path_data)?; + let merkle_path = MerklePath::read_from_bytes(merkle_path_data)?; let note = Note { block_num, @@ -469,7 +509,7 @@ pub fn select_block_header_by_block_num( match rows.next()? { Some(row) => { let data = row.get_ref(0)?.as_blob()?; - Ok(Some(deserialize(data)?)) + Ok(Some(BlockHeader::read_from_bytes(data)?)) }, None => Ok(None), } @@ -487,7 +527,7 @@ pub fn select_block_headers(conn: &mut Connection) -> Result> { let mut result = vec![]; while let Some(row) = rows.next()? { let block_header_data = row.get_ref(0)?.as_blob()?; - let block_header = deserialize(block_header_data)?; + let block_header = BlockHeader::read_from_bytes(block_header_data)?; result.push(block_header); } @@ -559,7 +599,7 @@ pub fn apply_block( block_header: &BlockHeader, notes: &[Note], nullifiers: &[Nullifier], - accounts: &[(AccountId, RpoDigest)], + accounts: &[(AccountId, Option, RpoDigest)], ) -> Result { let mut count = 0; count += insert_block_header(transaction, block_header)?; @@ -572,11 +612,6 @@ pub fn apply_block( // UTILITIES // ================================================================================================ -/// Decodes a blob from the database into a corresponding deserializable. -fn deserialize(data: &[u8]) -> Result { - T::read_from_bytes(data).map_err(DatabaseError::DeserializationError) -} - /// Returns the high 16 bits of the provided nullifier. pub(crate) fn get_nullifier_prefix(nullifier: &Nullifier) -> u32 { (nullifier.most_significant_felt().as_int() >> 48) as u32 @@ -609,3 +644,31 @@ fn column_value_as_u64( let value: i64 = row.get(index)?; Ok(value as u64) } + +/// Constructs `AccountHashUpdate` from the row of `accounts` table. +/// +/// Note: field ordering must be the same, as in `accounts` table! +fn account_hash_update_from_row(row: &rusqlite::Row<'_>) -> Result { + let account_id = column_value_as_u64(row, 0)?; + let account_hash_data = row.get_ref(1)?.as_blob()?; + let account_hash = RpoDigest::read_from_bytes(account_hash_data)?; + let block_num = row.get(2)?; + + Ok(AccountHashUpdate { + account_id: account_id.try_into()?, + account_hash, + block_num, + }) +} + +/// Constructs `AccountInfo` from the row of `accounts` table. +/// +/// Note: field ordering must be the same, as in `accounts` table! +fn account_info_from_row(row: &rusqlite::Row<'_>) -> Result { + let update = account_hash_update_from_row(row)?; + + let details = row.get_ref(3)?.as_bytes_or_null()?; + let details = details.map(Account::read_from_bytes).transpose()?; + + Ok(AccountInfo { update, details }) +} diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index 4658ef70c..61fc90cd8 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -1,5 +1,8 @@ +use miden_node_proto::domain::accounts::AccountHashUpdate; use miden_objects::{ - accounts::{AccountId, ACCOUNT_ID_OFF_CHAIN_SENDER}, + accounts::{ + AccountId, ACCOUNT_ID_OFF_CHAIN_SENDER, ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + }, block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, notes::{NoteMetadata, NoteType, Nullifier}, @@ -161,16 +164,21 @@ fn test_sql_select_accounts() { // test multiple entries let mut state = vec![]; for i in 0..10 { - let account_id = i; + let account_id = + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN + (i << 32) + 0b1111100000; let account_hash = num_to_rpo_digest(i); state.push(AccountInfo { - account_id, - account_hash, - block_num, + update: AccountHashUpdate { + account_id: account_id.try_into().unwrap(), + account_hash, + block_num, + }, + details: None, }); let transaction = conn.transaction().unwrap(); - let res = sql::upsert_accounts(&transaction, &[(account_id, account_hash)], block_num); + let res = + sql::upsert_accounts(&transaction, &[(account_id, None, account_hash)], block_num); assert_eq!(res.unwrap(), 1, "One element must have been inserted"); transaction.commit().unwrap(); let accounts = sql::select_accounts(&mut conn).unwrap(); @@ -375,17 +383,17 @@ fn test_db_account() { create_block(&mut conn, block_num); // test empty table - let account_ids = vec![0, 1, 2, 3, 4, 5]; + let account_ids = vec![ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, 1, 2, 3, 4, 5]; let res = sql::select_accounts_by_block_range(&mut conn, 0, u32::MAX, &account_ids).unwrap(); assert!(res.is_empty()); // test insertion - let account_id = 0; + let account_id = ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN; let account_hash = num_to_rpo_digest(0); let transaction = conn.transaction().unwrap(); let row_count = - sql::upsert_accounts(&transaction, &[(account_id, account_hash)], block_num).unwrap(); + sql::upsert_accounts(&transaction, &[(account_id, None, account_hash)], block_num).unwrap(); transaction.commit().unwrap(); assert_eq!(row_count, 1); @@ -394,10 +402,10 @@ fn test_db_account() { let res = sql::select_accounts_by_block_range(&mut conn, 0, u32::MAX, &account_ids).unwrap(); assert_eq!( res, - vec![AccountInfo { - account_id, + vec![AccountHashUpdate { + account_id: account_id.try_into().unwrap(), account_hash, - block_num + block_num, }] ); @@ -432,7 +440,6 @@ fn test_notes() { let note_index = 2u32; let note_id = num_to_rpo_digest(3); let tag = 5u64; - // Precomputed seed for regular off-chain account for zeroed initial seed: let sender = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER)); let note_metadata = NoteMetadata::new(sender, NoteType::OffChain, (tag as u32).into(), ZERO).unwrap(); diff --git a/store/src/errors.rs b/store/src/errors.rs index 3263333e5..4aa2d27fd 100644 --- a/store/src/errors.rs +++ b/store/src/errors.rs @@ -13,7 +13,7 @@ use rusqlite::types::FromSqlError; use thiserror::Error; use tokio::sync::oneshot::error::RecvError; -use crate::types::BlockNumber; +use crate::types::{AccountId, BlockNumber}; // INTERNAL ERRORS // ================================================================================================= @@ -42,12 +42,22 @@ pub enum DatabaseError { FromSqlError(#[from] FromSqlError), #[error("I/O error: {0}")] IoError(#[from] io::Error), + #[error("Account error: {0}")] + AccountError(#[from] AccountError), #[error("SQLite pool interaction task failed: {0}")] InteractError(String), #[error("Deserialization of BLOB data from database failed: {0}")] DeserializationError(DeserializationError), #[error("Block applying was broken because of closed channel on state side: {0}")] ApplyBlockFailedClosedChannel(RecvError), + #[error("Account {0} not found in the database")] + AccountNotFoundInDb(AccountId), +} + +impl From for DatabaseError { + fn from(value: DeserializationError) -> Self { + Self::DeserializationError(value) + } } // INITIALIZATION ERRORS diff --git a/store/src/server/api.rs b/store/src/server/api.rs index f0f24ac59..c4b8fa3af 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -5,15 +5,16 @@ use miden_node_proto::{ errors::ConversionError, generated::{ self, + account::AccountHashUpdate, note::NoteSyncRecord, requests::{ - ApplyBlockRequest, CheckNullifiersRequest, GetBlockHeaderByNumberRequest, - GetBlockInputsRequest, GetTransactionInputsRequest, ListAccountsRequest, - ListNotesRequest, ListNullifiersRequest, SyncStateRequest, + ApplyBlockRequest, CheckNullifiersRequest, GetAccountDetailsRequest, + GetBlockHeaderByNumberRequest, GetBlockInputsRequest, GetTransactionInputsRequest, + ListAccountsRequest, ListNotesRequest, ListNullifiersRequest, SyncStateRequest, }, responses::{ - AccountHashUpdate, AccountTransactionInputRecord, ApplyBlockResponse, - CheckNullifiersResponse, GetBlockHeaderByNumberResponse, GetBlockInputsResponse, + AccountTransactionInputRecord, ApplyBlockResponse, CheckNullifiersResponse, + GetAccountDetailsResponse, GetBlockHeaderByNumberResponse, GetBlockInputsResponse, GetTransactionInputsResponse, ListAccountsResponse, ListNotesResponse, ListNullifiersResponse, NullifierTransactionInputRecord, NullifierUpdate, SyncStateResponse, @@ -159,6 +160,32 @@ impl api_server::Api for StoreApi { })) } + /// Returns details for public (on-chain) account by id. + #[instrument( + target = "miden-store", + name = "store:get_account_details", + skip_all, + ret(level = "debug"), + err + )] + async fn get_account_details( + &self, + request: tonic::Request, + ) -> Result, Status> { + let request = request.into_inner(); + let account_info = self + .state + .get_account_details( + request.account_id.ok_or(invalid_argument("Account missing id"))?.into(), + ) + .await + .map_err(internal_error)?; + + Ok(Response::new(GetAccountDetailsResponse { + account: Some((&account_info).into()), + })) + } + // BLOCK PRODUCER ENDPOINTS // -------------------------------------------------------------------------------------------- @@ -195,6 +222,7 @@ impl api_server::Api for StoreApi { .map_err(|err: ConversionError| Status::invalid_argument(err.to_string()))?; Ok(( account_state.account_id.into(), + None, // TODO: Process account details (next PR) account_state .account_hash .ok_or(invalid_argument("Account update missing account hash"))?, @@ -367,12 +395,8 @@ impl api_server::Api for StoreApi { .list_accounts() .await .map_err(internal_error)? - .into_iter() - .map(|account_info| generated::account::AccountInfo { - account_id: Some(account_info.account_id.into()), - account_hash: Some(account_info.account_hash.into()), - block_num: account_info.block_num, - }) + .iter() + .map(Into::into) .collect(); Ok(Response::new(ListAccountsResponse { accounts })) } diff --git a/store/src/state.rs b/store/src/state.rs index 85099f7f5..aa18ccc71 100644 --- a/store/src/state.rs +++ b/store/src/state.rs @@ -4,7 +4,7 @@ //! data is atomically written, and that reads are consistent. use std::{mem, sync::Arc}; -use miden_node_proto::{AccountInputRecord, NullifierWitness}; +use miden_node_proto::{domain::accounts::AccountInfo, AccountInputRecord, NullifierWitness}; use miden_node_utils::formatting::{format_account_id, format_array}; use miden_objects::{ block::BlockNoteTree, @@ -13,6 +13,7 @@ use miden_objects::{ merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, SimpleSmt, SmtProof, ValuePath}, }, notes::{NoteMetadata, NoteType, Nullifier}, + transaction::AccountDetails, AccountError, BlockHeader, NoteError, ACCOUNT_TREE_DEPTH, ZERO, }; use tokio::{ @@ -22,7 +23,7 @@ use tokio::{ use tracing::{error, info, info_span, instrument}; use crate::{ - db::{AccountInfo, Db, Note, NoteCreated, NullifierInfo, StateSyncUpdate}, + db::{Db, Note, NoteCreated, NullifierInfo, StateSyncUpdate}, errors::{ ApplyBlockError, DatabaseError, GetBlockInputsError, StateInitializationError, StateSyncError, @@ -106,7 +107,7 @@ impl State { &self, block_header: BlockHeader, nullifiers: Vec, - accounts: Vec<(AccountId, RpoDigest)>, + accounts: Vec<(AccountId, Option, RpoDigest)>, notes: Vec, ) -> Result<(), ApplyBlockError> { let _ = self.writer.try_lock().map_err(|_| ApplyBlockError::ConcurrentWrite)?; @@ -180,7 +181,7 @@ impl State { // update account tree let mut account_tree = inner.account_tree.clone(); - for (account_id, account_hash) in accounts.iter() { + for (account_id, _details, account_hash) in accounts.iter() { account_tree.insert(LeafIndex::new_max_depth(*account_id), account_hash.into()); } @@ -463,6 +464,14 @@ impl State { pub async fn list_notes(&self) -> Result, DatabaseError> { self.db.select_notes().await } + + /// Returns details for public (on-chain) account. + pub async fn get_account_details( + &self, + id: AccountId, + ) -> Result { + self.db.select_account(id).await + } } // UTILITIES From 2a32cc876e7f25b74b9122c5946ecec64577f161 Mon Sep 17 00:00:00 2001 From: Paul-Henry Kajfasz <42912740+phklive@users.noreply.github.com> Date: Mon, 8 Apr 2024 20:51:23 +0100 Subject: [PATCH 17/29] Implement `get_notes_by_id` endpoint (#298) * Getting notes by id works * lint and test * Improved comments + Added conversion from digest::Digest to NoteId * Added rpc validation for NoteId's * Added documentation to Readmes * Lint * Fixed ci problems * Added test for endpoint + refactored sql query + improved documentation * Fixed lint * Order rpc and store endpoints alphabitically * Improved documentation with gh comments --- Cargo.lock | 217 +++++++++++++------------- proto/proto/note.proto | 2 +- proto/proto/requests.proto | 5 + proto/proto/responses.proto | 5 + proto/proto/rpc.proto | 1 + proto/proto/store.proto | 1 + proto/src/generated/account.rs | 1 + proto/src/generated/block_header.rs | 1 + proto/src/generated/block_producer.rs | 1 + proto/src/generated/digest.rs | 1 + proto/src/generated/merkle.rs | 1 + proto/src/generated/mmr.rs | 1 + proto/src/generated/note.rs | 1 + proto/src/generated/requests.rs | 9 ++ proto/src/generated/responses.rs | 9 ++ proto/src/generated/rpc.rs | 79 ++++++++++ proto/src/generated/smt.rs | 1 + proto/src/generated/store.rs | 79 ++++++++++ rpc/README.md | 57 ++++--- rpc/src/server/api.rs | 51 ++++-- store/README.md | 91 ++++++----- store/src/db/mod.rs | 18 ++- store/src/db/sql.rs | 56 ++++++- store/src/db/tests.rs | 11 +- store/src/server/api.rs | 61 +++++++- store/src/state.rs | 13 +- 26 files changed, 579 insertions(+), 194 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1014985bf..67ce1e165 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -216,7 +216,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -386,7 +386,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -397,7 +397,7 @@ checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -505,7 +505,7 @@ dependencies = [ "bitflags 2.5.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools", "lazy_static", "lazycell", "proc-macro2", @@ -513,7 +513,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -588,9 +588,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" @@ -615,9 +615,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.90" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" dependencies = [ "jobserver", "libc", @@ -694,7 +694,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -711,9 +711,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "comfy-table" -version = "7.1.0" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686" +checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" dependencies = [ "crossterm", "strum", @@ -1073,9 +1073,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", @@ -1096,9 +1096,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1162,15 +1162,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "http" version = "0.2.12" @@ -1312,15 +1303,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -1405,13 +1387,12 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.5.0", "libc", - "redox_syscall", ] [[package]] @@ -1471,7 +1452,16 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c000ca4d908ff18ac99b93a062cb8958d331c3220719c52e77cb19cc6ac5d2c1" dependencies = [ - "logos-derive", + "logos-derive 0.13.0", +] + +[[package]] +name = "logos" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161971eb88a0da7ae0c333e1063467c5b5727e7fb6b710b8db4814eade3a42e8" +dependencies = [ + "logos-derive 0.14.0", ] [[package]] @@ -1485,7 +1475,22 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.6.29", - "syn 2.0.55", + "syn 2.0.58", +] + +[[package]] +name = "logos-codegen" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e31badd9de5131fdf4921f6473d457e3dd85b11b7f091ceb50e4df7c3eeb12a" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax 0.8.3", + "syn 2.0.58", ] [[package]] @@ -1494,7 +1499,16 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbfc0d229f1f42d790440136d941afd806bc9e949e2bcb8faa813b0f00d1267e" dependencies = [ - "logos-codegen", + "logos-codegen 0.13.0", +] + +[[package]] +name = "logos-derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2a69b3eb68d5bd595107c9ee58d7e07fe2bb5e360cc85b0f084dedac80de0a" +dependencies = [ + "logos-codegen 0.14.0", ] [[package]] @@ -1693,7 +1707,7 @@ dependencies = [ "async-trait", "clap", "figment", - "itertools 0.12.1", + "itertools", "miden-air 0.9.1", "miden-node-proto 0.2.0", "miden-node-store", @@ -1826,7 +1840,7 @@ name = "miden-node-test-macro" version = "0.1.0" dependencies = [ "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -1837,7 +1851,7 @@ checksum = "7dd92792c7a90a2ea6e7e7e2f1c84d675b9ba84385cbf95ce04ab1f04cf46a1f" dependencies = [ "anyhow", "figment", - "itertools 0.12.1", + "itertools", "miden-objects 0.1.1", "serde", "tracing", @@ -1850,7 +1864,7 @@ version = "0.2.0" dependencies = [ "anyhow", "figment", - "itertools 0.12.1", + "itertools", "miden-objects 0.2.0", "serde", "tracing", @@ -2029,7 +2043,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2077,9 +2091,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.8.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] name = "nom" @@ -2211,7 +2225,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2296,7 +2310,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2332,14 +2346,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2372,7 +2386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" dependencies = [ "proc-macro2", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2401,7 +2415,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "version_check", "yansi", ] @@ -2428,9 +2442,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" dependencies = [ "bytes", "prost-derive", @@ -2438,13 +2452,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +checksum = "80b776a1b2dc779f5ee0641f8ade0125bc1298dd41a9a0c16d8bd57b42d222b1" dependencies = [ "bytes", - "heck 0.4.1", - "itertools 0.11.0", + "heck 0.5.0", + "itertools", "log", "multimap", "once_cell", @@ -2453,31 +2467,30 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.55", + "syn 2.0.58", "tempfile", - "which", ] [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] name = "prost-reflect" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9372e3227f3685376a0836e5c248611eafc95a0be900d44bc6cdf225b700f" +checksum = "6f5eec97d5d34bdd17ad2db2219aabf46b054c6c41bd5529767c9ce55be5898f" dependencies = [ - "logos", + "logos 0.14.0", "miette", "once_cell", "prost", @@ -2486,9 +2499,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" dependencies = [ "prost", ] @@ -2514,7 +2527,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "033b939d76d358f7c32120c86c71f515bae45e64f2bde455200356557276276c" dependencies = [ - "logos", + "logos 0.13.0", "miette", "prost-types", "thiserror", @@ -2605,9 +2618,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -2718,9 +2731,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "rusty-fork" @@ -2769,7 +2782,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2882,27 +2895,27 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.25.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", "rustversion", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -2939,9 +2952,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -3004,7 +3017,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3100,7 +3113,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3210,7 +3223,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3265,7 +3278,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] @@ -3488,7 +3501,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "wasm-bindgen-shared", ] @@ -3510,7 +3523,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3521,18 +3534,6 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix", -] - [[package]] name = "winapi" version = "0.3.9" @@ -3752,9 +3753,9 @@ dependencies = [ [[package]] name = "winter-math" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c91111b368b08c5a76009514e9b6d26af41fbb28604ea77a249282323b64d5" +checksum = "9c36d2a04b4f79f2c8c6945aab6545b7310a0cd6ae47b9210750400df6775a04" dependencies = [ "serde", "winter-utils", @@ -3839,7 +3840,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.58", ] [[package]] diff --git a/proto/proto/note.proto b/proto/proto/note.proto index fed3ea755..4bab3eefe 100644 --- a/proto/proto/note.proto +++ b/proto/proto/note.proto @@ -28,4 +28,4 @@ message NoteCreated { digest.Digest note_id = 3; account.AccountId sender = 4; fixed64 tag = 5; -} \ No newline at end of file +} diff --git a/proto/proto/requests.proto b/proto/proto/requests.proto index bf0082de0..529fc92fe 100644 --- a/proto/proto/requests.proto +++ b/proto/proto/requests.proto @@ -79,6 +79,11 @@ message SubmitProvenTransactionRequest { bytes transaction = 1; } +message GetNotesByIdRequest { + // List of NoteId's to be queried from the database + repeated digest.Digest note_ids = 1; +} + message ListNullifiersRequest {} message ListAccountsRequest {} diff --git a/proto/proto/responses.proto b/proto/proto/responses.proto index 239d7a7a1..27913a34f 100644 --- a/proto/proto/responses.proto +++ b/proto/proto/responses.proto @@ -93,6 +93,11 @@ message GetTransactionInputsResponse { message SubmitProvenTransactionResponse {} +message GetNotesByIdResponse { + // Lists Note's returned by the database + repeated note.Note notes = 1; +} + message ListNullifiersResponse { // Lists all nullifiers of the current chain repeated smt.SmtLeafEntry nullifiers = 1; diff --git a/proto/proto/rpc.proto b/proto/proto/rpc.proto index 10a61108a..b1caf2dc2 100644 --- a/proto/proto/rpc.proto +++ b/proto/proto/rpc.proto @@ -8,6 +8,7 @@ import "responses.proto"; service Api { rpc CheckNullifiers(requests.CheckNullifiersRequest) returns (responses.CheckNullifiersResponse) {} rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} + rpc GetNotesById(requests.GetNotesByIdRequest) returns (responses.GetNotesByIdResponse) {} rpc SyncState(requests.SyncStateRequest) returns (responses.SyncStateResponse) {} rpc SubmitProvenTransaction(requests.SubmitProvenTransactionRequest) returns (responses.SubmitProvenTransactionResponse) {} rpc GetAccountDetails(requests.GetAccountDetailsRequest) returns (responses.GetAccountDetailsResponse) {} diff --git a/proto/proto/store.proto b/proto/proto/store.proto index b307f2bcc..112e3cfe2 100644 --- a/proto/proto/store.proto +++ b/proto/proto/store.proto @@ -12,6 +12,7 @@ service Api { rpc CheckNullifiers(requests.CheckNullifiersRequest) returns (responses.CheckNullifiersResponse) {} rpc GetBlockHeaderByNumber(requests.GetBlockHeaderByNumberRequest) returns (responses.GetBlockHeaderByNumberResponse) {} rpc GetBlockInputs(requests.GetBlockInputsRequest) returns (responses.GetBlockInputsResponse) {} + rpc GetNotesById(requests.GetNotesByIdRequest) returns (responses.GetNotesByIdResponse) {} rpc GetTransactionInputs(requests.GetTransactionInputsRequest) returns (responses.GetTransactionInputsResponse) {} rpc SyncState(requests.SyncStateRequest) returns (responses.SyncStateResponse) {} rpc ListNullifiers(requests.ListNullifiersRequest) returns (responses.ListNullifiersResponse) {} diff --git a/proto/src/generated/account.rs b/proto/src/generated/account.rs index 72fd8afbf..44ff6017b 100644 --- a/proto/src/generated/account.rs +++ b/proto/src/generated/account.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/proto/src/generated/block_header.rs b/proto/src/generated/block_header.rs index 6ffc0c571..040dda511 100644 --- a/proto/src/generated/block_header.rs +++ b/proto/src/generated/block_header.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/proto/src/generated/block_producer.rs b/proto/src/generated/block_producer.rs index 52b17d119..2aabecafe 100644 --- a/proto/src/generated/block_producer.rs +++ b/proto/src/generated/block_producer.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. /// Generated client implementations. pub mod api_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] diff --git a/proto/src/generated/digest.rs b/proto/src/generated/digest.rs index f120dc152..fe45edf66 100644 --- a/proto/src/generated/digest.rs +++ b/proto/src/generated/digest.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. /// A hash digest, the result of a hash function. #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/proto/src/generated/merkle.rs b/proto/src/generated/merkle.rs index 4cf424f6f..ced20e747 100644 --- a/proto/src/generated/merkle.rs +++ b/proto/src/generated/merkle.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/proto/src/generated/mmr.rs b/proto/src/generated/mmr.rs index 388cf8335..e477ef2f4 100644 --- a/proto/src/generated/mmr.rs +++ b/proto/src/generated/mmr.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/proto/src/generated/note.rs b/proto/src/generated/note.rs index 05d5ef286..172755108 100644 --- a/proto/src/generated/note.rs +++ b/proto/src/generated/note.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/proto/src/generated/requests.rs b/proto/src/generated/requests.rs index 7bf353f9f..c39110684 100644 --- a/proto/src/generated/requests.rs +++ b/proto/src/generated/requests.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -103,6 +104,14 @@ pub struct SubmitProvenTransactionRequest { #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetNotesByIdRequest { + /// List of NoteId's to be queried from the database + #[prost(message, repeated, tag = "1")] + pub note_ids: ::prost::alloc::vec::Vec, +} +#[derive(Eq, PartialOrd, Ord, Hash)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct ListNullifiersRequest {} #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/proto/src/generated/responses.rs b/proto/src/generated/responses.rs index 553cbbd5a..c16613513 100644 --- a/proto/src/generated/responses.rs +++ b/proto/src/generated/responses.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -126,6 +127,14 @@ pub struct SubmitProvenTransactionResponse {} #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetNotesByIdResponse { + /// Lists Note's returned by the database + #[prost(message, repeated, tag = "1")] + pub notes: ::prost::alloc::vec::Vec, +} +#[derive(Eq, PartialOrd, Ord, Hash)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct ListNullifiersResponse { /// Lists all nullifiers of the current chain #[prost(message, repeated, tag = "1")] diff --git a/proto/src/generated/rpc.rs b/proto/src/generated/rpc.rs index 403d5ea57..e2948e9f0 100644 --- a/proto/src/generated/rpc.rs +++ b/proto/src/generated/rpc.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. /// Generated client implementations. pub mod api_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] @@ -134,6 +135,28 @@ pub mod api_client { .insert(GrpcMethod::new("rpc.Api", "GetBlockHeaderByNumber")); self.inner.unary(req, path, codec).await } + pub async fn get_notes_by_id( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/rpc.Api/GetNotesById"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("rpc.Api", "GetNotesById")); + self.inner.unary(req, path, codec).await + } pub async fn sync_state( &mut self, request: impl tonic::IntoRequest, @@ -234,6 +257,13 @@ pub mod api_server { tonic::Response, tonic::Status, >; + async fn get_notes_by_id( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; async fn sync_state( &self, request: tonic::Request, @@ -436,6 +466,55 @@ pub mod api_server { }; Box::pin(fut) } + "/rpc.Api/GetNotesById" => { + #[allow(non_camel_case_types)] + struct GetNotesByIdSvc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService< + super::super::requests::GetNotesByIdRequest, + > for GetNotesByIdSvc { + type Response = super::super::responses::GetNotesByIdResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::requests::GetNotesByIdRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_notes_by_id(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetNotesByIdSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/rpc.Api/SyncState" => { #[allow(non_camel_case_types)] struct SyncStateSvc(pub Arc); diff --git a/proto/src/generated/smt.rs b/proto/src/generated/smt.rs index a244915a7..82c78fba3 100644 --- a/proto/src/generated/smt.rs +++ b/proto/src/generated/smt.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. /// An entry in a leaf. #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/proto/src/generated/store.rs b/proto/src/generated/store.rs index 1820970bc..b41fb39d7 100644 --- a/proto/src/generated/store.rs +++ b/proto/src/generated/store.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. /// Generated client implementations. pub mod api_client { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] @@ -182,6 +183,28 @@ pub mod api_client { req.extensions_mut().insert(GrpcMethod::new("store.Api", "GetBlockInputs")); self.inner.unary(req, path, codec).await } + pub async fn get_notes_by_id( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/store.Api/GetNotesById"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("store.Api", "GetNotesById")); + self.inner.unary(req, path, codec).await + } pub async fn get_transaction_inputs( &mut self, request: impl tonic::IntoRequest< @@ -365,6 +388,13 @@ pub mod api_server { tonic::Response, tonic::Status, >; + async fn get_notes_by_id( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; async fn get_transaction_inputs( &self, request: tonic::Request, @@ -684,6 +714,55 @@ pub mod api_server { }; Box::pin(fut) } + "/store.Api/GetNotesById" => { + #[allow(non_camel_case_types)] + struct GetNotesByIdSvc(pub Arc); + impl< + T: Api, + > tonic::server::UnaryService< + super::super::requests::GetNotesByIdRequest, + > for GetNotesByIdSvc { + type Response = super::super::responses::GetNotesByIdResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::super::requests::GetNotesByIdRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_notes_by_id(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = GetNotesByIdSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/store.Api/GetTransactionInputs" => { #[allow(non_camel_case_types)] struct GetTransactionInputsSvc(pub Arc); diff --git a/rpc/README.md b/rpc/README.md index e356c0a98..28bfdf9cf 100644 --- a/rpc/README.md +++ b/rpc/README.md @@ -1,7 +1,7 @@ # Miden node RPC -The **RPC** is an externally-facing component through which clients can interact with the node. It receives client requests -(e.g., to synchronize with the latest state of the chain, or to submit transactions), performs basic validation, +The **RPC** is an externally-facing component through which clients can interact with the node. It receives client requests +(e.g., to synchronize with the latest state of the chain, or to submit transactions), performs basic validation, and forwards the requests to the appropriate components. **RPC** is one of components of the [Miden node](..). @@ -33,7 +33,7 @@ miden-node-rpc serve --config ## API -The **RPC** serves connections using the [gRPC protocol](https://grpc.io) on a port, set in the previously mentioned configuration file. +The **RPC** serves connections using the [gRPC protocol](https://grpc.io) on a port, set in the previously mentioned configuration file. Here is a brief description of supported methods. ### CheckNullifiers @@ -42,11 +42,11 @@ Gets a list of proofs for given nullifier hashes, each proof as a sparse Merkle **Parameters:** -* `nullifiers`: `[Digest]` – array of nullifier hashes. +- `nullifiers`: `[Digest]` – array of nullifier hashes. **Returns:** -* `proofs`: `[NullifierProof]` – array of nullifier proofs, positions correspond to the ones in request. +- `proofs`: `[NullifierProof]` – array of nullifier proofs, positions correspond to the ones in request. ### GetBlockHeaderByNumber @@ -54,11 +54,23 @@ Retrieves block header by given block number. **Parameters** -* `block_num`: `uint32` *(optional)* – the block number of the target block. If not provided, the latest known block will be returned. +- `block_num`: `uint32` _(optional)_ – the block number of the target block. If not provided, the latest known block will be returned. **Returns:** -* `block_header`: `BlockHeader` – block header. +- `block_header`: `BlockHeader` – block header. + +### GetNotesById + +Returns a list of notes matching the provided note IDs. + +**Parameters** + +- `note_ids`: `[NoteId]` - list of IDs of the notes we want to query. + +**Returns** + +- `notes`: `[Note]` - List of notes matching the list of requested NoteIds. ### GetAccountDetails @@ -66,11 +78,11 @@ Returns the latest state of an account with the specified ID. **Parameters** -* `account_id`: `AccountId` – account ID. +- `account_id`: `AccountId` – account ID. **Returns** -* `account`: `AccountInfo` – account state information. For public accounts there is also details describing current state, stored on-chain; +- `account`: `AccountInfo` – account state information. For public accounts there is also details describing current state, stored on-chain; for private accounts only hash of the latest known state is returned. ### SyncState @@ -78,10 +90,10 @@ Returns the latest state of an account with the specified ID. Returns info which can be used by the client to sync up to the latest state of the chain for the objects (accounts, notes, nullifiers) the client is interested in. -This request returns the next block containing requested data. It also returns `chain_tip` which is the latest block number in the chain. +This request returns the next block containing requested data. It also returns `chain_tip` which is the latest block number in the chain. Client is expected to repeat these requests in a loop until `response.block_header.block_num == response.chain_tip`, at which point the client is fully synchronized with the chain. -Each request also returns info about new notes, nullifiers etc. created. It also returns Chain MMR delta that can be used to update the state of Chain MMR. +Each request also returns info about new notes, nullifiers etc. created. It also returns Chain MMR delta that can be used to update the state of Chain MMR. This includes both chain MMR peaks and chain MMR nodes. For preserving some degree of privacy, note tags and nullifiers filters contain only high part of hashes. Thus, returned data @@ -89,19 +101,19 @@ contains excessive notes and nullifiers, client can make additional filtering of **Parameters** -* `block_num`: `uint32` – send updates to the client starting at this block. -* `account_ids`: `[AccountId]` – accounts filter. -* `note_tags`: `[uint32]` – note tags filter. Corresponds to the high 16 bits of the real values. -* `nullifiers`: `[uint32]` – nullifiers filter. Corresponds to the high 16 bits of the real values. +- `block_num`: `uint32` – send updates to the client starting at this block. +- `account_ids`: `[AccountId]` – accounts filter. +- `note_tags`: `[uint32]` – note tags filter. Corresponds to the high 16 bits of the real values. +- `nullifiers`: `[uint32]` – nullifiers filter. Corresponds to the high 16 bits of the real values. **Returns** -* `chain_tip`: `uint32` – number of the latest block in the chain. -* `block_header`: `BlockHeader` – block header of the block with the first note matching the specified criteria. -* `mmr_delta`: `MmrDelta` – data needed to update the partial MMR from `block_num + 1` to `block_header.block_num`. -* `accounts`: `[AccountHashUpdate]` – a list of account hashes updated after `block_num + 1` but not after `block_header.block_num`. -* `notes`: `[NoteSyncRecord]` – a list of all notes together with the Merkle paths from `block_header.note_root`. -* `nullifiers`: `[NullifierUpdate]` – a list of nullifiers created between `block_num + 1` and `block_header.block_num`. +- `chain_tip`: `uint32` – number of the latest block in the chain. +- `block_header`: `BlockHeader` – block header of the block with the first note matching the specified criteria. +- `mmr_delta`: `MmrDelta` – data needed to update the partial MMR from `block_num + 1` to `block_header.block_num`. +- `accounts`: `[AccountHashUpdate]` – a list of account hashes updated after `block_num + 1` but not after `block_header.block_num`. +- `notes`: `[NoteSyncRecord]` – a list of all notes together with the Merkle paths from `block_header.note_root`. +- `nullifiers`: `[NullifierUpdate]` – a list of nullifiers created between `block_num + 1` and `block_header.block_num`. ### SubmitProvenTransaction @@ -109,11 +121,12 @@ Submits proven transaction to the Miden network. **Parameters** -* `transaction`: `bytes` - transaction encoded using Miden's native format. +- `transaction`: `bytes` - transaction encoded using Miden's native format. **Returns** This method doesn't return any data. ## License + This project is [MIT licensed](../LICENSE). diff --git a/rpc/src/server/api.rs b/rpc/src/server/api.rs index 60b9b3b8c..35818f1ea 100644 --- a/rpc/src/server/api.rs +++ b/rpc/src/server/api.rs @@ -1,20 +1,23 @@ use anyhow::Result; -use miden_node_proto::generated::{ - block_producer::api_client as block_producer_client, - requests::{ - CheckNullifiersRequest, GetAccountDetailsRequest, GetBlockHeaderByNumberRequest, - SubmitProvenTransactionRequest, SyncStateRequest, +use miden_node_proto::{ + generated::{ + block_producer::api_client as block_producer_client, + requests::{ + CheckNullifiersRequest, GetAccountDetailsRequest, GetBlockHeaderByNumberRequest, + GetNotesByIdRequest, SubmitProvenTransactionRequest, SyncStateRequest, + }, + responses::{ + CheckNullifiersResponse, GetAccountDetailsResponse, GetBlockHeaderByNumberResponse, + GetNotesByIdResponse, SubmitProvenTransactionResponse, SyncStateResponse, + }, + rpc::api_server, + store::api_client as store_client, }, - responses::{ - CheckNullifiersResponse, GetAccountDetailsResponse, GetBlockHeaderByNumberResponse, - SubmitProvenTransactionResponse, SyncStateResponse, - }, - rpc::api_server, - store::api_client as store_client, + try_convert, }; use miden_objects::{ - accounts::AccountId, transaction::ProvenTransaction, utils::serde::Deserializable, Digest, - MIN_PROOF_SECURITY_LEVEL, + accounts::AccountId, crypto::hash::rpo::RpoDigest, transaction::ProvenTransaction, + utils::serde::Deserializable, Digest, MIN_PROOF_SECURITY_LEVEL, }; use miden_tx::TransactionVerifier; use tonic::{ @@ -110,6 +113,28 @@ impl api_server::Api for RpcApi { self.store.clone().sync_state(request).await } + #[instrument( + target = "miden-rpc", + name = "rpc:get_notes_by_id", + skip_all, + ret(level = "debug"), + err + )] + async fn get_notes_by_id( + &self, + request: Request, + ) -> Result, Status> { + debug!(target: COMPONENT, request = ?request.get_ref()); + + // Validation checking for correct NoteId's + let note_ids = request.get_ref().note_ids.clone(); + + let _: Vec = try_convert(note_ids) + .map_err(|err| Status::invalid_argument(format!("Invalid NoteId: {}", err)))?; + + self.store.clone().get_notes_by_id(request).await + } + #[instrument(target = "miden-rpc", name = "rpc:submit_proven_transaction", skip_all, err)] async fn submit_proven_transaction( &self, diff --git a/store/README.md b/store/README.md index e0969dae7..ddcbb3bfb 100644 --- a/store/README.md +++ b/store/README.md @@ -1,7 +1,7 @@ # Miden node store -The **Store** maintains the state of the chain. It serves as the "source of truth" for the chain - i.e., if it is not in -the store, the node does not consider it to be part of the chain. +The **Store** maintains the state of the chain. It serves as the "source of truth" for the chain - i.e., if it is not in +the store, the node does not consider it to be part of the chain. **Store** is one of components of the [Miden node](..). ## Architecture @@ -22,7 +22,7 @@ cargo install --path store ### Running the Store -In order to run Store, you must provide a genesis file. To generate a genesis file you will need to use [Miden node](../README.md#generating-the-genesis-file)'s `make-genesis` command. +In order to run Store, you must provide a genesis file. To generate a genesis file you will need to use [Miden node](../README.md#generating-the-genesis-file)'s `make-genesis` command. You will also need to provide a configuration file. We have an example config file in [store-example.toml](store-example.toml). @@ -34,7 +34,7 @@ miden-node-store serve --config ## API -The **Store** serves connections using the [gRPC protocol](https://grpc.io) on a port, set in the previously mentioned configuration file. +The **Store** serves connections using the [gRPC protocol](https://grpc.io) on a port, set in the previously mentioned configuration file. Here is a brief description of supported methods. ### ApplyBlock @@ -43,10 +43,10 @@ Applies changes of a new block to the DB and in-memory data structures. **Parameters** -* `block`: `BlockHeader` – block header ([src](../proto/proto/block_header.proto)). -* `accounts`: `[AccountUpdate]` – a list of account updates. -* `nullifiers`: `[Digest]` – a list of nullifier hashes. -* `notes`: `[NoteCreated]` – a list of notes created. +- `block`: `BlockHeader` – block header ([src](../proto/proto/block_header.proto)). +- `accounts`: `[AccountUpdate]` – a list of account updates. +- `nullifiers`: `[Digest]` – a list of nullifier hashes. +- `notes`: `[NoteCreated]` – a list of notes created. **Returns** @@ -58,11 +58,11 @@ Get a list of proofs for given nullifier hashes, each proof as a sparse Merkle T **Parameters:** -* `nullifiers`: `[Digest]` – array of nullifier hashes. +- `nullifiers`: `[Digest]` – array of nullifier hashes. **Returns:** -* `proofs`: `[NullifierProof]` – array of nullifier proofs, positions correspond to the ones in request. +- `proofs`: `[NullifierProof]` – array of nullifier proofs, positions correspond to the ones in request. ### GetBlockHeaderByNumber @@ -70,11 +70,11 @@ Retrieves block header by given block number. **Parameters** -* `block_num`: `uint32` *(optional)* – the block number of the target block. If not provided, the latest known block will be returned. +- `block_num`: `uint32` _(optional)_ – the block number of the target block. If not provided, the latest known block will be returned. **Returns:** -* `block_header`: `BlockHeader` – block header. +- `block_header`: `BlockHeader` – block header. ### GetBlockInputs @@ -82,29 +82,41 @@ Returns data needed by the block producer to construct and prove the next block. **Parameters** -* `account_ids`: `[AccountId]` – array of account IDs. -* `nullifiers`: `[Digest]` – array of nullifier hashes (not currently in use). +- `account_ids`: `[AccountId]` – array of account IDs. +- `nullifiers`: `[Digest]` – array of nullifier hashes (not currently in use). **Returns** -* `block_header`: `[BlockHeader]` – the latest block header. -* `mmr_peaks`: `[Digest]` – peaks of the above block's mmr, The `forest` value is equal to the block number. -* `account_states`: `[AccountBlockInputRecord]` – the hashes of the requested accouts and their authentication paths. -* `nullifiers`: `[NullifierBlockInputRecord]` – the requested nullifiers and their authentication paths. +- `block_header`: `[BlockHeader]` – the latest block header. +- `mmr_peaks`: `[Digest]` – peaks of the above block's mmr, The `forest` value is equal to the block number. +- `account_states`: `[AccountBlockInputRecord]` – the hashes of the requested accouts and their authentication paths. +- `nullifiers`: `[NullifierBlockInputRecord]` – the requested nullifiers and their authentication paths. ### GetTransactionInputs -Returns the data needed by the block producer to check validity of an incoming transaction. +Returns the data needed by the block producer to check validity of an incoming transaction. **Parameters** -* `account_id`: `AccountId` – ID of the account against which a transaction is executed. -* `nullifiers`: `[Digest]` – array of nullifiers for all notes consumed by a transaction. +- `account_id`: `AccountId` – ID of the account against which a transaction is executed. +- `nullifiers`: `[Digest]` – array of nullifiers for all notes consumed by a transaction. **Returns** -* `account_state`: `AccountTransactionInputRecord` – account's descriptors. -* `nullifiers`: `[NullifierTransactionInputRecord]` – the block numbers at which corresponding nullifiers have been consumed, zero if not consumed. +- `account_state`: `AccountTransactionInputRecord` – account's descriptors. +- `nullifiers`: `[NullifierTransactionInputRecord]` – the block numbers at which corresponding nullifiers have been consumed, zero if not consumed. + +### GetNotesById + +Returns a list of notes matching the provided note IDs. + +**Parameters** + +- `note_ids`: `[NoteId]` - list of IDs of the notes we want to query. + +**Returns** + +- `notes`: `[Note]` - List of notes matching the list of requested NoteIds. ### GetAccountDetails @@ -112,12 +124,12 @@ Returns the latest state of an account with the specified ID. **Parameters** -* `account_id`: `AccountId` – account ID. +- `account_id`: `AccountId` – account ID. **Returns** -* `account`: `AccountInfo` – account state information. For public accounts there is also details describing current state, stored on-chain; -for private accounts only hash of the latest known state is returned. +- `account`: `AccountInfo` – account state information. For public accounts there is also details describing current state, stored on-chain; + for private accounts only hash of the latest known state is returned. ### SyncState @@ -135,19 +147,19 @@ contains excessive notes and nullifiers, client can make additional filtering of **Parameters** -* `block_num`: `uint32` – send updates to the client starting at this block. -* `account_ids`: `[AccountId]` – accounts filter. -* `note_tags`: `[uint32]` – note tags filter. Corresponds to the high 16 bits of the real values. -* `nullifiers`: `[uint32]` – nullifiers filter. Corresponds to the high 16 bits of the real values. +- `block_num`: `uint32` – send updates to the client starting at this block. +- `account_ids`: `[AccountId]` – accounts filter. +- `note_tags`: `[uint32]` – note tags filter. Corresponds to the high 16 bits of the real values. +- `nullifiers`: `[uint32]` – nullifiers filter. Corresponds to the high 16 bits of the real values. **Returns** -* `chain_tip`: `uint32` – number of the latest block in the chain. -* `block_header`: `BlockHeader` – block header of the block with the first note matching the specified criteria. -* `mmr_delta`: `MmrDelta` – data needed to update the partial MMR from `block_num + 1` to `block_header.block_num`. -* `accounts`: `[AccountHashUpdate]` – a list of account hashes updated after `block_num + 1` but not after `block_header.block_num`. -* `notes`: `[NoteSyncRecord]` – a list of all notes together with the Merkle paths from `block_header.note_root`. -* `nullifiers`: `[NullifierUpdate]` – a list of nullifiers created between `block_num + 1` and `block_header.block_num`. +- `chain_tip`: `uint32` – number of the latest block in the chain. +- `block_header`: `BlockHeader` – block header of the block with the first note matching the specified criteria. +- `mmr_delta`: `MmrDelta` – data needed to update the partial MMR from `block_num + 1` to `block_header.block_num`. +- `accounts`: `[AccountHashUpdate]` – a list of account hashes updated after `block_num + 1` but not after `block_header.block_num`. +- `notes`: `[NoteSyncRecord]` – a list of all notes together with the Merkle paths from `block_header.note_root`. +- `nullifiers`: `[NullifierUpdate]` – a list of nullifiers created between `block_num + 1` and `block_header.block_num`. ## Methods for testing purposes @@ -161,7 +173,7 @@ This request doesn't have any parameters. **Returns** -* `nullifiers`: `[NullifierLeaf]` – lists of all nullifiers of the current chain. +- `nullifiers`: `[NullifierLeaf]` – lists of all nullifiers of the current chain. ### ListAccounts @@ -173,7 +185,7 @@ This request doesn't have any parameters. **Returns** -* `accounts`: `[AccountInfo]` – list of all accounts of the current chain. +- `accounts`: `[AccountInfo]` – list of all accounts of the current chain. ### ListNotes @@ -185,7 +197,8 @@ This request doesn't have any parameters. **Returns** -* `notes`: `[Note]` – list of all notes of the current chain. +- `notes`: `[Note]` – list of all notes of the current chain. ## License + This project is [MIT licensed](../LICENSE). diff --git a/store/src/db/mod.rs b/store/src/db/mod.rs index b448e9093..eaef75d60 100644 --- a/store/src/db/mod.rs +++ b/store/src/db/mod.rs @@ -5,7 +5,7 @@ use miden_node_proto::domain::accounts::{AccountHashUpdate, AccountInfo}; use miden_objects::{ block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath, utils::Deserializable}, - notes::Nullifier, + notes::{NoteId, Nullifier}, transaction::AccountDetails, BlockHeader, GENESIS_BLOCK, }; @@ -251,6 +251,22 @@ impl Db { })? } + /// Loads all the Note's matching a certain NoteId from the database. + #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] + pub async fn select_notes_by_id( + &self, + note_ids: Vec, + ) -> Result> { + self.pool + .get() + .await? + .interact(move |conn| sql::select_notes_by_id(conn, ¬e_ids)) + .await + .map_err(|err| { + DatabaseError::InteractError(format!("Select note by id task failed: {err}")) + })? + } + /// Inserts the data of a new block into the DB. /// /// `allow_acquire` and `acquire_done` are used to synchronize writes to the DB with writes to diff --git a/store/src/db/sql.rs b/store/src/db/sql.rs index 2088f1382..354bc666c 100644 --- a/store/src/db/sql.rs +++ b/store/src/db/sql.rs @@ -1,11 +1,12 @@ //! Wrapper functions for SQL statements. + use std::rc::Rc; use miden_node_proto::domain::accounts::{AccountHashUpdate, AccountInfo}; use miden_objects::{ accounts::Account, crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, - notes::Nullifier, + notes::{NoteId, Nullifier}, transaction::AccountDetails, utils::serde::{Deserializable, Serializable}, BlockHeader, @@ -460,6 +461,59 @@ pub fn select_notes_since_block_by_tag_and_sender( Ok(res) } +/// Select Note's matching the NoteId using the given [Connection]. +/// +/// # Returns +/// +/// - Empty vector if no matching `note`. +/// - Otherwise, notes which `note_hash` matches the `NoteId` as bytes. +pub fn select_notes_by_id( + conn: &mut Connection, + note_ids: &[NoteId], +) -> Result> { + let note_ids: Vec = note_ids.iter().map(|id| id.to_bytes().into()).collect(); + + let mut stmt = conn.prepare( + " + SELECT + block_num, + batch_index, + note_index, + note_hash, + sender, + tag, + merkle_path + FROM + notes + WHERE + note_hash IN rarray(?1) + ", + )?; + let mut rows = stmt.query(params![Rc::new(note_ids)])?; + + let mut notes = Vec::new(); + while let Some(row) = rows.next()? { + let note_id_data = row.get_ref(3)?.as_blob()?; + let note_id = NoteId::read_from_bytes(note_id_data)?; + + let merkle_path_data = row.get_ref(6)?.as_blob()?; + let merkle_path = MerklePath::read_from_bytes(merkle_path_data)?; + + notes.push(Note { + block_num: row.get(0)?, + note_created: NoteCreated { + batch_index: row.get(1)?, + note_index: row.get(2)?, + note_id: note_id.into(), + sender: column_value_as_u64(row, 4)?, + tag: column_value_as_u64(row, 5)?, + }, + merkle_path, + }) + } + Ok(notes) +} + // BLOCK CHAIN QUERIES // ================================================================================================ diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index 61fc90cd8..af00ad4aa 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -5,7 +5,7 @@ use miden_objects::{ }, block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, - notes::{NoteMetadata, NoteType, Nullifier}, + notes::{NoteId, NoteMetadata, NoteType, Nullifier}, BlockHeader, Felt, FieldElement, ZERO, }; use rusqlite::{vtab::array, Connection}; @@ -527,6 +527,15 @@ fn test_notes() { ) .unwrap(); assert_eq!(res, vec![note2.clone()]); + + // test query notes by id + let notes = vec![note, note2]; + let note_ids: Vec = + notes.clone().iter().map(|note| note.note_created.note_id).collect(); + let note_ids: Vec = note_ids.into_iter().map(From::from).collect(); + + let res = sql::select_notes_by_id(&mut conn, ¬e_ids).unwrap(); + assert_eq!(res, notes); } // UTILITIES diff --git a/store/src/server/api.rs b/store/src/server/api.rs index c4b8fa3af..0ba6a7608 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -9,22 +9,27 @@ use miden_node_proto::{ note::NoteSyncRecord, requests::{ ApplyBlockRequest, CheckNullifiersRequest, GetAccountDetailsRequest, - GetBlockHeaderByNumberRequest, GetBlockInputsRequest, GetTransactionInputsRequest, - ListAccountsRequest, ListNotesRequest, ListNullifiersRequest, SyncStateRequest, + GetBlockHeaderByNumberRequest, GetBlockInputsRequest, GetNotesByIdRequest, + GetTransactionInputsRequest, ListAccountsRequest, ListNotesRequest, + ListNullifiersRequest, SyncStateRequest, }, responses::{ AccountTransactionInputRecord, ApplyBlockResponse, CheckNullifiersResponse, GetAccountDetailsResponse, GetBlockHeaderByNumberResponse, GetBlockInputsResponse, - GetTransactionInputsResponse, ListAccountsResponse, ListNotesResponse, - ListNullifiersResponse, NullifierTransactionInputRecord, NullifierUpdate, - SyncStateResponse, + GetNotesByIdResponse, GetTransactionInputsResponse, ListAccountsResponse, + ListNotesResponse, ListNullifiersResponse, NullifierTransactionInputRecord, + NullifierUpdate, SyncStateResponse, }, smt::SmtLeafEntry, store::api_server, }, - AccountState, + try_convert, AccountState, +}; +use miden_objects::{ + crypto::hash::rpo::RpoDigest, + notes::{NoteId, Nullifier}, + BlockHeader, Felt, ZERO, }; -use miden_objects::{notes::Nullifier, BlockHeader, Felt, ZERO}; use tonic::{Response, Status}; use tracing::{debug, info, instrument}; @@ -160,6 +165,48 @@ impl api_server::Api for StoreApi { })) } + /// Returns a list of Note's for the specified NoteId's. + /// + /// If the list is empty or no Note matched the requested NoteId and empty list is returned. + #[instrument( + target = "miden-store", + name = "store:get_notes_by_id", + skip_all, + ret(level = "debug"), + err + )] + async fn get_notes_by_id( + &self, + request: tonic::Request, + ) -> Result, Status> { + info!(target: COMPONENT, ?request); + + let note_ids = request.into_inner().note_ids; + + let note_ids: Vec = try_convert(note_ids) + .map_err(|err| Status::invalid_argument(format!("Invalid NoteId: {}", err)))?; + + let note_ids: Vec = note_ids.into_iter().map(From::from).collect(); + + let notes = self + .state + .get_notes_by_id(note_ids) + .await + .map_err(internal_error)? + .into_iter() + .map(|note| generated::note::Note { + block_num: note.block_num, + note_index: note.note_created.note_index, + note_id: Some(note.note_created.note_id.into()), + sender: Some(note.note_created.sender.into()), + tag: note.note_created.tag, + merkle_path: Some(note.merkle_path.into()), + }) + .collect(); + + Ok(Response::new(GetNotesByIdResponse { notes })) + } + /// Returns details for public (on-chain) account by id. #[instrument( target = "miden-store", diff --git a/store/src/state.rs b/store/src/state.rs index aa18ccc71..099964971 100644 --- a/store/src/state.rs +++ b/store/src/state.rs @@ -12,7 +12,7 @@ use miden_objects::{ hash::rpo::RpoDigest, merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, SimpleSmt, SmtProof, ValuePath}, }, - notes::{NoteMetadata, NoteType, Nullifier}, + notes::{NoteId, NoteMetadata, NoteType, Nullifier}, transaction::AccountDetails, AccountError, BlockHeader, NoteError, ACCOUNT_TREE_DEPTH, ZERO, }; @@ -301,6 +301,17 @@ impl State { nullifiers.iter().map(|n| inner.nullifier_tree.open(n)).collect() } + /// Queries a list of [Note] from the database. + /// + /// If the provided list of [NoteId] given is empty or no [Note] matches the provided [NoteId] + /// an empty list is returned. + pub async fn get_notes_by_id( + &self, + note_ids: Vec, + ) -> Result, DatabaseError> { + self.db.select_notes_by_id(note_ids).await + } + /// Loads data to synchronize a client. /// /// The client's request contains a list of tag prefixes, this method will return the first From fa24b83b923df052b7990b69418d3bea1ceaa955 Mon Sep 17 00:00:00 2001 From: polydez <155382956+polydez@users.noreply.github.com> Date: Tue, 9 Apr 2024 14:37:13 +0500 Subject: [PATCH 18/29] Implemented writing on-chain accounts details on block applying (#294) * feat: add `account_details` table to the DB * refactor: rename `block_number` column in nullifiers table to `block_num` * refactor: use `BETWEEN` in interval comparison checks * feat: implement account details protobuf messages, domain objects and conversions * feat: (WIP) implement account details support * feat: (WIP) implement account details support * feat: (WIP) implement account details support * feat: (WIP) implement account details support * fix: db creation * docs: remove TODO * refactor: apply formatting * feat: implement endpoint for getting public account details * tests: add test for storage * feat: add rpc endpoint for getting account details * refactor: keep only domain object changes * fix: compilation errors * fix: use note tag conversion from `u64` * refactor: remove account details protobuf messages * fix: remove unused error invariants * refactor: introduce `UpdatedAccount` struct * fix: rollback details conversion * fix: compilation error * feat: account details in store * refactor: add constraint name for foreign key * refactor: small code improvement Co-authored-by: Augusto Hack * feat: account id validation * refactor: rename `get_account_details` to `select_*` * feat: return serialized account details * feat: add requirement of account id to be public in RPC * fix: remove error message used in different PR * fix: union account details with account and process them together * docs: remove `GetAccountDetails` from README.md * fix: remove unused error invariants * fix: use `Account` instead of `AccountDetails` in store * wip * feat: implement `GetAccountDetails` endpoint * docs: document `GetAccountDetails` endpoint * refactor: simplify code, make account details optional * fix: clippy warning * fix: address review comments * fix: update code to the latest miden-base * refactor: little code improvement * fix: remove error message used in different PR * fix: compilation errors chore: update `miden-base` dependency to fixed version refactor: extract `apply_delta` function fix: separate error invariant for missed details in store fix: make account details optional refactor: introduce `UpdatedAccount` struct fix: avoid cloning of block data feat: simple details validation in store feat: rewrite `sql::upsert_accounts` to simplified work with details and update test fix: compilation errors feat: use serialized account details feat: writing account details on block applying * fix: compilation errors, update test * refactor: rename protobuf messages * docs: update endpoint in README.md * tests: get rid of miden-mock dependency * tests: get rid of winter-rand-utils dependency * refactor: rename `AccountDetailsUpdate` to `AccountUpdateDetails` * feat: check for account hash for new on-chain accounts * formatting: run rustfmt * docs: address review comments --------- Co-authored-by: Augusto Hack --- block-producer/src/batch_builder/batch.rs | 15 +- block-producer/src/block.rs | 5 +- block-producer/src/block_builder/mod.rs | 2 +- .../src/block_builder/prover/block_witness.rs | 8 +- .../src/block_builder/prover/tests.rs | 4 +- block-producer/src/state_view/mod.rs | 4 +- .../src/state_view/tests/apply_block.rs | 14 +- block-producer/src/store/mod.rs | 10 +- block-producer/src/test_utils/block.rs | 8 +- block-producer/src/test_utils/store.rs | 6 +- proto/proto/account.proto | 6 +- proto/proto/requests.proto | 2 + proto/proto/responses.proto | 2 +- proto/src/domain/accounts.rs | 30 +-- proto/src/domain/blocks.rs | 10 +- proto/src/domain/notes.rs | 8 +- proto/src/generated/account.rs | 4 +- proto/src/generated/requests.rs | 3 + proto/src/generated/responses.rs | 2 +- rpc/README.md | 5 +- store/README.md | 5 +- store/src/db/mod.rs | 17 +- store/src/db/sql.rs | 118 ++++++++--- store/src/db/tests.rs | 187 ++++++++++++++++-- store/src/errors.rs | 11 ++ store/src/server/api.rs | 39 +++- store/src/state.rs | 15 +- 27 files changed, 415 insertions(+), 125 deletions(-) diff --git a/block-producer/src/batch_builder/batch.rs b/block-producer/src/batch_builder/batch.rs index b5784c433..9f6b550bd 100644 --- a/block-producer/src/batch_builder/batch.rs +++ b/block-producer/src/batch_builder/batch.rs @@ -1,11 +1,12 @@ use std::collections::BTreeMap; -use miden_node_proto::domain::accounts::UpdatedAccount; +use miden_node_proto::domain::accounts::AccountUpdateDetails; use miden_objects::{ accounts::AccountId, batches::BatchNoteTree, crypto::hash::blake::{Blake3Digest, Blake3_256}, notes::{NoteEnvelope, Nullifier}, + transaction::AccountDetails, Digest, MAX_NOTES_PER_BATCH, }; use tracing::instrument; @@ -54,6 +55,7 @@ impl TransactionBatch { AccountStates { initial_state: tx.initial_account_hash(), final_state: tx.final_account_hash(), + details: tx.account_details().cloned(), }, ) }) @@ -107,15 +109,15 @@ impl TransactionBatch { .map(|(account_id, account_states)| (*account_id, account_states.initial_state)) } - /// Returns an iterator over (account_id, new_state_hash) tuples for accounts that were + /// Returns an iterator over (account_id, details, new_state_hash) tuples for accounts that were /// modified in this transaction batch. - pub fn updated_accounts(&self) -> impl Iterator + '_ { + pub fn updated_accounts(&self) -> impl Iterator + '_ { self.updated_accounts .iter() - .map(|(&account_id, account_states)| UpdatedAccount { + .map(|(&account_id, account_states)| AccountUpdateDetails { account_id, final_state_hash: account_states.final_state, - details: None, // TODO: In the next PR: account_states.details.clone(), + details: account_states.details.clone(), }) } @@ -150,8 +152,9 @@ impl TransactionBatch { /// account. /// /// TODO: should this be moved into domain objects? -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq)] struct AccountStates { initial_state: Digest, final_state: Digest, + details: Option, } diff --git a/block-producer/src/block.rs b/block-producer/src/block.rs index 6e43674c5..8ca9d0b4c 100644 --- a/block-producer/src/block.rs +++ b/block-producer/src/block.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use miden_node_proto::{ - domain::accounts::UpdatedAccount, + domain::accounts::AccountUpdateDetails, errors::{ConversionError, MissingFieldHelper}, generated::responses::GetBlockInputsResponse, AccountInputRecord, NullifierWitness, @@ -18,11 +18,10 @@ use crate::store::BlockInputsError; #[derive(Debug, Clone)] pub struct Block { pub header: BlockHeader, - pub updated_accounts: Vec, + pub updated_accounts: Vec, pub created_notes: Vec<(usize, usize, NoteEnvelope)>, pub produced_nullifiers: Vec, // TODO: - // - full states for updated public accounts // - full states for created public notes // - zk proof } diff --git a/block-producer/src/block_builder/mod.rs b/block-producer/src/block_builder/mod.rs index cb0502528..eb92612b4 100644 --- a/block-producer/src/block_builder/mod.rs +++ b/block-producer/src/block_builder/mod.rs @@ -119,7 +119,7 @@ where info!(target: COMPONENT, block_num, %block_hash, "block built"); debug!(target: COMPONENT, ?block); - self.state_view.apply_block(block).await?; + self.state_view.apply_block(&block).await?; info!(target: COMPONENT, block_num, %block_hash, "block committed"); diff --git a/block-producer/src/block_builder/prover/block_witness.rs b/block-producer/src/block_builder/prover/block_witness.rs index c72c84ddc..026dff08f 100644 --- a/block-producer/src/block_builder/prover/block_witness.rs +++ b/block-producer/src/block_builder/prover/block_witness.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; -use miden_node_proto::domain::accounts::UpdatedAccount; +use miden_node_proto::domain::accounts::AccountUpdateDetails; use miden_objects::{ accounts::AccountId, crypto::merkle::{EmptySubtreeRoots, MerklePath, MerkleStore, MmrPeaks, SmtProof}, @@ -38,7 +38,7 @@ impl BlockWitness { let updated_accounts = { let mut account_initial_states: BTreeMap = - batches.iter().flat_map(|batch| batch.account_initial_states()).collect(); + batches.iter().flat_map(TransactionBatch::account_initial_states).collect(); let mut account_merkle_proofs: BTreeMap = block_inputs .accounts @@ -48,9 +48,9 @@ impl BlockWitness { batches .iter() - .flat_map(|batch| batch.updated_accounts()) + .flat_map(TransactionBatch::updated_accounts) .map( - |UpdatedAccount { + |AccountUpdateDetails { account_id, final_state_hash, .. diff --git a/block-producer/src/block_builder/prover/tests.rs b/block-producer/src/block_builder/prover/tests.rs index 1c1c38ab9..d7b3a292b 100644 --- a/block-producer/src/block_builder/prover/tests.rs +++ b/block-producer/src/block_builder/prover/tests.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, iter}; -use miden_node_proto::domain::accounts::UpdatedAccount; +use miden_node_proto::domain::accounts::AccountUpdateDetails; use miden_objects::{ accounts::{ AccountId, ACCOUNT_ID_OFF_CHAIN_SENDER, ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, @@ -239,7 +239,7 @@ async fn test_compute_account_root_success() { account_ids .iter() .zip(account_final_states.iter()) - .map(|(&account_id, &account_hash)| UpdatedAccount { + .map(|(&account_id, &account_hash)| AccountUpdateDetails { account_id, final_state_hash: account_hash.into(), details: None, diff --git a/block-producer/src/state_view/mod.rs b/block-producer/src/state_view/mod.rs index 8af9ebff6..89fde23a3 100644 --- a/block-producer/src/state_view/mod.rs +++ b/block-producer/src/state_view/mod.rs @@ -119,9 +119,9 @@ where #[instrument(target = "miden-block-producer", skip_all, err)] async fn apply_block( &self, - block: Block, + block: &Block, ) -> Result<(), ApplyBlockError> { - self.store.apply_block(block.clone()).await?; + self.store.apply_block(block).await?; let mut locked_accounts_in_flight = self.accounts_in_flight.write().await; let mut locked_nullifiers_in_flight = self.nullifiers_in_flight.write().await; diff --git a/block-producer/src/state_view/tests/apply_block.rs b/block-producer/src/state_view/tests/apply_block.rs index 0db199277..af143700d 100644 --- a/block-producer/src/state_view/tests/apply_block.rs +++ b/block-producer/src/state_view/tests/apply_block.rs @@ -6,7 +6,7 @@ use std::iter; -use miden_node_proto::domain::accounts::UpdatedAccount; +use miden_node_proto::domain::accounts::AccountUpdateDetails; use super::*; use crate::test_utils::{block::MockBlockBuilder, MockStoreSuccessBuilder}; @@ -34,7 +34,7 @@ async fn test_apply_block_ab1() { .await .account_updates( std::iter::once(account) - .map(|mock_account| UpdatedAccount { + .map(|mock_account| AccountUpdateDetails { account_id: mock_account.id, final_state_hash: mock_account.states[1], details: None, @@ -43,7 +43,7 @@ async fn test_apply_block_ab1() { ) .build(); - let apply_block_res = state_view.apply_block(block).await; + let apply_block_res = state_view.apply_block(&block).await; assert!(apply_block_res.is_ok()); assert_eq!(*store.num_apply_block_called.read().await, 1); @@ -81,7 +81,7 @@ async fn test_apply_block_ab2() { .account_updates( accounts_in_block .into_iter() - .map(|mock_account| UpdatedAccount { + .map(|mock_account| AccountUpdateDetails { account_id: mock_account.id, final_state_hash: mock_account.states[1], details: None, @@ -90,7 +90,7 @@ async fn test_apply_block_ab2() { ) .build(); - let apply_block_res = state_view.apply_block(block).await; + let apply_block_res = state_view.apply_block(&block).await; assert!(apply_block_res.is_ok()); let accounts_still_in_flight = state_view.accounts_in_flight.read().await; @@ -130,7 +130,7 @@ async fn test_apply_block_ab3() { accounts .clone() .into_iter() - .map(|mock_account| UpdatedAccount { + .map(|mock_account| AccountUpdateDetails { account_id: mock_account.id, final_state_hash: mock_account.states[1], details: None, @@ -139,7 +139,7 @@ async fn test_apply_block_ab3() { ) .build(); - let apply_block_res = state_view.apply_block(block).await; + let apply_block_res = state_view.apply_block(&block).await; assert!(apply_block_res.is_ok()); // Craft a new transaction which tries to consume the same note that was consumed in the diff --git a/block-producer/src/store/mod.rs b/block-producer/src/store/mod.rs index b02f9fa41..2407f6acd 100644 --- a/block-producer/src/store/mod.rs +++ b/block-producer/src/store/mod.rs @@ -49,7 +49,7 @@ pub trait Store: ApplyBlock { pub trait ApplyBlock: Send + Sync + 'static { async fn apply_block( &self, - block: Block, + block: &Block, ) -> Result<(), ApplyBlockError>; } @@ -131,13 +131,13 @@ impl ApplyBlock for DefaultStore { #[instrument(target = "miden-block-producer", skip_all, err)] async fn apply_block( &self, - block: Block, + block: &Block, ) -> Result<(), ApplyBlockError> { let request = tonic::Request::new(ApplyBlockRequest { - block: Some(block.header.into()), + block: Some((&block.header).into()), accounts: convert(&block.updated_accounts), - nullifiers: convert(block.produced_nullifiers), - notes: convert(block.created_notes), + nullifiers: convert(&block.produced_nullifiers), + notes: convert(&block.created_notes), }); let _ = self diff --git a/block-producer/src/test_utils/block.rs b/block-producer/src/test_utils/block.rs index 02c8a28cd..f3d877273 100644 --- a/block-producer/src/test_utils/block.rs +++ b/block-producer/src/test_utils/block.rs @@ -1,4 +1,4 @@ -use miden_node_proto::domain::accounts::UpdatedAccount; +use miden_node_proto::domain::accounts::AccountUpdateDetails; use miden_objects::{ block::BlockNoteTree, crypto::merkle::{Mmr, SimpleSmt}, @@ -68,7 +68,7 @@ pub async fn build_actual_block_header( let updated_accounts: Vec<_> = batches.iter().flat_map(TransactionBatch::updated_accounts).collect(); let produced_nullifiers: Vec = - batches.iter().flat_map(|batch| batch.produced_nullifiers()).collect(); + batches.iter().flat_map(TransactionBatch::produced_nullifiers).collect(); let block_inputs_from_store: BlockInputs = store .get_block_inputs( @@ -89,7 +89,7 @@ pub struct MockBlockBuilder { store_chain_mmr: Mmr, last_block_header: BlockHeader, - updated_accounts: Option>, + updated_accounts: Option>, created_notes: Option>, produced_nullifiers: Option>, } @@ -109,7 +109,7 @@ impl MockBlockBuilder { pub fn account_updates( mut self, - updated_accounts: Vec, + updated_accounts: Vec, ) -> Self { for update in &updated_accounts { self.store_accounts diff --git a/block-producer/src/test_utils/store.rs b/block-producer/src/test_utils/store.rs index abd6cba22..41c1f3192 100644 --- a/block-producer/src/test_utils/store.rs +++ b/block-producer/src/test_utils/store.rs @@ -174,7 +174,7 @@ impl MockStoreSuccess { impl ApplyBlock for MockStoreSuccess { async fn apply_block( &self, - block: Block, + block: &Block, ) -> Result<(), ApplyBlockError> { // Intentionally, we take and hold both locks, to prevent calls to `get_tx_inputs()` from going through while we're updating the store's data structure let mut locked_accounts = self.accounts.write().await; @@ -187,7 +187,7 @@ impl ApplyBlock for MockStoreSuccess { debug_assert_eq!(locked_accounts.root(), block.header.account_root()); // update nullifiers - for nullifier in block.produced_nullifiers { + for nullifier in &block.produced_nullifiers { locked_produced_nullifiers .insert(nullifier.inner(), [block.header.block_num().into(), ZERO, ZERO, ZERO]); } @@ -291,7 +291,7 @@ pub struct MockStoreFailure; impl ApplyBlock for MockStoreFailure { async fn apply_block( &self, - _block: Block, + _block: &Block, ) -> Result<(), ApplyBlockError> { Err(ApplyBlockError::GrpcClientError(String::new())) } diff --git a/proto/proto/account.proto b/proto/proto/account.proto index ec4b5b7eb..f73c5dac7 100644 --- a/proto/proto/account.proto +++ b/proto/proto/account.proto @@ -10,13 +10,13 @@ message AccountId { fixed64 id = 1; } -message AccountHashUpdate { - account.AccountId account_id = 1; +message AccountSummary { + AccountId account_id = 1; digest.Digest account_hash = 2; uint32 block_num = 3; } message AccountInfo { - AccountHashUpdate update = 1; + AccountSummary summary = 1; optional bytes details = 2; } diff --git a/proto/proto/requests.proto b/proto/proto/requests.proto index 529fc92fe..43e8301dc 100644 --- a/proto/proto/requests.proto +++ b/proto/proto/requests.proto @@ -10,6 +10,8 @@ import "note.proto"; message AccountUpdate { account.AccountId account_id = 1; digest.Digest account_hash = 2; + // Details for public (on-chain) account. + optional bytes details = 3; } message ApplyBlockRequest { diff --git a/proto/proto/responses.proto b/proto/proto/responses.proto index 27913a34f..b9f548d1d 100644 --- a/proto/proto/responses.proto +++ b/proto/proto/responses.proto @@ -36,7 +36,7 @@ message SyncStateResponse { mmr.MmrDelta mmr_delta = 3; // a list of account hashes updated after `block_num + 1` but not after `block_header.block_num` - repeated account.AccountHashUpdate accounts = 5; + repeated account.AccountSummary accounts = 5; // a list of all notes together with the Merkle paths from `block_header.note_root` repeated note.NoteSyncRecord notes = 6; diff --git a/proto/src/domain/accounts.rs b/proto/src/domain/accounts.rs index d8a1e383a..3f298cd63 100644 --- a/proto/src/domain/accounts.rs +++ b/proto/src/domain/accounts.rs @@ -13,8 +13,8 @@ use crate::{ errors::{ConversionError, MissingFieldHelper}, generated::{ account::{ - AccountHashUpdate as AccountHashUpdatePb, AccountId as AccountIdPb, - AccountInfo as AccountInfoPb, + AccountId as AccountIdPb, AccountInfo as AccountInfoPb, + AccountSummary as AccountSummaryPb, }, requests::AccountUpdate, responses::{AccountBlockInputRecord, AccountTransactionInputRecord}, @@ -51,6 +51,12 @@ impl From for AccountIdPb { } } +impl From<&AccountId> for AccountIdPb { + fn from(account_id: &AccountId) -> Self { + (*account_id).into() + } +} + impl From for AccountIdPb { fn from(account_id: AccountId) -> Self { Self { @@ -80,14 +86,14 @@ impl TryFrom for AccountId { // ================================================================================================ #[derive(Debug, PartialEq)] -pub struct AccountHashUpdate { +pub struct AccountSummary { pub account_id: AccountId, pub account_hash: RpoDigest, pub block_num: u32, } -impl From<&AccountHashUpdate> for AccountHashUpdatePb { - fn from(update: &AccountHashUpdate) -> Self { +impl From<&AccountSummary> for AccountSummaryPb { + fn from(update: &AccountSummary) -> Self { Self { account_id: Some(update.account_id.into()), account_hash: Some(update.account_hash.into()), @@ -98,32 +104,32 @@ impl From<&AccountHashUpdate> for AccountHashUpdatePb { #[derive(Debug, PartialEq)] pub struct AccountInfo { - pub update: AccountHashUpdate, + pub summary: AccountSummary, pub details: Option, } impl From<&AccountInfo> for AccountInfoPb { - fn from(AccountInfo { update, details }: &AccountInfo) -> Self { + fn from(AccountInfo { summary, details }: &AccountInfo) -> Self { Self { - update: Some(update.into()), + summary: Some(summary.into()), details: details.as_ref().map(|account| account.to_bytes()), } } } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct UpdatedAccount { +pub struct AccountUpdateDetails { pub account_id: AccountId, pub final_state_hash: Digest, pub details: Option, } -impl From<&UpdatedAccount> for AccountUpdate { - fn from(update: &UpdatedAccount) -> Self { +impl From<&AccountUpdateDetails> for AccountUpdate { + fn from(update: &AccountUpdateDetails) -> Self { Self { account_id: Some(update.account_id.into()), account_hash: Some(update.final_state_hash.into()), - // details: update.details.to_bytes(), + details: update.details.as_ref().map(|details| details.to_bytes()), } } } diff --git a/proto/src/domain/blocks.rs b/proto/src/domain/blocks.rs index c33ace75f..64ed90af6 100644 --- a/proto/src/domain/blocks.rs +++ b/proto/src/domain/blocks.rs @@ -8,8 +8,8 @@ use crate::{ // BLOCK HEADER // ================================================================================================ -impl From for block_header::BlockHeader { - fn from(header: BlockHeader) -> Self { +impl From<&BlockHeader> for block_header::BlockHeader { + fn from(header: &BlockHeader) -> Self { Self { prev_hash: Some(header.prev_hash().into()), block_num: header.block_num(), @@ -27,6 +27,12 @@ impl From for block_header::BlockHeader { } } +impl From for block_header::BlockHeader { + fn from(header: BlockHeader) -> Self { + (&header).into() + } +} + impl TryFrom<&block_header::BlockHeader> for BlockHeader { type Error = ConversionError; diff --git a/proto/src/domain/notes.rs b/proto/src/domain/notes.rs index 1ed040cef..893997499 100644 --- a/proto/src/domain/notes.rs +++ b/proto/src/domain/notes.rs @@ -5,11 +5,11 @@ use crate::generated::note; // NoteCreated // ================================================================================================ -impl From<(usize, usize, NoteEnvelope)> for note::NoteCreated { - fn from((batch_idx, note_idx, note): (usize, usize, NoteEnvelope)) -> Self { +impl From<&(usize, usize, NoteEnvelope)> for note::NoteCreated { + fn from((batch_idx, note_idx, note): &(usize, usize, NoteEnvelope)) -> Self { Self { - batch_index: batch_idx as u32, - note_index: note_idx as u32, + batch_index: *batch_idx as u32, + note_index: *note_idx as u32, note_id: Some(note.note_id().into()), sender: Some(note.metadata().sender().into()), tag: note.metadata().tag().into(), diff --git a/proto/src/generated/account.rs b/proto/src/generated/account.rs index 44ff6017b..c09f39298 100644 --- a/proto/src/generated/account.rs +++ b/proto/src/generated/account.rs @@ -13,7 +13,7 @@ pub struct AccountId { #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct AccountHashUpdate { +pub struct AccountSummary { #[prost(message, optional, tag = "1")] pub account_id: ::core::option::Option, #[prost(message, optional, tag = "2")] @@ -26,7 +26,7 @@ pub struct AccountHashUpdate { #[derive(Clone, PartialEq, ::prost::Message)] pub struct AccountInfo { #[prost(message, optional, tag = "1")] - pub update: ::core::option::Option, + pub summary: ::core::option::Option, #[prost(bytes = "vec", optional, tag = "2")] pub details: ::core::option::Option<::prost::alloc::vec::Vec>, } diff --git a/proto/src/generated/requests.rs b/proto/src/generated/requests.rs index c39110684..6a62c2f1a 100644 --- a/proto/src/generated/requests.rs +++ b/proto/src/generated/requests.rs @@ -7,6 +7,9 @@ pub struct AccountUpdate { pub account_id: ::core::option::Option, #[prost(message, optional, tag = "2")] pub account_hash: ::core::option::Option, + /// Details for public (on-chain) account. + #[prost(bytes = "vec", optional, tag = "3")] + pub details: ::core::option::Option<::prost::alloc::vec::Vec>, } #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/proto/src/generated/responses.rs b/proto/src/generated/responses.rs index c16613513..8caf31ad1 100644 --- a/proto/src/generated/responses.rs +++ b/proto/src/generated/responses.rs @@ -42,7 +42,7 @@ pub struct SyncStateResponse { pub mmr_delta: ::core::option::Option, /// a list of account hashes updated after `block_num + 1` but not after `block_header.block_num` #[prost(message, repeated, tag = "5")] - pub accounts: ::prost::alloc::vec::Vec, + pub accounts: ::prost::alloc::vec::Vec, /// a list of all notes together with the Merkle paths from `block_header.note_root` #[prost(message, repeated, tag = "6")] pub notes: ::prost::alloc::vec::Vec, diff --git a/rpc/README.md b/rpc/README.md index 28bfdf9cf..17e3e20df 100644 --- a/rpc/README.md +++ b/rpc/README.md @@ -82,8 +82,7 @@ Returns the latest state of an account with the specified ID. **Returns** -- `account`: `AccountInfo` – account state information. For public accounts there is also details describing current state, stored on-chain; - for private accounts only hash of the latest known state is returned. +- `account`: `AccountInfo` – latest state of the account. For public accounts, this will include full details describing the current account state. For private accounts, only the hash of the latest state and the time of the last update is returned. ### SyncState @@ -111,7 +110,7 @@ contains excessive notes and nullifiers, client can make additional filtering of - `chain_tip`: `uint32` – number of the latest block in the chain. - `block_header`: `BlockHeader` – block header of the block with the first note matching the specified criteria. - `mmr_delta`: `MmrDelta` – data needed to update the partial MMR from `block_num + 1` to `block_header.block_num`. -- `accounts`: `[AccountHashUpdate]` – a list of account hashes updated after `block_num + 1` but not after `block_header.block_num`. +- `accounts`: `[AccountSummary]` – account summaries for accounts updated after `block_num + 1` but not after `block_header.block_num`. - `notes`: `[NoteSyncRecord]` – a list of all notes together with the Merkle paths from `block_header.note_root`. - `nullifiers`: `[NullifierUpdate]` – a list of nullifiers created between `block_num + 1` and `block_header.block_num`. diff --git a/store/README.md b/store/README.md index ddcbb3bfb..e264b06d2 100644 --- a/store/README.md +++ b/store/README.md @@ -128,8 +128,7 @@ Returns the latest state of an account with the specified ID. **Returns** -- `account`: `AccountInfo` – account state information. For public accounts there is also details describing current state, stored on-chain; - for private accounts only hash of the latest known state is returned. +- `account`: `AccountInfo` – latest state of the account. For public accounts, this will include full details describing the current account state. For private accounts, only the hash of the latest state and the time of the last update is returned. ### SyncState @@ -157,7 +156,7 @@ contains excessive notes and nullifiers, client can make additional filtering of - `chain_tip`: `uint32` – number of the latest block in the chain. - `block_header`: `BlockHeader` – block header of the block with the first note matching the specified criteria. - `mmr_delta`: `MmrDelta` – data needed to update the partial MMR from `block_num + 1` to `block_header.block_num`. -- `accounts`: `[AccountHashUpdate]` – a list of account hashes updated after `block_num + 1` but not after `block_header.block_num`. +- `accounts`: `[AccountSummary]` – account summaries for accounts updated after `block_num + 1` but not after `block_header.block_num`. - `notes`: `[NoteSyncRecord]` – a list of all notes together with the Merkle paths from `block_header.note_root`. - `nullifiers`: `[NullifierUpdate]` – a list of nullifiers created between `block_num + 1` and `block_header.block_num`. diff --git a/store/src/db/mod.rs b/store/src/db/mod.rs index eaef75d60..52dd5ace1 100644 --- a/store/src/db/mod.rs +++ b/store/src/db/mod.rs @@ -1,12 +1,11 @@ use std::fs::{self, create_dir_all}; use deadpool_sqlite::{Config as SqliteConfig, Hook, HookError, Pool, Runtime}; -use miden_node_proto::domain::accounts::{AccountHashUpdate, AccountInfo}; +use miden_node_proto::domain::accounts::{AccountInfo, AccountSummary, AccountUpdateDetails}; use miden_objects::{ block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath, utils::Deserializable}, notes::{NoteId, Nullifier}, - transaction::AccountDetails, BlockHeader, GENESIS_BLOCK, }; use rusqlite::vtab::array; @@ -68,7 +67,7 @@ pub struct StateSyncUpdate { pub notes: Vec, pub block_header: BlockHeader, pub chain_tip: BlockNumber, - pub account_updates: Vec, + pub account_updates: Vec, pub nullifiers: Vec, } @@ -280,7 +279,7 @@ impl Db { block_header: BlockHeader, notes: Vec, nullifiers: Vec, - accounts: Vec<(AccountId, Option, RpoDigest)>, + accounts: Vec, ) -> Result<()> { self.pool .get() @@ -363,8 +362,14 @@ impl Db { let transaction = conn.transaction()?; let accounts: Vec<_> = account_smt .leaves() - .map(|(account_id, state_hash)| (account_id, None, state_hash.into())) - .collect(); + .map(|(account_id, state_hash)| { + Ok(AccountUpdateDetails { + account_id: account_id.try_into()?, + final_state_hash: state_hash.into(), + details: None, + }) + }) + .collect::>()?; sql::apply_block( &transaction, &expected_genesis_header, diff --git a/store/src/db/sql.rs b/store/src/db/sql.rs index 354bc666c..cbd4ee1f5 100644 --- a/store/src/db/sql.rs +++ b/store/src/db/sql.rs @@ -1,17 +1,21 @@ //! Wrapper functions for SQL statements. -use std::rc::Rc; +use std::{borrow::Cow, rc::Rc}; -use miden_node_proto::domain::accounts::{AccountHashUpdate, AccountInfo}; +use miden_node_proto::domain::accounts::{AccountInfo, AccountSummary, AccountUpdateDetails}; use miden_objects::{ - accounts::Account, + accounts::{Account, AccountDelta}, crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, notes::{NoteId, Nullifier}, transaction::AccountDetails, utils::serde::{Deserializable, Serializable}, BlockHeader, }; -use rusqlite::{params, types::Value, Connection, Transaction}; +use rusqlite::{ + params, + types::{Value, ValueRef}, + Connection, Transaction, +}; use super::{Note, NoteCreated, NullifierInfo, Result, StateSyncUpdate}; use crate::{ @@ -72,18 +76,18 @@ pub fn select_account_hashes(conn: &mut Connection) -> Result Result> { +) -> Result> { let account_ids: Vec = account_ids.iter().copied().map(u64_to_value).collect(); let mut stmt = conn.prepare( @@ -154,28 +158,58 @@ pub fn select_account( /// transaction. pub fn upsert_accounts( transaction: &Transaction, - accounts: &[(AccountId, Option, RpoDigest)], + accounts: &[AccountUpdateDetails], block_num: BlockNumber, ) -> Result { - let mut stmt = transaction.prepare( - " - INSERT OR REPLACE INTO - accounts (account_id, account_hash, block_num, details) - VALUES (?1, ?2, ?3, ?4); - ", + let mut upsert_stmt = transaction.prepare( + "INSERT OR REPLACE INTO accounts (account_id, account_hash, block_num, details) VALUES (?1, ?2, ?3, ?4);", )?; + let mut select_details_stmt = + transaction.prepare("SELECT details FROM accounts WHERE account_id = ?1;")?; let mut count = 0; - for (account_id, details, account_hash) in accounts.iter() { - // TODO: Process account details/delta (in the next PR) + for update in accounts.iter() { + let account_id = update.account_id.into(); + let full_account = match &update.details { + None => None, + Some(AccountDetails::Full(account)) => { + debug_assert!(account.is_new()); + debug_assert_eq!(account_id, u64::from(account.id())); + + if account.hash() != update.final_state_hash { + return Err(DatabaseError::ApplyBlockFailedAccountHashesMismatch { + calculated: account.hash(), + expected: update.final_state_hash, + }); + } + + Some(Cow::Borrowed(account)) + }, + Some(AccountDetails::Delta(delta)) => { + let mut rows = select_details_stmt.query(params![u64_to_value(account_id)])?; + let Some(row) = rows.next()? else { + return Err(DatabaseError::AccountNotFoundInDb(account_id)); + }; - count += stmt.execute(params![ - u64_to_value(*account_id), - account_hash.to_bytes(), + let account = + apply_delta(account_id, &row.get_ref(0)?, delta, &update.final_state_hash)?; + + Some(Cow::Owned(account)) + }, + }; + + let inserted = upsert_stmt.execute(params![ + u64_to_value(account_id), + update.final_state_hash.to_bytes(), block_num, - details.as_ref().map(|details| details.to_bytes()), - ])? + full_account.as_ref().map(|account| account.to_bytes()), + ])?; + + debug_assert_eq!(inserted, 1); + + count += inserted; } + Ok(count) } @@ -653,7 +687,7 @@ pub fn apply_block( block_header: &BlockHeader, notes: &[Note], nullifiers: &[Nullifier], - accounts: &[(AccountId, Option, RpoDigest)], + accounts: &[AccountUpdateDetails], ) -> Result { let mut count = 0; count += insert_block_header(transaction, block_header)?; @@ -699,16 +733,16 @@ fn column_value_as_u64( Ok(value as u64) } -/// Constructs `AccountHashUpdate` from the row of `accounts` table. +/// Constructs `AccountSummary` from the row of `accounts` table. /// /// Note: field ordering must be the same, as in `accounts` table! -fn account_hash_update_from_row(row: &rusqlite::Row<'_>) -> Result { +fn account_hash_update_from_row(row: &rusqlite::Row<'_>) -> Result { let account_id = column_value_as_u64(row, 0)?; let account_hash_data = row.get_ref(1)?.as_blob()?; let account_hash = RpoDigest::read_from_bytes(account_hash_data)?; let block_num = row.get(2)?; - Ok(AccountHashUpdate { + Ok(AccountSummary { account_id: account_id.try_into()?, account_hash, block_num, @@ -721,8 +755,38 @@ fn account_hash_update_from_row(row: &rusqlite::Row<'_>) -> Result) -> Result { let update = account_hash_update_from_row(row)?; - let details = row.get_ref(3)?.as_bytes_or_null()?; + let details = row.get_ref(3)?.as_blob_or_null()?; let details = details.map(Account::read_from_bytes).transpose()?; - Ok(AccountInfo { update, details }) + Ok(AccountInfo { + summary: update, + details, + }) +} + +/// Deserializes account and applies account delta. +fn apply_delta( + account_id: u64, + value: &ValueRef<'_>, + delta: &AccountDelta, + final_state_hash: &RpoDigest, +) -> Result { + let account = value.as_blob_or_null()?; + let account = account.map(Account::read_from_bytes).transpose()?; + + let Some(mut account) = account else { + return Err(DatabaseError::AccountNotOnChain(account_id)); + }; + + account.apply_delta(delta)?; + + let actual_hash = account.hash(); + if &actual_hash != final_state_hash { + return Err(DatabaseError::ApplyBlockFailedAccountHashesMismatch { + calculated: actual_hash, + expected: *final_state_hash, + }); + } + + Ok(account) } diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index af00ad4aa..db860ae27 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -1,12 +1,20 @@ -use miden_node_proto::domain::accounts::AccountHashUpdate; +use miden_lib::transaction::TransactionKernel; +use miden_node_proto::domain::accounts::{AccountSummary, AccountUpdateDetails}; use miden_objects::{ accounts::{ - AccountId, ACCOUNT_ID_OFF_CHAIN_SENDER, ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, + Account, AccountCode, AccountDelta, AccountId, AccountStorage, AccountStorageDelta, + AccountVaultDelta, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN, + ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_OFF_CHAIN_SENDER, + ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN, + ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN, }, + assembly::{Assembler, ModuleAst}, + assets::{Asset, AssetVault, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}, block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath}, notes::{NoteId, NoteMetadata, NoteType, Nullifier}, - BlockHeader, Felt, FieldElement, ZERO, + transaction::AccountDetails, + BlockHeader, Felt, FieldElement, Word, ONE, ZERO, }; use rusqlite::{vtab::array, Connection}; @@ -160,7 +168,6 @@ fn test_sql_select_accounts() { // test querying empty table let accounts = sql::select_accounts(&mut conn).unwrap(); assert!(accounts.is_empty()); - // test multiple entries let mut state = vec![]; for i in 0..10 { @@ -168,7 +175,7 @@ fn test_sql_select_accounts() { ACCOUNT_ID_REGULAR_ACCOUNT_UPDATABLE_CODE_OFF_CHAIN + (i << 32) + 0b1111100000; let account_hash = num_to_rpo_digest(i); state.push(AccountInfo { - update: AccountHashUpdate { + summary: AccountSummary { account_id: account_id.try_into().unwrap(), account_hash, block_num, @@ -177,8 +184,15 @@ fn test_sql_select_accounts() { }); let transaction = conn.transaction().unwrap(); - let res = - sql::upsert_accounts(&transaction, &[(account_id, None, account_hash)], block_num); + let res = sql::upsert_accounts( + &transaction, + &[AccountUpdateDetails { + account_id: account_id.try_into().unwrap(), + final_state_hash: account_hash, + details: None, + }], + block_num, + ); assert_eq!(res.unwrap(), 1, "One element must have been inserted"); transaction.commit().unwrap(); let accounts = sql::select_accounts(&mut conn).unwrap(); @@ -186,6 +200,134 @@ fn test_sql_select_accounts() { } } +#[test] +fn test_sql_public_account_details() { + let mut conn = create_db(); + + let block_num = 1; + create_block(&mut conn, block_num); + + let account_id = + AccountId::try_from(ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN).unwrap(); + let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + let non_fungible_faucet_id = + AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN).unwrap(); + + let mut storage = AccountStorage::new(vec![]).unwrap(); + storage.set_item(1, num_to_word(1)).unwrap(); + storage.set_item(3, num_to_word(3)).unwrap(); + storage.set_item(5, num_to_word(5)).unwrap(); + + let nft1 = Asset::NonFungible( + NonFungibleAsset::new( + &NonFungibleAssetDetails::new(non_fungible_faucet_id, vec![1, 2, 3]).unwrap(), + ) + .unwrap(), + ); + + let mut account = Account::new( + account_id, + AssetVault::new(&[ + Asset::Fungible(FungibleAsset::new(fungible_faucet_id, 150).unwrap()), + nft1, + ]) + .unwrap(), + storage, + mock_account_code(&TransactionKernel::assembler()), + ZERO, + ); + + // test querying empty table + let accounts_in_db = sql::select_accounts(&mut conn).unwrap(); + assert!(accounts_in_db.is_empty()); + + let transaction = conn.transaction().unwrap(); + let inserted = sql::upsert_accounts( + &transaction, + &[AccountUpdateDetails { + account_id, + final_state_hash: account.hash(), + details: Some(AccountDetails::Full(account.clone())), + }], + block_num, + ) + .unwrap(); + + assert_eq!(inserted, 1, "One element must have been inserted"); + + transaction.commit().unwrap(); + + let mut accounts_in_db = sql::select_accounts(&mut conn).unwrap(); + + assert_eq!(accounts_in_db.len(), 1, "One element must have been inserted"); + + let account_read = accounts_in_db.pop().unwrap().details.unwrap(); + assert_eq!(account_read, account); + + let storage_delta = AccountStorageDelta { + cleared_items: vec![3], + updated_items: vec![(4, num_to_word(5)), (5, num_to_word(6))], + }; + + let nft2 = Asset::NonFungible( + NonFungibleAsset::new( + &NonFungibleAssetDetails::new(non_fungible_faucet_id, vec![4, 5, 6]).unwrap(), + ) + .unwrap(), + ); + + let vault_delta = AccountVaultDelta { + added_assets: vec![nft2], + removed_assets: vec![nft1], + }; + + let delta = AccountDelta::new(storage_delta, vault_delta, Some(ONE)).unwrap(); + + account.apply_delta(&delta).unwrap(); + + let transaction = conn.transaction().unwrap(); + let inserted = sql::upsert_accounts( + &transaction, + &[AccountUpdateDetails { + account_id, + final_state_hash: account.hash(), + details: Some(AccountDetails::Delta(delta.clone())), + }], + block_num, + ) + .unwrap(); + + assert_eq!(inserted, 1, "One element must have been inserted"); + + transaction.commit().unwrap(); + + let mut accounts_in_db = sql::select_accounts(&mut conn).unwrap(); + + assert_eq!(accounts_in_db.len(), 1, "One element must have been inserted"); + + let mut account_read = accounts_in_db.pop().unwrap().details.unwrap(); + + assert_eq!(account_read.id(), account.id()); + assert_eq!(account_read.vault(), account.vault()); + assert_eq!(account_read.nonce(), account.nonce()); + + // Cleared item was not serialized, check it and apply delta only with clear item second time: + assert_eq!(account_read.storage().get_item(3), RpoDigest::default()); + + let storage_delta = AccountStorageDelta { + cleared_items: vec![3], + updated_items: vec![], + }; + account_read + .apply_delta( + &AccountDelta::new(storage_delta, AccountVaultDelta::default(), Some(Felt::new(2))) + .unwrap(), + ) + .unwrap(); + + assert_eq!(account_read.storage(), account.storage()); +} + #[test] fn test_sql_select_nullifiers_by_block_range() { let mut conn = create_db(); @@ -392,8 +534,16 @@ fn test_db_account() { let account_hash = num_to_rpo_digest(0); let transaction = conn.transaction().unwrap(); - let row_count = - sql::upsert_accounts(&transaction, &[(account_id, None, account_hash)], block_num).unwrap(); + let row_count = sql::upsert_accounts( + &transaction, + &[AccountUpdateDetails { + account_id: account_id.try_into().unwrap(), + final_state_hash: account_hash, + details: None, + }], + block_num, + ) + .unwrap(); transaction.commit().unwrap(); assert_eq!(row_count, 1); @@ -402,7 +552,7 @@ fn test_db_account() { let res = sql::select_accounts_by_block_range(&mut conn, 0, u32::MAX, &account_ids).unwrap(); assert_eq!( res, - vec![AccountHashUpdate { + vec![AccountSummary { account_id: account_id.try_into().unwrap(), account_hash, block_num, @@ -541,9 +691,24 @@ fn test_notes() { // UTILITIES // ------------------------------------------------------------------------------------------- fn num_to_rpo_digest(n: u64) -> RpoDigest { - RpoDigest::new([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(n)]) + RpoDigest::new(num_to_word(n)) +} + +fn num_to_word(n: u64) -> Word { + [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(n)] } fn num_to_nullifier(n: u64) -> Nullifier { Nullifier::from(num_to_rpo_digest(n)) } + +pub fn mock_account_code(assembler: &Assembler) -> AccountCode { + let account_code = "\ + export.account_procedure_1 + push.1.2 + add + end + "; + let account_module_ast = ModuleAst::parse(account_code).unwrap(); + AccountCode::new(account_module_ast, assembler).unwrap() +} diff --git a/store/src/errors.rs b/store/src/errors.rs index 4aa2d27fd..78e7e455d 100644 --- a/store/src/errors.rs +++ b/store/src/errors.rs @@ -3,6 +3,7 @@ use std::io; use deadpool_sqlite::PoolError; use miden_objects::{ crypto::{ + hash::rpo::RpoDigest, merkle::{MerkleError, MmrError}, utils::DeserializationError, }, @@ -48,10 +49,20 @@ pub enum DatabaseError { InteractError(String), #[error("Deserialization of BLOB data from database failed: {0}")] DeserializationError(DeserializationError), + #[error("Corrupted data: {0}")] + CorruptedData(String), #[error("Block applying was broken because of closed channel on state side: {0}")] ApplyBlockFailedClosedChannel(RecvError), #[error("Account {0} not found in the database")] AccountNotFoundInDb(AccountId), + #[error("Account {0} is not on the chain")] + AccountNotOnChain(AccountId), + #[error("Failed to apply block because of on-chain account final hashes mismatch (expected {expected}, \ + but calculated is {calculated}")] + ApplyBlockFailedAccountHashesMismatch { + expected: RpoDigest, + calculated: RpoDigest, + }, } impl From for DatabaseError { diff --git a/store/src/server/api.rs b/store/src/server/api.rs index 0ba6a7608..1b74daa84 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -2,10 +2,11 @@ use std::sync::Arc; use miden_node_proto::{ convert, + domain::accounts::AccountUpdateDetails, errors::ConversionError, generated::{ self, - account::AccountHashUpdate, + account::AccountSummary, note::NoteSyncRecord, requests::{ ApplyBlockRequest, CheckNullifiersRequest, GetAccountDetailsRequest, @@ -28,6 +29,8 @@ use miden_node_proto::{ use miden_objects::{ crypto::hash::rpo::RpoDigest, notes::{NoteId, Nullifier}, + transaction::AccountDetails, + utils::Deserializable, BlockHeader, Felt, ZERO, }; use tonic::{Response, Status}; @@ -127,7 +130,7 @@ impl api_server::Api for StoreApi { let accounts = state .account_updates .into_iter() - .map(|account_info| AccountHashUpdate { + .map(|account_info| AccountSummary { account_id: Some(account_info.account_id.into()), account_hash: Some(account_info.account_hash.into()), block_num: account_info.block_num, @@ -262,18 +265,38 @@ impl api_server::Api for StoreApi { let nullifiers = validate_nullifiers(&request.nullifiers)?; let accounts = request .accounts - .into_iter() + .iter() .map(|account_update| { let account_state: AccountState = account_update .try_into() .map_err(|err: ConversionError| Status::invalid_argument(err.to_string()))?; - Ok(( - account_state.account_id.into(), - None, // TODO: Process account details (next PR) - account_state + + match (account_state.account_id.is_on_chain(), account_update.details.is_some()) { + (true, false) => { + return Err(Status::invalid_argument("On-chain account must have details")); + }, + (false, true) => { + return Err(Status::invalid_argument( + "Off-chain account must not have details", + )); + }, + _ => (), + } + + let details = account_update + .details + .as_ref() + .map(|data| AccountDetails::read_from_bytes(data)) + .transpose() + .map_err(|err| Status::invalid_argument(err.to_string()))?; + + Ok(AccountUpdateDetails { + account_id: account_state.account_id, + details, + final_state_hash: account_state .account_hash .ok_or(invalid_argument("Account update missing account hash"))?, - )) + }) }) .collect::, Status>>()?; diff --git a/store/src/state.rs b/store/src/state.rs index 099964971..1eb02e860 100644 --- a/store/src/state.rs +++ b/store/src/state.rs @@ -4,7 +4,10 @@ //! data is atomically written, and that reads are consistent. use std::{mem, sync::Arc}; -use miden_node_proto::{domain::accounts::AccountInfo, AccountInputRecord, NullifierWitness}; +use miden_node_proto::{ + domain::accounts::{AccountInfo, AccountUpdateDetails}, + AccountInputRecord, NullifierWitness, +}; use miden_node_utils::formatting::{format_account_id, format_array}; use miden_objects::{ block::BlockNoteTree, @@ -13,7 +16,6 @@ use miden_objects::{ merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, SimpleSmt, SmtProof, ValuePath}, }, notes::{NoteId, NoteMetadata, NoteType, Nullifier}, - transaction::AccountDetails, AccountError, BlockHeader, NoteError, ACCOUNT_TREE_DEPTH, ZERO, }; use tokio::{ @@ -107,7 +109,7 @@ impl State { &self, block_header: BlockHeader, nullifiers: Vec, - accounts: Vec<(AccountId, Option, RpoDigest)>, + accounts: Vec, notes: Vec, ) -> Result<(), ApplyBlockError> { let _ = self.writer.try_lock().map_err(|_| ApplyBlockError::ConcurrentWrite)?; @@ -181,8 +183,11 @@ impl State { // update account tree let mut account_tree = inner.account_tree.clone(); - for (account_id, _details, account_hash) in accounts.iter() { - account_tree.insert(LeafIndex::new_max_depth(*account_id), account_hash.into()); + for update in &accounts { + account_tree.insert( + LeafIndex::new_max_depth(update.account_id.into()), + update.final_state_hash.into(), + ); } if account_tree.root() != block_header.account_root() { From 0b998195c90faf49becf0f8982892a539c52b883 Mon Sep 17 00:00:00 2001 From: igamigo Date: Tue, 9 Apr 2024 14:55:20 -0300 Subject: [PATCH 19/29] fix: Make code compatible with `miden-base`'s `next` (#307) * Refactor dependency usage * allow(clippy::blocks_in_conditions)] --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- README.md | 2 +- block-producer/src/batch_builder/batch.rs | 10 +++++++--- block-producer/src/batch_builder/mod.rs | 6 ++++++ block-producer/src/block_builder/mod.rs | 3 +++ block-producer/src/block_builder/prover/tests.rs | 11 +++++++---- block-producer/src/server/api.rs | 3 +++ block-producer/src/state_view/mod.rs | 6 ++++++ block-producer/src/store/mod.rs | 6 ++++++ block-producer/src/test_utils/batch.rs | 4 +++- block-producer/src/test_utils/block.rs | 4 ++-- block-producer/src/test_utils/proven_tx.rs | 15 +++++++-------- proto/src/domain/notes.rs | 2 +- rpc/src/server/api.rs | 3 +++ rust-toolchain | 2 +- store/src/server/api.rs | 3 +++ utils/src/formatting.rs | 10 +++++----- 18 files changed, 68 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 67ce1e165..ad53479a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1672,7 +1672,7 @@ dependencies = [ [[package]] name = "miden-lib" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#1d32f951e29c2e12526520f734aaba02555e8f79" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#60e3f9bcebabfcb1c9755b46e241af543065251b" dependencies = [ "miden-assembly 0.9.1", "miden-objects 0.2.0", @@ -1890,7 +1890,7 @@ dependencies = [ [[package]] name = "miden-objects" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#1d32f951e29c2e12526520f734aaba02555e8f79" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#60e3f9bcebabfcb1c9755b46e241af543065251b" dependencies = [ "miden-assembly 0.9.1", "miden-core 0.9.1", @@ -1982,7 +1982,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#1d32f951e29c2e12526520f734aaba02555e8f79" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#60e3f9bcebabfcb1c9755b46e241af543065251b" dependencies = [ "miden-lib 0.2.0", "miden-objects 0.2.0", diff --git a/Cargo.toml b/Cargo.toml index 02576560d..7ee40369c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ resolver = "2" [workspace.package] edition = "2021" -rust-version = "1.75" +rust-version = "1.77" license = "MIT" authors = ["Miden contributors"] readme = "README.md" diff --git a/README.md b/README.md index d21ba47c0..5174451a6 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The diagram below illustrates high-level design of each component as well as bas ## Usage -Before you can build and run the Miden node or any of its components, you'll need to make sure you have Rust [installed](https://www.rust-lang.org/tools/install). Miden node v0.1 requires Rust version **1.75** or later. +Before you can build and run the Miden node or any of its components, you'll need to make sure you have Rust [installed](https://www.rust-lang.org/tools/install). Miden node v0.1 requires Rust version **1.77** or later. Depending on the platform, you may need to install additional libraries. For example, on Ubuntu 22.04 the following command ensures that all required libraries are installed. diff --git a/block-producer/src/batch_builder/batch.rs b/block-producer/src/batch_builder/batch.rs index 9f6b550bd..be5861d2e 100644 --- a/block-producer/src/batch_builder/batch.rs +++ b/block-producer/src/batch_builder/batch.rs @@ -65,8 +65,12 @@ impl TransactionBatch { txs.iter().flat_map(|tx| tx.input_notes().iter()).cloned().collect(); let (created_notes, created_notes_smt) = { - let created_notes: Vec = - txs.iter().flat_map(|tx| tx.output_notes().iter()).cloned().collect(); + let created_notes: Vec = txs + .iter() + .flat_map(|tx| tx.output_notes().iter()) + .cloned() + .map(NoteEnvelope::from) + .collect(); if created_notes.len() > MAX_NOTES_PER_BATCH { return Err(BuildBatchError::TooManyNotesCreated(created_notes.len(), txs)); @@ -78,7 +82,7 @@ impl TransactionBatch { BatchNoteTree::with_contiguous_leaves( created_notes .iter() - .map(|note_envelope| (note_envelope.note_id(), note_envelope.metadata())), + .map(|note_envelope| (note_envelope.id(), note_envelope.metadata())), ) .map_err(|e| BuildBatchError::NotesSmtError(e, txs))?, ) diff --git a/block-producer/src/batch_builder/mod.rs b/block-producer/src/batch_builder/mod.rs index cd2ec454a..77ab57e03 100644 --- a/block-producer/src/batch_builder/mod.rs +++ b/block-producer/src/batch_builder/mod.rs @@ -57,6 +57,9 @@ pub struct DefaultBatchBuilder { options: DefaultBatchBuilderOptions, } +// FIXME: remove the allow when the upstream clippy issue is fixed: +// https://github.com/rust-lang/rust-clippy/issues/12281 +#[allow(clippy::blocks_in_conditions)] impl DefaultBatchBuilder where BB: BlockBuilder, @@ -117,6 +120,9 @@ where } } +// FIXME: remove the allow when the upstream clippy issue is fixed: +// https://github.com/rust-lang/rust-clippy/issues/12281 +#[allow(clippy::blocks_in_conditions)] #[async_trait] impl BatchBuilder for DefaultBatchBuilder where diff --git a/block-producer/src/block_builder/mod.rs b/block-producer/src/block_builder/mod.rs index eb92612b4..09fbdb7c8 100644 --- a/block-producer/src/block_builder/mod.rs +++ b/block-producer/src/block_builder/mod.rs @@ -60,6 +60,9 @@ where } } +// FIXME: remove the allow when the upstream clippy issue is fixed: +// https://github.com/rust-lang/rust-clippy/issues/12281 +#[allow(clippy::blocks_in_conditions)] #[async_trait] impl BlockBuilder for DefaultBlockBuilder where diff --git a/block-producer/src/block_builder/prover/tests.rs b/block-producer/src/block_builder/prover/tests.rs index d7b3a292b..93c5dc080 100644 --- a/block-producer/src/block_builder/prover/tests.rs +++ b/block-producer/src/block_builder/prover/tests.rs @@ -10,6 +10,7 @@ use miden_objects::{ SMT_DEPTH, }, notes::{NoteEnvelope, NoteMetadata, NoteType}, + transaction::OutputNote, BLOCK_OUTPUT_NOTES_TREE_DEPTH, ONE, ZERO, }; @@ -393,6 +394,7 @@ async fn test_compute_note_root_success() { note_digest.into(), NoteMetadata::new(account_id, NoteType::OffChain, 0.into(), ONE).unwrap(), ) + .expect("Hardcoded values should not fail") }) .collect(); @@ -413,8 +415,9 @@ async fn test_compute_note_root_success() { .iter() .zip(account_ids.iter()) .map(|(note, &account_id)| { + let note = OutputNote::Private(*note); MockProvenTxBuilder::with_account(account_id, Digest::default(), Digest::default()) - .notes_created(vec![*note]) + .notes_created(vec![note]) .build() }) .collect(); @@ -441,11 +444,11 @@ async fn test_compute_note_root_success() { // The first 2 txs were put in the first batch; the 3rd was put in the second. It will lie in // the second subtree of depth 12 let notes_smt = SimpleSmt::::with_leaves(vec![ - (0u64, notes_created[0].note_id().into()), + (0u64, notes_created[0].id().into()), (1u64, notes_created[0].metadata().into()), - (2u64, notes_created[1].note_id().into()), + (2u64, notes_created[1].id().into()), (3u64, notes_created[1].metadata().into()), - (2u64.pow(13), notes_created[2].note_id().into()), + (2u64.pow(13), notes_created[2].id().into()), (2u64.pow(13) + 1, notes_created[2].metadata().into()), ]) .unwrap(); diff --git a/block-producer/src/server/api.rs b/block-producer/src/server/api.rs index 192dd14e4..98b517716 100644 --- a/block-producer/src/server/api.rs +++ b/block-producer/src/server/api.rs @@ -29,6 +29,9 @@ impl BlockProducerApi { } } +// FIXME: remove the allow when the upstream clippy issue is fixed: +// https://github.com/rust-lang/rust-clippy/issues/12281 +#[allow(clippy::blocks_in_conditions)] #[tonic::async_trait] impl api_server::Api for BlockProducerApi where diff --git a/block-producer/src/state_view/mod.rs b/block-producer/src/state_view/mod.rs index 89fde23a3..17f875318 100644 --- a/block-producer/src/state_view/mod.rs +++ b/block-producer/src/state_view/mod.rs @@ -52,6 +52,9 @@ where } } +// FIXME: remove the allow when the upstream clippy issue is fixed: +// https://github.com/rust-lang/rust-clippy/issues/12281 +#[allow(clippy::blocks_in_conditions)] #[async_trait] impl TransactionValidator for DefaultStateView where @@ -111,6 +114,9 @@ where } } +// FIXME: remove the allow when the upstream clippy issue is fixed: +// https://github.com/rust-lang/rust-clippy/issues/12281 +#[allow(clippy::blocks_in_conditions)] #[async_trait] impl ApplyBlock for DefaultStateView where diff --git a/block-producer/src/store/mod.rs b/block-producer/src/store/mod.rs index 2407f6acd..def2d231b 100644 --- a/block-producer/src/store/mod.rs +++ b/block-producer/src/store/mod.rs @@ -126,6 +126,9 @@ impl DefaultStore { } } +// FIXME: remove the allow when the upstream clippy issue is fixed: +// https://github.com/rust-lang/rust-clippy/issues/12281 +#[allow(clippy::blocks_in_conditions)] #[async_trait] impl ApplyBlock for DefaultStore { #[instrument(target = "miden-block-producer", skip_all, err)] @@ -151,6 +154,9 @@ impl ApplyBlock for DefaultStore { } } +// FIXME: remove the allow when the upstream clippy issue is fixed: +// https://github.com/rust-lang/rust-clippy/issues/12281 +#[allow(clippy::blocks_in_conditions)] #[async_trait] impl Store for DefaultStore { #[instrument(target = "miden-block-producer", skip_all, err)] diff --git a/block-producer/src/test_utils/batch.rs b/block-producer/src/test_utils/batch.rs index 26c779d59..c2ab8c57d 100644 --- a/block-producer/src/test_utils/batch.rs +++ b/block-producer/src/test_utils/batch.rs @@ -26,7 +26,9 @@ impl TransactionBatchConstructor for TransactionBatch { .map(|(index, &num_notes)| { let starting_note_index = starting_account_index as u64 + index as u64; MockProvenTxBuilder::with_account_index(starting_account_index + index as u32) - .notes_created_range(starting_note_index..(starting_note_index + num_notes)) + .private_notes_created_range( + starting_note_index..(starting_note_index + num_notes), + ) .build() }) .collect(); diff --git a/block-producer/src/test_utils/block.rs b/block-producer/src/test_utils/block.rs index f3d877273..6bbbcd14d 100644 --- a/block-producer/src/test_utils/block.rs +++ b/block-producer/src/test_utils/block.rs @@ -168,7 +168,7 @@ pub(crate) fn note_created_smt_from_envelopes( note_iterator: impl Iterator ) -> BlockNoteTree { BlockNoteTree::with_entries(note_iterator.map(|(batch_idx, note_idx_in_batch, note)| { - (batch_idx, note_idx_in_batch, (note.note_id().into(), *note.metadata())) + (batch_idx, note_idx_in_batch, (note.id().into(), *note.metadata())) })) .unwrap() } @@ -178,7 +178,7 @@ pub(crate) fn note_created_smt_from_batches<'a>( ) -> BlockNoteTree { let note_leaf_iterator = batches.enumerate().flat_map(|(batch_idx, batch)| { batch.created_notes().enumerate().map(move |(note_idx_in_batch, note)| { - (batch_idx, note_idx_in_batch, (note.note_id().into(), *note.metadata())) + (batch_idx, note_idx_in_batch, (note.id().into(), *note.metadata())) }) }); diff --git a/block-producer/src/test_utils/proven_tx.rs b/block-producer/src/test_utils/proven_tx.rs index 51d1813a0..316a04084 100644 --- a/block-producer/src/test_utils/proven_tx.rs +++ b/block-producer/src/test_utils/proven_tx.rs @@ -4,7 +4,7 @@ use miden_air::HashFunction; use miden_objects::{ accounts::AccountId, notes::{NoteEnvelope, NoteMetadata, NoteType, Nullifier}, - transaction::{ProvenTransaction, ProvenTransactionBuilder}, + transaction::{OutputNote, ProvenTransaction, ProvenTransactionBuilder}, vm::ExecutionProof, Digest, Felt, Hasher, ONE, }; @@ -16,7 +16,7 @@ pub struct MockProvenTxBuilder { account_id: AccountId, initial_account_hash: Digest, final_account_hash: Digest, - notes_created: Option>, + notes_created: Option>, nullifiers: Option>, } @@ -52,7 +52,7 @@ impl MockProvenTxBuilder { pub fn notes_created( mut self, - notes: Vec, + notes: Vec, ) -> Self { self.notes_created = Some(notes); @@ -74,18 +74,17 @@ impl MockProvenTxBuilder { self.nullifiers(nullifiers) } - pub fn notes_created_range( + pub fn private_notes_created_range( self, range: Range, ) -> Self { let notes = range .map(|note_index| { let note_hash = Hasher::hash(¬e_index.to_be_bytes()); + let note_metadata = + NoteMetadata::new(self.account_id, NoteType::OffChain, 0.into(), ONE).unwrap(); - NoteEnvelope::new( - note_hash.into(), - NoteMetadata::new(self.account_id, NoteType::OffChain, 0.into(), ONE).unwrap(), - ) + OutputNote::Private(NoteEnvelope::new(note_hash.into(), note_metadata).unwrap()) }) .collect(); diff --git a/proto/src/domain/notes.rs b/proto/src/domain/notes.rs index 893997499..b60978aa2 100644 --- a/proto/src/domain/notes.rs +++ b/proto/src/domain/notes.rs @@ -10,7 +10,7 @@ impl From<&(usize, usize, NoteEnvelope)> for note::NoteCreated { Self { batch_index: *batch_idx as u32, note_index: *note_idx as u32, - note_id: Some(note.note_id().into()), + note_id: Some(note.id().into()), sender: Some(note.metadata().sender().into()), tag: note.metadata().tag().into(), } diff --git a/rpc/src/server/api.rs b/rpc/src/server/api.rs index 35818f1ea..3de3dde8f 100644 --- a/rpc/src/server/api.rs +++ b/rpc/src/server/api.rs @@ -56,6 +56,9 @@ impl RpcApi { } } +// FIXME: remove the allow when the upstream clippy issue is fixed: +// https://github.com/rust-lang/rust-clippy/issues/12281 +#[allow(clippy::blocks_in_conditions)] #[tonic::async_trait] impl api_server::Api for RpcApi { #[instrument( diff --git a/rust-toolchain b/rust-toolchain index 07cde9840..3245dca3d 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.75 +1.77 diff --git a/store/src/server/api.rs b/store/src/server/api.rs index 1b74daa84..0628dc85a 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -45,6 +45,9 @@ pub struct StoreApi { pub(super) state: Arc, } +// FIXME: remove the allow when the upstream clippy issue is fixed: +// https://github.com/rust-lang/rust-clippy/issues/12281 +#[allow(clippy::blocks_in_conditions)] #[tonic::async_trait] impl api_server::Api for StoreApi { // CLIENT ENDPOINTS diff --git a/utils/src/formatting.rs b/utils/src/formatting.rs index 5074e3405..36a65ded0 100644 --- a/utils/src/formatting.rs +++ b/utils/src/formatting.rs @@ -6,7 +6,7 @@ use miden_objects::{ hash::{blake::Blake3Digest, Digest}, utils::bytes_to_hex_string, }, - notes::{NoteEnvelope, Nullifier}, + notes::Nullifier, transaction::{InputNotes, OutputNotes}, }; @@ -22,12 +22,12 @@ pub fn format_input_notes(notes: &InputNotes) -> String { format_array(notes.iter().map(Nullifier::to_hex)) } -pub fn format_output_notes(notes: &OutputNotes) -> String { - format_array(notes.iter().map(|envelope| { - let metadata = envelope.metadata(); +pub fn format_output_notes(notes: &OutputNotes) -> String { + format_array(notes.iter().map(|output_note| { + let metadata = output_note.metadata(); format!( "{{ note_id: {}, note_metadata: {{sender: {}, tag: {} }}}}", - envelope.note_id().to_hex(), + output_note.id().to_hex(), metadata.sender(), metadata.tag(), ) From cf8e4fc1fcbf54b2a9971c58c00039378317d8da Mon Sep 17 00:00:00 2001 From: Martin Fraga Date: Wed, 10 Apr 2024 02:19:12 -0300 Subject: [PATCH 20/29] fix: fix create_basic_fungible_faucet and create_basic_wallet errors (#308) --- Cargo.lock | 10 +++++----- node/src/commands/genesis/mod.rs | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad53479a5..37e219b7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -976,9 +976,9 @@ checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "figment" -version = "0.10.15" +version = "0.10.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7270677e7067213e04f323b55084586195f18308cd7546cfac9f873344ccceb6" +checksum = "fdefe49ed1057d124dc81a0681c30dd07de56ad96e32adc7b64e8f28eaab31c4" dependencies = [ "atomic", "parking_lot", @@ -1672,7 +1672,7 @@ dependencies = [ [[package]] name = "miden-lib" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#60e3f9bcebabfcb1c9755b46e241af543065251b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#9830b5a3f2fe5ddb09d0b45342ecdf956ea2a7ae" dependencies = [ "miden-assembly 0.9.1", "miden-objects 0.2.0", @@ -1890,7 +1890,7 @@ dependencies = [ [[package]] name = "miden-objects" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#60e3f9bcebabfcb1c9755b46e241af543065251b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#9830b5a3f2fe5ddb09d0b45342ecdf956ea2a7ae" dependencies = [ "miden-assembly 0.9.1", "miden-core 0.9.1", @@ -1982,7 +1982,7 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#60e3f9bcebabfcb1c9755b46e241af543065251b" +source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#9830b5a3f2fe5ddb09d0b45342ecdf956ea2a7ae" dependencies = [ "miden-lib 0.2.0", "miden-objects 0.2.0", diff --git a/node/src/commands/genesis/mod.rs b/node/src/commands/genesis/mod.rs index f05fb9fb2..d1b21f2d5 100644 --- a/node/src/commands/genesis/mod.rs +++ b/node/src/commands/genesis/mod.rs @@ -12,7 +12,7 @@ use miden_lib::{ use miden_node_store::genesis::GenesisState; use miden_node_utils::config::load_config; use miden_objects::{ - accounts::{Account, AccountData, AccountType, AuthData}, + accounts::{Account, AccountData, AccountStorageType, AccountType, AuthData}, assets::TokenSymbol, crypto::{ dsa::rpo_falcon512::SecretKey, @@ -134,6 +134,7 @@ fn create_accounts( init_seed, auth_scheme, AccountType::RegularAccountImmutableCode, + AccountStorageType::OffChain, )?; AccountData::new(account, Some(account_seed), auth_info) @@ -151,6 +152,7 @@ fn create_accounts( inputs.decimals, Felt::try_from(inputs.max_supply) .expect("max supply value is greater than or equal to the field modulus"), + AccountStorageType::OffChain, auth_scheme, )?; From 516881b424d80d97315e368d741a2029eacc199a Mon Sep 17 00:00:00 2001 From: Paul-Henry Kajfasz <42912740+phklive@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:39:46 +0300 Subject: [PATCH 21/29] Add support for on-chain `notes` to the database (#300) * Getting notes by id works * lint and test * Improved comments + Added conversion from digest::Digest to NoteId * Added rpc validation for NoteId's * Added documentation to Readmes * Lint * Added details field to database * Fixed ci problems * Added details to Note type * Added test for endpoint + refactored sql query + improved documentation * Fixed lint * Fixed ci with test problem * lint * Fix nit in migration + add dabase to gitignore * Order rpc and store endpoints alphabitically * Change name to database to prevent gitignore problems * generated * Changed serialized None to Null + added test * Added documentation * chore: force refresh of readme files * chore: remove duplicate GetNoteById descriptions from readme files --------- Co-authored-by: Bobbin Threadbare --- .github/workflows/test.yml | 2 +- Makefile.toml | 2 +- node/miden-node.toml | 2 +- proto/proto/note.proto | 6 ++++++ proto/src/domain/notes.rs | 5 +++++ proto/src/generated/note.rs | 8 ++++++++ store/src/db/migrations.rs | 1 + store/src/db/mod.rs | 1 + store/src/db/sql.rs | 28 +++++++++++++++++++++++----- store/src/db/tests.rs | 10 ++++++++++ store/src/server/api.rs | 3 +++ 11 files changed, 60 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 625bd9487..7a4dd048c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,4 +20,4 @@ jobs: - name: Install cargo make run: cargo install cargo-make - name: cargo make - test - run: cargo make test + run: cargo make test-all diff --git a/Makefile.toml b/Makefile.toml index 2266170e7..ea8494c41 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -44,7 +44,7 @@ dependencies = [ ] # testing -[tasks.test] +[tasks.test-all] command = "cargo" args = ["test", "--all-features", "--workspace", "--", "--nocapture"] diff --git a/node/miden-node.toml b/node/miden-node.toml index 04f582981..527591c0f 100644 --- a/node/miden-node.toml +++ b/node/miden-node.toml @@ -17,5 +17,5 @@ store_url = "http://localhost:28943" [store] # port defined as: sum(ord(c)**p for (p, c) in enumerate('miden-store', 1)) % 2**16 endpoint = { host = "localhost", port = 28943 } -database_filepath = "db/miden-store.sqlite3" +database_filepath = "miden-store.sqlite3" genesis_filepath = "genesis.dat" diff --git a/proto/proto/note.proto b/proto/proto/note.proto index 4bab3eefe..acedd8283 100644 --- a/proto/proto/note.proto +++ b/proto/proto/note.proto @@ -12,6 +12,9 @@ message Note { account.AccountId sender = 4; fixed64 tag = 5; merkle.MerklePath merkle_path = 7; + // This field will be present when the note is on-chain. + // details contain the `Note` in a serialized format. + optional bytes details = 8; } message NoteSyncRecord { @@ -28,4 +31,7 @@ message NoteCreated { digest.Digest note_id = 3; account.AccountId sender = 4; fixed64 tag = 5; + // This field will be present when the note is on-chain. + // details contain the `Note` in a serialized format. + optional bytes details = 6; } diff --git a/proto/src/domain/notes.rs b/proto/src/domain/notes.rs index b60978aa2..5d871171a 100644 --- a/proto/src/domain/notes.rs +++ b/proto/src/domain/notes.rs @@ -13,6 +13,11 @@ impl From<&(usize, usize, NoteEnvelope)> for note::NoteCreated { note_id: Some(note.id().into()), sender: Some(note.metadata().sender().into()), tag: note.metadata().tag().into(), + // This is `None` for now as this conversion is used by the block-producer + // when using `apply_block`. The block-producer has not yet been updated to support + // on-chain notes. Will be set to the correct value when the block-producer is + // up-to-date. + details: None, } } } diff --git a/proto/src/generated/note.rs b/proto/src/generated/note.rs index 172755108..d1d65d4be 100644 --- a/proto/src/generated/note.rs +++ b/proto/src/generated/note.rs @@ -15,6 +15,10 @@ pub struct Note { pub tag: u64, #[prost(message, optional, tag = "7")] pub merkle_path: ::core::option::Option, + /// This field will be present when the note is on-chain. + /// details contain the `Note` in a serialized format. + #[prost(bytes = "vec", optional, tag = "8")] + pub details: ::core::option::Option<::prost::alloc::vec::Vec>, } #[derive(Eq, PartialOrd, Ord, Hash)] #[allow(clippy::derive_partial_eq_without_eq)] @@ -45,4 +49,8 @@ pub struct NoteCreated { pub sender: ::core::option::Option, #[prost(fixed64, tag = "5")] pub tag: u64, + /// This field will be present when the note is on-chain. + /// details contain the `Note` in a serialized format. + #[prost(bytes = "vec", optional, tag = "6")] + pub details: ::core::option::Option<::prost::alloc::vec::Vec>, } diff --git a/store/src/db/migrations.rs b/store/src/db/migrations.rs index 1f8d1226d..8200ed84a 100644 --- a/store/src/db/migrations.rs +++ b/store/src/db/migrations.rs @@ -24,6 +24,7 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { sender INTEGER NOT NULL, tag INTEGER NOT NULL, merkle_path BLOB NOT NULL, + details BLOB, PRIMARY KEY (block_num, batch_index, note_index), CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers (block_num), diff --git a/store/src/db/mod.rs b/store/src/db/mod.rs index 52dd5ace1..8d1168599 100644 --- a/store/src/db/mod.rs +++ b/store/src/db/mod.rs @@ -45,6 +45,7 @@ pub struct NoteCreated { pub note_id: RpoDigest, pub sender: AccountId, pub tag: u64, + pub details: Option>, } impl NoteCreated { diff --git a/store/src/db/sql.rs b/store/src/db/sql.rs index cbd4ee1f5..541af29a2 100644 --- a/store/src/db/sql.rs +++ b/store/src/db/sql.rs @@ -332,7 +332,8 @@ pub fn select_notes(conn: &mut Connection) -> Result> { note_hash, sender, tag, - merkle_path + merkle_path, + details FROM notes ORDER BY @@ -349,6 +350,9 @@ pub fn select_notes(conn: &mut Connection) -> Result> { let merkle_path_data = row.get_ref(6)?.as_blob()?; let merkle_path = MerklePath::read_from_bytes(merkle_path_data)?; + let details_data = row.get_ref(7)?.as_blob_or_null()?; + let details = details_data.map(>::read_from_bytes).transpose()?; + notes.push(Note { block_num: row.get(0)?, note_created: NoteCreated { @@ -357,6 +361,7 @@ pub fn select_notes(conn: &mut Connection) -> Result> { note_id, sender: column_value_as_u64(row, 4)?, tag: column_value_as_u64(row, 5)?, + details, }, merkle_path, }) @@ -389,16 +394,19 @@ pub fn insert_notes( note_hash, sender, tag, - merkle_path + merkle_path, + details ) VALUES ( - ?1, ?2, ?3, ?4, ?5, ?6, ?7 + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8 );", )?; let mut count = 0; for note in notes.iter() { + let details = note.note_created.details.as_ref().map(|details| details.to_bytes()); + count += stmt.execute(params![ note.block_num, note.note_created.batch_index, @@ -407,6 +415,7 @@ pub fn insert_notes( u64_to_value(note.note_created.sender), u64_to_value(note.note_created.tag), note.merkle_path.to_bytes(), + details ])?; } @@ -443,7 +452,8 @@ pub fn select_notes_since_block_by_tag_and_sender( note_hash, sender, tag, - merkle_path + merkle_path, + details FROM notes WHERE @@ -478,6 +488,8 @@ pub fn select_notes_since_block_by_tag_and_sender( let tag = column_value_as_u64(row, 5)?; let merkle_path_data = row.get_ref(6)?.as_blob()?; let merkle_path = MerklePath::read_from_bytes(merkle_path_data)?; + let details_data = row.get_ref(7)?.as_blob_or_null()?; + let details = details_data.map(>::read_from_bytes).transpose()?; let note = Note { block_num, @@ -487,6 +499,7 @@ pub fn select_notes_since_block_by_tag_and_sender( note_id, sender, tag, + details, }, merkle_path, }; @@ -516,7 +529,8 @@ pub fn select_notes_by_id( note_hash, sender, tag, - merkle_path + merkle_path, + details FROM notes WHERE @@ -533,11 +547,15 @@ pub fn select_notes_by_id( let merkle_path_data = row.get_ref(6)?.as_blob()?; let merkle_path = MerklePath::read_from_bytes(merkle_path_data)?; + let details_data = row.get_ref(7)?.as_blob_or_null()?; + let details = details_data.map(>::read_from_bytes).transpose()?; + notes.push(Note { block_num: row.get(0)?, note_created: NoteCreated { batch_index: row.get(1)?, note_index: row.get(2)?, + details, note_id: note_id.into(), sender: column_value_as_u64(row, 4)?, tag: column_value_as_u64(row, 5)?, diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index db860ae27..f2ecec053 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -144,6 +144,7 @@ fn test_sql_select_notes() { note_id: num_to_rpo_digest(i as u64), sender: i as u64, tag: i as u64, + details: Some(vec![1, 2, 3]), }, merkle_path: MerklePath::new(vec![]), }; @@ -596,6 +597,7 @@ fn test_notes() { let values = [(batch_index as usize, note_index as usize, (note_id, note_metadata))]; let notes_db = BlockNoteTree::with_entries(values.iter().cloned()).unwrap(); + let details = Some(vec![1, 2, 3]); let merkle_path = notes_db.get_note_path(batch_index as usize, note_index as usize).unwrap(); let note = Note { @@ -606,6 +608,7 @@ fn test_notes() { note_id, sender: sender.into(), tag, + details, }, merkle_path: merkle_path.clone(), }; @@ -650,6 +653,7 @@ fn test_notes() { note_id: num_to_rpo_digest(3), sender: note.note_created.sender, tag: note.note_created.tag, + details: None, }, merkle_path, }; @@ -686,6 +690,12 @@ fn test_notes() { let res = sql::select_notes_by_id(&mut conn, ¬e_ids).unwrap(); assert_eq!(res, notes); + + // test notes have correct details + let note_0 = res[0].clone(); + let note_1 = res[1].clone(); + assert_eq!(note_0.note_created.details, Some(vec![1, 2, 3])); + assert_eq!(note_1.note_created.details, None) } // UTILITIES diff --git a/store/src/server/api.rs b/store/src/server/api.rs index 0628dc85a..c67372be5 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -207,6 +207,7 @@ impl api_server::Api for StoreApi { sender: Some(note.note_created.sender.into()), tag: note.note_created.tag, merkle_path: Some(note.merkle_path.into()), + details: note.note_created.details, }) .collect(); @@ -319,6 +320,7 @@ impl api_server::Api for StoreApi { })?, sender: note.sender.ok_or(invalid_argument("Note missing sender"))?.into(), tag: note.tag, + details: note.details, }) }) .collect::, Status>>()?; @@ -446,6 +448,7 @@ impl api_server::Api for StoreApi { sender: Some(note.note_created.sender.into()), tag: note.note_created.tag, merkle_path: Some(note.merkle_path.into()), + details: note.note_created.details, }) .collect(); Ok(Response::new(ListNotesResponse { notes })) From 04c655f713d786e61611e6549af7ee5df3294d73 Mon Sep 17 00:00:00 2001 From: Martin Fraga Date: Wed, 10 Apr 2024 18:00:06 -0300 Subject: [PATCH 22/29] fix: remove assert (#313) --- store/src/db/sql.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/store/src/db/sql.rs b/store/src/db/sql.rs index 541af29a2..5b477a609 100644 --- a/store/src/db/sql.rs +++ b/store/src/db/sql.rs @@ -173,7 +173,6 @@ pub fn upsert_accounts( let full_account = match &update.details { None => None, Some(AccountDetails::Full(account)) => { - debug_assert!(account.is_new()); debug_assert_eq!(account_id, u64::from(account.id())); if account.hash() != update.final_state_hash { From 0c831fbf7f8b910ca9ebc0c25e2f12005d095f04 Mon Sep 17 00:00:00 2001 From: Paul-Henry Kajfasz <42912740+phklive@users.noreply.github.com> Date: Fri, 12 Apr 2024 01:05:58 +0300 Subject: [PATCH 23/29] feat: add support for on-chain notes to `block-producer` (#310) Co-authored-by: igamigo Co-authored-by: polydez <155382956+polydez@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- Cargo.lock | 69 ++++++++++--------- Cargo.toml | 6 +- block-producer/src/batch_builder/batch.rs | 63 ++++++++--------- block-producer/src/block.rs | 7 +- block-producer/src/block_builder/mod.rs | 13 +--- .../src/block_builder/prover/block_witness.rs | 9 +-- block-producer/src/store/mod.rs | 34 ++++++++- block-producer/src/test_utils/block.rs | 43 ++++-------- block-producer/src/test_utils/store.rs | 16 ++--- node/src/commands/genesis/mod.rs | 2 +- proto/proto/note.proto | 13 ++-- proto/src/domain/mod.rs | 1 - proto/src/domain/notes.rs | 23 ------- proto/src/generated/note.rs | 22 +++--- store/src/db/migrations.rs | 2 + store/src/db/mod.rs | 7 +- store/src/db/sql.rs | 43 +++++++----- store/src/db/tests.rs | 48 +++++-------- store/src/errors.rs | 2 + store/src/server/api.rs | 9 ++- store/src/state.rs | 15 ++-- 22 files changed, 219 insertions(+), 230 deletions(-) delete mode 100644 proto/src/domain/notes.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a4dd048c..54db4e284 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,5 +19,5 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Install cargo make run: cargo install cargo-make - - name: cargo make - test + - name: cargo make - test-all run: cargo make test-all diff --git a/Cargo.lock b/Cargo.lock index 37e219b7f..b5a3f99bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,9 +342,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "arrayref" @@ -391,9 +391,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", @@ -927,9 +927,9 @@ checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -1320,9 +1320,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +checksum = "f08474e32172238f2827bd160c67871cdb2801430f65c3979184dc362e3ca118" dependencies = [ "libc", ] @@ -1671,11 +1671,12 @@ dependencies = [ [[package]] name = "miden-lib" -version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#9830b5a3f2fe5ddb09d0b45342ecdf956ea2a7ae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "855aceec1ac4d01fc486a7c9c9cb348d05b3310c842580b25c5b79a65bd942e2" dependencies = [ "miden-assembly 0.9.1", - "miden-objects 0.2.0", + "miden-objects 0.2.1", "miden-stdlib 0.9.1", ] @@ -1686,12 +1687,12 @@ dependencies = [ "anyhow", "clap", "figment", - "miden-lib 0.2.0", + "miden-lib 0.2.1", "miden-node-block-producer", "miden-node-rpc", "miden-node-store", "miden-node-utils 0.2.0", - "miden-objects 0.2.0", + "miden-objects 0.2.1", "rand_chacha", "serde", "tokio", @@ -1713,7 +1714,7 @@ dependencies = [ "miden-node-store", "miden-node-test-macro", "miden-node-utils 0.2.0", - "miden-objects 0.2.0", + "miden-objects 0.2.1", "miden-processor 0.9.1", "miden-stdlib 0.9.1", "miden-tx 0.2.0", @@ -1773,7 +1774,7 @@ version = "0.2.0" dependencies = [ "hex", "miden-node-utils 0.2.0", - "miden-objects 0.2.0", + "miden-objects 0.2.1", "miette", "proptest", "prost", @@ -1797,7 +1798,7 @@ dependencies = [ "miden-node-proto 0.2.0", "miden-node-store", "miden-node-utils 0.2.0", - "miden-objects 0.2.0", + "miden-objects 0.2.1", "miden-tx 0.2.0", "prost", "serde", @@ -1818,10 +1819,10 @@ dependencies = [ "directories", "figment", "hex", - "miden-lib 0.2.0", + "miden-lib 0.2.1", "miden-node-proto 0.2.0", "miden-node-utils 0.2.0", - "miden-objects 0.2.0", + "miden-objects 0.2.1", "once_cell", "prost", "rusqlite", @@ -1865,7 +1866,7 @@ dependencies = [ "anyhow", "figment", "itertools", - "miden-objects 0.2.0", + "miden-objects 0.2.1", "serde", "tracing", "tracing-forest", @@ -1889,8 +1890,9 @@ dependencies = [ [[package]] name = "miden-objects" -version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#9830b5a3f2fe5ddb09d0b45342ecdf956ea2a7ae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f87a635484967ad871332d99791d7e1cada6857aa32cc4f72f1a8817c4016b4" dependencies = [ "miden-assembly 0.9.1", "miden-core 0.9.1", @@ -1982,10 +1984,11 @@ dependencies = [ [[package]] name = "miden-tx" version = "0.2.0" -source = "git+https://github.com/0xPolygonMiden/miden-base.git?branch=next#9830b5a3f2fe5ddb09d0b45342ecdf956ea2a7ae" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8deca145a7d66cb2db075517451948fbc823c9f1983ac77c800d463663a3376b" dependencies = [ - "miden-lib 0.2.0", - "miden-objects 0.2.0", + "miden-lib 0.2.1", + "miden-objects 0.2.1", "miden-processor 0.9.1", "miden-prover 0.9.1", "miden-verifier 0.9.1", @@ -2541,9 +2544,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -3032,9 +3035,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -3053,9 +3056,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -3183,7 +3186,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.5", + "winnow 0.6.6", ] [[package]] @@ -3708,9 +3711,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 7ee40369c..c3b268841 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,11 +24,11 @@ exclude = [".github/"] [workspace.dependencies] miden-air = { version = "0.9", default-features = false } -miden-lib = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } -miden-objects = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } +miden-lib = { version = "0.2"} +miden-objects = { version = "0.2" } miden-processor = { version = "0.9" } miden-stdlib = { version = "0.9", default-features = false } -miden-tx = { git = "https://github.com/0xPolygonMiden/miden-base.git", branch = "next" } +miden-tx = { version = "0.2" } thiserror = { version = "1.0" } tracing = { version = "0.1" } tracing-subscriber = { version = "0.3", features = [ diff --git a/block-producer/src/batch_builder/batch.rs b/block-producer/src/batch_builder/batch.rs index be5861d2e..a907e2d50 100644 --- a/block-producer/src/batch_builder/batch.rs +++ b/block-producer/src/batch_builder/batch.rs @@ -5,8 +5,8 @@ use miden_objects::{ accounts::AccountId, batches::BatchNoteTree, crypto::hash::blake::{Blake3Digest, Blake3_256}, - notes::{NoteEnvelope, Nullifier}, - transaction::AccountDetails, + notes::Nullifier, + transaction::{AccountDetails, OutputNote}, Digest, MAX_NOTES_PER_BATCH, }; use tracing::instrument; @@ -28,8 +28,7 @@ pub struct TransactionBatch { updated_accounts: BTreeMap, produced_nullifiers: Vec, created_notes_smt: BatchNoteTree, - /// The notes stored `created_notes_smt` - created_notes: Vec, + created_notes: Vec, } impl TransactionBatch { @@ -62,31 +61,29 @@ impl TransactionBatch { .collect(); let produced_nullifiers = - txs.iter().flat_map(|tx| tx.input_notes().iter()).cloned().collect(); - - let (created_notes, created_notes_smt) = { - let created_notes: Vec = txs - .iter() - .flat_map(|tx| tx.output_notes().iter()) - .cloned() - .map(NoteEnvelope::from) - .collect(); - - if created_notes.len() > MAX_NOTES_PER_BATCH { - return Err(BuildBatchError::TooManyNotesCreated(created_notes.len(), txs)); - } - - // TODO: document under what circumstances SMT creating can fail - ( - created_notes.clone(), - BatchNoteTree::with_contiguous_leaves( - created_notes - .iter() - .map(|note_envelope| (note_envelope.id(), note_envelope.metadata())), + txs.iter().flat_map(|tx| tx.input_notes().iter()).copied().collect(); + + let created_notes: Vec<_> = + txs.iter().flat_map(|tx| tx.output_notes().iter()).cloned().collect(); + + if created_notes.len() > MAX_NOTES_PER_BATCH { + return Err(BuildBatchError::TooManyNotesCreated(created_notes.len(), txs)); + } + + // TODO: document under what circumstances SMT creating can fail + let created_notes_smt = + BatchNoteTree::with_contiguous_leaves(created_notes.iter().map(|note| { + ( + note.id(), + // TODO: Substitute by using just note.metadata() once this getter returns reference + // Tracking PR: https://github.com/0xPolygonMiden/miden-base/pull/593 + match note { + OutputNote::Public(note) => note.metadata(), + OutputNote::Private(note) => note.metadata(), + }, ) - .map_err(|e| BuildBatchError::NotesSmtError(e, txs))?, - ) - }; + })) + .map_err(|e| BuildBatchError::NotesSmtError(e, txs))?; Ok(Self { id, @@ -130,16 +127,16 @@ impl TransactionBatch { self.produced_nullifiers.iter().cloned() } - /// Returns an iterator over created notes. - pub fn created_notes(&self) -> impl Iterator + '_ { - self.created_notes.iter() - } - /// Returns the root hash of the created notes SMT. pub fn created_notes_root(&self) -> Digest { self.created_notes_smt.root() } + /// Returns created notes list. + pub fn created_notes(&self) -> &Vec { + &self.created_notes + } + // HELPER FUNCTIONS // -------------------------------------------------------------------------------------------- diff --git a/block-producer/src/block.rs b/block-producer/src/block.rs index 8ca9d0b4c..6477cb2ab 100644 --- a/block-producer/src/block.rs +++ b/block-producer/src/block.rs @@ -9,17 +9,20 @@ use miden_node_proto::{ use miden_objects::{ accounts::AccountId, crypto::merkle::{MerklePath, MmrPeaks, SmtProof}, - notes::{NoteEnvelope, Nullifier}, + notes::Nullifier, + transaction::OutputNote, BlockHeader, Digest, }; use crate::store::BlockInputsError; +pub(crate) type NoteBatch = Vec; + #[derive(Debug, Clone)] pub struct Block { pub header: BlockHeader, pub updated_accounts: Vec, - pub created_notes: Vec<(usize, usize, NoteEnvelope)>, + pub created_notes: Vec, pub produced_nullifiers: Vec, // TODO: // - full states for created public notes diff --git a/block-producer/src/block_builder/mod.rs b/block-producer/src/block_builder/mod.rs index 09fbdb7c8..f98e5c40d 100644 --- a/block-producer/src/block_builder/mod.rs +++ b/block-producer/src/block_builder/mod.rs @@ -82,16 +82,9 @@ where let updated_accounts: Vec<_> = batches.iter().flat_map(TransactionBatch::updated_accounts).collect(); - let created_notes = batches - .iter() - .enumerate() - .flat_map(|(batch_idx, batch)| { - batch - .created_notes() - .enumerate() - .map(move |(note_idx_in_batch, note)| (batch_idx, note_idx_in_batch, *note)) - }) - .collect(); + + let created_notes = batches.iter().map(|batch| batch.created_notes().clone()).collect(); + let produced_nullifiers: Vec = batches.iter().flat_map(TransactionBatch::produced_nullifiers).collect(); diff --git a/block-producer/src/block_builder/prover/block_witness.rs b/block-producer/src/block_builder/prover/block_witness.rs index 026dff08f..8a4878dfc 100644 --- a/block-producer/src/block_builder/prover/block_witness.rs +++ b/block-producer/src/block_builder/prover/block_witness.rs @@ -78,13 +78,8 @@ impl BlockWitness { let batch_created_notes_roots = batches .iter() .enumerate() - .filter_map(|(batch_index, batch)| { - if batch.created_notes().next().is_none() { - None - } else { - Some((batch_index, batch.created_notes_root())) - } - }) + .filter(|(_, batch)| !batch.created_notes().is_empty()) + .map(|(batch_index, batch)| (batch_index, batch.created_notes_root())) .collect(); Ok(Self { diff --git a/block-producer/src/store/mod.rs b/block-producer/src/store/mod.rs index def2d231b..49ff06f54 100644 --- a/block-producer/src/store/mod.rs +++ b/block-producer/src/store/mod.rs @@ -9,6 +9,7 @@ use miden_node_proto::{ errors::{ConversionError, MissingFieldHelper}, generated::{ account, digest, + note::NoteCreated, requests::{ApplyBlockRequest, GetBlockInputsRequest, GetTransactionInputsRequest}, responses::{GetTransactionInputsResponse, NullifierTransactionInputRecord}, store::api_client as store_client, @@ -16,7 +17,9 @@ use miden_node_proto::{ AccountState, }; use miden_node_utils::formatting::{format_map, format_opt}; -use miden_objects::{accounts::AccountId, notes::Nullifier, Digest}; +use miden_objects::{ + accounts::AccountId, notes::Nullifier, transaction::OutputNote, utils::Serializable, Digest, +}; use tonic::transport::Channel; use tracing::{debug, info, instrument}; @@ -136,11 +139,38 @@ impl ApplyBlock for DefaultStore { &self, block: &Block, ) -> Result<(), ApplyBlockError> { + let notes = block + .created_notes + .iter() + .enumerate() + .flat_map(|(batch_idx, batch)| { + batch + .iter() + .enumerate() + .map(|(note_idx_in_batch, note)| { + let details = match note { + OutputNote::Public(note) => Some(note.to_bytes()), + OutputNote::Private(_) => None, + }; + NoteCreated { + batch_index: batch_idx as u32, + note_index: note_idx_in_batch as u32, + note_id: Some(note.id().into()), + note_type: note.metadata().note_type() as u32, + sender: Some(note.metadata().sender().into()), + tag: note.metadata().tag().into(), + details, + } + }) + .collect::>() + }) + .collect(); + let request = tonic::Request::new(ApplyBlockRequest { block: Some((&block.header).into()), accounts: convert(&block.updated_accounts), nullifiers: convert(&block.produced_nullifiers), - notes: convert(&block.created_notes), + notes, }); let _ = self diff --git a/block-producer/src/test_utils/block.rs b/block-producer/src/test_utils/block.rs index 6bbbcd14d..54fab34cb 100644 --- a/block-producer/src/test_utils/block.rs +++ b/block-producer/src/test_utils/block.rs @@ -2,13 +2,14 @@ use miden_node_proto::domain::accounts::AccountUpdateDetails; use miden_objects::{ block::BlockNoteTree, crypto::merkle::{Mmr, SimpleSmt}, - notes::{NoteEnvelope, Nullifier}, + notes::Nullifier, + transaction::OutputNote, BlockHeader, Digest, ACCOUNT_TREE_DEPTH, ONE, ZERO, }; use super::MockStoreSuccess; use crate::{ - block::{Block, BlockInputs}, + block::{Block, BlockInputs, NoteBatch}, block_builder::prover::{block_witness::BlockWitness, BlockProver}, store::Store, TransactionBatch, @@ -51,7 +52,7 @@ pub async fn build_expected_block_header( new_account_root, // FIXME: FILL IN CORRECT NULLIFIER ROOT Digest::default(), - note_created_smt_from_batches(batches.iter()).root(), + note_created_smt_from_batches(batches).root(), Digest::default(), Digest::default(), ZERO, @@ -90,7 +91,7 @@ pub struct MockBlockBuilder { last_block_header: BlockHeader, updated_accounts: Option>, - created_notes: Option>, + created_note: Option>, produced_nullifiers: Option>, } @@ -102,7 +103,7 @@ impl MockBlockBuilder { last_block_header: *store.last_block_header.read().await, updated_accounts: None, - created_notes: None, + created_note: None, produced_nullifiers: None, } } @@ -121,15 +122,6 @@ impl MockBlockBuilder { self } - pub fn created_notes( - mut self, - created_notes: Vec<(usize, usize, NoteEnvelope)>, - ) -> Self { - self.created_notes = Some(created_notes); - - self - } - pub fn produced_nullifiers( mut self, produced_nullifiers: Vec, @@ -140,7 +132,7 @@ impl MockBlockBuilder { } pub fn build(self) -> Block { - let created_notes = self.created_notes.unwrap_or_default(); + let created_notes = self.created_note.unwrap_or_default(); let header = BlockHeader::new( self.last_block_header.hash(), @@ -148,7 +140,7 @@ impl MockBlockBuilder { self.store_chain_mmr.peaks(self.store_chain_mmr.forest()).unwrap().hash_peaks(), self.store_accounts.root(), Digest::default(), - note_created_smt_from_envelopes(created_notes.iter().cloned()).root(), + note_created_smt_from_note_batches(created_notes.iter()).root(), Digest::default(), Digest::default(), ZERO, @@ -164,23 +156,18 @@ impl MockBlockBuilder { } } -pub(crate) fn note_created_smt_from_envelopes( - note_iterator: impl Iterator -) -> BlockNoteTree { - BlockNoteTree::with_entries(note_iterator.map(|(batch_idx, note_idx_in_batch, note)| { - (batch_idx, note_idx_in_batch, (note.id().into(), *note.metadata())) - })) - .unwrap() -} - -pub(crate) fn note_created_smt_from_batches<'a>( - batches: impl Iterator +pub(crate) fn note_created_smt_from_note_batches<'a>( + batches: impl Iterator + Clone + 'a)> ) -> BlockNoteTree { let note_leaf_iterator = batches.enumerate().flat_map(|(batch_idx, batch)| { - batch.created_notes().enumerate().map(move |(note_idx_in_batch, note)| { + batch.clone().into_iter().enumerate().map(move |(note_idx_in_batch, note)| { (batch_idx, note_idx_in_batch, (note.id().into(), *note.metadata())) }) }); BlockNoteTree::with_entries(note_leaf_iterator).unwrap() } + +pub(crate) fn note_created_smt_from_batches(batches: &[TransactionBatch]) -> BlockNoteTree { + note_created_smt_from_note_batches(batches.iter().map(|batch| batch.created_notes())) +} diff --git a/block-producer/src/test_utils/store.rs b/block-producer/src/test_utils/store.rs index 41c1f3192..f16bd37e8 100644 --- a/block-producer/src/test_utils/store.rs +++ b/block-producer/src/test_utils/store.rs @@ -4,7 +4,8 @@ use async_trait::async_trait; use miden_objects::{ block::BlockNoteTree, crypto::merkle::{Mmr, SimpleSmt, Smt, ValuePath}, - notes::{NoteEnvelope, Nullifier}, + notes::Nullifier, + transaction::OutputNote, BlockHeader, ACCOUNT_TREE_DEPTH, EMPTY_WORD, ONE, ZERO, }; @@ -15,7 +16,7 @@ use crate::{ store::{ ApplyBlock, ApplyBlockError, BlockInputsError, Store, TransactionInputs, TxInputsError, }, - test_utils::block::{note_created_smt_from_batches, note_created_smt_from_envelopes}, + test_utils::block::{note_created_smt_from_batches, note_created_smt_from_note_batches}, ProvenTransaction, }; @@ -31,18 +32,17 @@ pub struct MockStoreSuccessBuilder { impl MockStoreSuccessBuilder { pub fn from_batches<'a>(batches: impl Iterator) -> Self { - let batches: Vec<_> = batches.collect(); + let batches: Vec<_> = batches.cloned().collect(); let accounts_smt = { let accounts = batches .iter() - .cloned() .flat_map(TransactionBatch::account_initial_states) .map(|(account_id, hash)| (account_id.into(), hash.into())); SimpleSmt::::with_leaves(accounts).unwrap() }; - let created_notes = note_created_smt_from_batches(batches.iter().cloned()); + let created_notes = note_created_smt_from_batches(&batches); Self { accounts: Some(accounts_smt), @@ -69,11 +69,11 @@ impl MockStoreSuccessBuilder { } } - pub fn initial_notes( + pub fn initial_notes<'a>( mut self, - notes: impl Iterator, + notes: impl Iterator + Clone + 'a)>, ) -> Self { - self.notes = Some(note_created_smt_from_envelopes(notes)); + self.notes = Some(note_created_smt_from_note_batches(notes)); self } diff --git a/node/src/commands/genesis/mod.rs b/node/src/commands/genesis/mod.rs index d1b21f2d5..b9a0e3873 100644 --- a/node/src/commands/genesis/mod.rs +++ b/node/src/commands/genesis/mod.rs @@ -121,7 +121,7 @@ fn create_accounts( let mut final_accounts = Vec::new(); for account in accounts { - // build account data from account inputs + // build offchain account data from account inputs let mut account_data = match account { AccountInput::BasicWallet(inputs) => { print!("Creating basic wallet account..."); diff --git a/proto/proto/note.proto b/proto/proto/note.proto index acedd8283..b389bddb1 100644 --- a/proto/proto/note.proto +++ b/proto/proto/note.proto @@ -10,7 +10,8 @@ message Note { uint32 note_index = 2; digest.Digest note_id = 3; account.AccountId sender = 4; - fixed64 tag = 5; + fixed32 tag = 5; + uint32 note_type = 6; merkle.MerklePath merkle_path = 7; // This field will be present when the note is on-chain. // details contain the `Note` in a serialized format. @@ -21,7 +22,8 @@ message NoteSyncRecord { uint32 note_index = 1; digest.Digest note_id = 2; account.AccountId sender = 3; - fixed64 tag = 4; + fixed32 tag = 4; + uint32 note_type = 5; merkle.MerklePath merkle_path = 6; } @@ -29,9 +31,10 @@ message NoteCreated { uint32 batch_index = 1; uint32 note_index = 2; digest.Digest note_id = 3; - account.AccountId sender = 4; - fixed64 tag = 5; + uint32 note_type = 4; + account.AccountId sender = 5; + fixed32 tag = 6; // This field will be present when the note is on-chain. // details contain the `Note` in a serialized format. - optional bytes details = 6; + optional bytes details = 7; } diff --git a/proto/src/domain/mod.rs b/proto/src/domain/mod.rs index cb51a0eea..3afd115db 100644 --- a/proto/src/domain/mod.rs +++ b/proto/src/domain/mod.rs @@ -2,7 +2,6 @@ pub mod accounts; pub mod blocks; pub mod digest; pub mod merkle; -pub mod notes; pub mod nullifiers; // UTILITIES diff --git a/proto/src/domain/notes.rs b/proto/src/domain/notes.rs deleted file mode 100644 index 5d871171a..000000000 --- a/proto/src/domain/notes.rs +++ /dev/null @@ -1,23 +0,0 @@ -use miden_objects::notes::NoteEnvelope; - -use crate::generated::note; - -// NoteCreated -// ================================================================================================ - -impl From<&(usize, usize, NoteEnvelope)> for note::NoteCreated { - fn from((batch_idx, note_idx, note): &(usize, usize, NoteEnvelope)) -> Self { - Self { - batch_index: *batch_idx as u32, - note_index: *note_idx as u32, - note_id: Some(note.id().into()), - sender: Some(note.metadata().sender().into()), - tag: note.metadata().tag().into(), - // This is `None` for now as this conversion is used by the block-producer - // when using `apply_block`. The block-producer has not yet been updated to support - // on-chain notes. Will be set to the correct value when the block-producer is - // up-to-date. - details: None, - } - } -} diff --git a/proto/src/generated/note.rs b/proto/src/generated/note.rs index d1d65d4be..da2638f39 100644 --- a/proto/src/generated/note.rs +++ b/proto/src/generated/note.rs @@ -11,8 +11,10 @@ pub struct Note { pub note_id: ::core::option::Option, #[prost(message, optional, tag = "4")] pub sender: ::core::option::Option, - #[prost(fixed64, tag = "5")] - pub tag: u64, + #[prost(fixed32, tag = "5")] + pub tag: u32, + #[prost(uint32, tag = "6")] + pub note_type: u32, #[prost(message, optional, tag = "7")] pub merkle_path: ::core::option::Option, /// This field will be present when the note is on-chain. @@ -30,8 +32,10 @@ pub struct NoteSyncRecord { pub note_id: ::core::option::Option, #[prost(message, optional, tag = "3")] pub sender: ::core::option::Option, - #[prost(fixed64, tag = "4")] - pub tag: u64, + #[prost(fixed32, tag = "4")] + pub tag: u32, + #[prost(uint32, tag = "5")] + pub note_type: u32, #[prost(message, optional, tag = "6")] pub merkle_path: ::core::option::Option, } @@ -45,12 +49,14 @@ pub struct NoteCreated { pub note_index: u32, #[prost(message, optional, tag = "3")] pub note_id: ::core::option::Option, - #[prost(message, optional, tag = "4")] + #[prost(uint32, tag = "4")] + pub note_type: u32, + #[prost(message, optional, tag = "5")] pub sender: ::core::option::Option, - #[prost(fixed64, tag = "5")] - pub tag: u64, + #[prost(fixed32, tag = "6")] + pub tag: u32, /// This field will be present when the note is on-chain. /// details contain the `Note` in a serialized format. - #[prost(bytes = "vec", optional, tag = "6")] + #[prost(bytes = "vec", optional, tag = "7")] pub details: ::core::option::Option<::prost::alloc::vec::Vec>, } diff --git a/store/src/db/migrations.rs b/store/src/db/migrations.rs index 8200ed84a..d5abd8bc6 100644 --- a/store/src/db/migrations.rs +++ b/store/src/db/migrations.rs @@ -21,6 +21,7 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { batch_index INTEGER NOT NULL, -- Index of batch in block, starting from 0 note_index INTEGER NOT NULL, -- Index of note in batch, starting from 0 note_hash BLOB NOT NULL, + note_type INTEGER NOT NULL, sender INTEGER NOT NULL, tag INTEGER NOT NULL, merkle_path BLOB NOT NULL, @@ -28,6 +29,7 @@ pub static MIGRATIONS: Lazy = Lazy::new(|| { PRIMARY KEY (block_num, batch_index, note_index), CONSTRAINT fk_block_num FOREIGN KEY (block_num) REFERENCES block_headers (block_num), + CONSTRAINT notes_type_in_enum CHECK (note_type BETWEEN 1 AND 3), -- 1-Public (0b01), 2-OffChain (0b10), 3-Encrypted (0b11) CONSTRAINT notes_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF), CONSTRAINT notes_batch_index_is_u32 CHECK (batch_index BETWEEN 0 AND 0xFFFFFFFF) CONSTRAINT notes_note_index_is_u32 CHECK (note_index BETWEEN 0 AND 0xFFFFFFFF) diff --git a/store/src/db/mod.rs b/store/src/db/mod.rs index 8d1168599..95aa083a7 100644 --- a/store/src/db/mod.rs +++ b/store/src/db/mod.rs @@ -5,7 +5,7 @@ use miden_node_proto::domain::accounts::{AccountInfo, AccountSummary, AccountUpd use miden_objects::{ block::BlockNoteTree, crypto::{hash::rpo::RpoDigest, merkle::MerklePath, utils::Deserializable}, - notes::{NoteId, Nullifier}, + notes::{NoteId, NoteType, Nullifier}, BlockHeader, GENESIS_BLOCK, }; use rusqlite::vtab::array; @@ -43,14 +43,15 @@ pub struct NoteCreated { pub batch_index: u32, pub note_index: u32, pub note_id: RpoDigest, + pub note_type: NoteType, pub sender: AccountId, - pub tag: u64, + pub tag: u32, pub details: Option>, } impl NoteCreated { /// Returns the absolute position on the note tree based on the batch index - /// and local-to-the-subtree index + /// and local-to-the-subtree index. pub fn absolute_note_index(&self) -> u32 { BlockNoteTree::note_index(self.batch_index as usize, self.note_index as usize) as u32 } diff --git a/store/src/db/sql.rs b/store/src/db/sql.rs index 5b477a609..9ff38ecb1 100644 --- a/store/src/db/sql.rs +++ b/store/src/db/sql.rs @@ -329,6 +329,7 @@ pub fn select_notes(conn: &mut Connection) -> Result> { batch_index, note_index, note_hash, + note_type, sender, tag, merkle_path, @@ -346,10 +347,10 @@ pub fn select_notes(conn: &mut Connection) -> Result> { let note_id_data = row.get_ref(3)?.as_blob()?; let note_id = RpoDigest::read_from_bytes(note_id_data)?; - let merkle_path_data = row.get_ref(6)?.as_blob()?; + let merkle_path_data = row.get_ref(7)?.as_blob()?; let merkle_path = MerklePath::read_from_bytes(merkle_path_data)?; - let details_data = row.get_ref(7)?.as_blob_or_null()?; + let details_data = row.get_ref(8)?.as_blob_or_null()?; let details = details_data.map(>::read_from_bytes).transpose()?; notes.push(Note { @@ -358,8 +359,9 @@ pub fn select_notes(conn: &mut Connection) -> Result> { batch_index: row.get(1)?, note_index: row.get(2)?, note_id, - sender: column_value_as_u64(row, 4)?, - tag: column_value_as_u64(row, 5)?, + note_type: row.get::<_, u8>(4)?.try_into()?, + sender: column_value_as_u64(row, 5)?, + tag: row.get(6)?, details, }, merkle_path, @@ -391,6 +393,7 @@ pub fn insert_notes( batch_index, note_index, note_hash, + note_type, sender, tag, merkle_path, @@ -398,7 +401,7 @@ pub fn insert_notes( ) VALUES ( - ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8 + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9 );", )?; @@ -411,8 +414,9 @@ pub fn insert_notes( note.note_created.batch_index, note.note_created.note_index, note.note_created.note_id.to_bytes(), + note.note_created.note_type as u8, u64_to_value(note.note_created.sender), - u64_to_value(note.note_created.tag), + note.note_created.tag, note.merkle_path.to_bytes(), details ])?; @@ -449,6 +453,7 @@ pub fn select_notes_since_block_by_tag_and_sender( batch_index, note_index, note_hash, + note_type, sender, tag, merkle_path, @@ -463,7 +468,7 @@ pub fn select_notes_since_block_by_tag_and_sender( FROM notes WHERE - ((tag >> 48) IN rarray(?1) OR sender IN rarray(?2)) AND + (tag IN rarray(?1) OR sender IN rarray(?2)) AND block_num > ?3 ORDER BY block_num ASC @@ -471,7 +476,7 @@ pub fn select_notes_since_block_by_tag_and_sender( 1 ) AND -- filter the block's notes and return only the ones matching the requested tags - ((tag >> 48) IN rarray(?1) OR sender IN rarray(?2)); + (tag IN rarray(?1) OR sender IN rarray(?2)); ", )?; let mut rows = stmt.query(params![Rc::new(tags), Rc::new(account_ids), block_num])?; @@ -483,11 +488,12 @@ pub fn select_notes_since_block_by_tag_and_sender( let note_index = row.get(2)?; let note_id_data = row.get_ref(3)?.as_blob()?; let note_id = RpoDigest::read_from_bytes(note_id_data)?; - let sender = column_value_as_u64(row, 4)?; - let tag = column_value_as_u64(row, 5)?; - let merkle_path_data = row.get_ref(6)?.as_blob()?; + let note_type = row.get::<_, u8>(4)?.try_into()?; + let sender = column_value_as_u64(row, 5)?; + let tag = row.get(6)?; + let merkle_path_data = row.get_ref(7)?.as_blob()?; let merkle_path = MerklePath::read_from_bytes(merkle_path_data)?; - let details_data = row.get_ref(7)?.as_blob_or_null()?; + let details_data = row.get_ref(8)?.as_blob_or_null()?; let details = details_data.map(>::read_from_bytes).transpose()?; let note = Note { @@ -496,6 +502,7 @@ pub fn select_notes_since_block_by_tag_and_sender( batch_index, note_index, note_id, + note_type, sender, tag, details, @@ -526,6 +533,7 @@ pub fn select_notes_by_id( batch_index, note_index, note_hash, + note_type, sender, tag, merkle_path, @@ -543,10 +551,10 @@ pub fn select_notes_by_id( let note_id_data = row.get_ref(3)?.as_blob()?; let note_id = NoteId::read_from_bytes(note_id_data)?; - let merkle_path_data = row.get_ref(6)?.as_blob()?; + let merkle_path_data = row.get_ref(7)?.as_blob()?; let merkle_path = MerklePath::read_from_bytes(merkle_path_data)?; - let details_data = row.get_ref(7)?.as_blob_or_null()?; + let details_data = row.get_ref(8)?.as_blob_or_null()?; let details = details_data.map(>::read_from_bytes).transpose()?; notes.push(Note { @@ -556,8 +564,9 @@ pub fn select_notes_by_id( note_index: row.get(2)?, details, note_id: note_id.into(), - sender: column_value_as_u64(row, 4)?, - tag: column_value_as_u64(row, 5)?, + note_type: row.get::<_, u8>(4)?.try_into()?, + sender: column_value_as_u64(row, 5)?, + tag: row.get(6)?, }, merkle_path, }) @@ -738,7 +747,7 @@ fn u32_to_value(v: u32) -> Value { Value::Integer(v) } -/// Gets a `u64`` value from the database. +/// Gets a `u64` value from the database. /// /// Sqlite uses `i64` as its internal representation format, and so when retrieving /// we need to make sure we cast as `u64` to get the original value diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index f2ecec053..6c6dad2d1 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -142,8 +142,9 @@ fn test_sql_select_notes() { batch_index: 0, note_index: i, note_id: num_to_rpo_digest(i as u64), + note_type: NoteType::Public, sender: i as u64, - tag: i as u64, + tag: i, details: Some(vec![1, 2, 3]), }, merkle_path: MerklePath::new(vec![]), @@ -590,10 +591,9 @@ fn test_notes() { let batch_index = 0u32; let note_index = 2u32; let note_id = num_to_rpo_digest(3); - let tag = 5u64; + let tag = 5u32; let sender = AccountId::new_unchecked(Felt::new(ACCOUNT_ID_OFF_CHAIN_SENDER)); - let note_metadata = - NoteMetadata::new(sender, NoteType::OffChain, (tag as u32).into(), ZERO).unwrap(); + let note_metadata = NoteMetadata::new(sender, NoteType::OffChain, tag.into(), ZERO).unwrap(); let values = [(batch_index as usize, note_index as usize, (note_id, note_metadata))]; let notes_db = BlockNoteTree::with_entries(values.iter().cloned()).unwrap(); @@ -606,6 +606,7 @@ fn test_notes() { batch_index, note_index, note_id, + note_type: NoteType::Public, sender: sender.into(), tag, details, @@ -622,23 +623,14 @@ fn test_notes() { assert!(res.is_empty()); // test no updates - let res = sql::select_notes_since_block_by_tag_and_sender( - &mut conn, - &[(tag >> 48) as u32], - &[], - block_num_1, - ) - .unwrap(); + let res = sql::select_notes_since_block_by_tag_and_sender(&mut conn, &[tag], &[], block_num_1) + .unwrap(); assert!(res.is_empty()); // test match - let res = sql::select_notes_since_block_by_tag_and_sender( - &mut conn, - &[(tag >> 48) as u32], - &[], - block_num_1 - 1, - ) - .unwrap(); + let res = + sql::select_notes_since_block_by_tag_and_sender(&mut conn, &[tag], &[], block_num_1 - 1) + .unwrap(); assert_eq!(res, vec![note.clone()]); let block_num_2 = note.block_num + 1; @@ -651,6 +643,7 @@ fn test_notes() { batch_index: note.note_created.batch_index, note_index: note.note_created.note_index, note_id: num_to_rpo_digest(3), + note_type: NoteType::OffChain, sender: note.note_created.sender, tag: note.note_created.tag, details: None, @@ -663,23 +656,14 @@ fn test_notes() { transaction.commit().unwrap(); // only first note is returned - let res = sql::select_notes_since_block_by_tag_and_sender( - &mut conn, - &[(tag >> 48) as u32], - &[], - block_num_1 - 1, - ) - .unwrap(); + let res = + sql::select_notes_since_block_by_tag_and_sender(&mut conn, &[tag], &[], block_num_1 - 1) + .unwrap(); assert_eq!(res, vec![note.clone()]); // only the second note is returned - let res = sql::select_notes_since_block_by_tag_and_sender( - &mut conn, - &[(tag >> 48) as u32], - &[], - block_num_1, - ) - .unwrap(); + let res = sql::select_notes_since_block_by_tag_and_sender(&mut conn, &[tag], &[], block_num_1) + .unwrap(); assert_eq!(res, vec![note2.clone()]); // test query notes by id diff --git a/store/src/errors.rs b/store/src/errors.rs index 78e7e455d..a25b7b0fc 100644 --- a/store/src/errors.rs +++ b/store/src/errors.rs @@ -45,6 +45,8 @@ pub enum DatabaseError { IoError(#[from] io::Error), #[error("Account error: {0}")] AccountError(#[from] AccountError), + #[error("Note error: {0}")] + NoteError(#[from] NoteError), #[error("SQLite pool interaction task failed: {0}")] InteractError(String), #[error("Deserialization of BLOB data from database failed: {0}")] diff --git a/store/src/server/api.rs b/store/src/server/api.rs index c67372be5..750071a02 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -28,10 +28,10 @@ use miden_node_proto::{ }; use miden_objects::{ crypto::hash::rpo::RpoDigest, - notes::{NoteId, Nullifier}, + notes::{NoteId, NoteType, Nullifier}, transaction::AccountDetails, utils::Deserializable, - BlockHeader, Felt, ZERO, + BlockHeader, Felt, NoteError, ZERO, }; use tonic::{Response, Status}; use tracing::{debug, info, instrument}; @@ -145,6 +145,7 @@ impl api_server::Api for StoreApi { .into_iter() .map(|note| NoteSyncRecord { note_index: note.note_created.absolute_note_index(), + note_type: note.note_created.note_type as u32, note_id: Some(note.note_created.note_id.into()), sender: Some(note.note_created.sender.into()), tag: note.note_created.tag, @@ -206,6 +207,7 @@ impl api_server::Api for StoreApi { note_id: Some(note.note_created.note_id.into()), sender: Some(note.note_created.sender.into()), tag: note.note_created.tag, + note_type: note.note_created.note_type as u32, merkle_path: Some(note.merkle_path.into()), details: note.note_created.details, }) @@ -318,6 +320,8 @@ impl api_server::Api for StoreApi { .map_err(|err: ConversionError| { Status::invalid_argument(err.to_string()) })?, + note_type: NoteType::try_from(note.note_type as u64) + .map_err(|err: NoteError| Status::invalid_argument(err.to_string()))?, sender: note.sender.ok_or(invalid_argument("Note missing sender"))?.into(), tag: note.tag, details: note.details, @@ -447,6 +451,7 @@ impl api_server::Api for StoreApi { note_id: Some(note.note_created.note_id.into()), sender: Some(note.note_created.sender.into()), tag: note.note_created.tag, + note_type: note.note_created.note_type as u32, merkle_path: Some(note.merkle_path.into()), details: note.note_created.details, }) diff --git a/store/src/state.rs b/store/src/state.rs index 1eb02e860..5bb9ad4b1 100644 --- a/store/src/state.rs +++ b/store/src/state.rs @@ -15,8 +15,8 @@ use miden_objects::{ hash::rpo::RpoDigest, merkle::{LeafIndex, Mmr, MmrDelta, MmrPeaks, SimpleSmt, SmtProof, ValuePath}, }, - notes::{NoteId, NoteMetadata, NoteType, Nullifier}, - AccountError, BlockHeader, NoteError, ACCOUNT_TREE_DEPTH, ZERO, + notes::{NoteId, NoteMetadata, Nullifier}, + AccountError, BlockHeader, ACCOUNT_TREE_DEPTH, ZERO, }; use tokio::{ sync::{oneshot, Mutex, RwLock}, @@ -501,15 +501,8 @@ pub fn build_note_tree(notes: &[NoteCreated]) -> Result Date: Fri, 12 Apr 2024 01:14:12 -0300 Subject: [PATCH 24/29] Return absolute note index (#317) --- store/src/server/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/src/server/api.rs b/store/src/server/api.rs index 750071a02..5d7bcd284 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -203,7 +203,7 @@ impl api_server::Api for StoreApi { .into_iter() .map(|note| generated::note::Note { block_num: note.block_num, - note_index: note.note_created.note_index, + note_index: note.note_created.absolute_note_index(), note_id: Some(note.note_created.note_id.into()), sender: Some(note.note_created.sender.into()), tag: note.note_created.tag, From 18acc0aa80eedbdb5faeeda065792443d23a270b Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Thu, 11 Apr 2024 22:08:59 -0700 Subject: [PATCH 25/29] chore: update badges in main readme --- .github/ISSUE_TEMPLATE/1-bugreport.yml | 10 +++++----- .github/issue_template.md | 15 --------------- README.md | 9 +++++---- 3 files changed, 10 insertions(+), 24 deletions(-) delete mode 100644 .github/issue_template.md diff --git a/.github/ISSUE_TEMPLATE/1-bugreport.yml b/.github/ISSUE_TEMPLATE/1-bugreport.yml index f091d88f1..ae5aafd03 100644 --- a/.github/ISSUE_TEMPLATE/1-bugreport.yml +++ b/.github/ISSUE_TEMPLATE/1-bugreport.yml @@ -12,7 +12,7 @@ body: attributes: label: "Version" description: "Which node version are you running?" - placeholder: "v0.1.0" + placeholder: "v0.2.0" validations: required: true - type: textarea @@ -20,14 +20,14 @@ body: attributes: label: "Other packages versions" description: "Let us know of the versions of any other packages used. For example, which version of the client are you using?" - placeholder: "miden-client: v0.1.0" + placeholder: "miden-client: v0.2.0" validations: required: false - type: textarea id: what-happened attributes: label: "What happened?" - description: "Describe the behaviour you are experiencing." + description: "Describe the behavior you are experiencing." placeholder: "Tell us what you see!" validations: required: true @@ -35,7 +35,7 @@ body: id: expected-to-happen attributes: label: "What should have happened?" - description: "Describe the behaviour you expected to see." + description: "Describe the behavior you expected to see." placeholder: "Tell us what should have happened!" validations: required: true @@ -43,7 +43,7 @@ body: id: reproduce-steps attributes: label: "How can this be reproduced?" - description: "If possible, describe how to replicate the unexpected behaviour that you see." + description: "If possible, describe how to replicate the unexpected behavior that you see." placeholder: "Steps!" validations: required: true diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index 78b1c88d8..000000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,15 +0,0 @@ -# Task description - -Describe what should be done. - -# Implementation suggestion - -Describe how it should be implemented. - -# Definition of done - -Describe when this task is considered finished. - -# Context - -Add additional context in the from of other issues/PRs/discussions. diff --git a/README.md b/README.md index 5174451a6..7f2b28562 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Miden node -
- - +[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/0xPolygonMiden/miden-node/blob/main/LICENSE) +[![test](https://github.com/0xPolygonMiden/miden-node/actions/workflows/test.yml/badge.svg)](https://github.com/0xPolygonMiden/miden-node/actions/workflows/test.yml) +[![RUST_VERSION](https://img.shields.io/badge/rustc-1.77+-lightgray.svg)]() +[![crates.io](https://img.shields.io/crates/v/miden-node)](https://crates.io/crates/miden-node) This repository holds the Miden node; that is, the software which processes transactions and creates blocks for the Miden rollup. @@ -27,7 +28,7 @@ The diagram below illustrates high-level design of each component as well as bas ## Usage -Before you can build and run the Miden node or any of its components, you'll need to make sure you have Rust [installed](https://www.rust-lang.org/tools/install). Miden node v0.1 requires Rust version **1.77** or later. +Before you can build and run the Miden node or any of its components, you'll need to make sure you have Rust [installed](https://www.rust-lang.org/tools/install). Miden node v0.2 requires Rust version **1.77** or later. Depending on the platform, you may need to install additional libraries. For example, on Ubuntu 22.04 the following command ensures that all required libraries are installed. From de6eb7f37de8153e3bc251be0ea6e9d4b6e2695f Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Thu, 11 Apr 2024 22:12:15 -0700 Subject: [PATCH 26/29] chore: re-ordered sections in main readme (and minor fixes) --- LICENSE | 2 +- README.md | 45 +++++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/LICENSE b/LICENSE index c6fe12c86..7e84649f0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Polygon Miden +Copyright (c) 2024 Polygon Miden Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7f2b28562..b58bfef10 100644 --- a/README.md +++ b/README.md @@ -92,28 +92,6 @@ Please, refer to each component's documentation: Each directory containing the executables also contains an example configuration file. Make sure that the configuration files are mutually consistent. That is, make sure that the URLs are valid and point to the right endpoint. -### Debian Packages - -The debian packages allow for easy install for miden on debian based systems. Note that there are checksums available for the package. -Current support is for amd64, arm64 support coming soon. - -To install the debian package: -```sh -sudo dpkg -i $package_name.deb -``` -Note, when using the debian package to run the `make-genesis` function, you should define the location of your output: -```sh -miden-node make-genesis -i $input_location_for_gensis.toml -o $output_for_gensis.dat_and_accounts -``` -The debian package has a checksum, you can verify this checksum by download the debian package and checksum file to the same directory and running the following command: -```sh -sha256sum --check $checksumfile -``` -Please make sure you have the sha256sum program installed, for most linux operating systems this is already installed. If you wish to installe it on your macOS, you can use brew: -```sh -brew install coreutils -``` - ### Running the node using Docker If you intend on running the node inside a Docker container, you will need to follow these steps: @@ -142,5 +120,28 @@ If you intend on running the node inside a Docker container, you will need to fo After running this command you should see the name of the container `miden-node` being outputed and marked as `Up`. + +### Debian Packages + +The debian packages allow for easy install for miden on debian based systems. Note that there are checksums available for the package. +Current support is for amd64, arm64 support coming soon. + +To install the debian package: +```sh +sudo dpkg -i $package_name.deb +``` +Note, when using the debian package to run the `make-genesis` function, you should define the location of your output: +```sh +miden-node make-genesis -i $input_location_for_gensis.toml -o $output_for_gensis.dat_and_accounts +``` +The debian package has a checksum, you can verify this checksum by downloading the debian package and checksum file to the same directory and running the following command: +```sh +sha256sum --check $checksumfile +``` +Please make sure you have the sha256sum program installed, for most linux operating systems this is already installed. If you wish to install it on your macOS, you can use brew: +```sh +brew install coreutils +``` + ## License This project is [MIT licensed](./LICENSE). From f496332a8517f6c56bf35ec7e264f1a0d5947b89 Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Thu, 11 Apr 2024 23:09:02 -0700 Subject: [PATCH 27/29] chore: use the same rustfmt settings as in miden-base --- block-producer/src/batch_builder/mod.rs | 15 ++----- block-producer/src/batch_builder/tests/mod.rs | 32 +++----------- block-producer/src/block_builder/mod.rs | 15 ++----- .../src/block_builder/prover/block_witness.rs | 42 ++++++++---------- .../src/block_builder/prover/mod.rs | 9 +--- .../src/block_builder/prover/tests.rs | 8 +--- block-producer/src/config.rs | 5 +-- block-producer/src/state_view/mod.rs | 15 ++----- block-producer/src/store/mod.rs | 26 +++-------- block-producer/src/test_utils/account.rs | 5 +-- block-producer/src/test_utils/batch.rs | 20 ++------- block-producer/src/test_utils/block.rs | 12 ++--- block-producer/src/test_utils/proven_tx.rs | 20 ++------- block-producer/src/test_utils/store.rs | 31 +++---------- block-producer/src/txqueue/mod.rs | 12 ++--- block-producer/src/txqueue/tests/mod.rs | 35 +++------------ faucet/src/config.rs | 5 +-- faucet/src/handlers.rs | 5 +-- faucet/src/utils.rs | 4 +- node/src/commands/genesis/mod.rs | 10 +---- node/src/main.rs | 8 ++-- proto/src/domain/accounts.rs | 24 +++------- proto/src/domain/digest.rs | 31 ++++--------- proto/src/domain/merkle.rs | 11 ++--- rpc/src/config.rs | 5 +-- rpc/src/server/api.rs | 5 +-- rustfmt.toml | 11 +++-- store/src/config.rs | 5 +-- store/src/db/mod.rs | 15 ++----- store/src/db/sql.rs | 30 +++---------- store/src/db/tests.rs | 5 +-- store/src/genesis.rs | 19 ++------ store/src/main.rs | 10 ++--- store/src/nullifier_tree.rs | 12 ++--- store/src/server/api.rs | 4 +- store/src/server/mod.rs | 5 +-- store/src/state.rs | 44 +++++-------------- test-macro/src/lib.rs | 5 +-- utils/src/config.rs | 5 +-- utils/src/formatting.rs | 2 +- 40 files changed, 143 insertions(+), 439 deletions(-) diff --git a/block-producer/src/batch_builder/mod.rs b/block-producer/src/batch_builder/mod.rs index 77ab57e03..76a89e76a 100644 --- a/block-producer/src/batch_builder/mod.rs +++ b/block-producer/src/batch_builder/mod.rs @@ -30,10 +30,7 @@ use crate::errors::BuildBatchError; #[async_trait] pub trait BatchBuilder: Send + Sync + 'static { /// Start proving of a new batch. - async fn build_batch( - &self, - txs: Vec, - ) -> Result<(), BuildBatchError>; + async fn build_batch(&self, txs: Vec) -> Result<(), BuildBatchError>; } // DEFAULT BATCH BUILDER @@ -68,10 +65,7 @@ where // -------------------------------------------------------------------------------------------- /// Returns an new [BatchBuilder] instantiated with the provided [BlockBuilder] and the /// specified options. - pub fn new( - block_builder: Arc, - options: DefaultBatchBuilderOptions, - ) -> Self { + pub fn new(block_builder: Arc, options: DefaultBatchBuilderOptions) -> Self { Self { ready_batches: Arc::new(RwLock::new(Vec::new())), block_builder, @@ -129,10 +123,7 @@ where BB: BlockBuilder, { #[instrument(target = "miden-block-producer", skip_all, err, fields(batch_id))] - async fn build_batch( - &self, - txs: Vec, - ) -> Result<(), BuildBatchError> { + async fn build_batch(&self, txs: Vec) -> Result<(), BuildBatchError> { let num_txs = txs.len(); info!(target: COMPONENT, num_txs, "Building a transaction batch"); diff --git a/block-producer/src/batch_builder/tests/mod.rs b/block-producer/src/batch_builder/tests/mod.rs index c58e592e8..49ef1a5a7 100644 --- a/block-producer/src/batch_builder/tests/mod.rs +++ b/block-producer/src/batch_builder/tests/mod.rs @@ -12,10 +12,7 @@ struct BlockBuilderSuccess { #[async_trait] impl BlockBuilder for BlockBuilderSuccess { - async fn build_block( - &self, - batches: &[TransactionBatch], - ) -> Result<(), BuildBlockError> { + async fn build_block(&self, batches: &[TransactionBatch]) -> Result<(), BuildBlockError> { if batches.is_empty() { *self.num_empty_batches_received.write().await += 1; } else { @@ -31,10 +28,7 @@ struct BlockBuilderFailure; #[async_trait] impl BlockBuilder for BlockBuilderFailure { - async fn build_block( - &self, - _batches: &[TransactionBatch], - ) -> Result<(), BuildBlockError> { + async fn build_block(&self, _batches: &[TransactionBatch]) -> Result<(), BuildBlockError> { Err(BuildBlockError::TooManyBatchesInBlock(0)) } } @@ -53,10 +47,7 @@ async fn test_block_size_doesnt_exceed_limit() { let batch_builder = Arc::new(DefaultBatchBuilder::new( block_builder.clone(), - DefaultBatchBuilderOptions { - block_frequency, - max_batches_per_block, - }, + DefaultBatchBuilderOptions { block_frequency, max_batches_per_block }, )); // Add 3 batches in internal queue (remember: 2 batches/block) @@ -94,10 +85,7 @@ async fn test_build_block_called_when_no_batches() { let batch_builder = Arc::new(DefaultBatchBuilder::new( block_builder.clone(), - DefaultBatchBuilderOptions { - block_frequency, - max_batches_per_block, - }, + DefaultBatchBuilderOptions { block_frequency, max_batches_per_block }, )); // start batch builder @@ -122,10 +110,7 @@ async fn test_batches_added_back_to_queue_on_block_build_failure() { let batch_builder = Arc::new(DefaultBatchBuilder::new( block_builder.clone(), - DefaultBatchBuilderOptions { - block_frequency, - max_batches_per_block, - }, + DefaultBatchBuilderOptions { block_frequency, max_batches_per_block }, )); let internal_ready_batches = batch_builder.ready_batches.clone(); @@ -151,13 +136,10 @@ async fn test_batches_added_back_to_queue_on_block_build_failure() { // HELPERS // ================================================================================================ -fn dummy_tx_batch( - starting_acccount_index: u32, - num_txs_in_batch: usize, -) -> TransactionBatch { +fn dummy_tx_batch(starting_account_index: u32, num_txs_in_batch: usize) -> TransactionBatch { let txs = (0..num_txs_in_batch) .map(|index| { - MockProvenTxBuilder::with_account_index(starting_acccount_index + index as u32).build() + MockProvenTxBuilder::with_account_index(starting_account_index + index as u32).build() }) .collect(); TransactionBatch::new(txs).unwrap() diff --git a/block-producer/src/block_builder/mod.rs b/block-producer/src/block_builder/mod.rs index f98e5c40d..8c94746c9 100644 --- a/block-producer/src/block_builder/mod.rs +++ b/block-producer/src/block_builder/mod.rs @@ -30,10 +30,7 @@ pub trait BlockBuilder: Send + Sync + 'static { /// /// The `BlockBuilder` relies on `build_block()` to be called as a precondition to creating a /// block. In other words, if `build_block()` is never called, then no blocks are produced. - async fn build_block( - &self, - batches: &[TransactionBatch], - ) -> Result<(), BuildBlockError>; + async fn build_block(&self, batches: &[TransactionBatch]) -> Result<(), BuildBlockError>; } #[derive(Debug)] @@ -48,10 +45,7 @@ where S: Store, A: ApplyBlock, { - pub fn new( - store: Arc, - state_view: Arc, - ) -> Self { + pub fn new(store: Arc, state_view: Arc) -> Self { Self { store, state_view, @@ -70,10 +64,7 @@ where A: ApplyBlock, { #[instrument(target = "miden-block-producer", skip_all, err)] - async fn build_block( - &self, - batches: &[TransactionBatch], - ) -> Result<(), BuildBlockError> { + async fn build_block(&self, batches: &[TransactionBatch]) -> Result<(), BuildBlockError> { info!( target: COMPONENT, num_batches = batches.len(), diff --git a/block-producer/src/block_builder/prover/block_witness.rs b/block-producer/src/block_builder/prover/block_witness.rs index 8a4878dfc..9def5561a 100644 --- a/block-producer/src/block_builder/prover/block_witness.rs +++ b/block-producer/src/block_builder/prover/block_witness.rs @@ -49,29 +49,23 @@ impl BlockWitness { batches .iter() .flat_map(TransactionBatch::updated_accounts) - .map( - |AccountUpdateDetails { - account_id, - final_state_hash, - .. - }| { - let initial_state_hash = account_initial_states - .remove(&account_id) - .expect("already validated that key exists"); - let proof = account_merkle_proofs - .remove(&account_id) - .expect("already validated that key exists"); - - ( - account_id, - AccountUpdate { - initial_state_hash, - final_state_hash, - proof, - }, - ) - }, - ) + .map(|AccountUpdateDetails { account_id, final_state_hash, .. }| { + let initial_state_hash = account_initial_states + .remove(&account_id) + .expect("already validated that key exists"); + let proof = account_merkle_proofs + .remove(&account_id) + .expect("already validated that key exists"); + + ( + account_id, + AccountUpdate { + initial_state_hash, + final_state_hash, + proof, + }, + ) + }) .collect() }; @@ -93,7 +87,7 @@ impl BlockWitness { /// Converts [`BlockWitness`] into inputs to the block kernel program pub(super) fn into_program_inputs( - self + self, ) -> Result<(AdviceInputs, StackInputs), BlockProverError> { let stack_inputs = self.build_stack_inputs(); let advice_inputs = self.build_advice_inputs()?; diff --git a/block-producer/src/block_builder/prover/mod.rs b/block-producer/src/block_builder/prover/mod.rs index eb3696f53..3935e011e 100644 --- a/block-producer/src/block_builder/prover/mod.rs +++ b/block-producer/src/block_builder/prover/mod.rs @@ -203,16 +203,11 @@ impl BlockProver { .expect("failed to load account update program") }; - Self { - kernel: account_program, - } + Self { kernel: account_program } } // Note: this will eventually all be done in the VM, and also return an `ExecutionProof` - pub fn prove( - &self, - witness: BlockWitness, - ) -> Result { + pub fn prove(&self, witness: BlockWitness) -> Result { let prev_hash = witness.prev_header.hash(); let block_num = witness.prev_header.block_num() + 1; let version = witness.prev_header.version(); diff --git a/block-producer/src/block_builder/prover/tests.rs b/block-producer/src/block_builder/prover/tests.rs index 93c5dc080..179b723aa 100644 --- a/block-producer/src/block_builder/prover/tests.rs +++ b/block-producer/src/block_builder/prover/tests.rs @@ -496,13 +496,7 @@ fn test_block_witness_validation_inconsistent_nullifiers() { .iter() .flat_map(|batch| batch.account_initial_states()) .map(|(account_id, hash)| { - ( - account_id, - AccountWitness { - hash, - proof: MerklePath::default(), - }, - ) + (account_id, AccountWitness { hash, proof: MerklePath::default() }) }) .collect(); diff --git a/block-producer/src/config.rs b/block-producer/src/config.rs index 4febe9207..90462c4d9 100644 --- a/block-producer/src/config.rs +++ b/block-producer/src/config.rs @@ -32,10 +32,7 @@ impl BlockProducerConfig { } impl Display for BlockProducerConfig { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( "{{ endpoint: \"{}\", store_url: \"{}\" }}", self.endpoint, self.store_url diff --git a/block-producer/src/state_view/mod.rs b/block-producer/src/state_view/mod.rs index 17f875318..9e6cbb527 100644 --- a/block-producer/src/state_view/mod.rs +++ b/block-producer/src/state_view/mod.rs @@ -39,10 +39,7 @@ impl DefaultStateView where S: Store, { - pub fn new( - store: Arc, - verify_tx_proofs: bool, - ) -> Self { + pub fn new(store: Arc, verify_tx_proofs: bool) -> Self { Self { store, verify_tx_proofs, @@ -61,10 +58,7 @@ where S: Store, { #[instrument(skip_all, err)] - async fn verify_tx( - &self, - candidate_tx: &ProvenTransaction, - ) -> Result<(), VerifyTxError> { + async fn verify_tx(&self, candidate_tx: &ProvenTransaction) -> Result<(), VerifyTxError> { if self.verify_tx_proofs { // Make sure that the transaction proof is valid and meets the required security level let tx_verifier = TransactionVerifier::new(MIN_PROOF_SECURITY_LEVEL); @@ -123,10 +117,7 @@ where S: Store, { #[instrument(target = "miden-block-producer", skip_all, err)] - async fn apply_block( - &self, - block: &Block, - ) -> Result<(), ApplyBlockError> { + async fn apply_block(&self, block: &Block) -> Result<(), ApplyBlockError> { self.store.apply_block(block).await?; let mut locked_accounts_in_flight = self.accounts_in_flight.write().await; diff --git a/block-producer/src/store/mod.rs b/block-producer/src/store/mod.rs index 49ff06f54..01c851f6f 100644 --- a/block-producer/src/store/mod.rs +++ b/block-producer/src/store/mod.rs @@ -50,10 +50,7 @@ pub trait Store: ApplyBlock { #[async_trait] pub trait ApplyBlock: Send + Sync + 'static { - async fn apply_block( - &self, - block: &Block, - ) -> Result<(), ApplyBlockError>; + async fn apply_block(&self, block: &Block) -> Result<(), ApplyBlockError>; } // TRANSACTION INPUTS @@ -72,10 +69,7 @@ pub struct TransactionInputs { } impl Display for TransactionInputs { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( "{{ account_id: {}, account_hash: {}, nullifiers: {} }}", self.account_id, @@ -89,10 +83,7 @@ impl TryFrom for TransactionInputs { type Error = ConversionError; fn try_from(response: GetTransactionInputsResponse) -> Result { - let AccountState { - account_id, - account_hash, - } = response + let AccountState { account_id, account_hash } = response .account_state .ok_or(GetTransactionInputsResponse::missing_field(stringify!(account_state)))? .try_into()?; @@ -107,11 +98,7 @@ impl TryFrom for TransactionInputs { nullifiers.insert(nullifier, nullifier_record.block_num); } - Ok(Self { - account_id, - account_hash, - nullifiers, - }) + Ok(Self { account_id, account_hash, nullifiers }) } } @@ -135,10 +122,7 @@ impl DefaultStore { #[async_trait] impl ApplyBlock for DefaultStore { #[instrument(target = "miden-block-producer", skip_all, err)] - async fn apply_block( - &self, - block: &Block, - ) -> Result<(), ApplyBlockError> { + async fn apply_block(&self, block: &Block) -> Result<(), ApplyBlockError> { let notes = block .created_notes .iter() diff --git a/block-producer/src/test_utils/account.rs b/block-producer/src/test_utils/account.rs index f375427e2..13e8f985a 100644 --- a/block-producer/src/test_utils/account.rs +++ b/block-producer/src/test_utils/account.rs @@ -16,10 +16,7 @@ pub struct MockPrivateAccount { } impl MockPrivateAccount { - fn new( - init_seed: [u8; 32], - new_account: bool, - ) -> Self { + fn new(init_seed: [u8; 32], new_account: bool) -> Self { let account_seed = get_account_seed( init_seed, AccountType::RegularAccountUpdatableCode, diff --git a/block-producer/src/test_utils/batch.rs b/block-producer/src/test_utils/batch.rs index c2ab8c57d..adec43bd1 100644 --- a/block-producer/src/test_utils/batch.rs +++ b/block-producer/src/test_utils/batch.rs @@ -3,23 +3,14 @@ use crate::{test_utils::MockProvenTxBuilder, TransactionBatch}; pub trait TransactionBatchConstructor { /// Returns a `TransactionBatch` with `notes_per_tx.len()` transactions, where the i'th /// transaction has `notes_per_tx[i]` notes created - fn from_notes_created( - starting_account_index: u32, - notes_per_tx: &[u64], - ) -> Self; + fn from_notes_created(starting_account_index: u32, notes_per_tx: &[u64]) -> Self; /// Returns a `TransactionBatch` which contains `num_txs_in_batch` transactions - fn from_txs( - starting_account_index: u32, - num_txs_in_batch: u64, - ) -> Self; + fn from_txs(starting_account_index: u32, num_txs_in_batch: u64) -> Self; } impl TransactionBatchConstructor for TransactionBatch { - fn from_notes_created( - starting_account_index: u32, - notes_per_tx: &[u64], - ) -> Self { + fn from_notes_created(starting_account_index: u32, notes_per_tx: &[u64]) -> Self { let txs: Vec<_> = notes_per_tx .iter() .enumerate() @@ -36,10 +27,7 @@ impl TransactionBatchConstructor for TransactionBatch { Self::new(txs).unwrap() } - fn from_txs( - starting_account_index: u32, - num_txs_in_batch: u64, - ) -> Self { + fn from_txs(starting_account_index: u32, num_txs_in_batch: u64) -> Self { let txs: Vec<_> = (0..num_txs_in_batch) .enumerate() .map(|(index, _)| { diff --git a/block-producer/src/test_utils/block.rs b/block-producer/src/test_utils/block.rs index 54fab34cb..f8a2157da 100644 --- a/block-producer/src/test_utils/block.rs +++ b/block-producer/src/test_utils/block.rs @@ -108,10 +108,7 @@ impl MockBlockBuilder { } } - pub fn account_updates( - mut self, - updated_accounts: Vec, - ) -> Self { + pub fn account_updates(mut self, updated_accounts: Vec) -> Self { for update in &updated_accounts { self.store_accounts .insert(update.account_id.into(), update.final_state_hash.into()); @@ -122,10 +119,7 @@ impl MockBlockBuilder { self } - pub fn produced_nullifiers( - mut self, - produced_nullifiers: Vec, - ) -> Self { + pub fn produced_nullifiers(mut self, produced_nullifiers: Vec) -> Self { self.produced_nullifiers = Some(produced_nullifiers); self @@ -157,7 +151,7 @@ impl MockBlockBuilder { } pub(crate) fn note_created_smt_from_note_batches<'a>( - batches: impl Iterator + Clone + 'a)> + batches: impl Iterator + Clone + 'a)>, ) -> BlockNoteTree { let note_leaf_iterator = batches.enumerate().flat_map(|(batch_idx, batch)| { batch.clone().into_iter().enumerate().map(move |(note_idx_in_batch, note)| { diff --git a/block-producer/src/test_utils/proven_tx.rs b/block-producer/src/test_utils/proven_tx.rs index 316a04084..d934ddaad 100644 --- a/block-producer/src/test_utils/proven_tx.rs +++ b/block-producer/src/test_utils/proven_tx.rs @@ -41,28 +41,19 @@ impl MockProvenTxBuilder { } } - pub fn nullifiers( - mut self, - nullifiers: Vec, - ) -> Self { + pub fn nullifiers(mut self, nullifiers: Vec) -> Self { self.nullifiers = Some(nullifiers); self } - pub fn notes_created( - mut self, - notes: Vec, - ) -> Self { + pub fn notes_created(mut self, notes: Vec) -> Self { self.notes_created = Some(notes); self } - pub fn nullifiers_range( - self, - range: Range, - ) -> Self { + pub fn nullifiers_range(self, range: Range) -> Self { let nullifiers = range .map(|index| { let nullifier = Digest::from([ONE, ONE, ONE, Felt::new(index)]); @@ -74,10 +65,7 @@ impl MockProvenTxBuilder { self.nullifiers(nullifiers) } - pub fn private_notes_created_range( - self, - range: Range, - ) -> Self { + pub fn private_notes_created_range(self, range: Range) -> Self { let notes = range .map(|note_index| { let note_hash = Hasher::hash(¬e_index.to_be_bytes()); diff --git a/block-producer/src/test_utils/store.rs b/block-producer/src/test_utils/store.rs index f16bd37e8..b0a0f8aa3 100644 --- a/block-producer/src/test_utils/store.rs +++ b/block-producer/src/test_utils/store.rs @@ -78,28 +78,19 @@ impl MockStoreSuccessBuilder { self } - pub fn initial_nullifiers( - mut self, - nullifiers: BTreeSet, - ) -> Self { + pub fn initial_nullifiers(mut self, nullifiers: BTreeSet) -> Self { self.produced_nullifiers = Some(nullifiers); self } - pub fn initial_chain_mmr( - mut self, - chain_mmr: Mmr, - ) -> Self { + pub fn initial_chain_mmr(mut self, chain_mmr: Mmr) -> Self { self.chain_mmr = Some(chain_mmr); self } - pub fn initial_block_num( - mut self, - block_num: u32, - ) -> Self { + pub fn initial_block_num(mut self, block_num: u32) -> Self { self.block_num = Some(block_num); self @@ -172,10 +163,7 @@ impl MockStoreSuccess { #[async_trait] impl ApplyBlock for MockStoreSuccess { - async fn apply_block( - &self, - block: &Block, - ) -> Result<(), ApplyBlockError> { + async fn apply_block(&self, block: &Block) -> Result<(), ApplyBlockError> { // Intentionally, we take and hold both locks, to prevent calls to `get_tx_inputs()` from going through while we're updating the store's data structure let mut locked_accounts = self.accounts.write().await; let mut locked_produced_nullifiers = self.produced_nullifiers.write().await; @@ -261,10 +249,8 @@ impl Store for MockStoreSuccess { let accounts = { updated_accounts .map(|&account_id| { - let ValuePath { - value: hash, - path: proof, - } = locked_accounts.open(&account_id.into()); + let ValuePath { value: hash, path: proof } = + locked_accounts.open(&account_id.into()); (account_id, AccountWitness { hash, proof }) }) @@ -289,10 +275,7 @@ pub struct MockStoreFailure; #[async_trait] impl ApplyBlock for MockStoreFailure { - async fn apply_block( - &self, - _block: &Block, - ) -> Result<(), ApplyBlockError> { + async fn apply_block(&self, _block: &Block) -> Result<(), ApplyBlockError> { Err(ApplyBlockError::GrpcClientError(String::new())) } } diff --git a/block-producer/src/txqueue/mod.rs b/block-producer/src/txqueue/mod.rs index 4f93be983..c37975ae5 100644 --- a/block-producer/src/txqueue/mod.rs +++ b/block-producer/src/txqueue/mod.rs @@ -29,12 +29,9 @@ pub trait TransactionValidator: Send + Sync + 'static { /// This method should: /// - Verify the transaction is valid, against the current's rollup state, and also against /// in-flight transactions. - /// - Track the necessary state of the transaction until it is commited to the `store`, to + /// - Track the necessary state of the transaction until it is committed to the `store`, to /// perform the check above. - async fn verify_tx( - &self, - tx: &ProvenTransaction, - ) -> Result<(), VerifyTxError>; + async fn verify_tx(&self, tx: &ProvenTransaction) -> Result<(), VerifyTxError>; } // TRANSACTION QUEUE @@ -148,10 +145,7 @@ where /// This method will validate the `tx` and ensure it is valid w.r.t. the rollup state, and the /// current in-flight transactions. #[instrument(target = "miden-block-producer", skip_all, err)] - pub async fn add_transaction( - &self, - tx: ProvenTransaction, - ) -> Result<(), AddTransactionError> { + pub async fn add_transaction(&self, tx: ProvenTransaction) -> Result<(), AddTransactionError> { info!(target: COMPONENT, tx_id = %tx.id().to_hex(), account_id = %tx.account_id().to_hex()); self.tx_validator diff --git a/block-producer/src/txqueue/tests/mod.rs b/block-producer/src/txqueue/tests/mod.rs index f4ed5ddd3..3d8dd27cf 100644 --- a/block-producer/src/txqueue/tests/mod.rs +++ b/block-producer/src/txqueue/tests/mod.rs @@ -11,10 +11,7 @@ struct TransactionValidatorSuccess; #[async_trait] impl TransactionValidator for TransactionValidatorSuccess { - async fn verify_tx( - &self, - _tx: &ProvenTransaction, - ) -> Result<(), VerifyTxError> { + async fn verify_tx(&self, _tx: &ProvenTransaction) -> Result<(), VerifyTxError> { Ok(()) } } @@ -24,10 +21,7 @@ struct TransactionValidatorFailure; #[async_trait] impl TransactionValidator for TransactionValidatorFailure { - async fn verify_tx( - &self, - tx: &ProvenTransaction, - ) -> Result<(), VerifyTxError> { + async fn verify_tx(&self, tx: &ProvenTransaction) -> Result<(), VerifyTxError> { Err(VerifyTxError::AccountAlreadyModifiedByOtherTx(tx.account_id())) } } @@ -45,10 +39,7 @@ impl BatchBuilderSuccess { #[async_trait] impl BatchBuilder for BatchBuilderSuccess { - async fn build_batch( - &self, - txs: Vec, - ) -> Result<(), BuildBatchError> { + async fn build_batch(&self, txs: Vec) -> Result<(), BuildBatchError> { let batch = TransactionBatch::new(txs).expect("Tx batch building should have succeeded"); self.ready_batches .send(batch) @@ -64,10 +55,7 @@ struct BatchBuilderFailure; #[async_trait] impl BatchBuilder for BatchBuilderFailure { - async fn build_batch( - &self, - txs: Vec, - ) -> Result<(), BuildBatchError> { + async fn build_batch(&self, txs: Vec) -> Result<(), BuildBatchError> { Err(BuildBatchError::TooManyNotesCreated(0, txs)) } } @@ -87,10 +75,7 @@ async fn test_build_batch_success() { let tx_queue = Arc::new(TransactionQueue::new( Arc::new(TransactionValidatorSuccess), Arc::new(BatchBuilderSuccess::new(sender)), - TransactionQueueOptions { - build_batch_frequency, - batch_size, - }, + TransactionQueueOptions { build_batch_frequency, batch_size }, )); // Starts the transaction queue task. @@ -179,10 +164,7 @@ async fn test_tx_verify_failure() { let tx_queue = Arc::new(TransactionQueue::new( Arc::new(TransactionValidatorFailure), batch_builder.clone(), - TransactionQueueOptions { - build_batch_frequency, - batch_size, - }, + TransactionQueueOptions { build_batch_frequency, batch_size }, )); // Start the queue @@ -215,10 +197,7 @@ async fn test_build_batch_failure() { let tx_queue = TransactionQueue::new( Arc::new(TransactionValidatorSuccess), batch_builder.clone(), - TransactionQueueOptions { - build_batch_frequency, - batch_size, - }, + TransactionQueueOptions { build_batch_frequency, batch_size }, ); let internal_ready_queue = tx_queue.ready_queue.clone(); diff --git a/faucet/src/config.rs b/faucet/src/config.rs index 3479fc124..d292b6050 100644 --- a/faucet/src/config.rs +++ b/faucet/src/config.rs @@ -25,10 +25,7 @@ impl FaucetConfig { } impl Display for FaucetConfig { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( "{{ endpoint: \"{}\", store_url: \"{}\", block_producer_url: \"{}\" }}", self.endpoint, self.database_filepath, self.rpc_url diff --git a/faucet/src/handlers.rs b/faucet/src/handlers.rs index f97047ca0..1c546ed47 100644 --- a/faucet/src/handlers.rs +++ b/faucet/src/handlers.rs @@ -47,10 +47,7 @@ pub async fn get_tokens( FungibleAsset::new(state.id, state.asset_amount).expect("Failed to instantiate asset."); // Instantiate transaction template - let tx_template = TransactionTemplate::MintFungibleAsset { - asset, - target_account_id, - }; + let tx_template = TransactionTemplate::MintFungibleAsset { asset, target_account_id }; // Run transaction executor & execute transaction let tx_result = client diff --git a/faucet/src/utils.rs b/faucet/src/utils.rs index 05a2288e3..b5c07cae0 100644 --- a/faucet/src/utils.rs +++ b/faucet/src/utils.rs @@ -58,9 +58,7 @@ pub fn create_fungible_faucet( // Instantiate keypair and authscheme let auth_seed: [u8; 40] = [0; 40]; let keypair = KeyPair::from_seed(&auth_seed).expect("Failed to generate keypair."); - let auth_scheme = AuthScheme::RpoFalcon512 { - pub_key: keypair.public_key(), - }; + let auth_scheme = AuthScheme::RpoFalcon512 { pub_key: keypair.public_key() }; let (account, account_seed) = create_basic_fungible_faucet( init_seed, diff --git a/node/src/commands/genesis/mod.rs b/node/src/commands/genesis/mod.rs index b9a0e3873..6e627b86e 100644 --- a/node/src/commands/genesis/mod.rs +++ b/node/src/commands/genesis/mod.rs @@ -42,11 +42,7 @@ const DEFAULT_ACCOUNTS_DIR: &str = "accounts/"; /// This function returns a `Result` type. On successful creation of the genesis file, it returns /// `Ok(())`. If it fails at any point, due to issues like file existence checks or read/write /// operations, it returns an `Err` with a detailed error message. -pub fn make_genesis( - inputs_path: &PathBuf, - output_path: &PathBuf, - force: &bool, -) -> Result<()> { +pub fn make_genesis(inputs_path: &PathBuf, output_path: &PathBuf, force: &bool) -> Result<()> { let inputs_path = Path::new(inputs_path); let output_path = Path::new(output_path); @@ -190,9 +186,7 @@ fn parse_auth_inputs( let mut rng = ChaCha20Rng::from_seed(auth_seed); let secret = SecretKey::with_rng(&mut rng); - let auth_scheme = AuthScheme::RpoFalcon512 { - pub_key: secret.public_key(), - }; + let auth_scheme = AuthScheme::RpoFalcon512 { pub_key: secret.public_key() }; let auth_info = AuthData::RpoFalcon512Seed(auth_seed); Ok((auth_scheme, auth_info)) diff --git a/node/src/main.rs b/node/src/main.rs index c365e49c7..2dfa06a93 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -58,10 +58,8 @@ async fn main() -> anyhow::Result<()> { match &cli.command { Command::Start { config } => commands::start_node(config).await, - Command::MakeGenesis { - output_path, - force, - inputs_path, - } => commands::make_genesis(inputs_path, output_path, force), + Command::MakeGenesis { output_path, force, inputs_path } => { + commands::make_genesis(inputs_path, output_path, force) + }, } } diff --git a/proto/src/domain/accounts.rs b/proto/src/domain/accounts.rs index 3f298cd63..4204f1ce0 100644 --- a/proto/src/domain/accounts.rs +++ b/proto/src/domain/accounts.rs @@ -25,19 +25,13 @@ use crate::{ // ================================================================================================ impl Display for AccountIdPb { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("0x{:x}", self.id)) } } impl Debug for AccountIdPb { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } } @@ -59,9 +53,7 @@ impl From<&AccountId> for AccountIdPb { impl From for AccountIdPb { fn from(account_id: AccountId) -> Self { - Self { - id: account_id.into(), - } + Self { id: account_id.into() } } } @@ -188,10 +180,7 @@ pub struct AccountState { } impl Display for AccountState { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( "{{ account_id: {}, account_hash: {} }}", self.account_id, @@ -232,10 +221,7 @@ impl TryFrom for AccountState { Some(account_hash) }; - Ok(Self { - account_id, - account_hash, - }) + Ok(Self { account_id, account_hash }) } } diff --git a/proto/src/domain/digest.rs b/proto/src/domain/digest.rs index e9bfb00a8..b3f15416b 100644 --- a/proto/src/domain/digest.rs +++ b/proto/src/domain/digest.rs @@ -14,19 +14,13 @@ pub const DIGEST_DATA_SIZE: usize = 32; // ================================================================================================ impl Display for digest::Digest { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(&self.encode_hex::()) } } impl Debug for digest::Digest { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { Display::fmt(self, f) } } @@ -68,14 +62,12 @@ impl FromHex for digest::Digest { let data = hex::decode(hex)?; match data.len() { - size if size < DIGEST_DATA_SIZE => Err(ConversionError::InsufficientData { - expected: DIGEST_DATA_SIZE, - got: size, - }), - size if size > DIGEST_DATA_SIZE => Err(ConversionError::TooMuchData { - expected: DIGEST_DATA_SIZE, - got: size, - }), + size if size < DIGEST_DATA_SIZE => { + Err(ConversionError::InsufficientData { expected: DIGEST_DATA_SIZE, got: size }) + }, + size if size > DIGEST_DATA_SIZE => { + Err(ConversionError::TooMuchData { expected: DIGEST_DATA_SIZE, got: size }) + }, _ => { let d0 = u64::from_be_bytes(data[..8].try_into().unwrap()); let d1 = u64::from_be_bytes(data[8..16].try_into().unwrap()); @@ -229,12 +221,7 @@ mod test { let round_trip: Result = FromHex::from_hex::<&[u8]>(encoded.as_ref()); assert_eq!(digest, round_trip.unwrap()); - let digest = Digest { - d0: 0, - d1: 0, - d2: 0, - d3: 0, - }; + let digest = Digest { d0: 0, d1: 0, d2: 0, d3: 0 }; let encoded: String = ToHex::encode_hex(&digest); let round_trip: Result = FromHex::from_hex::<&[u8]>(encoded.as_ref()); assert_eq!(digest, round_trip.unwrap()); diff --git a/proto/src/domain/merkle.rs b/proto/src/domain/merkle.rs index 04d890247..161467d09 100644 --- a/proto/src/domain/merkle.rs +++ b/proto/src/domain/merkle.rs @@ -33,10 +33,7 @@ impl TryFrom for MerklePath { impl From for generated::mmr::MmrDelta { fn from(value: MmrDelta) -> Self { let data = value.data.into_iter().map(generated::digest::Digest::from).collect(); - generated::mmr::MmrDelta { - forest: value.forest as u64, - data, - } + generated::mmr::MmrDelta { forest: value.forest as u64, data } } } @@ -91,9 +88,9 @@ impl From for generated::smt::SmtLeaf { let leaf = match smt_leaf { SmtLeaf::Empty(leaf_index) => Leaf::Empty(leaf_index.value()), SmtLeaf::Single(entry) => Leaf::Single(entry.into()), - SmtLeaf::Multiple(entries) => Leaf::Multiple(generated::smt::SmtLeafEntries { - entries: convert(entries), - }), + SmtLeaf::Multiple(entries) => { + Leaf::Multiple(generated::smt::SmtLeafEntries { entries: convert(entries) }) + }, }; Self { leaf: Some(leaf) } diff --git a/rpc/src/config.rs b/rpc/src/config.rs index ab6b8e0a4..21db97497 100644 --- a/rpc/src/config.rs +++ b/rpc/src/config.rs @@ -24,10 +24,7 @@ impl RpcConfig { } impl Display for RpcConfig { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( "{{ endpoint: \"{}\", store_url: \"{}\", block_producer_url: \"{}\" }}", self.endpoint, self.store_url, self.block_producer_url diff --git a/rpc/src/server/api.rs b/rpc/src/server/api.rs index 3de3dde8f..2c8b7590c 100644 --- a/rpc/src/server/api.rs +++ b/rpc/src/server/api.rs @@ -49,10 +49,7 @@ impl RpcApi { "Block producer client initialized", ); - Ok(Self { - store, - block_producer, - }) + Ok(Self { store, block_producer }) } } diff --git a/rustfmt.toml b/rustfmt.toml index 7f53261af..ffe9adab6 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,13 +1,16 @@ +edition = "2021" array_width = 80 attr_fn_like_width = 80 chain_width = 80 -edition = "2021" +condense_wildcard_suffixes = true fn_call_width = 80 -fn_params_layout = "Vertical" group_imports = "StdExternalCrate" imports_granularity = "Crate" -match_block_trailing_comma = true newline_style = "Unix" +match_block_trailing_comma = true single_line_if_else_max_width = 60 +single_line_let_else_max_width = 60 +struct_lit_width = 40 +struct_variant_width = 40 use_field_init_shorthand = true -use_try_shorthand = true +use_try_shorthand = true \ No newline at end of file diff --git a/store/src/config.rs b/store/src/config.rs index 120d7cdf0..e75c98efa 100644 --- a/store/src/config.rs +++ b/store/src/config.rs @@ -28,10 +28,7 @@ impl StoreConfig { } impl Display for StoreConfig { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( "{{ endpoint: \"{}\", database_filepath: {:?}, genesis_filepath: {:?} }}", self.endpoint, self.database_filepath, self.genesis_filepath diff --git a/store/src/db/mod.rs b/store/src/db/mod.rs index 95aa083a7..7bc4aff76 100644 --- a/store/src/db/mod.rs +++ b/store/src/db/mod.rs @@ -207,10 +207,7 @@ impl Db { /// Loads public account details from the DB. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] - pub async fn select_account( - &self, - id: AccountId, - ) -> Result { + pub async fn select_account(&self, id: AccountId) -> Result { self.pool .get() .await? @@ -254,10 +251,7 @@ impl Db { /// Loads all the Note's matching a certain NoteId from the database. #[instrument(target = "miden-store", skip_all, ret(level = "debug"), err)] - pub async fn select_notes_by_id( - &self, - note_ids: Vec, - ) -> Result> { + pub async fn select_notes_by_id(&self, note_ids: Vec) -> Result> { self.pool .get() .await? @@ -317,10 +311,7 @@ impl Db { /// genesis block in the database is consistent with the genesis block data in the genesis JSON /// file. #[instrument(target = "miden-store", skip_all, err)] - async fn ensure_genesis_block( - &self, - genesis_filepath: &str, - ) -> Result<(), GenesisError> { + async fn ensure_genesis_block(&self, genesis_filepath: &str) -> Result<(), GenesisError> { let (expected_genesis_header, account_smt) = { let file_contents = fs::read(genesis_filepath).map_err(|error| { GenesisError::FailedToReadGenesisFile { diff --git a/store/src/db/sql.rs b/store/src/db/sql.rs index 9ff38ecb1..2e09a34bc 100644 --- a/store/src/db/sql.rs +++ b/store/src/db/sql.rs @@ -122,10 +122,7 @@ pub fn select_accounts_by_block_range( /// # Returns /// /// The latest account details, or an error. -pub fn select_account( - conn: &mut Connection, - account_id: AccountId, -) -> Result { +pub fn select_account(conn: &mut Connection, account_id: AccountId) -> Result { let mut stmt = conn.prepare( " SELECT @@ -304,10 +301,7 @@ pub fn select_nullifiers_by_block_range( let nullifier_data = row.get_ref(0)?.as_blob()?; let nullifier = Nullifier::read_from_bytes(nullifier_data)?; let block_num = row.get(1)?; - result.push(NullifierInfo { - nullifier, - block_num, - }); + result.push(NullifierInfo { nullifier, block_num }); } Ok(result) } @@ -380,10 +374,7 @@ pub fn select_notes(conn: &mut Connection) -> Result> { /// /// The [Transaction] object is not consumed. It's up to the caller to commit or rollback the /// transaction. -pub fn insert_notes( - transaction: &Transaction, - notes: &[Note], -) -> Result { +pub fn insert_notes(transaction: &Transaction, notes: &[Note]) -> Result { let mut stmt = transaction.prepare( " INSERT INTO @@ -520,10 +511,7 @@ pub fn select_notes_since_block_by_tag_and_sender( /// /// - Empty vector if no matching `note`. /// - Otherwise, notes which `note_hash` matches the `NoteId` as bytes. -pub fn select_notes_by_id( - conn: &mut Connection, - note_ids: &[NoteId], -) -> Result> { +pub fn select_notes_by_id(conn: &mut Connection, note_ids: &[NoteId]) -> Result> { let note_ids: Vec = note_ids.iter().map(|id| id.to_bytes().into()).collect(); let mut stmt = conn.prepare( @@ -587,10 +575,7 @@ pub fn select_notes_by_id( /// /// The [Transaction] object is not consumed. It's up to the caller to commit or rollback the /// transaction. -pub fn insert_block_header( - transaction: &Transaction, - block_header: &BlockHeader, -) -> Result { +pub fn insert_block_header(transaction: &Transaction, block_header: &BlockHeader) -> Result { let mut stmt = transaction .prepare("INSERT INTO block_headers (block_num, block_header) VALUES (?1, ?2);")?; Ok(stmt.execute(params![block_header.block_num(), block_header.to_bytes()])?) @@ -784,10 +769,7 @@ fn account_info_from_row(row: &rusqlite::Row<'_>) -> Result { let details = row.get_ref(3)?.as_blob_or_null()?; let details = details.map(Account::read_from_bytes).transpose()?; - Ok(AccountInfo { - summary: update, - details, - }) + Ok(AccountInfo { summary: update, details }) } /// Deserializes account and applies account delta. diff --git a/store/src/db/tests.rs b/store/src/db/tests.rs index 6c6dad2d1..e3f391ee8 100644 --- a/store/src/db/tests.rs +++ b/store/src/db/tests.rs @@ -28,10 +28,7 @@ fn create_db() -> Connection { conn } -fn create_block( - conn: &mut Connection, - block_num: u32, -) { +fn create_block(conn: &mut Connection, block_num: u32) { let block_header = BlockHeader::new( num_to_rpo_digest(1), block_num, diff --git a/store/src/genesis.rs b/store/src/genesis.rs index 0df14d14e..7bd354c76 100644 --- a/store/src/genesis.rs +++ b/store/src/genesis.rs @@ -18,21 +18,13 @@ pub struct GenesisState { } impl GenesisState { - pub fn new( - accounts: Vec, - version: u64, - timestamp: u64, - ) -> Self { - Self { - accounts, - version, - timestamp, - } + pub fn new(accounts: Vec, version: u64, timestamp: u64) -> Self { + Self { accounts, version, timestamp } } /// Returns the block header and the account SMT pub fn into_block_parts( - self + self, ) -> Result<(BlockHeader, SimpleSmt), MerkleError> { let account_smt: SimpleSmt = SimpleSmt::with_leaves( self.accounts @@ -65,10 +57,7 @@ impl GenesisState { // ================================================================================================ impl Serializable for GenesisState { - fn write_into( - &self, - target: &mut W, - ) { + fn write_into(&self, target: &mut W) { assert!(self.accounts.len() <= u64::MAX as usize, "too many accounts in GenesisState"); target.write_usize(self.accounts.len()); target.write_many(&self.accounts); diff --git a/store/src/main.rs b/store/src/main.rs index 3e57213a9..5e182916d 100644 --- a/store/src/main.rs +++ b/store/src/main.rs @@ -39,17 +39,13 @@ async fn main() -> Result<()> { /// Sends a gRPC request as specified by `command`. /// /// The request is sent to the endpoint defined in `config`. -async fn query( - config: StoreTopLevelConfig, - command: Query, -) -> Result<()> { +async fn query(config: StoreTopLevelConfig, command: Query) -> Result<()> { let mut client = api_client::ApiClient::connect(config.store.endpoint.to_string()).await?; match command { Query::GetBlockHeaderByNumber(args) => { - let request = tonic::Request::new(GetBlockHeaderByNumberRequest { - block_num: args.block_num, - }); + let request = + tonic::Request::new(GetBlockHeaderByNumberRequest { block_num: args.block_num }); let response = client.get_block_header_by_number(request).await?.into_inner(); match response.block_header { Some(block_header) => { diff --git a/store/src/nullifier_tree.rs b/store/src/nullifier_tree.rs index e4ef902b1..b5f3c542d 100644 --- a/store/src/nullifier_tree.rs +++ b/store/src/nullifier_tree.rs @@ -16,7 +16,7 @@ pub struct NullifierTree(Smt); impl NullifierTree { /// Construct new nullifier tree from list of items. pub fn with_entries( - entries: impl IntoIterator + entries: impl IntoIterator, ) -> Result { let leaves = entries.into_iter().map(|(nullifier, block_num)| { (nullifier.inner(), Self::block_num_to_leaf_value(block_num)) @@ -33,10 +33,7 @@ impl NullifierTree { } /// Returns an opening of the leaf associated with the given nullifier. - pub fn open( - &self, - nullifier: &Nullifier, - ) -> SmtProof { + pub fn open(&self, nullifier: &Nullifier) -> SmtProof { self.0.open(&nullifier.inner()) } @@ -62,10 +59,7 @@ impl NullifierTree { /// Returns block number stored for the given nullifier or `None` if the nullifier wasn't /// consumed. - pub fn get_block_num( - &self, - nullifier: &Nullifier, - ) -> Option { + pub fn get_block_num(&self, nullifier: &Nullifier) -> Option { let value = self.0.get_value(&nullifier.inner()); if value == Smt::EMPTY_VALUE { return None; diff --git a/store/src/server/api.rs b/store/src/server/api.rs index 5d7bcd284..2d8306aaf 100644 --- a/store/src/server/api.rs +++ b/store/src/server/api.rs @@ -102,9 +102,7 @@ impl api_server::Api for StoreApi { // Query the state for the request's nullifiers let proofs = self.state.check_nullifiers(&nullifiers).await; - Ok(Response::new(CheckNullifiersResponse { - proofs: convert(proofs), - })) + Ok(Response::new(CheckNullifiersResponse { proofs: convert(proofs) })) } /// Returns info which can be used by the client to sync up to the latest state of the chain diff --git a/store/src/server/mod.rs b/store/src/server/mod.rs index 3bb4e7ade..68d505a0e 100644 --- a/store/src/server/mod.rs +++ b/store/src/server/mod.rs @@ -12,10 +12,7 @@ mod api; // STORE INITIALIZER // ================================================================================================ -pub async fn serve( - config: StoreConfig, - db: Db, -) -> Result<()> { +pub async fn serve(config: StoreConfig, db: Db) -> Result<()> { info!(target: COMPONENT, %config, "Initializing server"); let state = Arc::new(State::load(db).await?); diff --git a/store/src/state.rs b/store/src/state.rs index 5bb9ad4b1..6f4a49821 100644 --- a/store/src/state.rs +++ b/store/src/state.rs @@ -73,11 +73,7 @@ impl State { let chain_mmr = load_mmr(&mut db).await?; let account_tree = load_accounts(&mut db).await?; - let inner = RwLock::new(InnerState { - nullifier_tree, - chain_mmr, - account_tree, - }); + let inner = RwLock::new(InnerState { nullifier_tree, chain_mmr, account_tree }); let writer = Mutex::new(()); let db = Arc::new(db); @@ -298,10 +294,7 @@ impl State { /// /// Note: these proofs are invalidated once the nullifier tree is modified, i.e. on a new block. #[instrument(target = "miden-store", skip_all, ret(level = "debug"))] - pub async fn check_nullifiers( - &self, - nullifiers: &[Nullifier], - ) -> Vec { + pub async fn check_nullifiers(&self, nullifiers: &[Nullifier]) -> Vec { let inner = self.inner.read().await; nullifiers.iter().map(|n| inner.nullifier_tree.open(n)).collect() } @@ -310,10 +303,7 @@ impl State { /// /// If the provided list of [NoteId] given is empty or no [Note] matches the provided [NoteId] /// an empty list is returned. - pub async fn get_notes_by_id( - &self, - note_ids: Vec, - ) -> Result, DatabaseError> { + pub async fn get_notes_by_id(&self, note_ids: Vec) -> Result, DatabaseError> { self.db.select_notes_by_id(note_ids).await } @@ -350,10 +340,7 @@ impl State { let delta = if block_num == state_sync.block_header.block_num() { // The client is in sync with the chain tip. - MmrDelta { - forest: block_num as usize, - data: vec![], - } + MmrDelta { forest: block_num as usize, data: vec![] } } else { // Important notes about the boundary conditions: // @@ -411,10 +398,8 @@ impl State { .iter() .cloned() .map(|account_id| { - let ValuePath { - value: account_hash, - path: proof, - } = inner.account_tree.open(&LeafIndex::new_max_depth(account_id)); + let ValuePath { value: account_hash, path: proof } = + inner.account_tree.open(&LeafIndex::new_max_depth(account_id)); Ok(AccountInputRecord { account_id: account_id.try_into()?, account_hash, @@ -428,10 +413,7 @@ impl State { .map(|nullifier| { let proof = inner.nullifier_tree.open(nullifier); - NullifierWitness { - nullifier: *nullifier, - proof, - } + NullifierWitness { nullifier: *nullifier, proof } }) .collect(); @@ -459,10 +441,7 @@ impl State { }) .collect(); - TransactionInputs { - account_hash, - nullifiers, - } + TransactionInputs { account_hash, nullifiers } } /// Lists all known nullifiers with their inclusion blocks, intended for testing. @@ -482,10 +461,7 @@ impl State { } /// Returns details for public (on-chain) account. - pub async fn get_account_details( - &self, - id: AccountId, - ) -> Result { + pub async fn get_account_details(&self, id: AccountId) -> Result { self.db.select_account(id).await } } @@ -542,7 +518,7 @@ async fn load_mmr(db: &mut Db) -> Result { #[instrument(target = "miden-store", skip_all)] async fn load_accounts( - db: &mut Db + db: &mut Db, ) -> Result, StateInitializationError> { let account_data: Vec<_> = db .select_account_hashes() diff --git a/test-macro/src/lib.rs b/test-macro/src/lib.rs index 9afce1c78..e0c0e7db2 100644 --- a/test-macro/src/lib.rs +++ b/test-macro/src/lib.rs @@ -3,10 +3,7 @@ use quote::ToTokens; use syn::{parse_macro_input, parse_quote, Block, ItemFn}; #[proc_macro_attribute] -pub fn enable_logging( - _attr: TokenStream, - item: TokenStream, -) -> TokenStream { +pub fn enable_logging(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut function = parse_macro_input!(item as ItemFn); let name = function.sig.ident.to_string(); diff --git a/utils/src/config.rs b/utils/src/config.rs index 4cb2d81f9..bd0a1be7f 100644 --- a/utils/src/config.rs +++ b/utils/src/config.rs @@ -29,10 +29,7 @@ impl ToSocketAddrs for Endpoint { } impl Display for Endpoint { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("http://{}:{}", self.host, self.port)) } } diff --git a/utils/src/formatting.rs b/utils/src/formatting.rs index 36a65ded0..74da6630f 100644 --- a/utils/src/formatting.rs +++ b/utils/src/formatting.rs @@ -35,7 +35,7 @@ pub fn format_output_notes(notes: &OutputNotes) -> String { } pub fn format_map<'a, K: Display + 'a, V: Display + 'a>( - map: impl IntoIterator + map: impl IntoIterator, ) -> String { let map_str = map.into_iter().map(|(key, val)| format!("{key}: {val}")).join(", "); if map_str.is_empty() { From 0ebc4ebda6cdca309b320255cc685d5e44dbf545 Mon Sep 17 00:00:00 2001 From: Bobbin Threadbare Date: Thu, 11 Apr 2024 23:21:54 -0700 Subject: [PATCH 28/29] chore: update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd49f0e07..da666ab8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.2.0 (2024-04-11) + +* Implemented Docker-based node deployment (#257). +* Improved build process (#267, #272, #278). +* Implemented Nullifier tree wrapper (#275). +* [BREAKING] Added support for public accounts (#287, #293, #294). +* [BREAKING] Added support for public notes (#300, #310). +* Added `GetNotesById` endpoint (#298). +* Implemented amd64 debian packager (#312). + ## 0.1.0 (2024-03-11) * Initial release. From 2482c06818a1f2ae7f496ca7cc9faaf1b9b8f1e7 Mon Sep 17 00:00:00 2001 From: Dominik Schmid <35031754+Dominik1999@users.noreply.github.com> Date: Fri, 12 Apr 2024 09:13:38 +0200 Subject: [PATCH 29/29] docs: cleaning up artefacts (#319) --- requirements.txt => scripts/docs_requirements.txt | 0 run.sh => scripts/serve-doc-site.sh | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename requirements.txt => scripts/docs_requirements.txt (100%) rename run.sh => scripts/serve-doc-site.sh (69%) diff --git a/requirements.txt b/scripts/docs_requirements.txt similarity index 100% rename from requirements.txt rename to scripts/docs_requirements.txt diff --git a/run.sh b/scripts/serve-doc-site.sh similarity index 69% rename from run.sh rename to scripts/serve-doc-site.sh index 7ec676851..219462915 100755 --- a/run.sh +++ b/scripts/serve-doc-site.sh @@ -3,5 +3,6 @@ set -euo pipefail virtualenv venv source venv/bin/activate -pip3 install -r requirements.txt +pip3 install -r docs_requirements.txt +cd .. mkdocs serve --strict \ No newline at end of file