From a62fe3af40b2fca3bfed8b3e7b1123b4448682cc Mon Sep 17 00:00:00 2001 From: Buffrr Date: Fri, 17 Apr 2026 18:41:24 +0200 Subject: [PATCH 1/2] chore: set up release-plz and consolidate publish workflows Sets up automated changelog/versioning/publishing for the libveritas Rust crates via release-plz. - Add release-plz.toml + release-plz.yml workflow - Add commitlint workflow + .commitlintrc.yml - Add LICENSE (Apache-2.0), CHANGELOG.md, CONTRIBUTING.md, SECURITY.md - Add lint + docs jobs to CI - Set [workspace.package] with shared version/edition/license/repo/authors - Mark non-publishable crates publish=false - Switch spaces_protocol/spaces_nums/sip7/borsh_utils/spacedb to crates.io - Add module-level //! docs to libveritas and libveritas_zk - Switch binding workflows (npm/pypi/kotlin/swift/react-native/go) to trigger on libveritas-v* tags and extract version accordingly - Update README install to "cargo add libveritas" + crates.io badges - cargo fmt + cargo clippy --fix cleanup pass --- .commitlintrc.yml | 21 + .github/workflows/commitlint.yml | 16 + .github/workflows/publish-go.yml | 6 +- .github/workflows/publish-kotlin-android.yml | 4 +- .github/workflows/publish-kotlin-jvm.yml | 4 +- .github/workflows/publish-npm.yml | 4 +- .github/workflows/publish-pypi.yml | 4 +- .github/workflows/publish-react-native.yml | 4 +- .github/workflows/publish-swift.yml | 4 +- .github/workflows/release-plz.yml | 49 ++ .github/workflows/test.yml | 23 + CHANGELOG.md | 6 + CONTRIBUTING.md | 45 ++ Cargo.lock | 21 +- Cargo.toml | 24 +- LICENSE | 201 ++++++++ README.md | 9 +- SECURITY.md | 21 + bindings/python/Cargo.toml | 6 +- bindings/uniffi/Cargo.toml | 8 +- bindings/uniffi/src/lib.rs | 493 ++++++++++++------- bindings/wasm/Cargo.toml | 8 +- bindings/wasm/src/lib.rs | 214 +++++--- examples/generate_fixture.rs | 5 +- methods/Cargo.toml | 6 +- methods/guest/Cargo.lock | 5 +- release-plz.toml | 86 ++++ testutil/Cargo.toml | 10 +- testutil/src/fixture.rs | 109 ++-- testutil/src/lib.rs | 30 +- veritas/Cargo.toml | 29 +- veritas/elfs/fold.bin | Bin 372816 -> 372804 bytes veritas/elfs/step.bin | Bin 381404 -> 381256 bytes veritas/src/builder.rs | 35 +- veritas/src/cert.rs | 178 +++++-- veritas/src/constants.rs | 4 +- veritas/src/lib.rs | 475 ++++++++++++------ veritas/src/msg.rs | 89 ++-- veritas/src/names.rs | 108 ++-- veritas/tests/fixture_tests.rs | 71 ++- veritas/tests/integration_tests.rs | 289 ++++++++--- zk/Cargo.toml | 15 +- zk/src/guest.rs | 46 +- zk/src/lib.rs | 11 +- 44 files changed, 2023 insertions(+), 773 deletions(-) create mode 100644 .commitlintrc.yml create mode 100644 .github/workflows/commitlint.yml create mode 100644 .github/workflows/release-plz.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 release-plz.toml diff --git a/.commitlintrc.yml b/.commitlintrc.yml new file mode 100644 index 0000000..67ff005 --- /dev/null +++ b/.commitlintrc.yml @@ -0,0 +1,21 @@ +extends: + - '@commitlint/config-conventional' + +rules: + # Allow slightly longer subjects; we have descriptive messages. + header-max-length: [2, always, 100] + # Enforce lowercase type (feat, fix, ...) and allow common scopes. + type-enum: + - 2 + - always + - - build + - chore + - ci + - docs + - feat + - fix + - perf + - refactor + - revert + - style + - test diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..a347bd3 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,16 @@ +name: Commitlint + +on: + pull_request: + types: [opened, reopened, edited, synchronize] + +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@v6 + with: + configFile: .commitlintrc.yml diff --git a/.github/workflows/publish-go.yml b/.github/workflows/publish-go.yml index 948cb33..fe48d2c 100644 --- a/.github/workflows/publish-go.yml +++ b/.github/workflows/publish-go.yml @@ -3,7 +3,7 @@ name: Publish Go bindings on: push: tags: - - "v*" + - "libveritas-v*" workflow_dispatch: permissions: @@ -100,7 +100,7 @@ jobs: - name: Setup Go module run: | - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${GITHUB_REF_NAME#libveritas-v}" if [[ ! "$GITHUB_REF" == refs/tags/* ]]; then VERSION="0.0.0-dev.$(date +%Y%m%d%H%M%S)" fi @@ -116,7 +116,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GO_PUBLISH_TOKEN }} run: | - VERSION="${GITHUB_REF_NAME}" + VERSION="v${GITHUB_REF_NAME#libveritas-v}" if [[ ! "$GITHUB_REF" == refs/tags/* ]]; then VERSION="v0.0.0-dev.$(date +%Y%m%d%H%M%S)" fi diff --git a/.github/workflows/publish-kotlin-android.yml b/.github/workflows/publish-kotlin-android.yml index 9bbe9e0..801b2c6 100644 --- a/.github/workflows/publish-kotlin-android.yml +++ b/.github/workflows/publish-kotlin-android.yml @@ -3,7 +3,7 @@ name: Publish Kotlin Android on: push: tags: - - "v*" + - "libveritas-v*" workflow_dispatch: permissions: @@ -57,7 +57,7 @@ jobs: - name: Set version from tag run: | - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${GITHUB_REF_NAME#libveritas-v}" if [[ ! "$GITHUB_REF" == refs/tags/* ]]; then VERSION="0.0.0-dev.$(date +%Y%m%d%H%M%S)" fi diff --git a/.github/workflows/publish-kotlin-jvm.yml b/.github/workflows/publish-kotlin-jvm.yml index e0bcf77..49498f4 100644 --- a/.github/workflows/publish-kotlin-jvm.yml +++ b/.github/workflows/publish-kotlin-jvm.yml @@ -3,7 +3,7 @@ name: Publish Kotlin JVM on: push: tags: - - "v*" + - "libveritas-v*" workflow_dispatch: permissions: @@ -92,7 +92,7 @@ jobs: - name: Set version run: | - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${GITHUB_REF_NAME#libveritas-v}" if [[ ! "$GITHUB_REF" == refs/tags/* ]]; then VERSION="0.0.0-dev.$(date +%Y%m%d%H%M%S)" fi diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 8cb5a7c..85399b8 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -3,7 +3,7 @@ name: Publish npm (WASM) on: push: tags: - - "v*" + - "libveritas-v*" workflow_dispatch: permissions: @@ -40,7 +40,7 @@ jobs: - name: Set package version from tag run: | - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${GITHUB_REF_NAME#libveritas-v}" if [[ ! "$GITHUB_REF" == refs/tags/* ]]; then VERSION="0.0.0-dev.$(date +%Y%m%d%H%M%S)" fi diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 6a311cc..762fdc2 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -3,7 +3,7 @@ name: Publish PyPI (Python) on: push: tags: - - "v*" + - "libveritas-v*" workflow_dispatch: permissions: @@ -18,7 +18,7 @@ jobs: steps: - id: ver run: | - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${GITHUB_REF_NAME#libveritas-v}" if [[ ! "$GITHUB_REF" == refs/tags/* ]]; then VERSION="0.0.0-dev.$(date +%Y%m%d%H%M%S)" fi diff --git a/.github/workflows/publish-react-native.yml b/.github/workflows/publish-react-native.yml index 1f0fad1..9b5768f 100644 --- a/.github/workflows/publish-react-native.yml +++ b/.github/workflows/publish-react-native.yml @@ -3,7 +3,7 @@ name: Publish React Native (npm) on: push: tags: - - "v*" + - "libveritas-v*" workflow_dispatch: permissions: @@ -52,7 +52,7 @@ jobs: - name: Set version from tag working-directory: bindings/react-native run: | - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${GITHUB_REF_NAME#libveritas-v}" if [[ ! "$GITHUB_REF" == refs/tags/* ]]; then VERSION="0.0.0-dev.$(date +%Y%m%d%H%M%S)" fi diff --git a/.github/workflows/publish-swift.yml b/.github/workflows/publish-swift.yml index c8a9171..0f6f87d 100644 --- a/.github/workflows/publish-swift.yml +++ b/.github/workflows/publish-swift.yml @@ -3,7 +3,7 @@ name: Publish Swift (XCFramework) on: push: tags: - - "v*" + - "libveritas-v*" workflow_dispatch: permissions: @@ -82,7 +82,7 @@ jobs: echo "checksum=$CHECKSUM" >> "$GITHUB_OUTPUT" echo "XCFramework checksum: $CHECKSUM" - VERSION="${GITHUB_REF_NAME}" + VERSION="v${GITHUB_REF_NAME#libveritas-v}" if [[ ! "$GITHUB_REF" == refs/tags/* ]]; then VERSION="v0.0.0-dev.$(date +%Y%m%d%H%M%S)" fi diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 0000000..e467cdb --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,49 @@ +name: Release-plz + +permissions: + pull-requests: write + contents: write + +on: + push: + branches: [main] + +jobs: + # Opens / updates the "release PR" that bumps versions and edits the CHANGELOG. + release-plz-pr: + name: Release-plz PR + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'spacesprotocol' }} + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + # Tags + publishes to crates.io when a release commit lands on main. + release-plz-release: + name: Release-plz publish + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'spacesprotocol' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b88aa90..2dc3d32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,29 @@ jobs: - name: Run tests run: cargo test -p libveritas + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all -- --check + - run: cargo clippy --workspace --exclude libveritas_methods --all-targets --all-features -- -D warnings + + docs: + name: Docs + runs-on: ubuntu-latest + env: + RUSTDOCFLAGS: -D warnings + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo doc --no-deps --all-features -p libveritas -p libveritas_zk + verify-elfs: runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9e0e2bb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b1cc5c8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing to libveritas + +Everyone is welcome to contribute towards development in the form of peer review, testing, and patches. This document explains the practical process and guidelines. + +## Getting started + +Reviewing and testing is highly valued and the most effective way to contribute as a new contributor. It also teaches you much more about the code and process than opening pull requests. + +### Good First Issue Label + +The purpose of the good-first-issue label is to highlight issues suitable for new contributors without a deep understanding of the codebase. + +You do not need to request permission to start working on an issue. However, it's helpful to leave a comment if you are planning to work on one — it helps other contributors track which issues are actively being addressed and is also a good way to request assistance. + +## Communication channels + +You can join the [Spaces telegram](https://t.me/spacesprotocol). + +Discussion about codebase improvements happens in GitHub issues and pull requests. + +## Contributor workflow + +The codebase is maintained using the "contributor workflow" where everyone contributes patch proposals using pull requests. + +To contribute a patch: + +1. Fork the repository (only the first time) +2. Create a topic branch +3. Commit patches using [conventional commits](https://www.conventionalcommits.org/) — this is enforced by CI and drives the changelog and release versioning. Examples: `feat: add lookup helper`, `fix(builder): handle empty record sets`, `docs: clarify SIG record semantics`. + +## Squashing commits + +If your pull request contains fixup commits or too fine-grained commits, squash them before review. See [how to write good commit messages](https://cbea.ms/git-commit/). + +## Pull request philosophy + +Keep patchsets focused: a PR should add a feature, fix a bug, or refactor code — not a mixture. Avoid super pull requests that try to do too much. + +## Releases + +Releases are automated via [release-plz](https://release-plz.dev/). When commits land on `main`, a release PR is opened automatically with version bumps and changelog entries derived from your conventional commits. Merging that PR tags the release and publishes to crates.io. + +## Copyright + +By contributing to this repository, you agree to license your work under the Apache-2.0 license. Any work contributed where you are not the original author must contain its license header with the original author(s) and source. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index b7500af..8a87d50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,7 +493,8 @@ dependencies = [ [[package]] name = "borsh_utils" version = "0.1.0" -source = "git+https://github.com/spacesprotocol/spaces.git?branch=subspaces#12adf5f9f28d1d1174c3bee012516df8857613c7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c0cd2efcc60ce44ca958baa626593a23006b5418b1fa8f038c23aa85f7a97f" dependencies = [ "bitcoin", "borsh", @@ -1401,7 +1402,7 @@ dependencies = [ [[package]] name = "libveritas_methods" -version = "0.1.2" +version = "0.1.0" dependencies = [ "risc0-build", ] @@ -2384,7 +2385,8 @@ dependencies = [ [[package]] name = "sip7" version = "0.1.0" -source = "git+https://github.com/spacesprotocol/spaces.git?branch=subspaces#12adf5f9f28d1d1174c3bee012516df8857613c7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b684197991554bec0801fb44663974608d59ac6df4f95d2d84a3955e82499a85" dependencies = [ "base64 0.22.1", "hex", @@ -2418,8 +2420,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "spacedb" -version = "0.0.12" -source = "git+https://github.com/spacesprotocol/spacedb.git#43f23e78e4e5fffb8d89661a4b7f39ab43a5a644" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a48cda82e951391df9d0a54c96f8b04e117ff39abad5359b181cb71c80798d77" dependencies = [ "borsh", "sha2", @@ -2428,7 +2431,8 @@ dependencies = [ [[package]] name = "spaces_nums" version = "0.1.0" -source = "git+https://github.com/spacesprotocol/spaces.git?branch=subspaces#12adf5f9f28d1d1174c3bee012516df8857613c7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2015555e35781fc95ac0eb8e50bef6e70929248ea61d4101b195b4d0abb13e1d" dependencies = [ "bech32", "bitcoin", @@ -2442,8 +2446,9 @@ dependencies = [ [[package]] name = "spaces_protocol" -version = "0.0.7" -source = "git+https://github.com/spacesprotocol/spaces.git?branch=subspaces#12adf5f9f28d1d1174c3bee012516df8857613c7" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f46b7eb14311193304301a394a30e5f7bdfd7fd9f70a5c8113610bf2b5cc2c" dependencies = [ "bitcoin", "borsh", diff --git a/Cargo.toml b/Cargo.toml index 6b1ee7b..3d85170 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,12 +2,26 @@ resolver = "2" members = ["methods", "zk", "veritas", "testutil", "bindings/wasm", "bindings/uniffi", "bindings/python"] +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +license = "Apache-2.0" +repository = "https://github.com/spacesprotocol/libveritas" +homepage = "https://spacesprotocol.org" +authors = ["Buffrr "] + [workspace.dependencies] -spaces_protocol = { git = "https://github.com/spacesprotocol/spaces.git", branch = "subspaces", features = ["std"] } -spaces_nums = { git = "https://github.com/spacesprotocol/spaces.git", branch = "subspaces", features = ["std"] } -sip7 = { git = "https://github.com/spacesprotocol/spaces.git", branch = "subspaces", features = ["serde", "std"] } -borsh_utils = { git = "https://github.com/spacesprotocol/spaces.git", branch = "subspaces" } -spacedb = { git = "https://github.com/spacesprotocol/spacedb.git", default-features = false, features = ["extras"] } +# Internal crates +libveritas = { path = "veritas", version = "0.1.0" } +libveritas_zk = { path = "zk", version = "0.1.0" } + +# External +spaces_protocol = { version = "0.1", features = ["std"] } +spaces_nums = { version = "0.1", features = ["std"] } +sip7 = { version = "0.1", features = ["serde", "std"] } +borsh_utils = { version = "0.1" } +spacedb = { version = "0.1", default-features = false, features = ["extras"] } # Always optimize; building and running the guest takes much longer without optimization. [profile.dev] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 82e76bb..30c261f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Libveritas +[![Crates.io](https://img.shields.io/crates/v/libveritas.svg)](https://crates.io/crates/libveritas) +[![Docs.rs](https://docs.rs/libveritas/badge.svg)](https://docs.rs/libveritas) +[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) + Stateless verification for Bitcoin handles using the [Spaces protocol](https://spacesprotocol.org). Similar to [BIP-353](https://en.bitcoin.it/wiki/BIP_0353), but replaces centralized ICANN signing keys with a permissionless trust anchor. @@ -8,9 +12,8 @@ Similar to [BIP-353](https://en.bitcoin.it/wiki/BIP_0353), but replaces centrali ### Rust -```toml -[dependencies] -libveritas = { git = "https://github.com/spacesprotocol/libveritas.git" } +```bash +cargo add libveritas ``` ### JavaScript / Node.js diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..e00384f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Latest published `0.1.x` is supported. + +## Reporting a Vulnerability + +To report security issues, contact one of the maintainers below. + +| Maintainer | Email | Fingerprint | +|------------|----------------------|---------------------------------------------------| +| buffrr | contact@buffrr.dev | 5E18 8EC1 571D 32AC F1B8 85CC 12B0 037C E1D4 54E5 | + +The keys may be used to communicate sensitive information to developers. You can import a key by running the following command with the maintainer's fingerprint: + +```bash +gpg --keyserver hkps://keys.openpgp.org --recv-keys "" +``` + +Ensure you put quotes around fingerprints containing spaces. \ No newline at end of file diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index 1473efd..ae316ac 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -1,7 +1,9 @@ [package] name = "libveritas-python" -version = "0.1.0" -edition = "2024" +version.workspace = true +edition.workspace = true +authors.workspace = true +publish = false [lib] crate-type = ["cdylib"] diff --git a/bindings/uniffi/Cargo.toml b/bindings/uniffi/Cargo.toml index c586661..eee6a0d 100644 --- a/bindings/uniffi/Cargo.toml +++ b/bindings/uniffi/Cargo.toml @@ -1,14 +1,16 @@ [package] name = "libveritas-uniffi" -version = "0.1.0" -edition = "2024" +version.workspace = true +edition.workspace = true +authors.workspace = true +publish = false [lib] crate-type = ["cdylib", "staticlib", "lib"] name = "libveritas_uniffi" [dependencies] -libveritas = { path = "../../veritas" } +libveritas = { workspace = true } sip7 = { workspace = true } serde = { version = "1.0" } serde_json = "1.0" diff --git a/bindings/uniffi/src/lib.rs b/bindings/uniffi/src/lib.rs index b8329e0..cde06a9 100644 --- a/bindings/uniffi/src/lib.rs +++ b/bindings/uniffi/src/lib.rs @@ -2,11 +2,11 @@ use std::sync::{Arc, RwLock}; use libveritas::builder; use libveritas::msg; -use spaces_protocol::sname::SName; use spaces_nums::RootAnchor; use spaces_nums::num_id::NumId; use spaces_protocol::bitcoin::ScriptBuf; use spaces_protocol::slabel::SLabel; +use spaces_protocol::sname::SName; use std::str::FromStr; uniffi::setup_scaffolding!(); @@ -25,7 +25,11 @@ pub enum VeritasError { #[derive(uniffi::Enum)] pub enum DelegateState { - Exists { script_pubkey: Vec, fallback_records: Vec, records: Vec }, + Exists { + script_pubkey: Vec, + fallback_records: Vec, + records: Vec, + }, Empty, Unknown, } @@ -69,15 +73,18 @@ fn trust_set_from_inner(ts: &libveritas::TrustSet) -> TrustSet { } fn parse_data_update(entry: &DataUpdateEntry) -> Result { - let handle = SName::from_str(&entry.name) - .map_err(|e| VeritasError::InvalidInput { - msg: format!("invalid name '{}': {}", entry.name, e), - })?; + let handle = SName::from_str(&entry.name).map_err(|e| VeritasError::InvalidInput { + msg: format!("invalid name '{}': {}", entry.name, e), + })?; - let records = entry.records.as_ref() + let records = entry + .records + .as_ref() .map(|b| sip7::RecordSet::new(b.clone())); - let delegate_records = entry.delegate_records.as_ref() + let delegate_records = entry + .delegate_records + .as_ref() .map(|b| sip7::RecordSet::new(b.clone())); Ok(builder::DataUpdateRequest { @@ -87,7 +94,9 @@ fn parse_data_update(entry: &DataUpdateEntry) -> Result Result, VeritasError> { +fn parse_data_updates( + entries: &[DataUpdateEntry], +) -> Result, VeritasError> { entries.iter().map(parse_data_update).collect() } @@ -149,33 +158,45 @@ fn zone_to_inner(z: &Zone) -> Result { let canonical = SName::from_str(&z.canonical).map_err(|e| VeritasError::InvalidInput { msg: format!("invalid canonical: {e}"), })?; - let alias = z.alias.as_ref() + let alias = z + .alias + .as_ref() .map(|a| SLabel::from_str_unprefixed(a)) .transpose() .map_err(|e| VeritasError::InvalidInput { msg: format!("invalid alias: {e}"), })?; - let num_id = z.num_id.as_ref() + let num_id = z + .num_id + .as_ref() .map(|n| NumId::from_str(n)) .transpose() .map_err(|e| VeritasError::InvalidInput { msg: format!("invalid num_id: {e}"), })?; let delegate = match &z.delegate { - DelegateState::Exists { script_pubkey, fallback_records, records } => { - libveritas::ProvableOption::Exists { - value: libveritas::Delegate { - script_pubkey: ScriptBuf::from_bytes(script_pubkey.clone()), - fallback_records: sip7::RecordSet::new(fallback_records.clone()), - records: sip7::RecordSet::new(records.clone()), - }, - } - } + DelegateState::Exists { + script_pubkey, + fallback_records, + records, + } => libveritas::ProvableOption::Exists { + value: libveritas::Delegate { + script_pubkey: ScriptBuf::from_bytes(script_pubkey.clone()), + fallback_records: sip7::RecordSet::new(fallback_records.clone()), + records: sip7::RecordSet::new(records.clone()), + }, + }, DelegateState::Empty => libveritas::ProvableOption::Empty, DelegateState::Unknown => libveritas::ProvableOption::Unknown, }; let commitment = match &z.commitment { - CommitmentState::Exists { state_root, prev_root, rolling_hash, block_height, receipt_hash } => { + CommitmentState::Exists { + state_root, + prev_root, + rolling_hash, + block_height, + receipt_hash, + } => { let mut sr = [0u8; 32]; sr.copy_from_slice(state_root); let mut rh = [0u8; 32]; @@ -230,6 +251,12 @@ pub struct QueryContext { inner: RwLock, } +impl Default for QueryContext { + fn default() -> Self { + Self::new() + } +} + #[uniffi::export] impl QueryContext { #[uniffi::constructor] @@ -242,19 +269,19 @@ impl QueryContext { /// Add a handle to verify (e.g. "alice@bitcoin"). /// If no requests are added, all handles in the message are verified. pub fn add_request(&self, handle: String) -> Result<(), VeritasError> { - let sname = SName::from_str(&handle) - .map_err(|e| VeritasError::InvalidInput { - msg: format!("invalid handle: {e}"), - })?; + let sname = SName::from_str(&handle).map_err(|e| VeritasError::InvalidInput { + msg: format!("invalid handle: {e}"), + })?; self.inner.write().unwrap().add_request(sname); Ok(()) } /// Add a known zone from stored bytes (from a previous verification). pub fn add_zone(&self, zone_bytes: Vec) -> Result<(), VeritasError> { - let zone = libveritas::Zone::from_slice(&zone_bytes).map_err(|e| VeritasError::InvalidInput { - msg: format!("invalid zone: {e}"), - })?; + let zone = + libveritas::Zone::from_slice(&zone_bytes).map_err(|e| VeritasError::InvalidInput { + msg: format!("invalid zone: {e}"), + })?; self.inner.write().unwrap().add_zone(zone); Ok(()) } @@ -270,11 +297,12 @@ impl Message { /// Decode a message from bytes. #[uniffi::constructor] pub fn new(bytes: Vec) -> Result { - let inner = msg::Message::from_slice(&bytes) - .map_err(|e| VeritasError::InvalidInput { - msg: format!("invalid message: {e}"), - })?; - Ok(Message { inner: RwLock::new(inner) }) + let inner = msg::Message::from_slice(&bytes).map_err(|e| VeritasError::InvalidInput { + msg: format!("invalid message: {e}"), + })?; + Ok(Message { + inner: RwLock::new(inner), + }) } /// Serialize the message to bytes. @@ -290,20 +318,34 @@ impl Message { } /// Set records on the message for a canonical name. - pub fn set_records(&self, canonical: String, records_bytes: Vec) -> Result<(), VeritasError> { + pub fn set_records( + &self, + canonical: String, + records_bytes: Vec, + ) -> Result<(), VeritasError> { let sname = SName::from_str(&canonical).map_err(|e| VeritasError::InvalidInput { msg: format!("invalid canonical: {e}"), })?; - self.inner.write().unwrap().set_records(&sname, sip7::RecordSet::new(records_bytes)); + self.inner + .write() + .unwrap() + .set_records(&sname, sip7::RecordSet::new(records_bytes)); Ok(()) } /// Set delegate records on the message for a canonical name. - pub fn set_delegate_records(&self, canonical: String, records_bytes: Vec) -> Result<(), VeritasError> { + pub fn set_delegate_records( + &self, + canonical: String, + records_bytes: Vec, + ) -> Result<(), VeritasError> { let sname = SName::from_str(&canonical).map_err(|e| VeritasError::InvalidInput { msg: format!("invalid canonical: {e}"), })?; - self.inner.write().unwrap().set_delegate_records(&sname, sip7::RecordSet::new(records_bytes)); + self.inner + .write() + .unwrap() + .set_delegate_records(&sname, sip7::RecordSet::new(records_bytes)); Ok(()) } } @@ -353,7 +395,12 @@ impl UnsignedRecordSet { /// Pack the Sig record with the given signature. Returns signed RecordSet wire bytes. pub fn pack_sig(&self, signature: Vec) -> Vec { - self.inner.read().unwrap().pack_sig(signature).as_slice().to_vec() + self.inner + .read() + .unwrap() + .pack_sig(signature) + .as_slice() + .to_vec() } } @@ -370,6 +417,12 @@ pub struct MessageBuilder { inner: RwLock>, } +impl Default for MessageBuilder { + fn default() -> Self { + Self::new() + } +} + #[uniffi::export] impl MessageBuilder { /// Create an empty builder. @@ -381,13 +434,20 @@ impl MessageBuilder { } /// Add a .spacecert chain with records (sip7 wire bytes). - pub fn add_handle(&self, chain_bytes: Vec, records_bytes: Vec) -> Result<(), VeritasError> { - let chain = libveritas::cert::CertificateChain::from_slice(&chain_bytes) - .map_err(|e| VeritasError::InvalidInput { + pub fn add_handle( + &self, + chain_bytes: Vec, + records_bytes: Vec, + ) -> Result<(), VeritasError> { + let chain = libveritas::cert::CertificateChain::from_slice(&chain_bytes).map_err(|e| { + VeritasError::InvalidInput { msg: format!("invalid chain: {e}"), - })?; + } + })?; let records = sip7::RecordSet::new(records_bytes); - self.inner.write().unwrap() + self.inner + .write() + .unwrap() .as_mut() .ok_or_else(|| VeritasError::InvalidInput { msg: "builder already consumed by build()".to_string(), @@ -398,11 +458,14 @@ impl MessageBuilder { /// Add all certificates from a .spacecert chain. pub fn add_chain(&self, chain_bytes: Vec) -> Result<(), VeritasError> { - let chain = libveritas::cert::CertificateChain::from_slice(&chain_bytes) - .map_err(|e| VeritasError::InvalidInput { + let chain = libveritas::cert::CertificateChain::from_slice(&chain_bytes).map_err(|e| { + VeritasError::InvalidInput { msg: format!("invalid chain: {e}"), - })?; - self.inner.write().unwrap() + } + })?; + self.inner + .write() + .unwrap() .as_mut() .ok_or_else(|| VeritasError::InvalidInput { msg: "builder already consumed by build()".to_string(), @@ -413,11 +476,14 @@ impl MessageBuilder { /// Add a single certificate. pub fn add_cert(&self, cert_bytes: Vec) -> Result<(), VeritasError> { - let cert = libveritas::cert::Certificate::from_slice(&cert_bytes) - .map_err(|e| VeritasError::InvalidInput { + let cert = libveritas::cert::Certificate::from_slice(&cert_bytes).map_err(|e| { + VeritasError::InvalidInput { msg: format!("invalid cert: {e}"), - })?; - self.inner.write().unwrap() + } + })?; + self.inner + .write() + .unwrap() .as_mut() .ok_or_else(|| VeritasError::InvalidInput { msg: "builder already consumed by build()".to_string(), @@ -428,12 +494,13 @@ impl MessageBuilder { /// Add records for a handle (sip7 wire bytes). pub fn add_records(&self, handle: String, records_bytes: Vec) -> Result<(), VeritasError> { - let sname = SName::from_str(&handle) - .map_err(|e| VeritasError::InvalidInput { - msg: format!("invalid handle: {e}"), - })?; + let sname = SName::from_str(&handle).map_err(|e| VeritasError::InvalidInput { + msg: format!("invalid handle: {e}"), + })?; let records = sip7::RecordSet::new(records_bytes); - self.inner.write().unwrap() + self.inner + .write() + .unwrap() .as_mut() .ok_or_else(|| VeritasError::InvalidInput { msg: "builder already consumed by build()".to_string(), @@ -445,7 +512,9 @@ impl MessageBuilder { /// Add a full data update (records + optional delegate records). pub fn add_update(&self, entry: DataUpdateEntry) -> Result<(), VeritasError> { let update = parse_data_update(&entry)?; - self.inner.write().unwrap() + self.inner + .write() + .unwrap() .as_mut() .ok_or_else(|| VeritasError::InvalidInput { msg: "builder already consumed by build()".to_string(), @@ -459,15 +528,11 @@ impl MessageBuilder { /// Send this to the provider/fabric to get the chain proofs needed for `build()`. pub fn chain_proof_request(&self) -> Result { let guard = self.inner.read().unwrap(); - let builder = guard - .as_ref() - .ok_or_else(|| VeritasError::InvalidInput { - msg: "builder already consumed by build()".to_string(), - })?; + let builder = guard.as_ref().ok_or_else(|| VeritasError::InvalidInput { + msg: "builder already consumed by build()".to_string(), + })?; serde_json::to_string(&builder.chain_proof_request()) - .map_err(|e| VeritasError::InvalidInput { - msg: e.to_string(), - }) + .map_err(|e| VeritasError::InvalidInput { msg: e.to_string() }) } /// Build the message from a ChainProof. @@ -475,28 +540,33 @@ impl MessageBuilder { /// Consumes the builder — cannot be called twice. /// Returns the message and unsigned record sets that need signing. pub fn build(&self, chain_proof: Vec) -> Result { - let builder = self - .inner - .write() - .unwrap() - .take() - .ok_or_else(|| VeritasError::InvalidInput { - msg: "builder already consumed by build()".to_string(), - })?; - let chain = msg::ChainProof::from_slice(&chain_proof) - .map_err(|e| VeritasError::InvalidInput { + let builder = + self.inner + .write() + .unwrap() + .take() + .ok_or_else(|| VeritasError::InvalidInput { + msg: "builder already consumed by build()".to_string(), + })?; + let chain = + msg::ChainProof::from_slice(&chain_proof).map_err(|e| VeritasError::InvalidInput { msg: format!("invalid chain proof: {e}"), })?; let (inner_msg, unsigned) = builder .build(chain) - .map_err(|e| VeritasError::InvalidInput { - msg: e.to_string(), - })?; + .map_err(|e| VeritasError::InvalidInput { msg: e.to_string() })?; Ok(BuildResult { - message: Arc::new(Message { inner: RwLock::new(inner_msg) }), - unsigned: unsigned.into_iter().map(|u| Arc::new(UnsignedRecordSet { - inner: RwLock::new(u), - })).collect(), + message: Arc::new(Message { + inner: RwLock::new(inner_msg), + }), + unsigned: unsigned + .into_iter() + .map(|u| { + Arc::new(UnsignedRecordSet { + inner: RwLock::new(u), + }) + }) + .collect(), }) } } @@ -510,8 +580,8 @@ pub struct Anchors { impl Anchors { #[uniffi::constructor] pub fn from_json(json: String) -> Result { - let inner: Vec = serde_json::from_str(&json) - .map_err(|e| VeritasError::InvalidInput { + let inner: Vec = + serde_json::from_str(&json).map_err(|e| VeritasError::InvalidInput { msg: format!("invalid anchors: {e}"), })?; Ok(Anchors { inner }) @@ -527,24 +597,65 @@ impl Anchors { /// A single SIP-7 record (for constructing/packing). #[derive(uniffi::Enum)] pub enum Record { - Seq { version: u64 }, - Txt { key: String, value: Vec }, - Addr { key: String, value: Vec }, - Blob { key: String, value: Vec }, - Sig { flags: u8, canonical: String, handle: String, sig: Vec }, - Unknown { rtype: u8, rdata: Vec }, + Seq { + version: u64, + }, + Txt { + key: String, + value: Vec, + }, + Addr { + key: String, + value: Vec, + }, + Blob { + key: String, + value: Vec, + }, + Sig { + flags: u8, + canonical: String, + handle: String, + sig: Vec, + }, + Unknown { + rtype: u8, + rdata: Vec, + }, } /// A parsed SIP-7 record (from unpacking). Includes `Malformed` for invalid rdata. #[derive(uniffi::Enum)] pub enum ParsedRecord { - Seq { version: u64 }, - Txt { key: String, value: Vec }, - Addr { key: String, value: Vec }, - Blob { key: String, value: Vec }, - Sig { flags: u8, canonical: String, handle: String, sig: Vec }, - Malformed { rtype: u8, rdata: Vec }, - Unknown { rtype: u8, rdata: Vec }, + Seq { + version: u64, + }, + Txt { + key: String, + value: Vec, + }, + Addr { + key: String, + value: Vec, + }, + Blob { + key: String, + value: Vec, + }, + Sig { + flags: u8, + canonical: String, + handle: String, + sig: Vec, + }, + Malformed { + rtype: u8, + rdata: Vec, + }, + Unknown { + rtype: u8, + rdata: Vec, + }, } impl<'a> From> for ParsedRecord { @@ -570,10 +681,12 @@ impl<'a> From> for ParsedRecord { sig: sig.sig.to_vec(), }, sip7::ParsedRecord::Malformed { rtype, rdata } => ParsedRecord::Malformed { - rtype, rdata: rdata.to_vec(), + rtype, + rdata: rdata.to_vec(), }, sip7::ParsedRecord::Unknown { rtype, rdata } => ParsedRecord::Unknown { - rtype, rdata: rdata.to_vec(), + rtype, + rdata: rdata.to_vec(), }, } } @@ -586,7 +699,12 @@ impl From for Record { sip7::Record::Txt { key, value } => Record::Txt { key, value }, sip7::Record::Addr { key, value } => Record::Addr { key, value }, sip7::Record::Blob { key, value } => Record::Blob { key, value }, - sip7::Record::Sig { flags, canonical, handle, sig } => Record::Sig { + sip7::Record::Sig { + flags, + canonical, + handle, + sig, + } => Record::Sig { flags, canonical: canonical.to_string(), handle: handle.to_string(), @@ -610,7 +728,12 @@ impl From for sip7::Record { sip7::Record::addr(&key, &refs) } Record::Blob { key, value } => sip7::Record::blob(&key, value), - Record::Sig { flags, canonical, handle, sig } => { + Record::Sig { + flags, + canonical, + handle, + sig, + } => { let canonical = SName::from_str(&canonical).expect("valid canonical"); let handle = if handle.is_empty() { SName::empty() @@ -635,7 +758,9 @@ impl RecordSet { /// Wrap raw wire bytes (lazy — no parsing until unpack). #[uniffi::constructor] pub fn new(data: Vec) -> Self { - RecordSet { inner: sip7::RecordSet::new(data) } + RecordSet { + inner: sip7::RecordSet::new(data), + } } /// Pack records into wire format. @@ -654,7 +779,8 @@ impl RecordSet { /// Parse all records. pub fn unpack(&self) -> Result, VeritasError> { - self.inner.unpack() + self.inner + .unpack() .map(|records| records.into_iter().map(Into::into).collect()) .map_err(|e| VeritasError::InvalidInput { msg: e.to_string() }) } @@ -670,7 +796,6 @@ impl RecordSet { } } - #[derive(uniffi::Object)] pub struct Veritas { inner: libveritas::Veritas, @@ -682,9 +807,7 @@ impl Veritas { pub fn new(anchors: &Anchors) -> Result { let inner = libveritas::Veritas::new() .with_anchors(anchors.inner.clone()) - .map_err(|e| VeritasError::InvalidInput { - msg: e.to_string(), - })?; + .map_err(|e| VeritasError::InvalidInput { msg: e.to_string() })?; Ok(Veritas { inner }) } @@ -720,9 +843,7 @@ impl Veritas { let inner = self .inner .verify(&ctx_guard, msg_inner.clone()) - .map_err(|e| VeritasError::VerificationFailed { - msg: e.to_string(), - })?; + .map_err(|e| VeritasError::VerificationFailed { msg: e.to_string() })?; Ok(Arc::new(VerifiedMessage { inner })) } @@ -740,9 +861,7 @@ impl Veritas { let inner = self .inner .verify_with_options(&ctx_guard, msg_inner.clone(), options) - .map_err(|e| VeritasError::VerificationFailed { - msg: e.to_string(), - })?; + .map_err(|e| VeritasError::VerificationFailed { msg: e.to_string() })?; Ok(Arc::new(VerifiedMessage { inner })) } @@ -760,18 +879,11 @@ impl VerifiedMessage { } pub fn zones(&self) -> Vec { - self.inner - .zones - .iter() - .map(zone_from_inner) - .collect() + self.inner.zones.iter().map(zone_from_inner).collect() } pub fn certificates(&self) -> Vec> { - self.inner - .certificates() - .map(|c| c.to_bytes()) - .collect() + self.inner.certificates().map(|c| c.to_bytes()).collect() } /// Get the verified message for rebroadcasting or updating. @@ -798,12 +910,17 @@ impl Lookup { /// Create a lookup from a list of handle name strings. #[uniffi::constructor] pub fn new(names: Vec) -> Result { - let snames: Vec = names.iter() - .map(|n| SName::from_str(n).map_err(|e| VeritasError::InvalidInput { - msg: format!("invalid name '{}': {}", n, e), - })) + let snames: Vec = names + .iter() + .map(|n| { + SName::from_str(n).map_err(|e| VeritasError::InvalidInput { + msg: format!("invalid name '{}': {}", n, e), + }) + }) .collect::>()?; - Ok(Lookup { inner: libveritas::names::Lookup::new(snames) }) + Ok(Lookup { + inner: libveritas::names::Lookup::new(snames), + }) } /// Returns the first batch of handles to look up. @@ -814,17 +931,20 @@ impl Lookup { /// Feed zones from a resolveAll response. /// Returns the next batch of handles to look up (empty = done). pub fn advance(&self, zones: Vec) -> Result, VeritasError> { - let inner_zones: Vec = zones.iter() - .map(zone_to_inner) - .collect::>()?; - Ok(self.inner.advance(&inner_zones).iter().map(|s| s.to_string()).collect()) + let inner_zones: Vec = + zones.iter().map(zone_to_inner).collect::>()?; + Ok(self + .inner + .advance(&inner_zones) + .iter() + .map(|s| s.to_string()) + .collect()) } /// Expand zone handles using the alias map accumulated during resolution. pub fn expand_zones(&self, zones: Vec) -> Result, VeritasError> { - let mut inner_zones: Vec = zones.iter() - .map(zone_to_inner) - .collect::>()?; + let mut inner_zones: Vec = + zones.iter().map(zone_to_inner).collect::>()?; self.inner.expand_zones(&mut inner_zones); Ok(inner_zones.iter().map(zone_from_inner).collect()) } @@ -834,31 +954,44 @@ impl Lookup { /// Create a .spacecert file from a subject name and certificate bytes. #[uniffi::export] -pub fn create_certificate_chain(subject: String, cert_bytes_list: Vec>) -> Result, VeritasError> { +pub fn create_certificate_chain( + subject: String, + cert_bytes_list: Vec>, +) -> Result, VeritasError> { let sname = SName::from_str(&subject).map_err(|e| VeritasError::InvalidInput { msg: format!("invalid subject: {e}"), })?; - let certs: Vec = cert_bytes_list.iter() - .map(|b| libveritas::cert::Certificate::from_slice(b) - .map_err(|e| VeritasError::InvalidInput { + let certs: Vec = cert_bytes_list + .iter() + .map(|b| { + libveritas::cert::Certificate::from_slice(b).map_err(|e| VeritasError::InvalidInput { msg: format!("invalid cert: {e}"), - })) + }) + }) .collect::>()?; let chain = libveritas::cert::CertificateChain::new(sname, certs); Ok(chain.to_bytes()) } #[uniffi::export] -pub fn verify_default() -> u32 { libveritas::VERIFY_DEFAULT } +pub fn verify_default() -> u32 { + libveritas::VERIFY_DEFAULT +} #[uniffi::export] -pub fn verify_dev_mode() -> u32 { libveritas::VERIFY_DEV_MODE } +pub fn verify_dev_mode() -> u32 { + libveritas::VERIFY_DEV_MODE +} #[uniffi::export] -pub fn verify_enable_snark() -> u32 { libveritas::VERIFY_ENABLE_SNARK } +pub fn verify_enable_snark() -> u32 { + libveritas::VERIFY_ENABLE_SNARK +} #[uniffi::export] -pub fn sig_primary_zone() -> u8 { sip7::SIG_PRIMARY_ZONE } +pub fn sig_primary_zone() -> u8 { + sip7::SIG_PRIMARY_ZONE +} /// Hash a message with the Spaces signed-message prefix (SHA256). /// Returns the 32-byte digest suitable for Schnorr signing/verification. @@ -874,16 +1007,21 @@ pub fn hash_signable_message(msg: Vec) -> Vec { /// - `signature`: 64-byte Schnorr signature /// - `pubkey`: 32-byte x-only public key #[uniffi::export] -pub fn verify_spaces_message(msg: Vec, signature: Vec, pubkey: Vec) -> Result<(), VeritasError> { - let sig: [u8; 64] = signature.try_into().map_err(|_| VeritasError::InvalidInput { - msg: "signature must be 64 bytes".to_string(), - })?; +pub fn verify_spaces_message( + msg: Vec, + signature: Vec, + pubkey: Vec, +) -> Result<(), VeritasError> { + let sig: [u8; 64] = signature + .try_into() + .map_err(|_| VeritasError::InvalidInput { + msg: "signature must be 64 bytes".to_string(), + })?; let pk: [u8; 32] = pubkey.try_into().map_err(|_| VeritasError::InvalidInput { msg: "pubkey must be 32 bytes".to_string(), })?; - libveritas::verify_spaces_message(&msg, &sig, &pk).map_err(|e| VeritasError::VerificationFailed { - msg: e.to_string(), - }) + libveritas::verify_spaces_message(&msg, &sig, &pk) + .map_err(|e| VeritasError::VerificationFailed { msg: e.to_string() }) } /// Verify a raw Schnorr signature (no prefix, caller provides the 32-byte message hash). @@ -892,28 +1030,34 @@ pub fn verify_spaces_message(msg: Vec, signature: Vec, pubkey: Vec) /// - `signature`: 64-byte Schnorr signature /// - `pubkey`: 32-byte x-only public key #[uniffi::export] -pub fn verify_schnorr(msg_hash: Vec, signature: Vec, pubkey: Vec) -> Result<(), VeritasError> { - let hash: [u8; 32] = msg_hash.try_into().map_err(|_| VeritasError::InvalidInput { - msg: "msg_hash must be 32 bytes".to_string(), - })?; - let sig: [u8; 64] = signature.try_into().map_err(|_| VeritasError::InvalidInput { - msg: "signature must be 64 bytes".to_string(), - })?; +pub fn verify_schnorr( + msg_hash: Vec, + signature: Vec, + pubkey: Vec, +) -> Result<(), VeritasError> { + let hash: [u8; 32] = msg_hash + .try_into() + .map_err(|_| VeritasError::InvalidInput { + msg: "msg_hash must be 32 bytes".to_string(), + })?; + let sig: [u8; 64] = signature + .try_into() + .map_err(|_| VeritasError::InvalidInput { + msg: "signature must be 64 bytes".to_string(), + })?; let pk: [u8; 32] = pubkey.try_into().map_err(|_| VeritasError::InvalidInput { msg: "pubkey must be 32 bytes".to_string(), })?; - libveritas::verify_schnorr(&hash, &sig, &pk).map_err(|e| VeritasError::VerificationFailed { - msg: e.to_string(), - }) + libveritas::verify_schnorr(&hash, &sig, &pk) + .map_err(|e| VeritasError::VerificationFailed { msg: e.to_string() }) } /// Decode stored zone bytes to a Zone record. #[uniffi::export] pub fn decode_zone(bytes: Vec) -> Result { - let zone = libveritas::Zone::from_slice(&bytes) - .map_err(|e| VeritasError::InvalidInput { - msg: format!("invalid zone: {e}"), - })?; + let zone = libveritas::Zone::from_slice(&bytes).map_err(|e| VeritasError::InvalidInput { + msg: format!("invalid zone: {e}"), + })?; Ok(zone_from_inner(&zone)) } @@ -928,9 +1072,7 @@ pub fn zone_to_bytes(zone: Zone) -> Result, VeritasError> { #[uniffi::export] pub fn zone_to_json(zone: Zone) -> Result { let inner = zone_to_inner(&zone)?; - serde_json::to_string(&inner).map_err(|e| VeritasError::InvalidInput { - msg: e.to_string(), - }) + serde_json::to_string(&inner).map_err(|e| VeritasError::InvalidInput { msg: e.to_string() }) } /// Compare two zones — returns true if `a` is fresher/better than `b`. @@ -938,19 +1080,18 @@ pub fn zone_to_json(zone: Zone) -> Result { pub fn zone_is_better_than(a: Zone, b: Zone) -> Result { let inner_a = zone_to_inner(&a)?; let inner_b = zone_to_inner(&b)?; - inner_a.is_better_than(&inner_b).map_err(|e| VeritasError::InvalidInput { - msg: e.to_string(), - }) + inner_a + .is_better_than(&inner_b) + .map_err(|e| VeritasError::InvalidInput { msg: e.to_string() }) } /// Decode stored certificate bytes to JSON. #[uniffi::export] pub fn decode_certificate(bytes: Vec) -> Result { - let cert = libveritas::cert::Certificate::from_slice(&bytes) - .map_err(|e| VeritasError::InvalidInput { + let cert = libveritas::cert::Certificate::from_slice(&bytes).map_err(|e| { + VeritasError::InvalidInput { msg: format!("invalid certificate: {e}"), - })?; - serde_json::to_string(&cert).map_err(|e| VeritasError::InvalidInput { - msg: e.to_string(), - }) + } + })?; + serde_json::to_string(&cert).map_err(|e| VeritasError::InvalidInput { msg: e.to_string() }) } diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 6160653..6d6edf8 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -1,13 +1,15 @@ [package] name = "libveritas-wasm" -version = "0.1.0" -edition = "2024" +version.workspace = true +edition.workspace = true +authors.workspace = true +publish = false [lib] crate-type = ["cdylib"] [dependencies] -libveritas = { path = "../../veritas" } +libveritas = { workspace = true } sip7 = { workspace = true } serde = { version = "1.0" } serde_json = "1.0" diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 05082a9..b3de2ba 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -8,16 +8,24 @@ use serde::Serialize; use spaces_nums::RootAnchor; #[wasm_bindgen(js_name = "VERIFY_DEFAULT")] -pub fn verify_default() -> u32 { libveritas::VERIFY_DEFAULT } +pub fn verify_default() -> u32 { + libveritas::VERIFY_DEFAULT +} #[wasm_bindgen(js_name = "VERIFY_DEV_MODE")] -pub fn verify_dev_mode() -> u32 { libveritas::VERIFY_DEV_MODE } +pub fn verify_dev_mode() -> u32 { + libveritas::VERIFY_DEV_MODE +} #[wasm_bindgen(js_name = "VERIFY_ENABLE_SNARK")] -pub fn verify_enable_snark() -> u32 { libveritas::VERIFY_ENABLE_SNARK } +pub fn verify_enable_snark() -> u32 { + libveritas::VERIFY_ENABLE_SNARK +} #[wasm_bindgen(js_name = "SIG_PRIMARY_ZONE")] -pub fn sig_primary_zone() -> u8 { sip7::SIG_PRIMARY_ZONE } +pub fn sig_primary_zone() -> u8 { + sip7::SIG_PRIMARY_ZONE +} /// Serialize through JSON to get human-readable serde output /// (hex hashes, string names, etc.) as a native JS object. @@ -45,11 +53,9 @@ fn parse_data_update(entry: &JsValue) -> Result Self { + Self::new() + } +} + #[wasm_bindgen] impl QueryContext { #[wasm_bindgen(constructor)] @@ -86,8 +98,8 @@ impl QueryContext { /// If no requests are added, all handles in the message are verified. #[wasm_bindgen(js_name = "addRequest")] pub fn add_request(&mut self, handle: &str) -> Result<(), JsError> { - let sname = SName::from_str(handle) - .map_err(|e| JsError::new(&format!("invalid handle: {e}")))?; + let sname = + SName::from_str(handle).map_err(|e| JsError::new(&format!("invalid handle: {e}")))?; self.inner.add_request(sname); Ok(()) } @@ -119,17 +131,18 @@ fn trust_set_to_js(ts: &libveritas::TrustSet) -> Result { arr.copy_from(r); roots.push(&arr); } - js_sys::Reflect::set(&obj, &"roots".into(), &roots).map_err(|_| JsError::new("failed to set roots"))?; + js_sys::Reflect::set(&obj, &"roots".into(), &roots) + .map_err(|_| JsError::new("failed to set roots"))?; Ok(obj.into()) } fn zone_from_js(val: &JsValue) -> Result { - let json = js_sys::JSON::stringify(val) - .map_err(|_| JsError::new("failed to stringify zone"))?; - let json_str = json.as_string() + let json = + js_sys::JSON::stringify(val).map_err(|_| JsError::new("failed to stringify zone"))?; + let json_str = json + .as_string() .ok_or_else(|| JsError::new("stringify returned non-string"))?; - serde_json::from_str(&json_str) - .map_err(|e| JsError::new(&format!("invalid zone: {e}"))) + serde_json::from_str(&json_str).map_err(|e| JsError::new(&format!("invalid zone: {e}"))) } /// A message containing chain proofs and handle data. @@ -176,16 +189,22 @@ impl Message { pub fn set_records(&mut self, canonical: &str, records_bytes: &[u8]) -> Result<(), JsError> { let sname = SName::from_str(canonical) .map_err(|e| JsError::new(&format!("invalid canonical: {e}")))?; - self.inner.set_records(&sname, sip7::RecordSet::new(records_bytes.to_vec())); + self.inner + .set_records(&sname, sip7::RecordSet::new(records_bytes.to_vec())); Ok(()) } /// Set delegate records on the message for a canonical name. #[wasm_bindgen(js_name = "setDelegateRecords")] - pub fn set_delegate_records(&mut self, canonical: &str, records_bytes: &[u8]) -> Result<(), JsError> { + pub fn set_delegate_records( + &mut self, + canonical: &str, + records_bytes: &[u8], + ) -> Result<(), JsError> { let sname = SName::from_str(canonical) .map_err(|e| JsError::new(&format!("invalid canonical: {e}")))?; - self.inner.set_delegate_records(&sname, sip7::RecordSet::new(records_bytes.to_vec())); + self.inner + .set_delegate_records(&sname, sip7::RecordSet::new(records_bytes.to_vec())); Ok(()) } } @@ -196,6 +215,12 @@ pub struct MessageBuilder { inner: Option, } +impl Default for MessageBuilder { + fn default() -> Self { + Self::new() + } +} + #[wasm_bindgen] impl MessageBuilder { /// Create an empty builder. @@ -213,7 +238,8 @@ impl MessageBuilder { } fn inner_mut(&mut self) -> Result<&mut builder::MessageBuilder, JsError> { - self.inner.as_mut() + self.inner + .as_mut() .ok_or_else(|| JsError::new("builder already consumed by build()")) } @@ -248,8 +274,8 @@ impl MessageBuilder { /// Add records for a handle (sip7 wire bytes). #[wasm_bindgen(js_name = "addRecords")] pub fn add_records(&mut self, handle: &str, records_bytes: &[u8]) -> Result<(), JsError> { - let sname = SName::from_str(handle) - .map_err(|e| JsError::new(&format!("invalid handle: {e}")))?; + let sname = + SName::from_str(handle).map_err(|e| JsError::new(&format!("invalid handle: {e}")))?; let records = sip7::RecordSet::new(records_bytes.to_vec()); self.inner_mut()?.add_records(sname, records); Ok(()) @@ -421,11 +447,7 @@ impl Veritas { } /// Verify a message with default options. - pub fn verify( - &self, - ctx: &QueryContext, - msg: &Message, - ) -> Result { + pub fn verify(&self, ctx: &QueryContext, msg: &Message) -> Result { let inner = self .inner .verify(&ctx.inner, msg.inner.clone()) @@ -474,12 +496,15 @@ impl VerifiedMessage { /// All certificates as serialized byte arrays. pub fn certificates(&self) -> Vec { - self.inner.certificates().map(|c| { - let bytes = c.to_bytes(); - let arr = js_sys::Uint8Array::new_with_length(bytes.len() as u32); - arr.copy_from(&bytes); - arr - }).collect() + self.inner + .certificates() + .map(|c| { + let bytes = c.to_bytes(); + let arr = js_sys::Uint8Array::new_with_length(bytes.len() as u32); + arr.copy_from(&bytes); + arr + }) + .collect() } /// Get the verified message for rebroadcasting or updating. @@ -507,10 +532,16 @@ impl Lookup { /// Create a lookup from an array of handle name strings. #[wasm_bindgen(constructor)] pub fn new(names: Vec) -> Result { - let snames: Vec = names.iter() - .map(|n| SName::from_str(n).map_err(|e| JsError::new(&format!("invalid name '{}': {}", n, e)))) + let snames: Vec = names + .iter() + .map(|n| { + SName::from_str(n) + .map_err(|e| JsError::new(&format!("invalid name '{}': {}", n, e))) + }) .collect::>()?; - Ok(Lookup { inner: libveritas::names::Lookup::new(snames) }) + Ok(Lookup { + inner: libveritas::names::Lookup::new(snames), + }) } /// Returns the first batch of handles to look up. @@ -525,7 +556,12 @@ impl Lookup { let inner_zones: Vec = (0..array.length()) .map(|i| zone_from_js(&array.get(i))) .collect::>()?; - Ok(self.inner.advance(&inner_zones).iter().map(|s| s.to_string()).collect()) + Ok(self + .inner + .advance(&inner_zones) + .iter() + .map(|s| s.to_string()) + .collect()) } /// Expand zone handles using the alias map accumulated during resolution. @@ -548,12 +584,18 @@ impl Lookup { /// /// Collects certificates from multiple verified messages into a single chain. #[wasm_bindgen(js_name = "createCertificateChain")] -pub fn create_certificate_chain(subject: &str, cert_bytes_list: Vec) -> Result, JsError> { - let sname = SName::from_str(subject) - .map_err(|e| JsError::new(&format!("invalid subject: {e}")))?; - let certs: Vec = cert_bytes_list.iter() - .map(|b| libveritas::cert::Certificate::from_slice(&b.to_vec()) - .map_err(|e| JsError::new(&format!("invalid cert: {e}")))) +pub fn create_certificate_chain( + subject: &str, + cert_bytes_list: Vec, +) -> Result, JsError> { + let sname = + SName::from_str(subject).map_err(|e| JsError::new(&format!("invalid subject: {e}")))?; + let certs: Vec = cert_bytes_list + .iter() + .map(|b| { + libveritas::cert::Certificate::from_slice(&b.to_vec()) + .map_err(|e| JsError::new(&format!("invalid cert: {e}"))) + }) .collect::>()?; let chain = libveritas::cert::CertificateChain::new(sname, certs); Ok(chain.to_bytes()) @@ -563,7 +605,8 @@ pub fn create_certificate_chain(subject: &str, cert_bytes_list: Vec Result { let rtype = js_sys::Reflect::get(obj, &"type".into()) - .ok().and_then(|v| v.as_string()) + .ok() + .and_then(|v| v.as_string()) .ok_or_else(|| JsError::new("record must have a 'type' field"))?; match rtype.as_str() { "seq" => { @@ -575,13 +618,16 @@ fn parse_js_record(obj: &JsValue) -> Result { u64::try_from(js_sys::BigInt::from(raw)) .map_err(|_| JsError::new("seq record: 'version' out of u64 range"))? } else { - return Err(JsError::new("seq record: 'version' must be a number or bigint")); + return Err(JsError::new( + "seq record: 'version' must be a number or bigint", + )); }; Ok(sip7::Record::seq(version)) } "txt" => { let key = js_sys::Reflect::get(obj, &"key".into()) - .ok().and_then(|v| v.as_string()) + .ok() + .and_then(|v| v.as_string()) .ok_or_else(|| JsError::new("txt record: 'key' must be a string"))?; let raw = js_sys::Reflect::get(obj, &"value".into()) .map_err(|_| JsError::new("txt record: 'value' is required"))?; @@ -590,18 +636,24 @@ fn parse_js_record(obj: &JsValue) -> Result { } else if js_sys::Array::is_array(&raw) { let arr = js_sys::Array::from(&raw); (0..arr.length()) - .map(|i| arr.get(i).as_string() - .ok_or_else(|| JsError::new("txt record: array values must be strings"))) + .map(|i| { + arr.get(i) + .as_string() + .ok_or_else(|| JsError::new("txt record: array values must be strings")) + }) .collect::, _>>()? } else { - return Err(JsError::new("txt record: 'value' must be a string or array of strings")); + return Err(JsError::new( + "txt record: 'value' must be a string or array of strings", + )); }; let refs: Vec<&str> = values.iter().map(|s| s.as_str()).collect(); Ok(sip7::Record::txt(&key, &refs)) } "addr" => { let key = js_sys::Reflect::get(obj, &"key".into()) - .ok().and_then(|v| v.as_string()) + .ok() + .and_then(|v| v.as_string()) .ok_or_else(|| JsError::new("addr record: 'key' must be a string"))?; let raw = js_sys::Reflect::get(obj, &"value".into()) .map_err(|_| JsError::new("addr record: 'value' is required"))?; @@ -610,18 +662,24 @@ fn parse_js_record(obj: &JsValue) -> Result { } else if js_sys::Array::is_array(&raw) { let arr = js_sys::Array::from(&raw); (0..arr.length()) - .map(|i| arr.get(i).as_string() - .ok_or_else(|| JsError::new("addr record: array values must be strings"))) + .map(|i| { + arr.get(i).as_string().ok_or_else(|| { + JsError::new("addr record: array values must be strings") + }) + }) .collect::, _>>()? } else { - return Err(JsError::new("addr record: 'value' must be a string or array of strings")); + return Err(JsError::new( + "addr record: 'value' must be a string or array of strings", + )); }; let refs: Vec<&str> = values.iter().map(|s| s.as_str()).collect(); Ok(sip7::Record::addr(&key, &refs)) } "blob" => { let key = js_sys::Reflect::get(obj, &"key".into()) - .ok().and_then(|v| v.as_string()) + .ok() + .and_then(|v| v.as_string()) .ok_or_else(|| JsError::new("blob record: 'key' must be a string"))?; let value = js_sys::Reflect::get(obj, &"value".into()) .map(|v| js_sys::Uint8Array::from(v).to_vec()) @@ -630,13 +688,16 @@ fn parse_js_record(obj: &JsValue) -> Result { } "sig" => { let canonical = js_sys::Reflect::get(obj, &"canonical".into()) - .ok().and_then(|v| v.as_string()) + .ok() + .and_then(|v| v.as_string()) .ok_or_else(|| JsError::new("sig record: 'canonical' must be a string"))?; let handle = js_sys::Reflect::get(obj, &"handle".into()) - .ok().and_then(|v| v.as_string()) + .ok() + .and_then(|v| v.as_string()) .unwrap_or_default(); let flags = js_sys::Reflect::get(obj, &"flags".into()) - .ok().and_then(|v| v.as_f64()) + .ok() + .and_then(|v| v.as_f64()) .unwrap_or(0.0) as u8; let sig = js_sys::Reflect::get(obj, &"sig".into()) .map(|v| js_sys::Uint8Array::from(v).to_vec()) @@ -653,8 +714,10 @@ fn parse_js_record(obj: &JsValue) -> Result { } "unknown" => { let rt = js_sys::Reflect::get(obj, &"rtype".into()) - .ok().and_then(|v| v.as_f64()) - .ok_or_else(|| JsError::new("unknown record: 'rtype' must be a number"))? as u8; + .ok() + .and_then(|v| v.as_f64()) + .ok_or_else(|| JsError::new("unknown record: 'rtype' must be a number"))? + as u8; let rdata = js_sys::Reflect::get(obj, &"rdata".into()) .map(|v| js_sys::Uint8Array::from(v).to_vec()) .map_err(|_| JsError::new("unknown record: 'rdata' must be a Uint8Array"))?; @@ -794,7 +857,9 @@ impl RecordSet { /// Wrap raw wire bytes (lazy — no parsing until unpack). #[wasm_bindgen(constructor)] pub fn new(data: &[u8]) -> RecordSet { - RecordSet { inner: sip7::RecordSet::new(data.to_vec()) } + RecordSet { + inner: sip7::RecordSet::new(data.to_vec()), + } } /// Pack records into wire format. @@ -817,7 +882,9 @@ impl RecordSet { /// Parse all records. pub fn unpack(&self) -> Result { - let records = self.inner.unpack() + let records = self + .inner + .unpack() .map_err(|e| JsError::new(&format!("unpack failed: {e}")))?; let array = js_sys::Array::new(); for record in &records { @@ -839,7 +906,6 @@ impl RecordSet { } } - /// Hash a message with the Spaces signed-message prefix (SHA256). /// Returns the 32-byte digest suitable for Schnorr signing/verification. #[wasm_bindgen(js_name = "hashSignableMessage")] @@ -851,25 +917,28 @@ pub fn hash_signable_message(msg: &[u8]) -> Vec { /// Verify a Schnorr signature over a message using the Spaces signed-message prefix. #[wasm_bindgen(js_name = "verifySpacesMessage")] pub fn verify_spaces_message(msg: &[u8], signature: &[u8], pubkey: &[u8]) -> Result<(), JsError> { - let sig: [u8; 64] = signature.try_into() + let sig: [u8; 64] = signature + .try_into() .map_err(|_| JsError::new("signature must be 64 bytes"))?; - let pk: [u8; 32] = pubkey.try_into() + let pk: [u8; 32] = pubkey + .try_into() .map_err(|_| JsError::new("pubkey must be 32 bytes"))?; - libveritas::verify_spaces_message(msg, &sig, &pk) - .map_err(|e| JsError::new(&e.to_string())) + libveritas::verify_spaces_message(msg, &sig, &pk).map_err(|e| JsError::new(&e.to_string())) } /// Verify a raw Schnorr signature (no prefix, caller provides the 32-byte message hash). #[wasm_bindgen(js_name = "verifySchnorr")] pub fn verify_schnorr(msg_hash: &[u8], signature: &[u8], pubkey: &[u8]) -> Result<(), JsError> { - let hash: [u8; 32] = msg_hash.try_into() + let hash: [u8; 32] = msg_hash + .try_into() .map_err(|_| JsError::new("msg_hash must be 32 bytes"))?; - let sig: [u8; 64] = signature.try_into() + let sig: [u8; 64] = signature + .try_into() .map_err(|_| JsError::new("signature must be 64 bytes"))?; - let pk: [u8; 32] = pubkey.try_into() + let pk: [u8; 32] = pubkey + .try_into() .map_err(|_| JsError::new("pubkey must be 32 bytes"))?; - libveritas::verify_schnorr(&hash, &sig, &pk) - .map_err(|e| JsError::new(&e.to_string())) + libveritas::verify_schnorr(&hash, &sig, &pk).map_err(|e| JsError::new(&e.to_string())) } /// Decode stored zone bytes to a plain JS object. @@ -892,7 +961,8 @@ pub fn zone_to_bytes(zone: JsValue) -> Result, JsError> { pub fn zone_is_better_than(a: JsValue, b: JsValue) -> Result { let inner_a = zone_from_js(&a)?; let inner_b = zone_from_js(&b)?; - inner_a.is_better_than(&inner_b) + inner_a + .is_better_than(&inner_b) .map_err(|e| JsError::new(&e.to_string())) } diff --git a/examples/generate_fixture.rs b/examples/generate_fixture.rs index 781435c..50b6799 100644 --- a/examples/generate_fixture.rs +++ b/examples/generate_fixture.rs @@ -3,7 +3,6 @@ /// Outputs: /// examples/fixture/anchors.json - JSON array of RootAnchors /// examples/fixture/message.bin - borsh-encoded Message - use libveritas::msg::QueryContext; use libveritas_testutil::fixture::{ChainState, FixtureRunner, single_commit_finalized}; use std::fs; @@ -41,7 +40,9 @@ fn main() { // Native verify to confirm the fixture is valid let veritas = state.veritas(); let ctx = QueryContext::new(); - let result = veritas.verify_with_options(&ctx, msg, libveritas::VERIFY_DEV_MODE).unwrap(); + let result = veritas + .verify_with_options(&ctx, msg, libveritas::VERIFY_DEV_MODE) + .unwrap(); println!("\nnative verify OK: {} zones", result.zones.len()); for z in &result.zones { println!(" {} -> {}", z.handle, z.sovereignty); diff --git a/methods/Cargo.toml b/methods/Cargo.toml index 36cd77c..a4520d2 100644 --- a/methods/Cargo.toml +++ b/methods/Cargo.toml @@ -1,7 +1,9 @@ [package] name = "libveritas_methods" -version = "0.1.2" -edition = "2021" +version.workspace = true +edition = "2021" # risc0 build environment is on 2021 +authors.workspace = true +publish = false [build-dependencies] risc0-build = { version = "3.0.5" } diff --git a/methods/guest/Cargo.lock b/methods/guest/Cargo.lock index ce8640a..505c51b 100644 --- a/methods/guest/Cargo.lock +++ b/methods/guest/Cargo.lock @@ -1210,8 +1210,9 @@ dependencies = [ [[package]] name = "spacedb" -version = "0.0.12" -source = "git+https://github.com/spacesprotocol/spacedb.git#92c150ea5a368c7ec894ef3530992abd2f7d6012" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a48cda82e951391df9d0a54c96f8b04e117ff39abad5359b181cb71c80798d77" dependencies = [ "borsh", "sha2", diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 0000000..fbef184 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,86 @@ +[workspace] +# Changelog lives at repo root alongside Cargo.toml. +changelog_path = "CHANGELOG.md" +# Always regenerate the changelog from conventional commits. +changelog_update = true +# Open a single release PR per push to main. +pr_draft = false + +[changelog] +header = """# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +""" + +body = """ +## [{{ version | trim_start_matches(pat=\"v\") }}]\ + {%- if release_link -%}\ + ({{ release_link }})\ + {% endif %} - {{ timestamp | date(format=\"%Y-%m-%d\") }} +{% for group, commits in commits | group_by(attribute=\"group\") %} +### {{ group | upper_first }} +{% for commit in commits %} +- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }}\ +{% endfor %} +{% endfor %} +""" + +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactor" }, + { message = "^docs", group = "Documentation" }, + { message = "^test", group = "Tests" }, + { message = "^build", group = "Build" }, + { message = "^ci", group = "CI" }, + { message = "^chore\\(release\\)", skip = true }, + { message = "^chore", group = "Chore" }, + { message = "^revert", group = "Revert" }, +] + +# Publishable crates — published to crates.io + GitHub release on tag. +[[package]] +name = "libveritas" +publish = true +git_release_enable = true +git_release_latest = false +git_tag_name = "libveritas-v{{ version }}" + +[[package]] +name = "libveritas_zk" +publish = true +git_release_enable = true +git_release_latest = false +git_tag_name = "libveritas_zk-v{{ version }}" + +# Non-publishable crates: skip versioning, changelog, and releases +[[package]] +name = "libveritas_methods" +release = false +publish = false + +[[package]] +name = "libveritas_testutil" +release = false +publish = false + +[[package]] +name = "libveritas-wasm" +release = false +publish = false + +[[package]] +name = "libveritas-uniffi" +release = false +publish = false + +[[package]] +name = "libveritas-python" +release = false +publish = false diff --git a/testutil/Cargo.toml b/testutil/Cargo.toml index ad1216d..b7b4ac0 100644 --- a/testutil/Cargo.toml +++ b/testutil/Cargo.toml @@ -1,11 +1,13 @@ [package] name = "libveritas_testutil" -version = "0.1.0" -edition = "2024" +version.workspace = true +edition.workspace = true +authors.workspace = true +publish = false [dependencies] -libveritas = { path = "../veritas" } -libveritas_zk = { path = "../zk" } +libveritas = { workspace = true } +libveritas_zk = { workspace = true } sip7 = { workspace = true } spacedb = { workspace = true } spaces_protocol = { workspace = true } diff --git a/testutil/src/fixture.rs b/testutil/src/fixture.rs index 0607e8c..85d63e0 100644 --- a/testutil/src/fixture.rs +++ b/testutil/src/fixture.rs @@ -1,20 +1,20 @@ +use crate::{TestChain, TestDelegatedSpace, TestHandleTree}; +use libveritas::cert::{HandleSubtree, NumsSubtree, SpacesSubtree}; +use libveritas::msg::{Bundle, ChainProof}; +use libveritas::{SovereigntyState, Veritas, msg}; use spacedb::subtree::SubTree; -use spaces_nums::constants::COMMITMENT_FINALITY_INTERVAL; use spaces_nums::RootAnchor; -use libveritas::{msg, SovereigntyState, Veritas}; +use spaces_nums::constants::COMMITMENT_FINALITY_INTERVAL; use spaces_protocol::sname::SName; -use libveritas::cert::{HandleSubtree, NumsSubtree, SpacesSubtree}; -use libveritas::msg::{Bundle, ChainProof}; -use crate::{TestChain, TestDelegatedSpace, TestHandleTree}; -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub enum Step { Stage(&'static [&'static str]), Commit, Finalize, } -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct Fixture { pub name: &'static str, pub steps: Vec, @@ -87,9 +87,9 @@ impl HandleStates { /// Expected sovereignty for a committed handle pub fn sovereignty(&self, handle: &str) -> Option { if self.staged.iter().find(|&&s| s == handle).is_some() { - return Some(SovereigntyState::Dependent) + return Some(SovereigntyState::Dependent); } - + let commit_idx = self.commit_index(handle)?; if commit_idx < self.finalized_count { Some(SovereigntyState::Sovereign) @@ -101,7 +101,10 @@ impl HandleStates { impl Fixture { pub fn new(name: &'static str) -> Self { - Self { name, steps: vec![] } + Self { + name, + steps: vec![], + } } pub fn stage(mut self, handles: &'static [&'static str]) -> Self { @@ -144,7 +147,11 @@ impl Fixture { } } - HandleStates { commits, staged, finalized_count } + HandleStates { + commits, + staged, + finalized_count, + } } } @@ -154,6 +161,12 @@ pub struct ChainState { pub anchors: Vec, } +impl Default for ChainState { + fn default() -> Self { + Self::new() + } +} + impl ChainState { pub fn new() -> Self { Self { @@ -161,15 +174,14 @@ impl ChainState { anchors: vec![], } } - + pub fn veritas(&self) -> Veritas { let mut anchors = self.anchors.clone(); if anchors.is_empty() { anchors.push(self.chain.current_root_anchor()); } anchors.reverse(); - Veritas::new() - .with_anchors(anchors).expect("valid anchors") + Veritas::new().with_anchors(anchors).expect("valid anchors") } pub fn message(&self, bundles: Vec) -> msg::Message { @@ -185,7 +197,7 @@ impl ChainState { } #[derive(Clone)] -pub struct FixtureRunner{ +pub struct FixtureRunner { pub fixture: Fixture, pub step: std::vec::IntoIter, pub space: TestDelegatedSpace, @@ -222,14 +234,15 @@ impl FixtureRunner { tree: HandleSubtree(c.handle_tree.clone()), handles: vec![], }; - for (_, handle) in &mut c.handles { - let signer = SName::join(&handle.name, &space_label) - .expect("join handle name"); + for handle in c.handles.values_mut() { + let signer = SName::join(&handle.name, &space_label).expect("join handle name"); // Add some off-chain data handle.set_records( - sip7::RecordSet::pack(vec![ - sip7::Record::txt("name", &[&handle.name.to_string()]), - ]).expect("pack records"), + sip7::RecordSet::pack(vec![sip7::Record::txt( + "name", + &[&handle.name.to_string()], + )]) + .expect("pack records"), &signer, ); @@ -244,19 +257,20 @@ impl FixtureRunner { } let mut empty_epoch = msg::Epoch { - tree: HandleSubtree(SubTree::empty()), + tree: HandleSubtree(SubTree::empty()), handles: vec![], }; let staging = bundle.epochs.last_mut().unwrap_or(&mut empty_epoch); - for (_, staged) in &mut self.handles.staged { - let signer = SName::join(&staged.handle.name, &space_label) - .expect("join handle name"); + for staged in self.handles.staged.values_mut() { + let signer = SName::join(&staged.handle.name, &space_label).expect("join handle name"); // add some off-chain data staged.handle.set_records( - sip7::RecordSet::pack(vec![ - sip7::Record::txt("name", &[&staged.handle.name.to_string()]), - ]).expect("pack records"), + sip7::RecordSet::pack(vec![sip7::Record::txt( + "name", + &[&staged.handle.name.to_string()], + )]) + .expect("pack records"), &signer, ); staging.handles.push(msg::Handle { @@ -292,7 +306,7 @@ impl FixtureRunner { } pub fn run(&mut self, state: &mut ChainState) { - while self.run_next(state).is_some() {} + while self.run_next(state).is_some() {} } } @@ -302,15 +316,12 @@ impl FixtureRunner { /// No commitments, just staged handles. Temp certs need no exclusion proof. pub fn staged_only() -> Fixture { - Fixture::new("@staged") - .stage(&["alice", "bob"]) + Fixture::new("@staged").stage(&["alice", "bob"]) } /// Single commitment, not yet finalized. Handles are Pending. pub fn single_commit_pending() -> Fixture { - Fixture::new("@pending") - .stage(&["alice", "bob"]) - .commit() + Fixture::new("@pending").stage(&["alice", "bob"]).commit() } /// Single commitment, finalized. Handles are Sovereign. No receipt needed. @@ -398,7 +409,10 @@ mod tests { assert!(states.is_committed("alice")); assert_eq!(states.commit_index("alice"), Some(0)); assert!(!states.has_pending_commit()); - assert_eq!(states.sovereignty("alice"), Some(SovereigntyState::Sovereign)); + assert_eq!( + states.sovereignty("alice"), + Some(SovereigntyState::Sovereign) + ); } #[test] @@ -411,12 +425,18 @@ mod tests { // alice is in finalized commit 0 assert_eq!(states.commit_index("alice"), Some(0)); - assert_eq!(states.sovereignty("alice"), Some(SovereigntyState::Sovereign)); + assert_eq!( + states.sovereignty("alice"), + Some(SovereigntyState::Sovereign) + ); assert!(!states.needs_receipt("alice")); // charlie is in pending commit 1 assert_eq!(states.commit_index("charlie"), Some(1)); - assert_eq!(states.sovereignty("charlie"), Some(SovereigntyState::Pending)); + assert_eq!( + states.sovereignty("charlie"), + Some(SovereigntyState::Pending) + ); assert!(states.needs_receipt("charlie")); } @@ -430,12 +450,18 @@ mod tests { // Commit 0 (finalized): alice, bob assert_eq!(states.in_commit(0), &["alice", "bob"]); - assert_eq!(states.sovereignty("alice"), Some(SovereigntyState::Sovereign)); + assert_eq!( + states.sovereignty("alice"), + Some(SovereigntyState::Sovereign) + ); assert!(!states.needs_receipt("alice")); // Commit 1 (finalized): charlie, dave assert_eq!(states.in_commit(1), &["charlie", "dave"]); - assert_eq!(states.sovereignty("charlie"), Some(SovereigntyState::Sovereign)); + assert_eq!( + states.sovereignty("charlie"), + Some(SovereigntyState::Sovereign) + ); assert!(states.needs_receipt("charlie")); // commit > 0 // Commit 2 (pending): eve, frank @@ -447,6 +473,9 @@ mod tests { assert_eq!(states.staged, vec!["grace", "heidi"]); assert!(states.is_staged("grace")); assert!(!states.is_committed("grace")); - assert_eq!(states.sovereignty("grace"), Some(SovereigntyState::Dependent)); + assert_eq!( + states.sovereignty("grace"), + Some(SovereigntyState::Dependent) + ); } } diff --git a/testutil/src/lib.rs b/testutil/src/lib.rs index 0cc660f..9ff326a 100644 --- a/testutil/src/lib.rs +++ b/testutil/src/lib.rs @@ -5,7 +5,7 @@ pub mod fixture; -use bitcoin::hashes::{Hash as BitcoinHash}; +use bitcoin::hashes::Hash as BitcoinHash; use bitcoin::key::Keypair; use bitcoin::key::rand::Rng; use bitcoin::secp256k1::Secp256k1; @@ -14,18 +14,21 @@ use bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}; use borsh::{BorshDeserialize, BorshSerialize}; use libveritas::cert::{HandleOut, HandleSubtree, KeyHash, NumsSubtree, Signature, SpacesSubtree}; use libveritas::msg::{self, ChainProof, Message}; -use spaces_protocol::sname::{Subname, SName}; use libveritas::{ProvableOption, SovereigntyState, Veritas, Zone, hash_signable_message}; use risc0_zkvm::{FakeReceipt, InnerReceipt, Receipt, ReceiptClaim}; use spacedb::Sha256Hasher; use spacedb::subtree::{ProofType, SubTree, ValueOrHash}; -use spaces_protocol::constants::{ChainAnchor}; +use spaces_nums::num_id::NumId; +use spaces_nums::snumeric::SNumeric; +use spaces_nums::{ + CommitmentKey, CommitmentTipKey, DelegatorKey, FullNumOut, Num, NumOut, NumOutpointKey, + RootAnchor, rolling_hash, +}; +use spaces_protocol::constants::ChainAnchor; use spaces_protocol::hasher::{KeyHasher, OutpointKey, SpaceKey}; use spaces_protocol::slabel::SLabel; +use spaces_protocol::sname::{SName, Subname}; use spaces_protocol::{Covenant, FullSpaceOut, Space, SpaceOut}; -use spaces_nums::num_id::NumId; -use spaces_nums::{CommitmentKey, FullNumOut, Num, NumOut, NumOutpointKey, CommitmentTipKey, RootAnchor, rolling_hash, DelegatorKey}; -use spaces_nums::snumeric::SNumeric; use std::collections::HashMap; use std::str::FromStr; // ───────────────────────────────────────────────────────────────────────────── @@ -52,7 +55,7 @@ pub fn label(s: &str) -> Subname { } pub fn sign_zone(zone: &Zone, keypair: &Keypair) -> Signature { - sign_mesage(&zone.signing_bytes(), &keypair) + sign_mesage(&zone.signing_bytes(), keypair) } pub fn gen_p2tr_spk() -> (ScriptBuf, Keypair) { @@ -227,7 +230,7 @@ impl TestNum { } pub fn id(&self) -> NumId { - self.fso.numout.num.id.clone() + self.fso.numout.num.id } pub fn outpoint_key(&self) -> NumOutpointKey { @@ -269,7 +272,7 @@ impl TestChain { pub fn chain_proof(&self, anchor: &ChainAnchor) -> ChainProof { ChainProof { - anchor: anchor.clone(), + anchor: *anchor, spaces: SpacesSubtree(self.spaces_tree.clone()), nums: NumsSubtree(self.nums_tree.clone()), } @@ -331,7 +334,7 @@ impl TestChain { self.nums_tree .insert(num.id().into(), ValueOrHash::Value(num.outpoint_bytes())) .expect("insert outpoint"); - self.nums.insert(num.id().into(), num.clone()); + self.nums.insert(num.id(), num.clone()); num } @@ -653,7 +656,7 @@ impl TestHandleTree { Message { chain: msg::ChainProof { - anchor: anchor.clone(), + anchor: *anchor, spaces: SpacesSubtree(spaces_proof), nums: NumsSubtree(nums_proof), }, @@ -715,7 +718,7 @@ impl TestHandleTree { Message { chain: msg::ChainProof { - anchor: anchor.clone(), + anchor: *anchor, spaces: SpacesSubtree(spaces_proof), nums: NumsSubtree(nums_proof), }, @@ -743,6 +746,5 @@ impl TestHandleTree { // ───────────────────────────────────────────────────────────────────────────── pub fn veritas_from_anchors(anchors: Vec) -> Veritas { - Veritas::new() - .with_anchors(anchors).expect("valid anchors") + Veritas::new().with_anchors(anchors).expect("valid anchors") } diff --git a/veritas/Cargo.toml b/veritas/Cargo.toml index 107479f..0d354e7 100644 --- a/veritas/Cargo.toml +++ b/veritas/Cargo.toml @@ -1,26 +1,35 @@ [package] name = "libveritas" -version = "0.1.0" -edition = "2024" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +authors.workspace = true +description = "Offline verification library for Spaces protocol certificates and zone records." +documentation = "https://docs.rs/libveritas" +readme = "../README.md" +keywords = ["spaces", "verification", "merkle", "zk"] +categories = ["cryptography", "data-structures"] [features] elf = [] [dependencies] +libveritas_zk = { workspace = true } spacedb = { workspace = true } -spaces_protocol = {workspace = true} -spaces_nums = {workspace = true} -sip7 = {workspace = true} +spaces_protocol = { workspace = true } +spaces_nums = { workspace = true } +sip7 = { workspace = true } serde = { version = "1.0", features = ["derive"] } base64 = "0.22" risc0-zkvm = { version = "3.0.5", default-features = false, features = ["std"] } borsh = { version = "1.6", features = ["derive", "std"] } hex = "0.4" serde_json = "1.0" -libveritas_zk = {path = "../zk"} [dev-dependencies] -borsh_utils = {workspace = true} -bitcoin = { version = "0.32", default-features = false, features = [ "rand-std"] } -libveritas_testutil = { path = "../testutil" } - +borsh_utils = { workspace = true } +bitcoin = { version = "0.32", default-features = false, features = ["rand-std"] } +libveritas_testutil = { path = "../testutil" } \ No newline at end of file diff --git a/veritas/elfs/fold.bin b/veritas/elfs/fold.bin index 5fc04deb3b17e524efd82adcf4532b6cf9916762..e05239744891d51fbb97f5aa4de41f9bba820aa8 100644 GIT binary patch delta 15478 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 GhyEXgh9;Z< diff --git a/veritas/src/builder.rs b/veritas/src/builder.rs index 1b66864..3fa73c5 100644 --- a/veritas/src/builder.rs +++ b/veritas/src/builder.rs @@ -1,12 +1,12 @@ -use std::collections::HashMap; -use sip7::SIG_PRIMARY_ZONE; +use crate::MessageError; use crate::cert::{Certificate, CertificateChain, ChainProofRequestUtils, Witness}; use crate::msg::{ChainProof, Message, UnsignedRecordSet}; use crate::names::NameResolver; -use spaces_protocol::sname::{NameLike, SName}; -use crate::MessageError; +use sip7::SIG_PRIMARY_ZONE; use spaces_nums::ChainProofRequest; use spaces_protocol::slabel::SLabel; +use spaces_protocol::sname::{NameLike, SName}; +use std::collections::HashMap; pub struct DataUpdateRequest { pub handle: SName, @@ -19,6 +19,12 @@ pub struct MessageBuilder { updates: Vec, } +impl Default for MessageBuilder { + fn default() -> Self { + Self::new() + } +} + impl MessageBuilder { pub fn new() -> Self { Self { @@ -81,7 +87,10 @@ impl MessageBuilder { /// Returns the message and unsigned record sets that need signing. /// Call `unsigned.signing_id()` to get the hash, sign it, /// then `unsigned.pack_sig(sig)` and `msg.set_records(canonical, signed)`. - pub fn build(self, chain: ChainProof) -> Result<(Message, Vec), MessageError> { + pub fn build( + self, + chain: ChainProof, + ) -> Result<(Message, Vec), MessageError> { let certs = dedup_root_certs(self.certs, &chain); let resolver = NameResolver::from_certificates(&certs, &chain.nums); let mut msg = Message::try_from_certificates(chain, certs)?; @@ -143,14 +152,20 @@ impl Message { /// Resolve the block height for a root certificate's receipt by looking up /// its commitment in the chain proof's nums tree. fn root_cert_block_height(cert: &Certificate, chain: &ChainProof) -> u32 { - let Some(space) = cert.subject.space() else { return 0 }; + let Some(space) = cert.subject.space() else { + return 0; + }; let receipt = match &cert.witness { Witness::Root { receipt } => receipt.as_ref(), _ => return 0, }; let Some(receipt) = receipt else { return 0 }; - let Ok(zkc) = receipt.journal.decode::() else { return 0 }; - chain.nums.find_commitment(&space, zkc.final_root) + let Ok(zkc) = receipt.journal.decode::() else { + return 0; + }; + chain + .nums + .find_commitment(&space, zkc.final_root) .ok() .flatten() .map(|c| c.block_height) @@ -173,7 +188,9 @@ fn dedup_root_certs(certs: Vec, chain: &ChainProof) -> Vec= height => continue, - _ => { best_roots.insert(space, (cert, height)); } + _ => { + best_roots.insert(space, (cert, height)); + } } } diff --git a/veritas/src/cert.rs b/veritas/src/cert.rs index 2e7bd45..9b5f788 100644 --- a/veritas/src/cert.rs +++ b/veritas/src/cert.rs @@ -1,18 +1,21 @@ -use std::fmt; -use std::io::{Read, Write}; -use spacedb::{Hash, NodeHasher, Sha256Hasher}; -use spacedb::subtree::{SubTree, SubtreeIter}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::Receipt; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use spacedb::subtree::{SubTree, SubtreeIter}; +use spacedb::{Hash, NodeHasher, Sha256Hasher}; +use spaces_nums::num_id::NumId; +use spaces_nums::{ + ChainProofRequest, Commitment, CommitmentKey, CommitmentTipKey, NumKeyKind, NumOut, NumericKey, + snumeric::SNumeric, +}; +use spaces_protocol::SpaceOut; use spaces_protocol::bitcoin::ScriptBuf; use spaces_protocol::hasher::{KeyHasher, OutpointKey}; use spaces_protocol::slabel::SLabel; -use spaces_protocol::SpaceOut; -use spaces_nums::{snumeric::SNumeric, ChainProofRequest, Commitment, CommitmentKey, NumericKey, NumKeyKind, NumOut, CommitmentTipKey}; -use spaces_nums::num_id::NumId; -use spaces_protocol::sname::{Subname, NameLike, SName}; +use spaces_protocol::sname::{NameLike, SName, Subname}; +use std::fmt; +use std::io::{Read, Write}; /// Current certificate version. pub const CERTIFICATE_VERSION: u8 = 0; @@ -76,7 +79,10 @@ impl CertificateChain { use std::io::{Error, ErrorKind}; if bytes.len() < 7 { - return Err(Error::new(ErrorKind::InvalidData, "too short for chain header")); + return Err(Error::new( + ErrorKind::InvalidData, + "too short for chain header", + )); } if &bytes[0..4] != CHAIN_MAGIC { return Err(Error::new(ErrorKind::InvalidData, "invalid magic bytes")); @@ -100,7 +106,10 @@ impl CertificateChain { let mut certs = Vec::with_capacity(count); for _ in 0..count { if offset + 4 > bytes.len() { - return Err(Error::new(ErrorKind::UnexpectedEof, "truncated cert length")); + return Err(Error::new( + ErrorKind::UnexpectedEof, + "truncated cert length", + )); } let len = u32::from_le_bytes([ bytes[offset], @@ -120,7 +129,6 @@ impl CertificateChain { } } - /// Offline certificate for space handle ownership. /// /// Contains data not recoverable from on-chain state: @@ -148,6 +156,7 @@ pub struct Certificate { /// Witness for a certificate, containing only non-recoverable proof data. #[derive(Clone, Serialize, Deserialize)] +#[allow(clippy::large_enum_variant)] // boxing the Receipt would change the wire format pub enum Witness { /// Root certificate for a top-level space. Root { @@ -199,7 +208,10 @@ impl Certificate { pub fn is_temporary(&self) -> bool { matches!( self.witness, - Witness::Leaf { signature: Some(_), .. } + Witness::Leaf { + signature: Some(_), + .. + } ) } @@ -223,7 +235,8 @@ impl Certificate { /// Returns the NumId derived from the genesis script pubkey if this is a leaf certificate. pub fn num_id(&self) -> Option { - self.genesis_spk().map(|spk| NumId::from_spk::(spk.clone())) + self.genesis_spk() + .map(|spk| NumId::from_spk::(spk.clone())) } } @@ -252,7 +265,11 @@ impl ChainProofRequestUtils for ChainProofRequest { // Registry key for commitment tip let registry_key = CommitmentTipKey::from_slabel::(&space); - if !self.nums.iter().any(|k| matches!(k, NumKeyKind::CommitmentTip(r) if *r == registry_key)) { + if !self + .nums + .iter() + .any(|k| matches!(k, NumKeyKind::CommitmentTip(r) if *r == registry_key)) + { self.nums.push(NumKeyKind::CommitmentTip(registry_key)); } @@ -262,18 +279,30 @@ impl ChainProofRequestUtils for ChainProofRequest { if let Some(receipt) = receipt { if let Ok(zkc) = receipt.journal.decode::() { let ck = CommitmentKey::new::(&space, zkc.final_root); - if !self.nums.iter().any(|k| matches!(k, NumKeyKind::Commitment(c) if *c == ck)) { + if !self + .nums + .iter() + .any(|k| matches!(k, NumKeyKind::Commitment(c) if *c == ck)) + { self.nums.push(NumKeyKind::Commitment(ck)); } } } } - Witness::Leaf { genesis_spk, handles, .. } => { + Witness::Leaf { + genesis_spk, + handles, + .. + } => { // Commitment key for epoch root (only if tree is non-empty) if !handles.0.is_empty() { if let Ok(root) = handles.compute_root() { let ck = CommitmentKey::new::(&space, root); - if !self.nums.iter().any(|k| matches!(k, NumKeyKind::Commitment(c) if *c == ck)) { + if !self + .nums + .iter() + .any(|k| matches!(k, NumKeyKind::Commitment(c) if *c == ck)) + { self.nums.push(NumKeyKind::Commitment(ck)); } } @@ -281,7 +310,11 @@ impl ChainProofRequestUtils for ChainProofRequest { // NumId key for key rotation lookup let num_id = NumId::from_spk::(genesis_spk.clone()); - if !self.nums.iter().any(|k| matches!(k, NumKeyKind::Id(s) if *s == num_id)) { + if !self + .nums + .iter() + .any(|k| matches!(k, NumKeyKind::Id(s) if *s == num_id)) + { self.nums.push(NumKeyKind::Id(num_id)); } } @@ -289,7 +322,7 @@ impl ChainProofRequestUtils for ChainProofRequest { } /// Build from an iterator of certificates. - fn from_certificates<'a>(certs: impl Iterator) -> Self { + fn from_certificates<'a>(certs: impl Iterator) -> Self { let mut req = Self { spaces: vec![], nums: vec![], @@ -309,7 +342,11 @@ impl ChainProofRequestUtils for ChainProofRequest { // Registry key for commitment tip let registry_key = CommitmentTipKey::from_slabel::(space); - if !self.nums.iter().any(|k| matches!(k, NumKeyKind::CommitmentTip(r) if *r == registry_key)) { + if !self + .nums + .iter() + .any(|k| matches!(k, NumKeyKind::CommitmentTip(r) if *r == registry_key)) + { self.nums.push(NumKeyKind::CommitmentTip(registry_key)); } @@ -320,7 +357,11 @@ impl ChainProofRequestUtils for ChainProofRequest { // Commitment key for subtree root if let Ok(root) = handles.compute_root() { let ck = CommitmentKey::new::(space, root); - if !self.nums.iter().any(|k| matches!(k, NumKeyKind::Commitment(c) if *c == ck)) { + if !self + .nums + .iter() + .any(|k| matches!(k, NumKeyKind::Commitment(c) if *c == ck)) + { self.nums.push(NumKeyKind::Commitment(ck)); } } @@ -329,7 +370,11 @@ impl ChainProofRequestUtils for ChainProofRequest { for (_, value) in handles.0.iter() { if let Ok(handle_out) = HandleOut::from_slice(value) { let num_id = NumId::from_spk::(handle_out.spk); - if !self.nums.iter().any(|k| matches!(k, NumKeyKind::Id(s) if *s == num_id)) { + if !self + .nums + .iter() + .any(|k| matches!(k, NumKeyKind::Id(s) if *s == num_id)) + { self.nums.push(NumKeyKind::Id(num_id)); } } @@ -339,7 +384,11 @@ impl ChainProofRequestUtils for ChainProofRequest { fn add_space(&mut self, space: SLabel) { if space.is_numeric() { let numeric: SNumeric = space.try_into().expect("valid numeric"); - if !self.nums.iter().any(|k| matches!(k, NumKeyKind::Num(n) if *n == numeric)) { + if !self + .nums + .iter() + .any(|k| matches!(k, NumKeyKind::Num(n) if *n == numeric)) + { self.nums.push(NumKeyKind::Num(numeric)); } return; @@ -355,7 +404,7 @@ impl ChainProofRequestUtils for ChainProofRequest { } fn add_numeric(&mut self, numeric: SNumeric) { - self.nums.push(NumKeyKind::Num(numeric)); + self.nums.push(NumKeyKind::Num(numeric)); } } @@ -457,16 +506,22 @@ impl HandleSubtree { &mut self.0 } - pub fn contains_subspace(&self, label: &Subname, genesis_spk: &ScriptBuf) -> Result { + pub fn contains_subspace( + &self, + label: &Subname, + genesis_spk: &ScriptBuf, + ) -> Result { let key = Sha256Hasher::hash(label.as_slabel().as_ref()); if !self.0.contains(&key)? { return Ok(false); } - let matches = self.0.iter() - .any(|(k, v)| *k == key && HandleOut::from_slice(v) - .is_ok_and(|h| h.spk.as_bytes() == genesis_spk.as_bytes())); + let matches = self.0.iter().any(|(k, v)| { + *k == key + && HandleOut::from_slice(v) + .is_ok_and(|h| h.spk.as_bytes() == genesis_spk.as_bytes()) + }); Ok(matches) } } @@ -588,7 +643,11 @@ impl NumsSubtree { /// - `Ok(true)` if the commitment tip for this space matches state_root /// - `Ok(false)` if the commitment tip exists but doesn't match /// - `Err` if the tip cannot be proven - pub fn is_latest_commitment(&self, space: &SLabel, state_root: Hash) -> Result { + pub fn is_latest_commitment( + &self, + space: &SLabel, + state_root: Hash, + ) -> Result { let key: Hash = CommitmentTipKey::from_slabel::(space).into(); // Find the commitment tip entry @@ -629,7 +688,7 @@ impl NumsSubtree { } } - let numeric : Hash = NumericKey::from_numeric::(numeric).into(); + let numeric: Hash = NumericKey::from_numeric::(numeric).into(); // Not found in UTXOs - verify the num provably doesn't exist. // If contains() returns true, the proof is incomplete (has key but missing UTXO). @@ -672,7 +731,11 @@ impl NumsSubtree { Ok(None) } - pub fn find_commitment(&self, space: &SLabel, commitment_root: Hash) -> Result, SubtreeError> { + pub fn find_commitment( + &self, + space: &SLabel, + commitment_root: Hash, + ) -> Result, SubtreeError> { let ck = CommitmentKey::new::(space, commitment_root); let key: Hash = ck.into(); @@ -680,11 +743,15 @@ impl NumsSubtree { if !self.0.contains(&key)? { return Ok(None); } - let (_, data) = self.0.iter().find(|(k, _)| **k == key) + let (_, data) = self + .0 + .iter() + .find(|(k, _)| **k == key) .expect("commitment must be found after checking with contains"); - let v: Commitment = borsh::from_slice(data) - .map_err(|e| SubtreeError::DecodeFailed { reason: e.to_string() })?; + let v: Commitment = borsh::from_slice(data).map_err(|e| SubtreeError::DecodeFailed { + reason: e.to_string(), + })?; Ok(Some(v)) } @@ -740,7 +807,7 @@ impl Iterator for SpacesIter<'_> { if OutpointKey::is_valid(k) { let result = borsh::from_slice::(v.as_slice()) .ok() - .map(|raw| SpacesValue::UTXO(raw)); + .map(SpacesValue::UTXO); return (*k, result.unwrap_or(SpacesValue::Unknown(v.clone()))); } (*k, SpacesValue::Unknown(v.clone())) @@ -748,7 +815,6 @@ impl Iterator for SpacesIter<'_> { } } - // Serde implementations for subtree types (uses SubTreeEncoder for wire format) impl Serialize for SpacesSubtree { @@ -787,9 +853,13 @@ impl<'de> Deserialize<'de> for HandleSubtree { } } -fn serialize_subtree(subtree: &SubTree, serializer: S) -> Result { +fn serialize_subtree( + subtree: &SubTree, + serializer: S, +) -> Result { use serde::ser::Error; - let buf = subtree.to_vec() + let buf = subtree + .to_vec() .map_err(|e| S::Error::custom(format!("SubTree encode error: {}", e)))?; if serializer.is_human_readable() { @@ -800,19 +870,21 @@ fn serialize_subtree(subtree: &SubTree, serializer: } } -fn deserialize_subtree<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { +fn deserialize_subtree<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { use serde::de::Error; let buf = if deserializer.is_human_readable() { let encoded = ::deserialize(deserializer)?; - BASE64.decode(&encoded) + BASE64 + .decode(&encoded) .map_err(|e| D::Error::custom(format!("base64 decode error: {}", e)))? } else { as Deserialize>::deserialize(deserializer)? }; - SubTree::from_slice(&buf) - .map_err(|e| D::Error::custom(format!("SubTreeEncoder error: {}", e))) + SubTree::from_slice(&buf).map_err(|e| D::Error::custom(format!("SubTreeEncoder error: {}", e))) } // Manual Borsh implementations for Certificate and CertificateWitness @@ -831,7 +903,11 @@ impl BorshDeserialize for Certificate { let version = u8::deserialize_reader(reader)?; let subject = SName::deserialize_reader(reader)?; let witness = Witness::deserialize_reader(reader)?; - Ok(Certificate { version, subject, witness }) + Ok(Certificate { + version, + subject, + witness, + }) } } @@ -842,7 +918,11 @@ impl BorshSerialize for Witness { BorshSerialize::serialize(&0u8, writer)?; BorshSerialize::serialize(receipt, writer) } - Witness::Leaf { genesis_spk, handles, signature } => { + Witness::Leaf { + genesis_spk, + handles, + signature, + } => { BorshSerialize::serialize(&1u8, writer)?; BorshSerialize::serialize(&genesis_spk.as_bytes().to_vec(), writer)?; BorshSerialize::serialize(handles, writer)?; @@ -865,7 +945,11 @@ impl BorshDeserialize for Witness { let genesis_spk = ScriptBuf::from_bytes(spk_bytes); let handles = HandleSubtree::deserialize_reader(reader)?; let signature = Option::::deserialize_reader(reader)?; - Ok(Witness::Leaf { genesis_spk, handles, signature }) + Ok(Witness::Leaf { + genesis_spk, + handles, + signature, + }) } _ => Err(std::io::Error::new( std::io::ErrorKind::InvalidData, @@ -908,4 +992,4 @@ impl From for SubtreeError { reason: e.to_string(), } } -} \ No newline at end of file +} diff --git a/veritas/src/constants.rs b/veritas/src/constants.rs index 82b7019..116978b 100644 --- a/veritas/src/constants.rs +++ b/veritas/src/constants.rs @@ -5,8 +5,8 @@ // To update after changing guest programs, run: // ./update-elfs.sh -pub const FOLD_ID: [u32; 8] = [2137388158, 139300334, 1332819426, 2098328572, 332487338, 683648994, 3447504890, 2081197365]; -pub const STEP_ID: [u32; 8] = [3974921952, 1965078643, 1566874199, 1666253710, 1661334525, 217836664, 127468841, 245176993]; +pub const FOLD_ID: [u32; 8] = [3538164873, 3494660837, 1605885420, 2756930862, 1952720968, 91802116, 3635727049, 436347682]; +pub const STEP_ID: [u32; 8] = [2719979593, 62333512, 1158600685, 3512173834, 1442236244, 869560259, 553115519, 3467999922]; #[cfg(feature = "elf")] pub const FOLD_ELF: &[u8] = include_bytes!("../elfs/fold.bin"); diff --git a/veritas/src/lib.rs b/veritas/src/lib.rs index dcbe2d2..45a040e 100644 --- a/veritas/src/lib.rs +++ b/veritas/src/lib.rs @@ -1,26 +1,49 @@ -use crate::cert::{Certificate, KeyHash, Witness, Signature}; +//! Offline verification library for the [Spaces protocol](https://spacesprotocol.org). +//! +//! `libveritas` verifies space handle ownership and zone records against on-chain +//! anchors using ZK receipts and Merkle proofs. It is the verifier counterpart to +//! the Spaces fabric / relay infrastructure. +//! +//! # Quick start +//! +//! ```ignore +//! use libveritas::{Veritas, msg::QueryContext}; +//! +//! let veritas = Veritas::new().with_anchors(anchors)?; +//! let result = veritas.verify(&QueryContext::new(), message)?; +//! for zone in &result.zones { +//! // ... +//! } +//! ``` +//! +//! # Features +//! +//! - `elf` — embed the prover ELF binaries (`FOLD_ELF`, `STEP_ELF`) alongside +//! the image IDs. Verifiers only need the image IDs and can skip this feature. + +use crate::cert::{Certificate, KeyHash, Signature, Witness}; use borsh::{BorshDeserialize, BorshSerialize}; use libveritas_zk::guest::CommitmentKind; use risc0_zkvm::{Receipt, VerifierContext}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use spacedb::subtree::SubTree; use spacedb::{Hash, NodeHasher, Sha256Hasher}; -use spaces_protocol::bitcoin::hashes::{Hash as HashUtil, sha256, HashEngine}; +use spaces_nums::RootAnchor; +use spaces_nums::constants::COMMITMENT_FINALITY_INTERVAL; +use spaces_protocol::bitcoin::ScriptBuf; +use spaces_protocol::bitcoin::hashes::{Hash as HashUtil, HashEngine, sha256}; use spaces_protocol::bitcoin::secp256k1::{self, XOnlyPublicKey}; -use spaces_protocol::bitcoin::{ScriptBuf}; -use spaces_protocol::sname::{SName}; use spaces_protocol::constants::SPACES_SIGNED_MSG_PREFIX; use spaces_protocol::slabel::SLabel; -use spaces_nums::constants::COMMITMENT_FINALITY_INTERVAL; +use spaces_protocol::sname::SName; use std::collections::HashSet; use std::fmt; use std::io::{Read, Write}; -use spacedb::subtree::SubTree; -use spaces_nums::RootAnchor; +pub mod builder; pub mod cert; -pub mod msg; pub mod constants; -pub mod builder; +pub mod msg; pub mod names; pub use sip7; @@ -186,7 +209,6 @@ pub struct Zone { pub num_id: Option, } - /// Information about a space's commitment state. #[derive(Clone, Serialize, Deserialize)] pub struct CommitmentInfo { @@ -203,7 +225,8 @@ pub struct CommitmentInfo { impl CommitmentInfo { pub fn empty() -> Self { let empty_root = SubTree::::empty() - .compute_root().expect("valid"); + .compute_root() + .expect("valid"); Self { onchain: spaces_nums::Commitment { state_root: empty_root, @@ -290,7 +313,10 @@ impl BorshDeserialize for CommitmentInfo { fn deserialize_reader(reader: &mut R) -> std::io::Result { let onchain = spaces_nums::Commitment::deserialize_reader(reader)?; let receipt_hash = Option::::deserialize_reader(reader)?; - Ok(CommitmentInfo { onchain, receipt_hash }) + Ok(CommitmentInfo { + onchain, + receipt_hash, + }) } } @@ -352,7 +378,8 @@ impl BorshDeserialize for Zone { let fallback_bytes: Vec = Vec::deserialize_reader(reader)?; let records_bytes: Vec = Vec::deserialize_reader(reader)?; let delegate: ProvableOption = ProvableOption::deserialize_reader(reader)?; - let commitment: ProvableOption = ProvableOption::deserialize_reader(reader)?; + let commitment: ProvableOption = + ProvableOption::deserialize_reader(reader)?; let script_pubkey = ScriptBuf::from_bytes(spk_bytes); let num_id = Option::::deserialize_reader(reader)?; @@ -415,9 +442,12 @@ pub fn hash_signable_message(msg: &[u8]) -> secp256k1::Message { /// - `msg`: the raw message bytes (will be prefixed and hashed internally) /// - `signature`: 64-byte Schnorr signature /// - `pubkey`: 32-byte x-only public key -pub fn verify_spaces_message(msg: &[u8], signature: &[u8; 64], pubkey: &[u8; 32]) -> Result<(), SignatureError> { - let xonly = XOnlyPublicKey::from_slice(pubkey) - .map_err(|_| SignatureError::InvalidPublicKey)?; +pub fn verify_spaces_message( + msg: &[u8], + signature: &[u8; 64], + pubkey: &[u8; 32], +) -> Result<(), SignatureError> { + let xonly = XOnlyPublicKey::from_slice(pubkey).map_err(|_| SignatureError::InvalidPublicKey)?; let sig = secp256k1::schnorr::Signature::from_slice(signature) .map_err(|_| SignatureError::InvalidSignature)?; let hashed = hash_signable_message(msg); @@ -431,9 +461,12 @@ pub fn verify_spaces_message(msg: &[u8], signature: &[u8; 64], pubkey: &[u8; 32] /// - `msg_hash`: 32-byte SHA256 hash of the message /// - `signature`: 64-byte Schnorr signature /// - `pubkey`: 32-byte x-only public key -pub fn verify_schnorr(msg_hash: &[u8; 32], signature: &[u8; 64], pubkey: &[u8; 32]) -> Result<(), SignatureError> { - let xonly = XOnlyPublicKey::from_slice(pubkey) - .map_err(|_| SignatureError::InvalidPublicKey)?; +pub fn verify_schnorr( + msg_hash: &[u8; 32], + signature: &[u8; 64], + pubkey: &[u8; 32], +) -> Result<(), SignatureError> { + let xonly = XOnlyPublicKey::from_slice(pubkey).map_err(|_| SignatureError::InvalidPublicKey)?; let sig = secp256k1::schnorr::Signature::from_slice(signature) .map_err(|_| SignatureError::InvalidSignature)?; let msg = secp256k1::Message::from_digest(*msg_hash); @@ -482,7 +515,11 @@ impl Zone { /// /// The message is the borsh-serialized zone data (with anchor zeroed), /// prefixed with the spaces signed message prefix and hashed with SHA256. - pub fn verify_signature(&self, signature: &Signature, signer: &ScriptBuf) -> Result<(), SignatureError> { + pub fn verify_signature( + &self, + signature: &Signature, + signer: &ScriptBuf, + ) -> Result<(), SignatureError> { let script_bytes = signer.as_bytes(); if script_bytes.len() != secp256k1::constants::SCHNORR_PUBLIC_KEY_SIZE + 2 { return Err(SignatureError::InvalidPublicKey); @@ -499,7 +536,6 @@ impl Zone { .map_err(|_| SignatureError::VerificationFailed) } - /// Returns true if self is fresher/better than other. /// /// Comparison order: @@ -520,10 +556,10 @@ impl Zone { // Higher commitment height = newer committed state match (&self.commitment, &other.commitment) { - (ProvableOption::Exists { value: a }, ProvableOption::Exists { value: b }) => { - if a.onchain.block_height != b.onchain.block_height { - return Ok(a.onchain.block_height > b.onchain.block_height); - } + (ProvableOption::Exists { value: a }, ProvableOption::Exists { value: b }) + if a.onchain.block_height != b.onchain.block_height => + { + return Ok(a.onchain.block_height > b.onchain.block_height); } (ProvableOption::Exists { .. }, _) => return Ok(true), (_, ProvableOption::Exists { .. }) => return Ok(false), @@ -534,17 +570,25 @@ impl Zone { // Delegate knowledge match (&self.delegate, &other.delegate) { - (ProvableOption::Exists { value: a }, ProvableOption::Exists { value: b }) => { - if !a.records.is_empty() || !b.records.is_empty() { - if a.records.is_empty() { return Ok(false); } - if b.records.is_empty() { return Ok(true); } - if records_is_better(&a.records, &b.records) { - return Ok(true); - } + (ProvableOption::Exists { value: a }, ProvableOption::Exists { value: b }) + if (!a.records.is_empty() || !b.records.is_empty()) => + { + if a.records.is_empty() { + return Ok(false); + } + if b.records.is_empty() { + return Ok(true); + } + if records_is_better(&a.records, &b.records) { + return Ok(true); } } - (ProvableOption::Exists { .. }, ProvableOption::Empty | ProvableOption::Unknown) => return Ok(true), - (ProvableOption::Empty | ProvableOption::Unknown, ProvableOption::Exists { .. }) => return Ok(false), + (ProvableOption::Exists { .. }, ProvableOption::Empty | ProvableOption::Unknown) => { + return Ok(true); + } + (ProvableOption::Empty | ProvableOption::Unknown, ProvableOption::Exists { .. }) => { + return Ok(false); + } (ProvableOption::Empty, ProvableOption::Unknown) => return Ok(true), (ProvableOption::Unknown, ProvableOption::Empty) => return Ok(false), _ => {} @@ -552,8 +596,12 @@ impl Zone { // Higher records seq = newer owner-signed records if !self.records.is_empty() || !other.records.is_empty() { - if self.records.is_empty() { return Ok(false); } - if other.records.is_empty() { return Ok(true); } + if self.records.is_empty() { + return Ok(false); + } + if other.records.is_empty() { + return Ok(true); + } if records_is_better(&self.records, &other.records) { return Ok(true); } @@ -570,16 +618,15 @@ impl Zone { /// Copy receipt_hash from other if commitment roots match. /// Avoids re-verifying ZK receipts for commitments we've already verified. pub fn update_receipt_cache(&mut self, other: &Self) { - if let ( - ProvableOption::Exists { value: mine }, - ProvableOption::Exists { value: theirs }, - ) = (&mut self.commitment, &other.commitment) { + if let (ProvableOption::Exists { value: mine }, ProvableOption::Exists { value: theirs }) = + (&mut self.commitment, &other.commitment) + { if mine.onchain.state_root == theirs.onchain.state_root && mine.receipt_hash.is_none() { mine.receipt_hash = theirs.receipt_hash; } } } - + /// Returns true if the zone has a commitment that requires ZK verification. /// /// Returns false if: @@ -593,9 +640,7 @@ impl Zone { if value.receipt_hash.is_some() { return None; } - if value.onchain.prev_root.is_none() { - return None; - } + value.onchain.prev_root?; Some(value) } _ => None, @@ -603,7 +648,12 @@ impl Zone { } } -fn verify_receipt(ci: &mut CommitmentInfo, space: &SLabel, receipt: &Receipt, options: u32) -> Result<(), MessageError> { +fn verify_receipt( + ci: &mut CommitmentInfo, + space: &SLabel, + receipt: &Receipt, + options: u32, +) -> Result<(), MessageError> { let space_str = space.to_string(); let zkc = decode_journal(receipt, space)?; verify_zk_journal_matches_onchain(space, &zkc, &ci.onchain)?; @@ -690,6 +740,12 @@ impl fmt::Display for AnchorError { impl std::error::Error for AnchorError {} +impl Default for Veritas { + fn default() -> Self { + Self::new() + } +} + impl Veritas { pub fn new() -> Self { Veritas { @@ -738,7 +794,11 @@ impl Veritas { } /// Verify a message with default options. - pub fn verify(&self, ctx: &msg::QueryContext, msg: crate::msg::Message) -> Result { + pub fn verify( + &self, + ctx: &msg::QueryContext, + msg: crate::msg::Message, + ) -> Result { self.verify_with_options(ctx, msg, VERIFY_DEFAULT) } @@ -783,7 +843,6 @@ impl Veritas { }) } - fn verify_bundle( &self, ctx: &msg::QueryContext, @@ -805,19 +864,23 @@ impl Veritas { (Some(cached), Some(zone)) => { zone.update_receipt_cache(cached); if zone.is_better_than(cached).unwrap_or(false) { - receipt_verified = maybe_verify_receipt(zone, bundle.receipt.as_ref(), &space, options)?; + receipt_verified = + maybe_verify_receipt(zone, bundle.receipt.as_ref(), &space, options)?; zone } else { - *cached + cached } } - (Some(cached), None) => *cached, + (Some(cached), None) => cached, (None, Some(zone)) => { - receipt_verified = maybe_verify_receipt(zone, bundle.receipt.as_ref(), &space, options)?; + receipt_verified = + maybe_verify_receipt(zone, bundle.receipt.as_ref(), &space, options)?; zone } (None, None) => { - return Err(MessageError::ParentZoneRequired { space: space.to_string() }); + return Err(MessageError::ParentZoneRequired { + space: space.to_string(), + }); } }; @@ -834,7 +897,11 @@ impl Veritas { let verified_bundle = if wants_root { Some(msg::Bundle { subject: space, - receipt: if receipt_verified { bundle.receipt } else { None }, + receipt: if receipt_verified { + bundle.receipt + } else { + None + }, epochs: vec![], records: bundle.records, delegate_records: bundle.delegate_records, @@ -850,11 +917,14 @@ impl Veritas { let mut verified_epochs: Vec = Vec::new(); for epoch in bundle.epochs { - let root = epoch.tree.compute_root() - .map_err(|e| MessageError::HandleProofMalformed { - handle: format!("*@{}", space), - reason: e.to_string(), - })?; + let root = + epoch + .tree + .compute_root() + .map_err(|e| MessageError::HandleProofMalformed { + handle: format!("*@{}", space), + reason: e.to_string(), + })?; if checked.contains(&root) { return Err(MessageError::DuplicateEpoch { @@ -868,15 +938,21 @@ impl Veritas { let sovereignty = if epoch.tree.0.is_empty() { SovereigntyState::Dependent } else { - let onchain = chain.nums.find_commitment(&space, root) - .map_err(|e| MessageError::NumsProofMalformed { reason: e.to_string() })? + let onchain = chain + .nums + .find_commitment(&space, root) + .map_err(|e| MessageError::NumsProofMalformed { + reason: e.to_string(), + })? .ok_or_else(|| MessageError::CommitmentNotFound { space: space.to_string(), root, })?; if onchain.block_height > verified_tip.onchain.block_height { - return Err(MessageError::EpochExceedsTip { space: space.to_string() }); + return Err(MessageError::EpochExceedsTip { + space: space.to_string(), + }); } self.sovereignty_for(onchain.block_height) @@ -885,10 +961,11 @@ impl Veritas { let mut verified_handles: Vec = Vec::new(); for handle in epoch.handles { - let subject = SName::join(&handle.name, &space) - .map_err(|_| MessageError::InvalidSubject { + let subject = SName::join(&handle.name, &space).map_err(|_| { + MessageError::InvalidSubject { subject: format!("{}@{}", handle.name, space), - })?; + } + })?; if !ctx.wants(&subject) { continue; @@ -899,12 +976,25 @@ impl Veritas { return Err(MessageError::TemporaryRequiresTip { handle: subject.to_string(), tip: verified_tip.onchain.state_root, - got: root + got: root, }); } - verify_temporary_handle(chain.anchor.height, &handle, &subject, &epoch.tree, target_zone)? + verify_temporary_handle( + chain.anchor.height, + &handle, + &subject, + &epoch.tree, + target_zone, + )? } else { - verify_final_handle(chain.anchor.height, &handle, &subject, &epoch.tree, &chain.nums, sovereignty)? + verify_final_handle( + chain.anchor.height, + &handle, + &subject, + &epoch.tree, + &chain.nums, + sovereignty, + )? }; push_best_zone(ctx, &mut zones, zone); @@ -923,7 +1013,11 @@ impl Veritas { let verified_bundle = if wants_root || !verified_epochs.is_empty() { Some(msg::Bundle { subject: space, - receipt: if receipt_verified { bundle.receipt } else { None }, + receipt: if receipt_verified { + bundle.receipt + } else { + None + }, epochs: verified_epochs, records: bundle.records, delegate_records: bundle.delegate_records, @@ -935,10 +1029,7 @@ impl Veritas { Ok((zones, verified_bundle)) } - fn check_msg_anchor( - &self, - msg: &crate::msg::Message, - ) -> Result { + fn check_msg_anchor(&self, msg: &crate::msg::Message) -> Result { let height = msg.chain.anchor.height; if height < self.oldest_anchor { @@ -954,7 +1045,8 @@ impl Veritas { }); } - let anchor = self.find_by_anchor(height) + let anchor = self + .find_by_anchor(height) .ok_or(MessageError::NoAnchorAtHeight { anchor: height })? .clone(); @@ -974,12 +1066,14 @@ impl Veritas { msg: &crate::msg::Message, anchor: &RootAnchor, ) -> Result<(), MessageError> { - let spaces_root = msg.chain.spaces - .compute_root() - .map_err(|_| MessageError::SpacesRootMismatch { - expected: anchor.spaces_root, - got: [0u8; 32], - })?; + let spaces_root = + msg.chain + .spaces + .compute_root() + .map_err(|_| MessageError::SpacesRootMismatch { + expected: anchor.spaces_root, + got: [0u8; 32], + })?; if spaces_root != anchor.spaces_root { return Err(MessageError::SpacesRootMismatch { @@ -989,12 +1083,14 @@ impl Veritas { } if let Some(expected) = anchor.nums_root { - let nums_root = msg.chain.nums - .compute_root() - .map_err(|_| MessageError::NumsRootMismatch { - expected: Some(expected), - got: [0u8; 32], - })?; + let nums_root = + msg.chain + .nums + .compute_root() + .map_err(|_| MessageError::NumsRootMismatch { + expected: Some(expected), + got: [0u8; 32], + })?; if nums_root != expected { return Err(MessageError::NumsRootMismatch { @@ -1025,28 +1121,44 @@ impl Veritas { } /// Extract parent zone from chain proofs and set sovereignty based on commitment finality. - fn extract_parent_zone(&self, chain: &msg::ChainProof, bundle: &msg::Bundle) -> Result, MessageError> { + fn extract_parent_zone( + &self, + chain: &msg::ChainProof, + bundle: &msg::Bundle, + ) -> Result, MessageError> { let mut num_id = None; let (spk, records) = if !bundle.subject.is_numeric() { let Some(spaceout) = chain.spaces.find_space(&bundle.subject) else { - return Err(MessageError::SpaceNotFound { space: bundle.subject.to_string() }) + return Err(MessageError::SpaceNotFound { + space: bundle.subject.to_string(), + }); }; let Some(space) = spaceout.space else { - return Err(MessageError::SpaceNotFound { space: bundle.subject.to_string() }); + return Err(MessageError::SpaceNotFound { + space: bundle.subject.to_string(), + }); }; - let data = space.data() + let data = space + .data() .filter(|d| !d.is_empty()) .map(|d| sip7::RecordSet::new(d.to_vec())) .unwrap_or_default(); (spaceout.script_pubkey, data) } else { - let Some(numout) = chain.nums + let Some(numout) = chain + .nums .find_numeric(&bundle.subject.clone().try_into().expect("numeric")) - .ok().flatten() else { - return Err(MessageError::NumericNotFound { numeric: bundle.subject.to_string() }) + .ok() + .flatten() + else { + return Err(MessageError::NumericNotFound { + numeric: bundle.subject.to_string(), + }); }; num_id = Some(numout.num.id); - let data = numout.num.data + let data = numout + .num + .data .filter(|d| !d.is_empty()) .map(|d| sip7::RecordSet::new(d.to_vec())) .unwrap_or_default(); @@ -1071,11 +1183,12 @@ impl Veritas { // Verify records signature if present if let Some(records) = &bundle.records { - msg::verify_records(records, &z.script_pubkey, &z.canonical) - .map_err(|e| MessageError::RecordsInvalid { + msg::verify_records(records, &z.script_pubkey, &z.canonical).map_err(|e| { + MessageError::RecordsInvalid { handle: z.handle.to_string(), reason: e.to_string(), - })?; + } + })?; z.records = records.clone(); } @@ -1096,7 +1209,9 @@ impl Veritas { z.delegate = ProvableOption::Exists { value: Delegate { script_pubkey: delegate.script_pubkey, - fallback_records: delegate.num.data + fallback_records: delegate + .num + .data .filter(|d| !d.is_empty()) .map(|d| sip7::RecordSet::new(d.to_vec())) .unwrap_or_default(), @@ -1118,7 +1233,7 @@ impl Veritas { value: CommitmentInfo { onchain: commitment, receipt_hash: None, - } + }, }; } } @@ -1138,32 +1253,38 @@ fn verify_temporary_handle( parent_zone: &Zone, ) -> Result { // Empty tree = nothing exists, otherwise check exclusion - let exists = !epoch_tree.0.is_empty() && epoch_tree - .contains_subspace(&handle.name, &handle.genesis_spk) - .map_err(|e| MessageError::HandleProofMalformed { - handle: subject.to_string(), - reason: e.to_string(), - })?; + let exists = !epoch_tree.0.is_empty() + && epoch_tree + .contains_subspace(&handle.name, &handle.genesis_spk) + .map_err(|e| MessageError::HandleProofMalformed { + handle: subject.to_string(), + reason: e.to_string(), + })?; if exists { - return Err(MessageError::HandleAlreadyExists { handle: subject.to_string() }); + return Err(MessageError::HandleAlreadyExists { + handle: subject.to_string(), + }); } let signer = match &parent_zone.delegate { ProvableOption::Exists { value: delegate } => &delegate.script_pubkey, ProvableOption::Empty => &parent_zone.script_pubkey, ProvableOption::Unknown => { - return Err(MessageError::ParentDelegateUnknown { handle: subject.to_string() }); + return Err(MessageError::ParentDelegateUnknown { + handle: subject.to_string(), + }); } }; let mut verified_records = sip7::RecordSet::default(); if let Some(records) = &handle.records { - msg::verify_records(records, &handle.genesis_spk, &subject) - .map_err(|e| MessageError::RecordsInvalid { + msg::verify_records(records, &handle.genesis_spk, subject).map_err(|e| { + MessageError::RecordsInvalid { handle: subject.to_string(), reason: e.to_string(), - })?; + } + })?; verified_records = records.clone(); } @@ -1182,13 +1303,11 @@ fn verify_temporary_handle( num_id, }; - zone.verify_signature( - handle.signature.as_ref().unwrap(), - signer, - ).map_err(|e| MessageError::SignatureInvalid { - handle: zone.handle.to_string(), - reason: e.to_string(), - })?; + zone.verify_signature(handle.signature.as_ref().unwrap(), signer) + .map_err(|e| MessageError::SignatureInvalid { + handle: zone.handle.to_string(), + reason: e.to_string(), + })?; Ok(zone) } @@ -1203,7 +1322,9 @@ fn verify_final_handle( sovereignty: SovereigntyState, ) -> Result { if epoch_tree.0.is_empty() { - return Err(MessageError::FinalCertRequiresTree { handle: subject.to_string() }); + return Err(MessageError::FinalCertRequiresTree { + handle: subject.to_string(), + }); } let included = epoch_tree @@ -1214,34 +1335,44 @@ fn verify_final_handle( })?; if !included { - return Err(MessageError::HandleNotFound { handle: subject.to_string() }); + return Err(MessageError::HandleNotFound { + handle: subject.to_string(), + }); } // Key rotation lookup - let numout = nums - .find_num(&handle.genesis_spk) - .map_err(|e| MessageError::NumsProofMalformed { reason: e.to_string() })?; + let numout = + nums.find_num(&handle.genesis_spk) + .map_err(|e| MessageError::NumsProofMalformed { + reason: e.to_string(), + })?; let (num_id, spk, onchain_data, alias) = match numout { Some(numout) => ( numout.num.id, numout.script_pubkey, - numout.num.data + numout + .num + .data .filter(|d| !d.is_empty()) .map(|d| sip7::RecordSet::new(d.to_vec())) .unwrap_or_default(), - Some(numout.num.name.to_slabel()) + Some(numout.num.name.to_slabel()), + ), + None => ( + NumId::from_spk::(handle.genesis_spk.clone()), + handle.genesis_spk.clone(), + sip7::RecordSet::default(), + None, ), - None => (NumId::from_spk::(handle.genesis_spk.clone()), handle.genesis_spk.clone(), sip7::RecordSet::default(), None), }; let mut verified_records = sip7::RecordSet::default(); if let Some(records) = &handle.records { - msg::verify_records(records, &spk, &subject) - .map_err(|e| MessageError::RecordsInvalid { - handle: subject.to_string(), - reason: e.to_string(), - })?; + msg::verify_records(records, &spk, subject).map_err(|e| MessageError::RecordsInvalid { + handle: subject.to_string(), + reason: e.to_string(), + })?; verified_records = records.clone(); } @@ -1272,7 +1403,11 @@ pub enum MessageError { /// No anchor exists at this height NoAnchorAtHeight { anchor: u32 }, /// Anchor hash doesn't match our known anchor at this height - AnchorHashMismatch { height: u32, expected: Hash, got: Hash }, + AnchorHashMismatch { + height: u32, + expected: Hash, + got: Hash, + }, /// Duplicate space in message bundles DuplicateSpace { space: String }, /// Receipt journal could not be decoded @@ -1302,7 +1437,11 @@ pub enum MessageError { /// Subject name is invalid InvalidSubject { subject: String }, /// Temporary certificate must prove against the tip state - TemporaryRequiresTip { handle: String, tip: Hash, got: Hash }, + TemporaryRequiresTip { + handle: String, + tip: Hash, + got: Hash, + }, /// Handle already exists when exclusion proof expected HandleAlreadyExists { handle: String }, /// Parent delegate is unknown, cannot verify signature @@ -1335,11 +1474,17 @@ impl fmt::Display for MessageError { Self::NoAnchorAtHeight { anchor } => { write!(f, "no anchor at height {}", anchor) } - Self::AnchorHashMismatch { height, expected, got } => { + Self::AnchorHashMismatch { + height, + expected, + got, + } => { write!( f, "anchor hash mismatch at {}: expected {}, got {}", - height, hex::encode(expected), hex::encode(got) + height, + hex::encode(expected), + hex::encode(got) ) } Self::DuplicateSpace { space } => { @@ -1355,7 +1500,8 @@ impl fmt::Display for MessageError { write!( f, "spaces root mismatch: expected {}, got {}", - hex::encode(expected), hex::encode(got) + hex::encode(expected), + hex::encode(got) ) } Self::NumsRootMismatch { expected, got } => { @@ -1373,7 +1519,12 @@ impl fmt::Display for MessageError { write!(f, "numeric space {} not found in proof", numeric) } Self::CommitmentNotFound { space, root } => { - write!(f, "commitment {} not found for {}", hex::encode(root), space) + write!( + f, + "commitment {} not found for {}", + hex::encode(root), + space + ) } Self::ReceiptRequired { space } => { write!(f, "receipt required for {}", space) @@ -1395,8 +1546,11 @@ impl fmt::Display for MessageError { } Self::TemporaryRequiresTip { handle, tip, got } => { write!( - f, "Temporary handle {} verifies against {} but requires tip {}", - handle, hex::encode(got), hex::encode(tip) + f, + "Temporary handle {} verifies against {} but requires tip {}", + handle, + hex::encode(got), + hex::encode(tip) ) } Self::HandleAlreadyExists { handle } => { @@ -1412,7 +1566,11 @@ impl fmt::Display for MessageError { write!(f, "records invalid for {}: {}", handle, reason) } Self::FinalCertRequiresTree { handle } => { - write!(f, "final certificate requires non-empty tree for {}", handle) + write!( + f, + "final certificate requires non-empty tree for {}", + handle + ) } Self::HandleNotFound { handle } => { write!(f, "handle {} not found", handle) @@ -1432,7 +1590,6 @@ impl fmt::Display for MessageError { impl std::error::Error for MessageError {} - /// Push the better zone: if cached exists and is better, push cached; otherwise push the new zone. fn push_best_zone(ctx: &msg::QueryContext, zones: &mut Vec, zone: Zone) { let Some(cached) = ctx.get_zone(&zone.canonical) else { @@ -1457,7 +1614,9 @@ fn maybe_verify_receipt( let Some(ci) = zone.requires_receipt() else { return Ok(false); }; - let receipt = receipt.ok_or_else(|| MessageError::ReceiptRequired { space: space.to_string() })?; + let receipt = receipt.ok_or_else(|| MessageError::ReceiptRequired { + space: space.to_string(), + })?; verify_receipt(ci, space, receipt, options)?; Ok(true) } @@ -1467,16 +1626,16 @@ fn decode_journal( receipt: &risc0_zkvm::Receipt, space: &SLabel, ) -> Result { - receipt.journal.decode().map_err(|e| MessageError::MalformedReceipt { - space: space.to_string(), - reason: e.to_string(), - }) + receipt + .journal + .decode() + .map_err(|e| MessageError::MalformedReceipt { + space: space.to_string(), + reason: e.to_string(), + }) } -fn serialize_option_hash( - hash: &Option, - serializer: S, -) -> Result +fn serialize_option_hash(hash: &Option, serializer: S) -> Result where S: Serializer, { @@ -1512,23 +1671,31 @@ where } } -fn verify_zk_journal_matches_onchain(space: &SLabel, zk: &libveritas_zk::guest::Commitment, onchain: &spaces_nums::Commitment) -> Result<(), MessageError> { +fn verify_zk_journal_matches_onchain( + space: &SLabel, + zk: &libveritas_zk::guest::Commitment, + onchain: &spaces_nums::Commitment, +) -> Result<(), MessageError> { let space_str = space.to_string(); if zk.policy_fold != constants::FOLD_ID || zk.policy_step != constants::STEP_ID { return Err(MessageError::ReceiptPolicyMismatch { space: space_str }); } if zk.final_root != onchain.state_root { - return Err(MessageError::CommitmentReceiptMismatch { space: space_str.clone(), field: "state_root" }); + return Err(MessageError::CommitmentReceiptMismatch { + space: space_str.clone(), + field: "state_root", + }); } if zk.rolling_hash != onchain.rolling_hash { - return Err(MessageError::CommitmentReceiptMismatch { space: space_str, field: "rolling_hash" }); + return Err(MessageError::CommitmentReceiptMismatch { + space: space_str, + field: "rolling_hash", + }); } Ok(()) } // Retrieve parent zone without zk verification fn hash_receipt(receipt: &Receipt) -> Hash { - Sha256Hasher::hash( - &borsh::to_vec(receipt).unwrap_or_default() - ) + Sha256Hasher::hash(&borsh::to_vec(receipt).unwrap_or_default()) } diff --git a/veritas/src/msg.rs b/veritas/src/msg.rs index 359b6d8..f312aa5 100644 --- a/veritas/src/msg.rs +++ b/veritas/src/msg.rs @@ -1,15 +1,15 @@ +use crate::cert::{Certificate, HandleSubtree, NumsSubtree, Signature, SpacesSubtree, Witness}; +use crate::{MessageError, Zone}; use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::Receipt; use serde::{Deserialize, Serialize}; +use sip7::{Record, RecordSet}; use spaces_protocol::bitcoin::{ScriptBuf, secp256k1}; use spaces_protocol::constants::ChainAnchor; use spaces_protocol::slabel::SLabel; +use spaces_protocol::sname::{NameLike, SName, Subname}; use std::collections::HashMap; use std::io::{Read, Write}; -use sip7::{Record, RecordSet}; -use crate::{MessageError, Zone}; -use crate::cert::{Certificate, HandleSubtree, NumsSubtree, Signature, SpacesSubtree, Witness}; -use spaces_protocol::sname::{Subname, NameLike, SName}; /// Context for a verification query. /// @@ -22,6 +22,12 @@ pub struct QueryContext { pub zones: Vec, } +impl Default for QueryContext { + fn default() -> Self { + Self::new() + } +} + impl QueryContext { /// Create an empty context (verify all, no prior zones). pub fn new() -> Self { @@ -48,7 +54,11 @@ impl QueryContext { /// Add a zone to the context. Replaces if handle already exists. pub fn add_zone(&mut self, zone: Zone) { - if let Some(existing) = self.zones.iter_mut().find(|z| z.canonical == zone.canonical) { + if let Some(existing) = self + .zones + .iter_mut() + .find(|z| z.canonical == zone.canonical) + { *existing = zone; } else { self.zones.push(zone); @@ -107,13 +117,14 @@ impl Message { fn set_records_inner(&mut self, canonical: &SName, data: sip7::RecordSet, delegate: bool) { let (space, subspace) = match canonical.label_count() { 1 => (canonical.space().unwrap(), None), - 2 => (canonical.space().unwrap(), Some(canonical.subspace().unwrap())), + 2 => ( + canonical.space().unwrap(), + Some(canonical.subspace().unwrap()), + ), _ => return, }; - let Some(bundle) = self.spaces - .iter_mut() - .find(|b| b.subject == space) else { + let Some(bundle) = self.spaces.iter_mut().find(|b| b.subject == space) else { return; }; @@ -121,7 +132,7 @@ impl Message { None => match delegate { true => bundle.delegate_records = Some(data), false => bundle.records = Some(data), - } + }, Some(name) => { if let Some(handle) = bundle .epochs @@ -175,14 +186,21 @@ impl Message { let root = leaf.subject.space().unwrap(); let (genesis_spk, handles, signature) = match leaf.witness { Witness::Root { .. } => continue, - Witness::Leaf { genesis_spk, handles, signature } => - (genesis_spk, handles, signature), + Witness::Leaf { + genesis_spk, + handles, + signature, + } => (genesis_spk, handles, signature), }; let Some(bundle) = bundles.get_mut(&root) else { continue; }; let epoch_root = handles.compute_root().expect("todo bubble error"); - match bundle.epochs.iter_mut().find(|e| e.tree.compute_root().unwrap() == epoch_root) { + match bundle + .epochs + .iter_mut() + .find(|e| e.tree.compute_root().unwrap() == epoch_root) + { Some(e) => { let subtree = std::mem::replace(&mut e.tree, HandleSubtree::empty()); e.tree = subtree.merge(handles).expect("todo bubble error"); @@ -195,17 +213,14 @@ impl Message { } None => bundle.epochs.push(Epoch { tree: handles, - handles: vec![ - Handle { - name: leaf.subject.subspace().unwrap(), - genesis_spk, - records: None, - signature, - } - ], - }) + handles: vec![Handle { + name: leaf.subject.subspace().unwrap(), + genesis_spk, + records: None, + signature, + }], + }), }; - } msg.spaces = bundles.into_values().collect(); Ok(msg) @@ -287,7 +302,9 @@ pub fn verify_records( expected_canonical: &SName, ) -> Result<(), crate::SignatureError> { let signable = records.signable(); - let sig_data = signable.sig.ok_or_else(|| crate::SignatureError::InvalidSignature)?; + let sig_data = signable + .sig + .ok_or(crate::SignatureError::InvalidSignature)?; use secp256k1::XOnlyPublicKey; @@ -309,9 +326,6 @@ pub fn verify_records( .map_err(|_| crate::SignatureError::VerificationFailed) } - - - /// An unsigned record set pending signature. pub struct UnsignedRecordSet { /// Original handle name (e.g., `example.alice@bitcoin`). @@ -338,7 +352,9 @@ impl UnsignedRecordSet { self.handle.clone(), vec![0u8; 64], self.flags, - ).pack().expect("valid sig"); + ) + .pack() + .expect("valid sig"); buf.extend(&dummy); let full = RecordSet::new(buf); full.signable().bytes.to_vec() @@ -358,7 +374,9 @@ impl UnsignedRecordSet { self.handle.clone(), signature, self.flags, - ).pack().expect("valid sig"); + ) + .pack() + .expect("valid sig"); buf.extend(r); RecordSet::new(buf) } @@ -403,7 +421,11 @@ impl BorshDeserialize for ChainProof { let anchor = ChainAnchor::deserialize_reader(reader)?; let spaces = SpacesSubtree::deserialize_reader(reader)?; let nums = NumsSubtree::deserialize_reader(reader)?; - Ok(ChainProof { anchor, spaces, nums }) + Ok(ChainProof { + anchor, + spaces, + nums, + }) } } @@ -414,7 +436,10 @@ impl BorshSerialize for Bundle { BorshSerialize::serialize(&self.epochs, writer)?; let records_bytes: Option> = self.records.as_ref().map(|d| d.as_slice().to_vec()); BorshSerialize::serialize(&records_bytes, writer)?; - let delegate_bytes: Option> = self.delegate_records.as_ref().map(|d| d.as_slice().to_vec()); + let delegate_bytes: Option> = self + .delegate_records + .as_ref() + .map(|d| d.as_slice().to_vec()); BorshSerialize::serialize(&delegate_bytes, writer) } } @@ -475,4 +500,4 @@ impl BorshDeserialize for Handle { signature, }) } -} \ No newline at end of file +} diff --git a/veritas/src/names.rs b/veritas/src/names.rs index 0225b72..d6df3ab 100644 --- a/veritas/src/names.rs +++ b/veritas/src/names.rs @@ -1,11 +1,10 @@ -use std::collections::HashMap; -use std::str::FromStr; -use std::sync::Mutex; -use crate::cert::{Certificate, NumsSubtree}; use crate::Zone; +use crate::cert::{Certificate, NumsSubtree}; use spaces_protocol::slabel::SLabel; use spaces_protocol::sname::{NameLike, SName, Subname}; - +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Mutex; /// Bidirectional name resolver for space handle hierarchies. /// @@ -27,7 +26,8 @@ pub struct NameResolver { impl NameResolver { fn from_aliases(aliases: HashMap) -> Self { - let reverse = aliases.iter() + let reverse = aliases + .iter() .map(|(handle, numeric)| (numeric.clone(), handle.clone())) .collect(); Self { aliases, reverse } @@ -40,9 +40,15 @@ impl NameResolver { pub fn from_certificates(certs: &[Certificate], nums: &NumsSubtree) -> Self { let mut aliases = HashMap::new(); for cert in certs { - let Some(genesis_spk) = cert.genesis_spk() else { continue }; - if cert.subject.space().is_none() { continue }; - let Ok(Some(numout)) = nums.find_num(genesis_spk) else { continue }; + let Some(genesis_spk) = cert.genesis_spk() else { + continue; + }; + if cert.subject.space().is_none() { + continue; + }; + let Ok(Some(numout)) = nums.find_num(genesis_spk) else { + continue; + }; aliases.insert(cert.subject.clone(), numout.num.name.to_slabel()); } Self::from_aliases(aliases) @@ -73,7 +79,9 @@ impl NameResolver { } let labels: Vec<&[u8]> = name.iter().collect(); - let Some(space) = name.space() else { return name.clone() }; + let Some(space) = name.space() else { + return name.clone(); + }; let mut current = match build_2label(labels[count - 2], &space) { Some(n) => n, @@ -105,12 +113,16 @@ impl NameResolver { return name.clone(); } - let Some(space) = name.space() else { return name.clone() }; + let Some(space) = name.space() else { + return name.clone(); + }; if !space.is_numeric() { return name.clone(); } - let Some(subspace) = name.subspace() else { return name.clone() }; + let Some(subspace) = name.subspace() else { + return name.clone(); + }; let sub_str = subspace.to_string(); // Resolve the numeric space to a human-readable parent handle, @@ -159,7 +171,8 @@ impl LookupEntry { if count == 0 { return None; } - let labels: Vec = name.iter() + let labels: Vec = name + .iter() .map(|l| std::str::from_utf8(l).unwrap_or("").to_string()) .collect(); let space = name.space()?; @@ -211,9 +224,7 @@ pub struct Lookup { impl Lookup { pub fn new(names: Vec) -> Self { - let entries = names.iter() - .filter_map(|n| LookupEntry::new(n)) - .collect(); + let entries = names.iter().filter_map(LookupEntry::new).collect(); Self { state: Mutex::new(LookupState { entries, @@ -225,7 +236,9 @@ impl Lookup { /// Returns the first batch of handles to look up. pub fn start(&self) -> Vec { let state = self.state.lock().unwrap(); - state.entries.iter() + state + .entries + .iter() .filter_map(|e| e.current_handle()) .collect() } @@ -237,8 +250,14 @@ impl Lookup { for zone in zones { if let Some(alias) = &zone.alias { - state.resolver.aliases.insert(zone.canonical.clone(), alias.clone()); - state.resolver.reverse.insert(alias.clone(), zone.canonical.clone()); + state + .resolver + .aliases + .insert(zone.canonical.clone(), alias.clone()); + state + .resolver + .reverse + .insert(alias.clone(), zone.canonical.clone()); } } @@ -246,15 +265,21 @@ impl Lookup { if entry.done { continue; } - let Some(handle) = entry.current_handle() else { continue }; - let Some(zone) = zones.iter().find(|z| z.canonical == handle) else { continue }; + let Some(handle) = entry.current_handle() else { + continue; + }; + let Some(zone) = zones.iter().find(|z| z.canonical == handle) else { + continue; + }; match &zone.alias { Some(alias) if entry.cursor > 0 => entry.advance(alias.clone()), _ => entry.done = true, } } - state.entries.iter() + state + .entries + .iter() .filter(|e| !e.done) .filter_map(|e| e.current_handle()) .collect() @@ -271,12 +296,12 @@ impl Lookup { mod tests { use super::*; use crate::cert::{HandleSubtree, KeyHash, Witness}; + use spacedb::NodeHasher; use spacedb::Sha256Hasher; use spacedb::subtree::{SubTree, ValueOrHash}; - use spacedb::NodeHasher; - use spaces_nums::{Num, NumOut}; use spaces_nums::num_id::NumId; use spaces_nums::snumeric::SNumeric; + use spaces_nums::{Num, NumOut}; use spaces_protocol::bitcoin::ScriptBuf; use std::str::FromStr; @@ -427,7 +452,10 @@ mod tests { let flat = SName::from_str("pancakes#822-88-22").unwrap(); let expanded = flattener.expand(&flat); - assert_eq!(expanded, SName::from_str("pancakes.nested1.alice@bitcoin").unwrap()); + assert_eq!( + expanded, + SName::from_str("pancakes.nested1.alice@bitcoin").unwrap() + ); } #[test] @@ -442,9 +470,7 @@ mod tests { #[test] fn lookup_2_labels_resolves_immediately() { - let lookup = Lookup::new(vec![ - SName::from_str("alice@bitcoin").unwrap(), - ]); + let lookup = Lookup::new(vec![SName::from_str("alice@bitcoin").unwrap()]); let batch = lookup.start(); assert_eq!(batch, vec![SName::from_str("alice@bitcoin").unwrap()]); @@ -457,9 +483,7 @@ mod tests { #[test] fn lookup_3_labels() { // nested1.alice@bitcoin requires: alice@bitcoin → #800-12-12 → nested1#800-12-12 - let lookup = Lookup::new(vec![ - SName::from_str("nested1.alice@bitcoin").unwrap(), - ]); + let lookup = Lookup::new(vec![SName::from_str("nested1.alice@bitcoin").unwrap()]); let batch = lookup.start(); assert_eq!(batch, vec![SName::from_str("alice@bitcoin").unwrap()]); @@ -486,7 +510,10 @@ mod tests { let next = lookup.advance(&zones); assert_eq!(next, vec![SName::from_str("nested1#800-12-12").unwrap()]); - let zones2 = vec![make_zone("nested1#800-12-12", Some(SNumeric::new(822, 88, 22)))]; + let zones2 = vec![make_zone( + "nested1#800-12-12", + Some(SNumeric::new(822, 88, 22)), + )]; let next2 = lookup.advance(&zones2); assert_eq!(next2, vec![SName::from_str("pancakes#822-88-22").unwrap()]); @@ -524,9 +551,7 @@ mod tests { #[test] fn lookup_single_label() { - let lookup = Lookup::new(vec![ - SName::from_str("@bitcoin").unwrap(), - ]); + let lookup = Lookup::new(vec![SName::from_str("@bitcoin").unwrap()]); let names = lookup.start(); assert_eq!(names[0], SName::from_str("@bitcoin").unwrap()); @@ -535,19 +560,18 @@ mod tests { #[test] fn lookup_expand_zones_at_end() { - let lookup = Lookup::new(vec![ - SName::from_str("nested1.alice@bitcoin").unwrap(), - ]); + let lookup = Lookup::new(vec![SName::from_str("nested1.alice@bitcoin").unwrap()]); let _ = lookup.start(); let zones = vec![make_zone("alice@bitcoin", Some(SNumeric::new(800, 12, 12)))]; let _ = lookup.advance(&zones); // Now expand a zone with numeric handle - let mut zones_to_expand = vec![ - make_zone("nested1#800-12-12", None), - ]; + let mut zones_to_expand = vec![make_zone("nested1#800-12-12", None)]; lookup.expand_zones(&mut zones_to_expand); - assert_eq!(zones_to_expand[0].handle, SName::from_str("nested1.alice@bitcoin").unwrap()); + assert_eq!( + zones_to_expand[0].handle, + SName::from_str("nested1.alice@bitcoin").unwrap() + ); } } diff --git a/veritas/tests/fixture_tests.rs b/veritas/tests/fixture_tests.rs index eae055d..cc3e710 100644 --- a/veritas/tests/fixture_tests.rs +++ b/veritas/tests/fixture_tests.rs @@ -1,9 +1,9 @@ -use spacedb::subtree::{ProofType}; use libveritas::cert::{NumsSubtree, SpacesSubtree}; +use libveritas::msg::QueryContext; use libveritas::{ProvableOption, SovereigntyState}; -use libveritas::msg::{QueryContext}; +use libveritas_testutil::fixture::{ChainState, FixtureRunner, kitchen_sink}; +use spacedb::subtree::ProofType; use spaces_protocol::sname::NameLike; -use libveritas_testutil::fixture::{kitchen_sink, ChainState, FixtureRunner}; #[test] fn test_space_not_found_in_chain_proof() { @@ -15,12 +15,19 @@ fn test_space_not_found_in_chain_proof() { // omit space from chain proof msg.chain.spaces = SpacesSubtree( - msg.chain.spaces.0 - .prove(&[[0u8;32]], ProofType::Standard).expect("proving failed") + msg.chain + .spaces + .0 + .prove(&[[0u8; 32]], ProofType::Standard) + .expect("proving failed"), ); let veritas = state.veritas(); let ctx = QueryContext::new(); - assert!(veritas.verify_with_options(&ctx, msg, libveritas::VERIFY_DEV_MODE).is_err()); + assert!( + veritas + .verify_with_options(&ctx, msg, libveritas::VERIFY_DEV_MODE) + .is_err() + ); } #[test] @@ -32,33 +39,39 @@ fn test_no_delegate_info_provided() { let initial_bundle = runner.build_bundle(); let mut msg = state.message(vec![initial_bundle.clone()]); msg.chain.nums = NumsSubtree( - msg.chain.nums.0 - .prove(&[[64u8;32]], ProofType::Standard).expect("proving failed") + msg.chain + .nums + .0 + .prove(&[[64u8; 32]], ProofType::Standard) + .expect("proving failed"), ); let veritas = state.veritas(); let ctx = QueryContext::new(); - let res = veritas.verify_with_options(&ctx, msg, libveritas::VERIFY_DEV_MODE).expect("valid"); + let res = veritas + .verify_with_options(&ctx, msg, libveritas::VERIFY_DEV_MODE) + .expect("valid"); assert_eq!(res.zones.len(), 1, "expected 1 zones"); let zone = res.zones.first().unwrap(); assert!(matches!(zone.delegate, ProvableOption::Unknown)); - assert!(matches!(zone.sovereignty, SovereigntyState::Sovereign)); - assert!(!matches!(zone.commitment, ProvableOption::Exists {..})); + assert!(matches!(zone.sovereignty, SovereigntyState::Sovereign)); + assert!(!matches!(zone.commitment, ProvableOption::Exists { .. })); // Now create the message without omitting chain proofs let msg = state.message(vec![initial_bundle]); let mut ctx = QueryContext::new(); ctx.add_zone(zone.clone()); - let res = veritas.verify_with_options(&ctx, msg, libveritas::VERIFY_DEV_MODE).expect("valid"); + let res = veritas + .verify_with_options(&ctx, msg, libveritas::VERIFY_DEV_MODE) + .expect("valid"); assert_eq!(res.zones.len(), 1, "expected 1 zones"); let zone = res.zones.first().unwrap(); assert!(matches!(zone.delegate, ProvableOption::Exists { .. })); - assert!(matches!(zone.sovereignty, SovereigntyState::Sovereign)); - assert!(matches!(zone.commitment, ProvableOption::Empty)); + assert!(matches!(zone.sovereignty, SovereigntyState::Sovereign)); + assert!(matches!(zone.commitment, ProvableOption::Empty)); } - #[test] fn test_kitchen_sink() { let mut state = ChainState::new(); @@ -67,24 +80,36 @@ fn test_kitchen_sink() { let mut runner = FixtureRunner::new(&mut state, fixture); runner.run(&mut state); - let latest_root = runner.handles.handle_tree.compute_root().expect("compute root"); + let latest_root = runner + .handles + .handle_tree + .compute_root() + .expect("compute root"); let bundle = runner.build_bundle(); let msg = state.message(vec![bundle]); let ctx = QueryContext::new(); let veritas = state.veritas(); - let res = veritas.verify_with_options(&ctx, msg, libveritas::VERIFY_DEV_MODE).expect("valid"); + let res = veritas + .verify_with_options(&ctx, msg, libveritas::VERIFY_DEV_MODE) + .expect("valid"); assert_eq!( states.staged.len(), - res.zones.iter().filter(|z| z.sovereignty == SovereigntyState::Dependent).count() + res.zones + .iter() + .filter(|z| z.sovereignty == SovereigntyState::Dependent) + .count() ); - let parent_zone = res.zones.iter().find(|z| z.handle.is_single_label()) + let parent_zone = res + .zones + .iter() + .find(|z| z.handle.is_single_label()) .expect("missing parent"); - let ProvableOption::Exists { value : commitment } = &parent_zone.commitment else { + let ProvableOption::Exists { value: commitment } = &parent_zone.commitment else { panic!("commit should exist"); }; @@ -95,9 +120,9 @@ fn test_kitchen_sink() { if zone.handle.is_single_label() { continue; } - let expected = states.sovereignty( - &zone.handle.subspace().unwrap().to_string() - ).expect("handle exists"); + let expected = states + .sovereignty(&zone.handle.subspace().unwrap().to_string()) + .expect("handle exists"); assert_eq!(expected, zone.sovereignty); } diff --git a/veritas/tests/integration_tests.rs b/veritas/tests/integration_tests.rs index fd5232c..508d166 100644 --- a/veritas/tests/integration_tests.rs +++ b/veritas/tests/integration_tests.rs @@ -1,27 +1,32 @@ -use bitcoin::hashes::{Hash as BitcoinHash}; +use bitcoin::hashes::Hash as BitcoinHash; use bitcoin::key::Keypair; use bitcoin::key::rand::Rng; use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::rand; use bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}; use borsh::{BorshDeserialize, BorshSerialize}; -use libveritas::cert::{Certificate, HandleOut, HandleSubtree, KeyHash, NumsSubtree, Signature, SpacesSubtree, Witness}; +use libveritas::cert::{ + Certificate, HandleOut, HandleSubtree, KeyHash, NumsSubtree, Signature, SpacesSubtree, Witness, +}; +use libveritas::msg::{self, Message, QueryContext}; +use libveritas::{ProvableOption, SovereigntyState, Veritas, Zone, hash_signable_message}; +use risc0_zkvm::{FakeReceipt, InnerReceipt, Receipt, ReceiptClaim}; use spacedb::Sha256Hasher; use spacedb::subtree::{ProofType, SubTree, ValueOrHash}; +use spaces_nums::constants::COMMITMENT_FINALITY_INTERVAL; +use spaces_nums::num_id::NumId; +use spaces_nums::snumeric::SNumeric; +use spaces_nums::{ + CommitmentKey, CommitmentTipKey, DelegatorKey, FullNumOut, Num, NumOut, NumOutpointKey, + RootAnchor, rolling_hash, +}; +use spaces_protocol::constants::ChainAnchor; use spaces_protocol::hasher::{KeyHasher, OutpointKey, SpaceKey}; use spaces_protocol::slabel::SLabel; +use spaces_protocol::sname::{SName, Subname}; use spaces_protocol::{Covenant, FullSpaceOut, Space, SpaceOut}; -use spaces_nums::num_id::NumId; -use spaces_nums::{rolling_hash, CommitmentKey, FullNumOut, Num, NumOut, NumOutpointKey, CommitmentTipKey, RootAnchor, DelegatorKey}; -use spaces_nums::snumeric::SNumeric; use std::collections::HashMap; use std::str::FromStr; -use risc0_zkvm::{FakeReceipt, InnerReceipt, Receipt, ReceiptClaim}; -use spaces_protocol::constants::ChainAnchor; -use spaces_nums::constants::COMMITMENT_FINALITY_INTERVAL; -use libveritas::{hash_signable_message, ProvableOption, SovereigntyState, Veritas, Zone}; -use libveritas::msg::{self, Message, QueryContext}; -use spaces_protocol::sname::{Subname, SName}; fn sname(s: &str) -> SName { SName::from_str(s).unwrap() @@ -52,8 +57,8 @@ pub struct EncodableOutpoint( ); fn gen_p2tr_spk() -> (ScriptBuf, Keypair) { - use bitcoin::script::Builder; use bitcoin::opcodes::all::OP_PUSHNUM_1; + use bitcoin::script::Builder; let secp = Secp256k1::new(); let (secret_key, public_key) = secp.generate_keypair(&mut rand::thread_rng()); @@ -172,7 +177,7 @@ impl TestNum { } pub fn id(&self) -> NumId { - self.fso.numout.num.id.clone() + self.fso.numout.num.id } pub fn outpoint_key(&self) -> NumOutpointKey { @@ -211,12 +216,18 @@ pub struct StagedHandle { } pub struct TestCommitmentBundle { - root: [u8;32], + root: [u8; 32], handles: HashMap, handle_tree: SubTree, receipt: Option, } +impl Default for TestChain { + fn default() -> Self { + Self::new() + } +} + impl TestChain { pub fn new() -> Self { Self { @@ -259,8 +270,8 @@ impl TestChain { let spaces_root = self.spaces_tree.compute_root().expect("spaces root"); let nums_root = self.nums_tree.compute_root().expect("nums root"); - let block_hash = BlockHash - ::from_byte_array(rolling_hash::(spaces_root, nums_root)); + let block_hash = + BlockHash::from_byte_array(rolling_hash::(spaces_root, nums_root)); RootAnchor { spaces_root, @@ -288,7 +299,7 @@ impl TestChain { self.nums_tree .insert(num.id().into(), ValueOrHash::Value(num.outpoint_bytes())) .expect("insert outpoint"); - self.nums.insert(num.id().into(), num.clone()); + self.nums.insert(num.id(), num.clone()); num } @@ -308,9 +319,12 @@ impl TestChain { TestDelegatedSpace { space, ptr: num } } - pub fn insert_commitment(&mut self, ds: &TestDelegatedSpace, root: [u8; 32]) -> spaces_nums::Commitment { - let prev_finalized = self - .rollback_to_finalized_commitment(&ds.space.label()); + pub fn insert_commitment( + &mut self, + ds: &TestDelegatedSpace, + root: [u8; 32], + ) -> spaces_nums::Commitment { + let prev_finalized = self.rollback_to_finalized_commitment(&ds.space.label()); let commitment = match prev_finalized { None => spaces_nums::Commitment { @@ -324,17 +338,22 @@ impl TestChain { prev_root: Some(prev.state_root), rolling_hash: rolling_hash::(prev.rolling_hash, root), block_height: self.block_height, - } + }, }; let commitment_key = CommitmentKey::new::(&ds.space.label(), root); let commitment_bytes = borsh::to_vec(&commitment).expect("valid"); - self.nums_tree.insert(commitment_key.into(), ValueOrHash::Value(commitment_bytes)) + self.nums_tree + .insert(commitment_key.into(), ValueOrHash::Value(commitment_bytes)) .expect("insert commitment"); let registry_key = CommitmentTipKey::from_slabel::(&ds.space.label()); - self.nums_tree.update(registry_key.into(), ValueOrHash::Value(commitment.state_root.to_vec())) + self.nums_tree + .update( + registry_key.into(), + ValueOrHash::Value(commitment.state_root.to_vec()), + ) .expect("insert registry"); commitment @@ -374,7 +393,10 @@ impl TestChain { }) } - pub fn rollback_to_finalized_commitment(&mut self, space: &SLabel) -> Option { + pub fn rollback_to_finalized_commitment( + &mut self, + space: &SLabel, + ) -> Option { let commitment = self.get_commitment(space, None)?; if commitment.is_finalized(self.block_height) { return Some(commitment); @@ -393,7 +415,11 @@ impl TestChain { let finalized = self.get_commitment(space, Some(prev_root))?; // update tip pointer - self.nums_tree.update(registry_key.into(), ValueOrHash::Value(finalized.state_root.to_vec())) + self.nums_tree + .update( + registry_key.into(), + ValueOrHash::Value(finalized.state_root.to_vec()), + ) .expect("update"); Some(finalized) @@ -403,7 +429,7 @@ impl TestChain { pub struct TestHandle { pub name: Subname, pub genesis_spk: ScriptBuf, - pub keypair: Keypair + pub keypair: Keypair, } impl TestHandleTree { @@ -421,7 +447,10 @@ impl TestHandleTree { let label = label(name); let label_hash = KeyHash::hash(label.as_slabel().as_ref()); assert!( - !self.handle_tree.contains(&label_hash).expect("complete tree"), + !self + .handle_tree + .contains(&label_hash) + .expect("complete tree"), "already exists" ); assert!(!self.staged.contains_key(&label), "already staged"); @@ -450,10 +479,7 @@ impl TestHandleTree { }; let signature = sign_zone(&zone, &self.ds.ptr.keypair); - let staged = StagedHandle { - handle, - signature, - }; + let staged = StagedHandle { handle, signature }; self.staged.insert(staged.handle.name.clone(), staged); } @@ -491,16 +517,17 @@ impl TestHandleTree { kind: libveritas_zk::guest::CommitmentKind::Fold, }; - // Serialize using risc0 serde format (u32 words → le bytes), // matching what a real guest would write via env::commit() let words = risc0_zkvm::serde::to_vec(&commitment).expect("serialize commitment"); let journal_bytes: Vec = words.iter().flat_map(|w| w.to_le_bytes()).collect(); - let receipt_claim = ReceiptClaim::ok(libveritas::constants::FOLD_ID, journal_bytes.clone()); - Some( - Receipt::new(InnerReceipt::Fake(FakeReceipt::new(receipt_claim)), journal_bytes) - ) + let receipt_claim = + ReceiptClaim::ok(libveritas::constants::FOLD_ID, journal_bytes.clone()); + Some(Receipt::new( + InnerReceipt::Fake(FakeReceipt::new(receipt_claim)), + journal_bytes, + )) } else { None }; @@ -535,10 +562,8 @@ impl TestHandleTree { ]; // --- Nums tree keys --- - let mut nums_keys: Vec<[u8; 32]> = vec![ - self.ds.ptr.outpoint_key().into(), - self.ds.ptr.id().into(), - ]; + let mut nums_keys: Vec<[u8; 32]> = + vec![self.ds.ptr.outpoint_key().into(), self.ds.ptr.id().into()]; // Registry key (commitment tip pointer) nums_keys.push(CommitmentTipKey::from_slabel::(&self.space).into()); @@ -590,7 +615,7 @@ impl TestHandleTree { // --- Build message --- Message { chain: msg::ChainProof { - anchor: anchor.clone(), + anchor: *anchor, spaces: SpacesSubtree(spaces_proof), nums: NumsSubtree(nums_proof), }, @@ -619,7 +644,9 @@ impl TestHandleTree { anchor: &ChainAnchor, ) -> Message { let tcb = &self.commitments[commitment_idx]; - let staged = self.staged.get(&label(handle_name)) + let staged = self + .staged + .get(&label(handle_name)) .expect("handle must be staged"); // --- Spaces tree keys --- @@ -629,10 +656,8 @@ impl TestHandleTree { ]; // --- Nums tree keys --- - let mut nums_keys: Vec<[u8; 32]> = vec![ - self.ds.ptr.outpoint_key().into(), - self.ds.ptr.id().into(), - ]; + let mut nums_keys: Vec<[u8; 32]> = + vec![self.ds.ptr.outpoint_key().into(), self.ds.ptr.id().into()]; nums_keys.push(CommitmentTipKey::from_slabel::(&self.space).into()); nums_keys.push(CommitmentKey::new::(&self.space, tcb.root).into()); @@ -645,19 +670,22 @@ impl TestHandleTree { let handle_keys: Vec<[u8; 32]> = vec![handle_key]; // --- Create proved subtrees --- - let spaces_proof = chain.spaces_tree + let spaces_proof = chain + .spaces_tree .prove(&spaces_keys, ProofType::Standard) .expect("prove spaces"); - let nums_proof = chain.nums_tree + let nums_proof = chain + .nums_tree .prove(&nums_keys, ProofType::Standard) .expect("prove nums"); - let handles_proof = tcb.handle_tree + let handles_proof = tcb + .handle_tree .prove(&handle_keys, ProofType::Standard) .expect("prove handles exclusion"); Message { chain: msg::ChainProof { - anchor: anchor.clone(), + anchor: *anchor, spaces: SpacesSubtree(spaces_proof), nums: NumsSubtree(nums_proof), }, @@ -731,30 +759,31 @@ impl Fixture { fn veritas(&self) -> Veritas { let anchors = vec![self.latest_anchor.clone(), self.finalized_anchor.clone()]; - Veritas::new() - .with_anchors(anchors).expect("valid anchors") + Veritas::new().with_anchors(anchors).expect("valid anchors") } /// Message proving commitment 0 (finalized) against the finalized anchor. fn finalized_message(&self, handles: &[&str]) -> Message { self.handles.build_message( - &self.finalized_chain, 0, handles, + &self.finalized_chain, + 0, + handles, &self.finalized_anchor.block, ) } /// Message proving commitment 1 (pending) against the latest anchor. fn pending_message(&self, handles: &[&str]) -> Message { - self.handles.build_message( - &self.latest_chain, 1, handles, - &self.latest_anchor.block, - ) + self.handles + .build_message(&self.latest_chain, 1, handles, &self.latest_anchor.block) } /// Temporary certificate message for a staged handle (not yet committed). fn temporary_message(&self, handle_name: &str) -> Message { self.handles.build_temporary_message( - &self.latest_chain, 1, handle_name, + &self.latest_chain, + 1, + handle_name, &self.latest_anchor.block, ) } @@ -766,7 +795,9 @@ fn verify_root_finalized() { let veritas = f.veritas(); let ctx = QueryContext::new(); - let result = veritas.verify_with_options(&ctx,f.finalized_message(&[]), libveritas::VERIFY_DEV_MODE).expect("verify"); + let result = veritas + .verify_with_options(&ctx, f.finalized_message(&[]), libveritas::VERIFY_DEV_MODE) + .expect("verify"); assert_eq!(result.zones.len(), 1); let zone = &result.zones[0]; @@ -786,15 +817,35 @@ fn verify_leaf_finalized() { let veritas = f.veritas(); let ctx = QueryContext::new(); - let result = veritas.verify_with_options(&ctx,f.finalized_message(&["alice"]), libveritas::VERIFY_DEV_MODE).expect("verify"); + let result = veritas + .verify_with_options( + &ctx, + f.finalized_message(&["alice"]), + libveritas::VERIFY_DEV_MODE, + ) + .expect("verify"); // Should have root zone + alice zone assert_eq!(result.zones.len(), 2); - let alice = result.zones.iter().find(|z| z.handle == sname("alice@bitcoin")).expect("alice"); + let alice = result + .zones + .iter() + .find(|z| z.handle == sname("alice@bitcoin")) + .expect("alice"); assert!(matches!(alice.sovereignty, SovereigntyState::Sovereign)); - let result = veritas.verify_with_options(&ctx,f.finalized_message(&["bob"]), libveritas::VERIFY_DEV_MODE).expect("verify"); - let bob = result.zones.iter().find(|z| z.handle == sname("bob@bitcoin")).expect("bob"); + let result = veritas + .verify_with_options( + &ctx, + f.finalized_message(&["bob"]), + libveritas::VERIFY_DEV_MODE, + ) + .expect("verify"); + let bob = result + .zones + .iter() + .find(|z| z.handle == sname("bob@bitcoin")) + .expect("bob"); assert!(matches!(bob.sovereignty, SovereigntyState::Sovereign)); } @@ -804,7 +855,9 @@ fn verify_root_pending() { let veritas = f.veritas(); let ctx = QueryContext::new(); - let result = veritas.verify_with_options(&ctx,f.pending_message(&[]), libveritas::VERIFY_DEV_MODE).expect("verify"); + let result = veritas + .verify_with_options(&ctx, f.pending_message(&[]), libveritas::VERIFY_DEV_MODE) + .expect("verify"); assert_eq!(result.zones.len(), 1); let zone = &result.zones[0]; @@ -822,8 +875,18 @@ fn verify_leaf_pending() { let veritas = f.veritas(); let ctx = QueryContext::new(); - let result = veritas.verify_with_options(&ctx,f.pending_message(&["charlie"]), libveritas::VERIFY_DEV_MODE).expect("verify"); - let charlie = result.zones.iter().find(|z| z.handle == sname("charlie@bitcoin")).expect("charlie"); + let result = veritas + .verify_with_options( + &ctx, + f.pending_message(&["charlie"]), + libveritas::VERIFY_DEV_MODE, + ) + .expect("verify"); + let charlie = result + .zones + .iter() + .find(|z| z.handle == sname("charlie@bitcoin")) + .expect("charlie"); assert!(matches!(charlie.sovereignty, SovereigntyState::Pending)); } @@ -834,8 +897,18 @@ fn verify_leaf_across_anchors() { let ctx = QueryContext::new(); // alice was committed in commitment 0, verified against the latest anchor - let result = veritas.verify_with_options(&ctx,f.pending_message(&["alice"]), libveritas::VERIFY_DEV_MODE).expect("verify"); - let alice = result.zones.iter().find(|z| z.handle == sname("alice@bitcoin")).expect("alice"); + let result = veritas + .verify_with_options( + &ctx, + f.pending_message(&["alice"]), + libveritas::VERIFY_DEV_MODE, + ) + .expect("verify"); + let alice = result + .zones + .iter() + .find(|z| z.handle == sname("alice@bitcoin")) + .expect("alice"); assert_eq!(alice.handle, sname("alice@bitcoin")); } @@ -846,8 +919,18 @@ fn verify_leaf_temporary() { let ctx = QueryContext::new(); // "staged" is in staged but not committed — uses delegate's signature - let result = veritas.verify_with_options(&ctx,f.temporary_message("staged"), libveritas::VERIFY_DEV_MODE).expect("verify"); - let staged = result.zones.iter().find(|z| z.handle == sname("staged@bitcoin")).expect("staged"); + let result = veritas + .verify_with_options( + &ctx, + f.temporary_message("staged"), + libveritas::VERIFY_DEV_MODE, + ) + .expect("verify"); + let staged = result + .zones + .iter() + .find(|z| z.handle == sname("staged@bitcoin")) + .expect("staged"); assert_eq!(staged.handle, sname("staged@bitcoin")); assert!(matches!(staged.sovereignty, SovereigntyState::Dependent)); } @@ -861,7 +944,13 @@ fn verify_with_request_filter() { let mut ctx = QueryContext::new(); ctx.add_request(sname("alice@bitcoin")); - let result = veritas.verify_with_options(&ctx,f.finalized_message(&["alice", "bob"]), libveritas::VERIFY_DEV_MODE).expect("verify"); + let result = veritas + .verify_with_options( + &ctx, + f.finalized_message(&["alice", "bob"]), + libveritas::VERIFY_DEV_MODE, + ) + .expect("verify"); // Should only return alice (root not requested, bob not requested) assert_eq!(result.zones.len(), 1); @@ -875,15 +964,27 @@ fn verify_with_cached_parent_zone() { // First verify to get parent zone let ctx = QueryContext::new(); - let result = veritas.verify_with_options(&ctx,f.finalized_message(&[]), libveritas::VERIFY_DEV_MODE).expect("verify"); + let result = veritas + .verify_with_options(&ctx, f.finalized_message(&[]), libveritas::VERIFY_DEV_MODE) + .expect("verify"); let parent_zone = result.zones[0].clone(); // Now verify with cached parent let ctx = QueryContext::from_zones(vec![parent_zone]); - let result = veritas.verify_with_options(&ctx,f.finalized_message(&["alice"]), libveritas::VERIFY_DEV_MODE).expect("verify"); + let result = veritas + .verify_with_options( + &ctx, + f.finalized_message(&["alice"]), + libveritas::VERIFY_DEV_MODE, + ) + .expect("verify"); // Should succeed and include alice - let alice = result.zones.iter().find(|z| z.handle == sname("alice@bitcoin")).expect("alice"); + let alice = result + .zones + .iter() + .find(|z| z.handle == sname("alice@bitcoin")) + .expect("alice"); assert_eq!(alice.handle, sname("alice@bitcoin")); } @@ -908,10 +1009,20 @@ fn verify_uses_better_cached_zone() { }; let ctx = QueryContext::from_zones(vec![cached_zone.clone()]); - let result = veritas.verify_with_options(&ctx,f.finalized_message(&["alice"]), libveritas::VERIFY_DEV_MODE).expect("verify"); + let result = veritas + .verify_with_options( + &ctx, + f.finalized_message(&["alice"]), + libveritas::VERIFY_DEV_MODE, + ) + .expect("verify"); // Should return the newly verified zone (better anchor) - let alice = result.zones.iter().find(|z| z.handle == sname("alice@bitcoin")).expect("alice"); + let alice = result + .zones + .iter() + .find(|z| z.handle == sname("alice@bitcoin")) + .expect("alice"); assert!(alice.anchor > 0); assert!(matches!(alice.sovereignty, SovereigntyState::Sovereign)); } @@ -923,7 +1034,13 @@ fn certificate_iterator() { let ctx = QueryContext::new(); // Verify root + two leaves - let result = veritas.verify_with_options(&ctx,f.finalized_message(&["alice", "bob"]), libveritas::VERIFY_DEV_MODE).expect("verify"); + let result = veritas + .verify_with_options( + &ctx, + f.finalized_message(&["alice", "bob"]), + libveritas::VERIFY_DEV_MODE, + ) + .expect("verify"); let certs: Vec = result.certificates().collect(); @@ -935,10 +1052,16 @@ fn certificate_iterator() { assert!(matches!(certs[0].witness, Witness::Root { .. })); // Then leaves - let alice_cert = certs.iter().find(|c| c.subject == sname("alice@bitcoin")).expect("alice cert"); + let alice_cert = certs + .iter() + .find(|c| c.subject == sname("alice@bitcoin")) + .expect("alice cert"); assert!(matches!(alice_cert.witness, Witness::Leaf { .. })); - let bob_cert = certs.iter().find(|c| c.subject == sname("bob@bitcoin")).expect("bob cert"); + let bob_cert = certs + .iter() + .find(|c| c.subject == sname("bob@bitcoin")) + .expect("bob cert"); assert!(matches!(bob_cert.witness, Witness::Leaf { .. })); } @@ -951,7 +1074,13 @@ fn certificate_iterator_leaves_only() { let mut ctx = QueryContext::new(); ctx.add_request(sname("alice@bitcoin")); - let result = veritas.verify_with_options(&ctx,f.finalized_message(&["alice"]), libveritas::VERIFY_DEV_MODE).expect("verify"); + let result = veritas + .verify_with_options( + &ctx, + f.finalized_message(&["alice"]), + libveritas::VERIFY_DEV_MODE, + ) + .expect("verify"); let certs: Vec = result.certificates().collect(); diff --git a/zk/Cargo.toml b/zk/Cargo.toml index a600f59..f229d79 100644 --- a/zk/Cargo.toml +++ b/zk/Cargo.toml @@ -1,9 +1,18 @@ [package] name = "libveritas_zk" -version = "0.1.0" -edition = "2024" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +authors.workspace = true +description = "ZK guest types and helpers for libveritas." +documentation = "https://docs.rs/libveritas_zk" +keywords = ["spaces", "zk", "risc0"] +categories = ["cryptography"] [dependencies] spacedb = { workspace = true } borsh = { version = "1.6", default-features = false, features = ["derive"] } -serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } +serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } \ No newline at end of file diff --git a/zk/src/guest.rs b/zk/src/guest.rs index f36bff0..90df0b4 100644 --- a/zk/src/guest.rs +++ b/zk/src/guest.rs @@ -1,9 +1,11 @@ +use crate::BatchReader; use alloc::vec::Vec; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; -use spacedb::{Hash, Sha256Hasher, subtree::{SubTree, ValueOrHash}, VerifyError, NodeHasher}; -use crate::BatchReader; - +use spacedb::{ + Hash, NodeHasher, Sha256Hasher, VerifyError, + subtree::{SubTree, ValueOrHash}, +}; #[derive(Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] pub struct Commitment { @@ -29,7 +31,12 @@ pub enum GuestError { pub type Result = core::result::Result; -pub fn run(subtree: Vec, input: Vec, policy_step: [u32; 8], policy_fold: [u32; 8]) -> Result { +pub fn run( + subtree: Vec, + input: Vec, + policy_step: [u32; 8], + policy_fold: [u32; 8], +) -> Result { let mut subtree: SubTree = borsh::from_slice(&subtree).expect("decoding subtree error"); @@ -37,19 +44,26 @@ pub fn run(subtree: Vec, input: Vec, policy_step: [u32; 8], policy_fold: let reader = BatchReader(&input); for entry in reader.iter() { - subtree.insert( - entry.handle.try_into().expect("32 byte subspace hash slice"), - ValueOrHash::Hash(entry.value_hash.try_into().expect("32 byte value hash slice")), - ) + subtree + .insert( + entry + .handle + .try_into() + .expect("32 byte subspace hash slice"), + ValueOrHash::Hash( + entry + .value_hash + .try_into() + .expect("32 byte value hash slice"), + ), + ) .map_err(|e| match e { - spacedb::Error::Verify(e) => { - match e { - VerifyError::IncompleteProof => GuestError::IncompleteSubTree, - VerifyError::KeyNotFound => GuestError::IncompleteSubTree, - VerifyError::RootMismatch => GuestError::IncompleteSubTree, - VerifyError::KeyExists => GuestError::KeyExists, - } - } + spacedb::Error::Verify(e) => match e { + VerifyError::IncompleteProof => GuestError::IncompleteSubTree, + VerifyError::KeyNotFound => GuestError::IncompleteSubTree, + VerifyError::RootMismatch => GuestError::IncompleteSubTree, + VerifyError::KeyExists => GuestError::KeyExists, + }, _ => { unreachable!("expected verify error") } diff --git a/zk/src/lib.rs b/zk/src/lib.rs index 050b892..05bc0ec 100644 --- a/zk/src/lib.rs +++ b/zk/src/lib.rs @@ -1,3 +1,8 @@ +//! ZK guest types and helpers for [`libveritas`](https://docs.rs/libveritas). +//! +//! Defines the [`guest::Commitment`] proven by the RISC Zero guest programs +//! and a [`BatchReader`] for reading the host-prepared input batches. + extern crate alloc; extern crate core; @@ -18,11 +23,9 @@ impl<'a> BatchReader<'a> { pub fn new(data: &'a [u8]) -> Self { BatchReader(data) } - + pub fn iter(&self) -> BodyIterator<'a> { - BodyIterator { - data: &self.0, - } + BodyIterator { data: self.0 } } } From 68611b6d7a0e4341872b8fd4b8c7da6f0631a0e1 Mon Sep 17 00:00:00 2001 From: Buffrr Date: Fri, 17 Apr 2026 18:45:58 +0200 Subject: [PATCH 2/2] ci: skip rustfmt on autogenerated guest image ID arrays --- update-elfs.sh | 2 ++ veritas/src/constants.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/update-elfs.sh b/update-elfs.sh index 3a18d76..bfb4f01 100755 --- a/update-elfs.sh +++ b/update-elfs.sh @@ -59,7 +59,9 @@ cat > "${CONSTANTS_FILE}" << EOF // To update after changing guest programs, run: // ./update-elfs.sh +#[rustfmt::skip] pub const FOLD_ID: [u32; 8] = ${FOLD_U32}; +#[rustfmt::skip] pub const STEP_ID: [u32; 8] = ${STEP_U32}; #[cfg(feature = "elf")] diff --git a/veritas/src/constants.rs b/veritas/src/constants.rs index 116978b..87be798 100644 --- a/veritas/src/constants.rs +++ b/veritas/src/constants.rs @@ -5,7 +5,9 @@ // To update after changing guest programs, run: // ./update-elfs.sh +#[rustfmt::skip] pub const FOLD_ID: [u32; 8] = [3538164873, 3494660837, 1605885420, 2756930862, 1952720968, 91802116, 3635727049, 436347682]; +#[rustfmt::skip] pub const STEP_ID: [u32; 8] = [2719979593, 62333512, 1158600685, 3512173834, 1442236244, 869560259, 553115519, 3467999922]; #[cfg(feature = "elf")]