From 4d7ea797d6be3e17a44d2895da533b0e4aedd973 Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 9 Sep 2026 11:06:50 -0600 Subject: [PATCH 1/6] remove serai submodule --- .gitmodules | 3 --- src/serai | 1 - 2 files changed, 4 deletions(-) delete mode 100644 .gitmodules delete mode 160000 src/serai diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 01500a7..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/serai"] - path = src/serai - url = https://github.com/kayabaNerve/serai diff --git a/src/serai b/src/serai deleted file mode 160000 index 90ee77d..0000000 --- a/src/serai +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 90ee77d805224d8c6641e4123adc2cc74b701c9f From d69ad8113f2c337312a0459c0f973e3d07250d7a Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 9 Sep 2026 11:07:20 -0600 Subject: [PATCH 2/6] Squashed 'src/serai/' content from commit 90ee77d git-subtree-dir: src/serai git-subtree-split: 90ee77d805224d8c6641e4123adc2cc74b701c9f --- .gitattributes | 5 + .github/actions/LICENSE | 21 + .github/actions/bitcoin/action.yml | 47 + .github/actions/build-dependencies/action.yml | 36 + .github/actions/test-dependencies/action.yml | 26 + .github/nightly-version | 1 + .github/workflows/coins-tests.yml | 35 + .github/workflows/common-tests.yml | 33 + .github/workflows/crypto-tests.yml | 42 + .github/workflows/daily-deny.yml | 27 + .github/workflows/lint.yml | 66 + .github/workflows/monthly-nightly-update.yml | 53 + .github/workflows/no-std.yml | 37 + .gitignore | 3 + .rustfmt.toml | 17 + CONTRIBUTING.md | 37 + Cargo.lock | 1444 +++++++++++++++++ Cargo.toml | 30 + LICENSE | 8 + README.md | 62 + .../Audit.pdf | Bin 0 -> 147514 bytes .../LICENSE | 21 + .../README.md | 6 + .../Cypher Stack crypto March 2023/Audit.pdf | Bin 0 -> 292738 bytes audits/Cypher Stack crypto March 2023/LICENSE | 21 + .../Cypher Stack crypto March 2023/README.md | 7 + coins/bitcoin/Cargo.toml | 62 + coins/bitcoin/LICENSE | 21 + coins/bitcoin/README.md | 4 + coins/bitcoin/src/crypto.rs | 172 ++ coins/bitcoin/src/lib.rs | 21 + coins/bitcoin/src/tests/crypto.rs | 47 + coins/bitcoin/src/tests/mod.rs | 1 + coins/bitcoin/src/wallet/mod.rs | 185 +++ coins/bitcoin/src/wallet/send.rs | 420 +++++ coins/bitcoin/tests/runner.rs | 48 + coins/bitcoin/tests/wallet.rs | 92 ++ crypto/dkg/Cargo.toml | 57 + crypto/dkg/LICENSE | 21 + crypto/dkg/README.md | 16 + crypto/dkg/src/encryption.rs | 487 ++++++ crypto/dkg/src/frost.rs | 607 +++++++ crypto/dkg/src/lib.rs | 536 ++++++ crypto/dkg/src/musig.rs | 138 ++ crypto/dkg/src/promote.rs | 125 ++ crypto/dkg/src/resharing/common.rs | 137 ++ crypto/dkg/src/resharing/mod.rs | 7 + crypto/dkg/src/resharing/reshared.rs | 180 ++ crypto/dkg/src/resharing/resharer.rs | 180 ++ crypto/dkg/src/tests/frost.rs | 315 ++++ crypto/dkg/src/tests/mod.rs | 103 ++ crypto/dkg/src/tests/musig.rs | 61 + crypto/dkg/src/tests/promote.rs | 62 + crypto/dkg/src/tests/resharing.rs | 87 + crypto/frost/Cargo.toml | 57 + crypto/frost/LICENSE | 21 + crypto/frost/README.md | 22 + crypto/frost/src/algorithm.rs | 222 +++ crypto/frost/src/curve/dalek.rs | 47 + crypto/frost/src/curve/ed448.rs | 44 + crypto/frost/src/curve/kp256.rs | 43 + crypto/frost/src/curve/mod.rs | 114 ++ crypto/frost/src/lib.rs | 73 + crypto/frost/src/nonce.rs | 310 ++++ crypto/frost/src/sign.rs | 516 ++++++ crypto/frost/src/tests/literal/dalek.rs | 32 + crypto/frost/src/tests/literal/ed448.rs | 65 + crypto/frost/src/tests/literal/kp256.rs | 35 + crypto/frost/src/tests/literal/mod.rs | 6 + .../literal/vectors/frost-ed25519-sha512.json | 77 + .../literal/vectors/frost-ed448-shake256.json | 77 + .../literal/vectors/frost-p256-sha256.json | 77 + .../vectors/frost-ristretto255-sha512.json | 77 + .../vectors/frost-secp256k1-sha256.json | 77 + crypto/frost/src/tests/mod.rs | 272 ++++ crypto/frost/src/tests/nonces.rs | 231 +++ crypto/frost/src/tests/vectors.rs | 369 +++++ deny.toml | 61 + hrf/Cargo.toml | 33 + hrf/LICENSE | 21 + hrf/cbindgen.toml | 6 + hrf/header.h | 428 +++++ hrf/src/key_gen.rs | 525 ++++++ hrf/src/lib.rs | 101 ++ hrf/src/resharing.rs | 420 +++++ hrf/src/sign.rs | 498 ++++++ tests/no-std/Cargo.toml | 22 + tests/no-std/LICENSE | 21 + tests/no-std/README.md | 3 + tests/no-std/src/lib.rs | 8 + 90 files changed, 11288 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/actions/LICENSE create mode 100644 .github/actions/bitcoin/action.yml create mode 100644 .github/actions/build-dependencies/action.yml create mode 100644 .github/actions/test-dependencies/action.yml create mode 100644 .github/nightly-version create mode 100644 .github/workflows/coins-tests.yml create mode 100644 .github/workflows/common-tests.yml create mode 100644 .github/workflows/crypto-tests.yml create mode 100644 .github/workflows/daily-deny.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/monthly-nightly-update.yml create mode 100644 .github/workflows/no-std.yml create mode 100644 .gitignore create mode 100644 .rustfmt.toml create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 audits/Cypher Stack coins bitcoin August 2023/Audit.pdf create mode 100644 audits/Cypher Stack coins bitcoin August 2023/LICENSE create mode 100644 audits/Cypher Stack coins bitcoin August 2023/README.md create mode 100644 audits/Cypher Stack crypto March 2023/Audit.pdf create mode 100644 audits/Cypher Stack crypto March 2023/LICENSE create mode 100644 audits/Cypher Stack crypto March 2023/README.md create mode 100644 coins/bitcoin/Cargo.toml create mode 100644 coins/bitcoin/LICENSE create mode 100644 coins/bitcoin/README.md create mode 100644 coins/bitcoin/src/crypto.rs create mode 100644 coins/bitcoin/src/lib.rs create mode 100644 coins/bitcoin/src/tests/crypto.rs create mode 100644 coins/bitcoin/src/tests/mod.rs create mode 100644 coins/bitcoin/src/wallet/mod.rs create mode 100644 coins/bitcoin/src/wallet/send.rs create mode 100644 coins/bitcoin/tests/runner.rs create mode 100644 coins/bitcoin/tests/wallet.rs create mode 100644 crypto/dkg/Cargo.toml create mode 100644 crypto/dkg/LICENSE create mode 100644 crypto/dkg/README.md create mode 100644 crypto/dkg/src/encryption.rs create mode 100644 crypto/dkg/src/frost.rs create mode 100644 crypto/dkg/src/lib.rs create mode 100644 crypto/dkg/src/musig.rs create mode 100644 crypto/dkg/src/promote.rs create mode 100644 crypto/dkg/src/resharing/common.rs create mode 100644 crypto/dkg/src/resharing/mod.rs create mode 100644 crypto/dkg/src/resharing/reshared.rs create mode 100644 crypto/dkg/src/resharing/resharer.rs create mode 100644 crypto/dkg/src/tests/frost.rs create mode 100644 crypto/dkg/src/tests/mod.rs create mode 100644 crypto/dkg/src/tests/musig.rs create mode 100644 crypto/dkg/src/tests/promote.rs create mode 100644 crypto/dkg/src/tests/resharing.rs create mode 100644 crypto/frost/Cargo.toml create mode 100644 crypto/frost/LICENSE create mode 100644 crypto/frost/README.md create mode 100644 crypto/frost/src/algorithm.rs create mode 100644 crypto/frost/src/curve/dalek.rs create mode 100644 crypto/frost/src/curve/ed448.rs create mode 100644 crypto/frost/src/curve/kp256.rs create mode 100644 crypto/frost/src/curve/mod.rs create mode 100644 crypto/frost/src/lib.rs create mode 100644 crypto/frost/src/nonce.rs create mode 100644 crypto/frost/src/sign.rs create mode 100644 crypto/frost/src/tests/literal/dalek.rs create mode 100644 crypto/frost/src/tests/literal/ed448.rs create mode 100644 crypto/frost/src/tests/literal/kp256.rs create mode 100644 crypto/frost/src/tests/literal/mod.rs create mode 100644 crypto/frost/src/tests/literal/vectors/frost-ed25519-sha512.json create mode 100644 crypto/frost/src/tests/literal/vectors/frost-ed448-shake256.json create mode 100644 crypto/frost/src/tests/literal/vectors/frost-p256-sha256.json create mode 100644 crypto/frost/src/tests/literal/vectors/frost-ristretto255-sha512.json create mode 100644 crypto/frost/src/tests/literal/vectors/frost-secp256k1-sha256.json create mode 100644 crypto/frost/src/tests/mod.rs create mode 100644 crypto/frost/src/tests/nonces.rs create mode 100644 crypto/frost/src/tests/vectors.rs create mode 100644 deny.toml create mode 100644 hrf/Cargo.toml create mode 100644 hrf/LICENSE create mode 100644 hrf/cbindgen.toml create mode 100644 hrf/header.h create mode 100644 hrf/src/key_gen.rs create mode 100644 hrf/src/lib.rs create mode 100644 hrf/src/resharing.rs create mode 100644 hrf/src/sign.rs create mode 100644 tests/no-std/Cargo.toml create mode 100644 tests/no-std/LICENSE create mode 100644 tests/no-std/README.md create mode 100644 tests/no-std/src/lib.rs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2787681 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Auto detect text files and perform LF normalization +* text=auto +* text eol=lf + +*.pdf binary diff --git a/.github/actions/LICENSE b/.github/actions/LICENSE new file mode 100644 index 0000000..6779f0e --- /dev/null +++ b/.github/actions/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2023 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.github/actions/bitcoin/action.yml b/.github/actions/bitcoin/action.yml new file mode 100644 index 0000000..8dc8c78 --- /dev/null +++ b/.github/actions/bitcoin/action.yml @@ -0,0 +1,47 @@ +name: bitcoin-regtest +description: Spawns a regtest Bitcoin daemon + +inputs: + version: + description: "Version to download and run" + required: false + default: 24.0.1 + +runs: + using: "composite" + steps: + - name: Bitcoin Daemon Cache + id: cache-bitcoind + uses: actions/cache@v3 + with: + path: bitcoin.tar.gz + key: bitcoind-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }} + + - name: Download the Bitcoin Daemon + if: steps.cache-bitcoind.outputs.cache-hit != 'true' + shell: bash + run: | + RUNNER_OS=linux + RUNNER_ARCH=x86_64 + FILE=bitcoin-${{ inputs.version }}-$RUNNER_ARCH-$RUNNER_OS-gnu.tar.gz + + wget https://bitcoincore.org/bin/bitcoin-core-${{ inputs.version }}/$FILE + mv $FILE bitcoin.tar.gz + + - name: Extract the Bitcoin Daemon + shell: bash + run: | + tar xzvf bitcoin.tar.gz + cd bitcoin-${{ inputs.version }} + sudo mv bin/* /bin && sudo mv lib/* /lib + + - name: Bitcoin Regtest Daemon + shell: bash + run: | + RPC_USER=serai + RPC_PASS=seraidex + + bitcoind -txindex -regtest \ + -rpcuser=$RPC_USER -rpcpassword=$RPC_PASS \ + -rpcbind=127.0.0.1 -rpcbind=$(hostname) -rpcallowip=0.0.0.0/0 \ + -daemon diff --git a/.github/actions/build-dependencies/action.yml b/.github/actions/build-dependencies/action.yml new file mode 100644 index 0000000..ed28bf2 --- /dev/null +++ b/.github/actions/build-dependencies/action.yml @@ -0,0 +1,36 @@ +name: build-dependencies +description: Installs build dependencies for Serai + +inputs: + github-token: + description: "GitHub token to install Protobuf with" + require: true + default: + + rust-toolchain: + description: "Rust toolchain to install" + required: false + default: stable + + rust-components: + description: "Rust components to install" + required: false + default: + +runs: + using: "composite" + steps: + - name: Install Protobuf + uses: arduino/setup-protoc@v2.0.0 + with: + repo-token: ${{ inputs.github-token }} + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ inputs.rust-toolchain }} + components: ${{ inputs.rust-components }} + targets: wasm32-unknown-unknown, riscv32imac-unknown-none-elf + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 diff --git a/.github/actions/test-dependencies/action.yml b/.github/actions/test-dependencies/action.yml new file mode 100644 index 0000000..310140d --- /dev/null +++ b/.github/actions/test-dependencies/action.yml @@ -0,0 +1,26 @@ +name: test-dependencies +description: Installs test dependencies for Serai + +inputs: + github-token: + description: "GitHub token to install Protobuf with" + require: true + default: + + bitcoin-version: + description: "Bitcoin version to download and run as a regtest node" + required: false + default: 24.0.1 + +runs: + using: "composite" + steps: + - name: Install Build Dependencies + uses: ./.github/actions/build-dependencies + with: + github-token: ${{ inputs.github-token }} + + - name: Run a Bitcoin Regtest Node + uses: ./.github/actions/bitcoin + with: + version: ${{ inputs.bitcoin-version }} diff --git a/.github/nightly-version b/.github/nightly-version new file mode 100644 index 0000000..fda315a --- /dev/null +++ b/.github/nightly-version @@ -0,0 +1 @@ +nightly-2023-08-01 diff --git a/.github/workflows/coins-tests.yml b/.github/workflows/coins-tests.yml new file mode 100644 index 0000000..584bc4f --- /dev/null +++ b/.github/workflows/coins-tests.yml @@ -0,0 +1,35 @@ +name: coins/ Tests + +on: + push: + branches: + - develop + paths: + - "common/**" + - "crypto/**" + - "coins/**" + + pull_request: + paths: + - "common/**" + - "crypto/**" + - "coins/**" + + workflow_dispatch: + +jobs: + test-coins: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Test Dependencies + uses: ./.github/actions/test-dependencies + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Tests + run: | + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \ + -p bitcoin-serai \ + -p hrf-api diff --git a/.github/workflows/common-tests.yml b/.github/workflows/common-tests.yml new file mode 100644 index 0000000..023ab28 --- /dev/null +++ b/.github/workflows/common-tests.yml @@ -0,0 +1,33 @@ +name: common/ Tests + +on: + push: + branches: + - develop + paths: + - "common/**" + + pull_request: + paths: + - "common/**" + + workflow_dispatch: + +jobs: + test-common: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Test Dependencies + uses: ./.github/actions/test-dependencies + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Tests + run: | + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \ + -p std-shims \ + -p zalloc \ + -p serai-db \ + -p serai-env diff --git a/.github/workflows/crypto-tests.yml b/.github/workflows/crypto-tests.yml new file mode 100644 index 0000000..cf371f5 --- /dev/null +++ b/.github/workflows/crypto-tests.yml @@ -0,0 +1,42 @@ +name: crypto/ Tests + +on: + push: + branches: + - develop + paths: + - "common/**" + - "crypto/**" + + pull_request: + paths: + - "common/**" + - "crypto/**" + + workflow_dispatch: + +jobs: + test-crypto: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Test Dependencies + uses: ./.github/actions/test-dependencies + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run Tests + run: | + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \ + -p flexible-transcript \ + -p ff-group-tests \ + -p dalek-ff-group \ + -p minimal-ed448 \ + -p ciphersuite \ + -p multiexp \ + -p schnorr-signatures \ + -p dleq \ + -p dkg \ + -p modular-frost \ + -p frost-schnorrkel diff --git a/.github/workflows/daily-deny.yml b/.github/workflows/daily-deny.yml new file mode 100644 index 0000000..460f4b5 --- /dev/null +++ b/.github/workflows/daily-deny.yml @@ -0,0 +1,27 @@ +name: Daily Deny Check + +on: + schedule: + - cron: "0 0 * * *" + +jobs: + deny: + name: Run cargo deny + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Advisory Cache + uses: actions/cache@v3 + with: + path: ~/.cargo/advisory-db + key: rust-advisory-db + + - name: Install cargo + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo deny + run: cargo install --locked cargo-deny + + - name: Run cargo deny + run: cargo deny -L error --all-features check diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..c0f327d --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,66 @@ +name: Lint + +on: + push: + branches: + - develop + pull_request: + workflow_dispatch: + +jobs: + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Get nightly version to use + id: nightly + run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT + + - name: Build Dependencies + uses: ./.github/actions/build-dependencies + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + rust-toolchain: ${{ steps.nightly.outputs.version }} + rust-components: clippy + + - name: Run Clippy + run: cargo clippy --all-features --all-targets -- -D warnings -A clippy::items_after_test_module + + deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Advisory Cache + uses: actions/cache@v3 + with: + path: ~/.cargo/advisory-db + key: rust-advisory-db + + - name: Install cargo + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo deny + run: cargo install --locked cargo-deny + + - name: Run cargo deny + run: cargo deny -L error --all-features check + + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Get nightly version to use + id: nightly + run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT + + - name: Install rustfmt + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ steps.nightly.outputs.version }} + components: rustfmt + + - name: Run rustfmt + run: cargo +${{ steps.nightly.outputs.version }} fmt -- --check diff --git a/.github/workflows/monthly-nightly-update.yml b/.github/workflows/monthly-nightly-update.yml new file mode 100644 index 0000000..daeb576 --- /dev/null +++ b/.github/workflows/monthly-nightly-update.yml @@ -0,0 +1,53 @@ +name: Monthly Nightly Update + +on: + schedule: + - cron: "0 0 1 * *" + +jobs: + update: + name: Update nightly + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + + - name: Write nightly version + run: echo $(date +"nightly-%Y-%m"-01) > .github/nightly-version + + - name: Create the commit + run: | + git config user.name "GitHub Actions" + git config user.email "<>" + + git checkout -b $(date +"nightly-%Y-%m") + + git add .github/nightly-version + git commit -m "Update nightly" + git push -u origin $(date +"nightly-%Y-%m") + + - name: Pull Request + uses: actions/github-script@v6 + with: + script: | + const { repo, owner } = context.repo; + + const result = await github.rest.pulls.create({ + title: (new Date()).toLocaleString( + false, + { month: "long", year: "numeric" } + ) + " - Rust Nightly Update", + owner, + repo, + head: "nightly-" + (new Date()).toISOString().split("-").splice(0, 2).join("-"), + base: "develop", + body: "PR auto-generated by a GitHub workflow." + }); + + github.rest.issues.addLabels({ + owner, + repo, + issue_number: result.data.number, + labels: ["improvement"] + }); diff --git a/.github/workflows/no-std.yml b/.github/workflows/no-std.yml new file mode 100644 index 0000000..3791eb3 --- /dev/null +++ b/.github/workflows/no-std.yml @@ -0,0 +1,37 @@ +name: no-std build + +on: + push: + branches: + - develop + paths: + - "common/**" + - "crypto/**" + - "coins/**" + - "tests/no-std/**" + + pull_request: + paths: + - "common/**" + - "crypto/**" + - "coins/**" + - "tests/no-std/**" + + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Build Dependencies + uses: ./.github/actions/build-dependencies + with: + github-token: ${{ inputs.github-token }} + + - name: Install RISC-V Toolchain + run: sudo apt install -y gcc-riscv64-unknown-elf gcc-multilib + + - name: Verify no-std builds + run: cd tests/no-std && CFLAGS=-I/usr/include cargo build --target riscv32imac-unknown-none-elf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4220b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +.vscode +.test-logs diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..87b50da --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,17 @@ +edition = "2021" +tab_spaces = 2 + +max_width = 100 +# Let the developer decide based on the 100 char line limit +use_small_heuristics = "Max" + +error_on_line_overflow = true +error_on_unformatted = true + +imports_granularity = "Crate" +reorder_imports = false +reorder_modules = false + +unstable_features = true +spaces_around_ranges = true +binop_separator = "Back" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a8ff9d2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing + +Contributions come in a variety of forms. Developing Serai, helping document it, +using its libraries in another project, using and testing it, and simply sharing +it are all valuable ways of contributing. + +This document will specifically focus on contributions to this repository in the +form of code and documentation. + +### Rules + +- Stable native Rust, nightly wasm and tools. +- `cargo fmt` must be used. +- `cargo clippy` must pass, except for the ignored rules (`type_complexity` and +`dead_code`). +- The CI must pass. + +- Only use uppercase variable names when relevant to cryptography. + +- Use a two-space ident when possible. +- Put a space after comment markers. +- Don't use multiple newlines between sections of code. +- Have a newline before EOF. + +### Guidelines + +- Sort inputs as core, std, third party, and then Serai. +- Comment code reasonably. +- Include tests for new features. +- Sign commits. + +### Submission + +All submissions should be through GitHub. Contributions to a crate will be +licensed according to the crate's existing license, with the crate's copyright +holders (distinct from authors) having the right to re-license the crate via a +unanimous decision. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3580c44 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1444 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" +dependencies = [ + "bech32", + "bitcoin-private", + "bitcoin_hashes", + "core2", + "hex_lit", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin-serai" +version = "0.3.0" +dependencies = [ + "bitcoin", + "flexible-transcript", + "hex", + "k256", + "modular-frost", + "rand_core", + "secp256k1", + "serde", + "serde_json", + "sha2", + "std-shims", + "thiserror", + "tokio", + "zeroize", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", + "core2", + "serde", +] + +[[package]] +name = "bitflags" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "ciphersuite" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae44ce6224d75ced726e5597265b8f334b9bf4767b8b42e058f2304005d8475" +dependencies = [ + "dalek-ff-group", + "digest", + "elliptic-curve", + "ff", + "flexible-transcript", + "group", + "k256", + "minimal-ed448", + "p256", + "rand_core", + "sha2", + "sha3", + "std-shims", + "subtle", + "zeroize", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "group", + "rand_core", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dalek-ff-group" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231684731518402967286b76bedadedfd6b0867f814e2cd41d4a82bedea2902a" +dependencies = [ + "crypto-bigint", + "curve25519-dalek", + "digest", + "ff", + "group", + "rand_core", + "rustversion", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dkg" +version = "0.5.1" +dependencies = [ + "chacha20", + "ciphersuite", + "dleq", + "flexible-transcript", + "multiexp", + "rand_core", + "schnorr-signatures", + "serde", + "std-shims", + "thiserror", + "zeroize", +] + +[[package]] +name = "dleq" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d3af08cb0a3253930b6b9629fd61202ea34da117940b59ab58f3e6f5e1ba6c" +dependencies = [ + "digest", + "ff", + "flexible-transcript", + "group", + "rand_core", + "rustversion", + "zeroize", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "elliptic-curve", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array 0.14.7", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "tap", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + +[[package]] +name = "flexible-transcript" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "329fdf3a39d36921c94b665764caa152fdf08f0ed631085ba4ee7e51899d93b4" +dependencies = [ + "blake2", + "digest", + "merlin", + "std-shims", + "subtle", + "zeroize", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "generic-array" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96512db27971c2c3eece70a1e106fbe6c87760234e31e8f7e5634912fe52794a" +dependencies = [ + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hrf-api" +version = "0.1.0" +dependencies = [ + "base64ct", + "bincode", + "bitcoin-serai", + "ciphersuite", + "flexible-transcript", + "hex", + "k256", + "modular-frost", + "rand_chacha", + "rand_core", + "serde", + "sha2", + "tiny-bip39", + "zeroize", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "minimal-ed448" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209ec7c2b5573660a3c5d0f64c90f8ff286171b15ad2234e7a3fbf8f26180be" +dependencies = [ + "crypto-bigint", + "ff", + "generic-array 1.1.0", + "group", + "rand_core", + "rustversion", + "subtle", + "zeroize", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "modular-frost" +version = "0.8.1" +dependencies = [ + "ciphersuite", + "dalek-ff-group", + "digest", + "dkg", + "dleq", + "flexible-transcript", + "hex", + "minimal-ed448", + "multiexp", + "rand_chacha", + "rand_core", + "schnorr-signatures", + "serde_json", + "subtle", + "thiserror", + "zeroize", +] + +[[package]] +name = "multiexp" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec2ce93a6f06ac6cae04c1da3f2a6a24fcfc1f0eb0b4e0f3d302f0df45326cb" +dependencies = [ + "ff", + "group", + "rand_core", + "rustversion", + "std-shims", + "zeroize", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", +] + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schnorr-signatures" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fe30c28740bba75dc4649a8985861784e8a44fd636706076edb7cb2aec5765" +dependencies = [ + "ciphersuite", + "flexible-transcript", + "multiexp", + "rand_core", + "std-shims", + "zeroize", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.7", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serai-no-std-tests" +version = "0.1.0" +dependencies = [ + "bitcoin-serai", + "dkg", + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.221" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341877e04a22458705eb4e131a1508483c877dca2792b3781d4e5d8a6019ec43" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.221" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c459bc0a14c840cb403fc14b148620de1e0778c96ecd6e0c8c3cacb6d8d00fe" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.221" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6185cf75117e20e62b1ff867b9518577271e58abe0037c40bb4794969355ab0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56177480b00303e689183f110b4e727bb4211d692c62d4fcd16d02be93077d40" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "std-shims" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fbb9ed849fede2765386134c64bc8984081e8c8408bc9201b99c6e3143ec5e7" +dependencies = [ + "hashbrown", + "rustversion", + "spin", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-bip39" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" +dependencies = [ + "anyhow", + "hmac", + "once_cell", + "pbkdf2", + "rand", + "rustc-hash", + "sha2", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7ca9483 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[workspace] +resolver = "2" +members = [ + "crypto/dkg", + "crypto/frost", + + "coins/bitcoin", + + "hrf", + + "tests/no-std", +] + +# Always compile Monero (and a variety of dependencies) with optimizations due +# to the extensive operations required for Bulletproofs +[profile.dev.package] +subtle = { opt-level = 3 } +curve25519-dalek = { opt-level = 3 } + +ff = { opt-level = 3 } +group = { opt-level = 3 } + +crypto-bigint = { opt-level = 3 } +dalek-ff-group = { opt-level = 3 } +minimal-ed448 = { opt-level = 3 } + +multiexp = { opt-level = 3 } + +[profile.release] +panic = "unwind" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..34f2feb --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +Serai crates are licensed under one of two licenses, either MIT or AGPL-3.0, +depending on the crate in question. Each crate declares their license in their +`Cargo.toml` and includes a `LICENSE` file detailing its status. Additionally, +a full copy of the AGPL-3.0 License is included in the root of this repository +as a reference text. This copy should be provided with any distribution of a +crate licensed under the AGPL-3.0, as per its terms. + +The GitHub actions (`.github/actions`) are licensed under the MIT license. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f3ad7bf --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Serai + +Serai is a new DEX, built from the ground up, initially planning on listing +Bitcoin, Ethereum, DAI, and Monero, offering a liquidity-pool-based trading +experience. Funds are stored in an economically secured threshold-multisig +wallet. + +[Getting Started](docs/Getting%20Started.md) + +### Layout + +- `audits`: Audits for various parts of Serai. + +- `docs`: Documentation on the Serai protocol. + +- `common`: Crates containing utilities common to a variety of areas under + Serai, none neatly fitting under another category. + +- `crypto`: A series of composable cryptographic libraries built around the + `ff`/`group` APIs, achieving a variety of tasks. These range from generic + infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as + needed for Bitcoin-Monero atomic swaps. + +- `coins`: Various coin libraries intended for usage in Serai yet also by the + wider community. This means they will always support the functionality Serai + needs, yet won't disadvantage other use cases when possible. + +- `message-queue`: An ordered message server so services can talk to each other, + even when the other is offline. + +- `processor`: A generic chain processor to process data for Serai and process + events from Serai, executing transactions as expected and needed. + +- `coordinator`: A service to manage processors and communicate over a P2P + network with other validators. + +- `substrate`: Substrate crates used to instantiate the Serai network. + +- `orchestration`: Dockerfiles and scripts to deploy a Serai node/test + environment. + +- `tests`: Tests for various crates. Generally, `crate/src/tests` is used, or + `crate/tests`, yet any tests requiring crates' binaries are placed here. + +### Security + +Serai hosts a bug bounty program via +[Immunefi](https://immunefi.com/bounty/serai/). For in-scope critical +vulnerabilities, we will reward whitehats with up to $30,000. + +Anything not in-scope should still be submitted through Immunefi, with rewards +issued at the discretion of the Immunefi program managers. + +### Links + +- [Twitter](https://twitter.com/SeraiDEX): https://twitter.com/SeraiDEX +- [Mastodon](https://cryptodon.lol/@serai): https://cryptodon.lol/@serai +- [Discord](https://discord.gg/mpEUtJR3vz): https://discord.gg/mpEUtJR3vz +- [Matrix](https://matrix.to/#/#serai:matrix.org): +https://matrix.to/#/#serai:matrix.org +- [Immunefi](https://immunefi.com/bounty/serai/): +https://immunefi.com/bounty/serai/ diff --git a/audits/Cypher Stack coins bitcoin August 2023/Audit.pdf b/audits/Cypher Stack coins bitcoin August 2023/Audit.pdf new file mode 100644 index 0000000000000000000000000000000000000000..de6944cbe350e535d1706819db6f52b825d3782f GIT binary patch literal 147514 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�b}ms(aT zdC=|>N%;Lj=2YY|pDQEK>Nv%81oe-9oQ%X6VdvOQaqa+_XTe$dM6(6#G`y;ss}JrxO2zHo~~JXkO$%PWw!-*j8lbxhFCl)$C|bV-#Rff@v=& zrN^s8(ai7-YH*#Nyn+!3=O}|Zr9Ek|wFWPzFKW)DHm;vBk>`HI%FZg((UWNU&v%?V z1tBX$-ITvmnu#6uux1F!)}sMc>I=Xd^=>95mfmEz!&(w`NzK6j|KJqdUWQyM=$P4+02 z=B(5p`KLIiR2K;twO;VkPFHIl!~;uB`a$Cd8#H-l;rBECCxen;u#UTZ&rBG;`4W%U zaNhK?fAMWwF65z@uzM#)Q`*L_*0QNot#4(eSLI`WemIQREd zE23|`5V5faDD@r31dzoP2o-60fLXG&ZYR{i#Pwu-Lg#E{l>b(V_~bPfaD=hKb3WWw z0pcSMN4sw763D%8uht;zEFojJL5jRhGuYh?Hyx!HB3Mmx7$W4}^{aExZ(#ec%aNo% z@BQO_hX2U{Bn3TfuLLxx`MSo2vM@F}(|$_JOrGm4Wyl#La?uPj!7AW(T$T@PHn^V_ zH5$$PEo0y{ZG$^w-=a1O6OW0c#v zbRne_uz?{43MZhJb^4On#@-am)64YIq5NfU9&$R{#UEDAETHe%!KVlWL!v?UGA&C& z*)n0Hzfj+8#<8P_Qf@6tpG8HYw|!SP6HJCL z4aydlp|5Hl)vWU;onGOwbaL)Mc?~8V$7r|YV5Z-p;m&*6d;w_+pma`!a1_654YR_3 z{SW>j2zW^fqcfu~Q?mb(NpNRyyGl2|5>*Y7>CYOVkHt^3^64sfQFISB;iEzDLg`&_ z3*6I8IjA#U*3LCmVl&F@Hl-qiGt7<7(_prkC?)oV#vGc`KXs^?g-Y$k0mM)aBb(O| zMcZ=}2&1rg`|pA<#1`b7`qaS=y{@}EO*aJ!^7c^Aq=fPeNajB)wM`AL$VdM5@-oID z2JkzUHC)G5qxwDx;)VAtn~DTQ;b1T3GXz0?<)tliB=7s6DCq$XeU;B>G zoG(>1n5g5j=yY;QjakyDxu^vuo(Slfk`jL+qUXeyrz;K?l!nNlG59(<Z45zB!4$7LtdwzYIBN;gsS>;zFHd}(_l*_Gd=|0LiF{=C%)F9QtUy* z`v4gTQ9_LsBxtQ?Gpw2zRWyX7GJhS?d?y9n&~ExLd|A2SYoHNS<`$Q=EB2|0( z0epDo626rc`G=K?>BnVgvgSl{Cs!eE#-Id<;$}*Pw1De9x^vF_z!pl$F9V7&7L#{` z5oP5H#B|7qKXht08~i-D-7blLMb!$r0|<9^pC*uv(Dr+Z+XOh6PD^E&Q{5bL$(p58pbnXH|$eFjQz0O zolLJA%}DkC?i$pJiTh+7lSp~XgY(9*5{|{qpCbT-E}#r6_5(&k!rvT^frd4@5sO~A zSUE??xSS>U)@_!Lv%ItF`^CYCmckU^Kb57eKVb+zr9u;%boC$nM$g{cf)v}&;KRc= zpkxP>Q%}pOyz)y2a-A|cktuSQD?6?Xd!ndn#zkM;yW*EC6^4AGRnA|+=9RyE6-y0j zN=};U=K+KSyUu)l@~L z=UfV}C)cR@j2)N5PKFCXJSu`9;Jg*LE9#BR)NHF>di#vdS=CsJ)$n$3(jE2-Z&QiT zGIee;^ZR<1Yx%D+os|dOY|MNirD`-erQyKf7J5}4HT*fKXYFU|g*Bf(D5Gj%LAEGs zYLrN(`tu5qH7v5$J)`hyV4gPJ+PF*^)m%{hkcZ&FVw!6*LmgYi@~^x*=-_T?e)2vo z@{b<}gvVwADtIy3tyc&mi*gS)kX~pyv99g^_>RQ!(wzV~TAHmed36cIi)iBgfT2+m){@J-5-jy&wxs8U{&X_eXW>j`dC;`RN&LAs3smRr1U+`)>B4JtDs=%^1j1 z!<)fc#RTLZd{ji?PzOg*>r0L!50mF9yQUxCaa3UojMp6-Jnziu)=QYoau9#ZLPoDO zP{mIuP#Tw!aMrz6Md0*nG7|ViNymU0z#^+e({hr<_J@ph4_corV*#e z)^t=bqQ^@^=+`?6kU&*~nXajb700Zg89^5SfpEfWQ<-v}7J`fPyj?8KOCgE#dv*tJkwc2#&!STFlmgf zMPj-FzVd6QnUEbK(^rGc@58Q|Zu3YK=IqO3EAO(^(Xkcyqu-K#o8+gS;TdHS!4NMp zm7xdJf#a`sjd<1Pd>mdI!bk?j$7JW=Jv(Q`)$Ue%ARRP}4i?rTG$k$r^>EZhlp{U;!PI*7Z4d~qTPmmQl6?$_iAuz-UFg?5_ECN9WQG`be}|0GxwQWy71ernc(7M&qqLs<+vQvo*Ucimxtf zF}QOUrk5Jwh0gC)h)`?Bh1e?t{-^-|#=LwE$?E;5#E&wO&89K)cV2j%o0M>erYShA z%xlE{`n#NC0ggsXYAPa zQr>GjIhMm8E%=f+y@l(z@pTkpDaZR}02-%h$`J&A#v8QAibKJdgnCfGnf_NLJko&- zp&p8xQU(jE?THdScKZsn!5`Ibc4v}AB%-R+8pZ4!$BTrQwTx?iYHikARt{m8U7E7y z{xTPj(iIxWxZFa&`j8>oqrYk0D!fHj_G{4JT6S%uv2N%0lW(8Z5LpL6P|~z0)D+lT zp1_6xz3dC6I;Lg7W&pd=p58ew4bPxq@f(`jONv zRLmDQz8e^iIA5gQL1`0?c?grsmeOukYL7Qmt{zH>UIAB>;nK<#Hk$G3SfJqS?~5hc z)u-zw=rc?-Uenp+0%lwhsLkgC3Rd0LlKGcRCQ5)W`wH+DSz|W!MU6!Ys;X**NZHa*mVFli z!mOJ1SnTmbZ(o}$Gvefy1!+nVH_RU-`gfGfGj73uRfq$+XrrQMa+E3$gHccQQ(VMK z+b#Bqxy}WH!~`dwNQ!z+DqTlkLk^<*-tJt7t-}17M^+aF3*#Lbq^qp{lvjS#u346% zfpQXdW&q>dpu1^;dtAObI7`qB%*LE`(6=qkXZs%9ajG~Hg6rG!^z9jOBSsdxy8R$Z zK5Fi)@a;Hby}S)eWhSv!{N+m+p$ zGTV9YHBx>rveP(jCBl@*qZ8EB^oTIlwUElrn>Ab`EiAww)yUp)Q-95!o0$ z2lIZpG>ZbqUY@X5&h|$IIx%3-36;9fkOoU(GTX)?Z=$OD5jug9c40j%_C?VULp(u1t5t2fj0UG!@da>)3FD605sPl)J44~uty*)=@B7Kw82 z*t=uYB$fbfjLQ59F3-b>cb_u-jzHsT1)>pw(U~z%iEqi=3BZeK%vAytdK*@^GMLU$ zv@e)A8|z*58#CnwS89nv?jkyqj8SW+-G-by+0R919Rm#dS0!=Ilm0}G{{BxHIdbJ1 zu{T^WcePF3h+KblNvlraC)RF0m0YbE#Yqp&(tfp7U+m7MrNmAYRk}L@NlMNbpjI>8 zYV5_D!IyP-LbwG$8chH!y4KoMU+2+S(G1cX$qxLN&J#L{?Q?~*+{F&@sb)*cWA2X1 zjDqh(*p`$#wqfJKy(dauS!>8P*EdSunYpMa$Z{CbgQGY^uUeucteDM=(bnrqhDk+~ za92wIIh|He)kZx8O&-NAu+!aI`J%9CbsY61rMy2#o*kB0eZPNcw4$7rrZ~!st||Mj(y&-tU{Pa~ zfJvX1;3?2+oP4oIwPR1$!5grc%I3FfTV5IGPYui;pAWTtO1WkNBCcHpN-%FvuI2r| zt{RE$bICIB8)zG++pNhc@8d^nF*Ph*ctz``*-?3OF|Y#PLBUKpHdZyQ`HVf{yaMJT{2YFVFJ5} z$y9MfeI$!ciphK?V~~@@oGC4(CUvezRW&V*!Hf>6DD+)TMY);EbUY1VF{tM~zVzv* z?&7D;Meii%#OwOREC0mr=3`m&c)v`+$nld%2cCG2!tl=2@aXhfo)Cl=02Don!i0@| zT^|VN_h7=xON38wOn8`&sLmTfF6g(y3JmRfNEk?M3K|4Vh?xjAV#(DEhT?-0HPCnv|au$dTduPZE2OP%=3aSDt?YY40nfp8~G^b3;!i*r7x zuwuR*6K;1V=q(wT173x&=#DUudyNrK_|-BB(MXUtuoiyn5;u?qLzuiDJ{gFQv4txz zkj$5XBb84dQk*==%9WcULq=r4h!lv?h#f4)4zE6l@i%?}QH&E|?DR>&TeBkeRM}f` zVC0HbUs@yn+#FQ-@xh`OqekE8z0vb)%-=iaQ}#_lSQ>dp#t_KYjJxk@BYaVb7d~7L zx4%M=5+S1gO&N>>hH>%zh|W&Gc-Ap-|aX}4ZM zuJ?2;i6t>TidjU(oVj%SL`MUQ49P?M854cADZ#*aiBQqrTy`~JL+3+()`7sHx3BD%jKybd89&{p8L{xkHJi+|%cHy?)l zmh=Oeqrk=lL7qp%%uEnms#DXoyj8_`CCGG#Oa_%D&ldAI(-|yOft|}s3WtAfNkf4+ zq62FVnA?I-7+9SD7hp&JAvzIo@amcRBlWPu(}>o%j^cDB)fX^i)z8S|3;p|Eol=l( zcT6ZX@DqL;NDg`x$+&M|cc<7L1iKO(8oBSgCy{Xn9X=S7zCLg!Yj>n%(?r+87W20* zr?2OyIq*9e?{^y}HN7{Yx6iGTw0sDmDo<~%W$+wVj8h_#lz8CC!TbP|XNr_4GL$H? zA{%o-Jrpu1F8Uti9BQCE26zT8zWJx5Tnum=`*l`5Bp7cWkwB(s;K8>kHVEzw1s-PC>!VMw{+>uTA zv$QFD^X8ur_nP4a4f-?_9^op|yD4Bk#vrsT69 zafb^Lv4?s6p5TNvD+9HoD~muz5Omm-4giuAEw(HAc_^$quhfmcO_^5K%N?sr5&yAI&&XAi>@evkT;@y?AAlC-gszm^&qc1vyRUyj*k zf`BiC@>SZMu{KUKj59U1KP{qLYlnA=I-`V9h%E7_OE>Q!J+2%%euj$o^i_t(*F!6X z^W{1cEbZu6OD)4>4(G@4#mrdV0?l0=wNFcHG*=Qvb}U~UWqX;RGRfWi=4RbfLtYo? zkGXdphl`AjgR({r8I3iXH44dzS@W1U#?vaRaxUnZ6z^GA(;jNXTUp)L4)d|-lS|D{ zG}7VOZlj!oaQ8N|vzsb@v?8W|e};FH|L8*9)H=AF&Cp?Bvr02eq>FXQrb=SKR9+wh zCWxg7RvZ>t>T2(F$twmX_%!Hfi=QRGyIOq&y)%P_$1TNm`km@N#X9jRdlTT+-7?US z_VGNu0aE$KG)4f|llN*4scqU)?R<1_czh2FQR9h})h8;&mo~}8b6<~=JDIQZpraJD zXD|$^ek{Okoab@FYr^`x+~c;EeVr^|hRi2mj$7Zo)kvQ2t0l>Hm2I?eX7WdJ#`_-7 zdx?bQGqer&2Myh#ZJp-!WqV?U(@<*N zqk`>|n#;CA%p<%mORblE^*VD+Wz5C{r~=Q&G_>9Iv4x9CmaYPRV<$Rx_GfF7^RuyT z;>y>{o*NFl(sJ}`-Mztrw5q%|=Jvk_R@=2?E=(Rh&XOg+y_;u*83R|>v3F2P%NzGi zBOX(&gE(3`zC)f7V+(T9%^xw`OCGN#-$fUa)N3r9vYV+k&chGZdV3pf1)p#gxrJ03E>dzKH>Y>s z_#)j@%hvG3@f9Yj)i4Z+ug9un*`p3kn&TO%%Wv=7kZx)`W923R+RG>%TDp(7ce1eDZCjn_o4n1 z7G1;M0b&~}JAfi&40U*qUw%$Y8=uVY^0QORO`#p^^Lg5>$4;PFTAOwsr<;7AFD(-3 zF;S2wYi82s<=T5(_CNb+*JnRX`-}UvB~|m_v~H7$2+ADFQc7~rLARMNSZJg!oeZQD z!79P>5Eu66%P$pIGd?G)jdDy8ClvpB+jBk5 zVb)1vWGB1Kk@{%FHgxgv^ga$qfPNiU8Pm?E10FF?(ZqZlJaD$1mGhF+?Khq62zk`; zR6O*YOimjn(#5ZFt~Ue4`53gsk65}v;4SrAs2?I?ad~}Po*uo{4Y@epYg*FV)wyqxS1dAl zyz((7JK*g&Ep=PZf}*#whT@&ovW1FW(-lmIZY}jA4fgmc=qlEF_J3!WbP}1julSHe z*l|ONmzq=Thsl_W)o%JS{Ny!STi#Uq&rZ@6U1Q(1&ZHF%U6M*{2+Ga?BUiT$yO^l` zscX4ZI?&G+%0HmziCzw-L$4lZa_IM$4)`2*k^g9hNLA`tl?sD-wxf|f?YblCsTQUt zbB{mLm_$pJAl7{`|L_*TQ;I7p_2i;k-nnc`>>#|enPfL3v*^C{3aCrLhiT2L@L`b9 zxgD5qi8^_P?WYAbP& z0H})y=H^>jH&&%_wp)A;x|q6A#o0exW{hskN^W^wjARU*g3!1I4t2FS5qVq3n6ZT25T<|Mp+A&v zeubx$Ik!-NjI3ALCCSJB5QMb*J?TsQpv6%$Z_x^P!iCl182CHC^ZR}gZ1&-C4yVf8 zJ7PycQYOF+r>dP#jBSz9@fFX@VwX&$n|JnL(&CW2q9saT(e3&*1Mod)?DTZy$5(B# zJi<~WMOU;f@c{Ew_BOML_*NbYdL(5T+pFZixG=e3v=*Jxu9`z0PL;|Dq z-`Wti|JH_Zaxnj=+h8VS<7DFe&+h-WA?#f2|4Z{A{C^}*yZ*!E>ov7J(#w@X@e&i} zhER)qP3e-BZ5dBz}WO1wI- zki|3M#tIcAGzK80%sWz|JE;6NzT>q+MgDts4jDKMT%SU#sf`W>G9*Zt`GcrrsU--0 z9##+DEEbqeNs22nUL526EJ|{TSO04A{eZC>di^(kt_qfT`-A zYUYV?sMsMRO8Ey>;wg#mi9HFH5sQ*YrUl|eaUW*qb?bLoLqhE%7PE9%q2-K>GA!4aLG$HJ-!0*ZM zU79t%&(6U9i@Q>oSp6*jyt3aR1vc_8GO|k00fqAl^3sTU;zo&y0UVgU5iJgJdCs7c@=gyjLfbE<+%#|VkQqJ}?&Qe6mPPD2>)1e2EkV)y)o z7zv?&2Ga(L@*3Yo5J5$+z7dD+ApbF(hstq|et**d$=CnDNCOqZ{|I(Pi{bI+;O+UV zhqMxX2fbcje}Uh@aeZ;Y--sTBe~!lON-HkqeR{B;>)ZMG&iHS=erMmki+!;HGxqfL zPCy)1_J4BSp!RQnQ>!D(kM6mD<|V)WW{g#c9@MA0E&cp=1I$;n9^T?rpJh-Vcm?lC z9f&{tf`BsY#rqTd4vP0DJG@tkf%9GcvDXCLS*+-8_I$Q`)cRWB^#lh#+`G>(*|>-@ zUFUYYVeh>g88WN$)+R3<4Dca7gx)^Rp^Dz>`6X!j_64|`8jFiB*>4~Fk_scM?@Frh zF2N9dq3wMq6`Rn*tSqCbZ_Gft$1-Shs|%TTLd#n8c{pxq=3H3$2C3P|U-Xr6jL4H3 zGxc>NH5Jm$ZSAmlNdb+$oVYMWvKr_KPcWHMul=oz@{2=I3Hx`lG?B<)4E9z!+;C3{ zr(1LA*Pj-Art!09d2nH{h;q`FVKHp;>FQ*$(4iYTrK-#o&_2Z3R&>>40w$k8?WcZV zANF*2ulkS=jL?X|f6l>D05RLe*}rgg!Axqr;U?$d;5(#o%>e)_Ej2 zY{Ah~zr?j^YeIKT9413Gn~Him|EKE$J1>__mDr9rLmiC_A{OHa~svw}?WG+3IUje$0Ad&>CBM zB6Wrkq)Fq1&?;fuMD;b~oiNT{@~ON9o8N-FgZiV(2n>$i4;I}q{2Z$>#+P`Bh5)2? z8@9T>+XFy~u|L;Gejb9IXbrGtz)+0NGJzzruN4W7Qkh%zq)YVCR1Sfy6O4=qN3uv+ zRYyKX?n-)(RGX+tAhW`wDB?OAqUnTX%AG*Z>jp5-=xRmaDx>(KWh<&$W1&0eV#h=Yv5#8v_dm07YD|lL z&2U5EtJl~mbPMgvJIOho9MT2aO*|_2TwAP6+{&bTlQEcmn&)p20z-3Id;{}cbQro~ zpHq#}-+$AxTF!JTHg}GfEA*b0rLgFXcN7Rj9sKE%*lOc3hBSsG+vDvj8~OPhs0#4O z>5mkjlzlKg?VEy(ZEK@klJjpkJyvlIOV+QA@W1vz^G{mREQ3q%aGx)((xt5Oz`Vs) zWx#8w>TVZjR=_#5J}Je!p9R-1sfotEuoBQk(<-JNeVm?j^3O*=)@O7rU*dtSsKhwhyYZ}E^6p~4|&Rrl6k zl-oFq>-N~=sNHdxm(1?D7M)svUsPXn(>pXq+5bqbXKb2U{T^5rbGhlx8Eh0w0!My^ zTDwFlI*)!-b5aIl#t~hISkAA$`&VPWPZ~rN?R*bwa$e%42lNwVOh>J7*DK7H*?&Kf z%%K%5?R3*1+3fMShaLAw)ibCLanM(z;Ht2cudOh}QwjbUU6)>EPW5mz^f&^u$UGOH za_tYXeYh_iX_kz%3hX|Pf3MZv%03)&wwk*73}r&l4lvlaBp4+{i7e;Y`5#-|Ik+8n zeMW8ANuhC_4jy3s-d<*bfTmGRG_Qb0ko*ckkK>soryJ+TN+`LjMI}mbM9@YP2dH3H zT<5*+sAnH?z>Uq+6?r-i?}prTYS}Psmuieo+t#-JqN-DPF7L!Ks>fY=;uyTqp#VHX zE;Jf?{2gvEN`rugW1>VWI@vKGn^VeqsbH*=A5$zO6v7Oc}7N`64ywot16kDfycrVkz;=P(OOfKcn z2`$G&bf*OsB_G@kZ3~rA<s)m~KeI2;`2>D-zB~D}f9Dsuw$L~EcjAwa%uLjf&F9Mk0-bA9G&9AH zLf065m)U*#UI=CvcQsK@SCR><%oqo9b*{UB2NyjR73-R`PzwXwk#TRvJxtu-77nDDLy0L!Ias8wSTm|O)(;LzuslDuwqIfdrLZjn6dJM zYfhtF7vF1kmAj`as&vGrOm$eX@LaHYtQTzc z)v>)$JKw|loX`l^&F1W2x>0NyS&0D9-V{=R;AE?%yJnz8AXdJw^PcmKwYj&`QcwMk(hu(N&ZRRa@Ss+vi})VI#!ZP9hdo+@imOs&p{IoTFbNI@>Qq>zZhy-H7ePbZ>Hz zu^O_j7gTCn&P zp6+@s{m6_Hk2$lhxu}*>1oD584TVTss{Z{@sjP2t_43dzf7ewoqF!Wroo&W^H$xW3 zl%9Rm1{%BC-(y-792sj)&JHaZVdEoLZf8!}K5w6&q**ojRtrVet^(H>UK+Mysg_Y* z8Mlo%qnaqGl8UU|lv(9#b&#$#tt_uk%dGMfhRE~n968n~4K~jl>jpny>%g6V96Nv- zKP=3hT(z=!V7_K^VGGYIQDC_2XT9-qnGg0_#V;wx!z1?f7C^&~629p5L>kpk!&(=M zL?<#1_xEOK5wO*+o>p)?s9~+xGmO(U_0iI!IrApA_bQuz5DSe}*qQ$Y*~ITnF+^hx z)SjC?JUZ+!Mg#n6kpQbD1(>+uvgmD{aU z`^`J|xW?nF`y@LW!G00yZD|jfpwE^qmGSAQiiFE1%w%fVxI$^k5vjE4@zD?Gj=mgx zq%6SAQtNhgfkC`b>LEjx7d}~)INFzi8By@C#6WOAG#X-JauREE+2n~U3 z)L<;4YkwSd7i!@_aY|EUoqEs2hJ*v~q-oUI$$B_+8^1BrFCA)bBXO;md`n5EWd+4e zg&qmhbLXi45N?ik1Vy_V4vbJoe(ilwuzA6Ohr?&dX_jt&+V6O&bEp?AgLgcKmZt`z znuH!fFTQF>RR06~N%Nv)pz3p@lw6Rc)jMncinoJB5$>*viin+Y>c-)4aZ;5kV^$6#jiX?%X0g{Fu~cxq%&t>}t`o1)0Q)xMD+ z-stayQcgr|y}@n~qao2INoQ_df+u0}MYi8nE_$%>(7F?A3YU&^{s0E*M9~;f-otc_ z0!4ms5sMqh7w!CMJORGv^%B0MC(lOXr6pV0HA*aQ?Y>01O&C?doYo|9l<$$bw ze3)V9mtRw7ivTG&z-^$rQk>=p68n%2j)5gYvM%yWN**A9n@fO*11lD-c-h3Bb&W=rxI@j-p&Rx?l zpO@gNN~@OBqz#&sJh;{3BQ?YsMY;_)VND@;vGvFV*|-{iPFP+RBH> zKZ$bOMPPbT`v<2YvvAsF;CV0R?>5aMued1w50)^dG%22`RG*~e1qsEn4%t4hR)v?Z zgda2mp2DrG$XG}=siF4@8#cRT*6G4KmC?nsKr6O2GzU5{{p^V|J{YJo(l4)~)EC-u z`9KY1lMqT9P8TBSLjtca0N+8#Tz(nOwBQ>(QJg%Dro-CvAkawJw|9g zB{H&9hedmra1}x*nYj~b4T>1Pg#)5aUWhA5r1NR2xYC?_(`wCSI`0*#AzlXwIZsga z+tt}ovvHV8>fBCWdNVE@v6PYM!SqGwWU!e0oSka>3fI*KGDk zL$RA}?&E&W`&JGwgBxx~+Hq}ASWB5dtCeSV(%n~F?psMIT(Oa1-YPd*+wi45mw>tk z`IZGuS~e>MloQiI@uFWAh&Q(2OX7$+@Xd(dj(N4(*)Br0JB-mUyv?^sBQugEvLbgx zvilHLGlgJoNw->d6u~0FsA?!7utC{xz|goVYR!~`uV<9vNxv}8D$Yha`5E@YuL%>h z#KPIdl;*-%FDMNUxZ#^^rad@QhXKo6$HY6kR^4tKZ=zmNWwr;WCG4RM)(wAHmrp?b zFx$mm<&?j58`7-EZG-4_aRXP)NPD0U>QbeX^#D_vhFTS7VA+)rTr&t8UH0{VlQ2tQDv3l8Z>+J&0suw8Wx*8@l=M-7@JNoH#uMNGqH$5r zelefi7nLa)E-GL&1yoT#g;0bt7#7M~$BeW*&J0Z9IevGGf5}v@d8>O>8=nkThuxsA zFl>JOO^Iwrr4tDg>zvOx2r(sl6vvuY8U!dbOC|SkrwB$qI!aN zf~IyAD|iTy|QFEvwqH zmaaS8-eDUZFyt3T)aYKuy#_h;u*!sG_w03>wUQm@W zJ0d3Id)wMW#YNZ8sPS7Q$4rVd)G4@L_oA3Xm-g*ME?#EWQTyj|1w6GB=)Ltj2Wj9G z^3avATtIQqM{?As*`d^LY9O^w$_hCVt);NOPi>CfKzV7|JnmYM3?-Jn-(Yb(sNCn{ z*&uCMU1ca~ZSHg%+zqBT01$w0lC|Y-t_6hSrE(Bq@}~O}iYXedl7kG z@u1veK}!e@;halWq{v+Ee4*2_Yx>uCMZX96-t;NDE^+ zffie;sz=tl`UXt5>2mSyEJw+r*s?&s=b%DPW-?V9GYJ5Sf86GmBektI zX_A(FeP7U!jLoP_00W3)nSv|=*hTLv=G*PTLF?yyayt1cPH?X5MGwhd3MOqxI+H-K23^c}1_!2oEa4#`8=pHb0p!z@p6B|!zDraK)2|>Lp8!42cdC*}uorF02T+iPC}r3YBfJq4m`u1Ba~%^O zA0HzhKxh~(tL&Ye9}HOd7cf+`U;x^1)QPxL01O55F9$-@fl$ajDkVe>umMA)kP@L9 z!JvI0P1yiQk>T5}64vt|AXdBqsBT86*l{5+?K(12Y9*jGT6tg?NJS<{gyq&E(agMD zxJZY$Pq*s;jabBer{o4)I(5e*BT=@3#XwASBtr1SfwIVxE_)r7AmCatjVei9>ND`- zvnODT?(iBxSgA)F@E}p>BSZ&7!+RBQ;C(_66cJA77DFeZjv?d;L(;>a`)>mWsDhnv zx-yj;TwA1|MPUOn(&PS+9ji#hdF`fvQK6_GWZ+1{P@j5&g^z+CMW!%fEdS`G+Iq%0 zmfXqp5j2HV1cQPpaj|qv@!|ma({9v*1Ji#8C<9TejihF0nLuB&yuy-ub;zW1We+Dp zS^hHfu>zVxLh1J;2)}ZE@v<{o1Y84NzFk=C?MnMCKRZHvFwH9l_9CN8K{*XM~F`5(B zJ`C|Ld1$g}qA4Zjb}nf533Ij;_HyW@JU1d+c^lTr=Bg^R8KduKbsKpDan&T<+b!0g zy|6y>fzULKa3QyyntdX_zR9gO4|k{eJayka(imEl;!E59gLBN!E*Y>aj zZdI?2cT-H4mCU*zfJUaRT#?{S^!wUbuf%-I#z{toc-DFK75=%?k( zOd)n(ipgiXCGqXK~KU*kSgE~x#IySq~3sv8gVO)Qm3=zp3{V2YQ91 z!}idhHFTNx8nHgV0HybwH3W|r_r0-nV3N9c>+n|3VVg`&V@mU}3Ui_rKBxyzTE^on z(6yAFv!B_@rsu_abYJHBz(ha*%y6Z+`mP(!|BR1fZ`X?RG1WYG{qX^(S4J>Y{bvk>x-tAwsM-Jp>NJOoF^0kV^9RF>_aM!3$R5|5czWdZ! zUhDNmY_)HAyFB^RV{K#GgR$FJj@9?qzR<#E%S}%q=ZT*!)tk-HjBf$bpH;3C>zOPx7I>G^IrhU*yhzvPH`5qnZb3dXr-t^h;(y%B z?4?D^lIe5!xSE>AlqbH(mMK`)C1{Qv1K!iRiY;FxyLTN!4P7?9tDmn+79*Yf_Mg@= zRaOrfP1Cizm7u$aDaNev^Pd&o`a4(lb(iwgexhSB`#ha4Gu04>e%~@eFmc!~HJS74iA8Ge)roZPUZ5pfk4*-NKQDZWvBIa#Ixsz5m+h8RlSs&QfB3)JZ$hl%Zqi3O z9=@zQOC|NU?*$(33{u3+{jAaa4)W|X8qbsmMFR1|-D~(r%=Kd!N}@YVDs*phpWj3a z4^sh%5VfOi8RX$=HtF?LT6pG~h|McJ$n4Hebu7?hp6@biphadcFVF@BZjwj7QP6-? znf2Huhm}80i)p&_=Zhm3mH6ynx}DWhxRhB*YgL^5-rcJmJ>w54?ZmBm4n@!8y0IA* z`kfyk-;~bj(0OeD5Y3Fp`S`@s-R<4r>`o0^-+&>i>M~ndFVvD24dV!Hzxk7REB^z> z)qS^@b%sl_4JJ?tHQTVqUqyrrZ`;0&`~5HUP-P~w{$2E_l(MiYc5mq3TK*8o=ok+F zNfrM&CkXA0te|;$|4aIq2>%yVWc!cy6($bm|FMhSR>_w$`3+l~rG*8zgxK8XeD#2J z;dX{mIE(R+Mc>pYoaNG|%ftHE*Rz`?gN{IiofTeevVkH|>rzTn^IXH0V1bGUEVwMXfe>Pn5frCJ6|zL3F{B`dHeE2GD0MO+GZ+L> zI;t|^0p@MJaDjvB(4?(HdsIvb6=oPP5@;}`C^V>?0pc!2AI#i8QyVY?W~gHUD%iM~ zk|^vaB2)CgsTLvhV4w>bun}wup-kq1Z7O&^G$sY>K%s-|uTMS?yWBuv-o5_%BgX*5 z6d(b60FU$#kM&v|W*VJsD-5D_f=U~DS0a|>YB6f=oH5}rEf#8Wjg9aDyVIM(KA15@t5i?Q81o8x8 z`TftmpYbcAa14T-MbikE1-S45Ujmp0atI=JWl@MyjM$_HW!F8!cO6*5q}sb-3_&JA zib0vm+ot3+_9UpS$ZMdET;0p|CmHdPn2oqR2{I#s=RN0A(L1zLP0&jG|Vy z)T5+W;%OScOw>DkG~B1;oFDnP?_FMdb7_+WEDm7Kkf|<4p^7kt$N5u3PIACT6+(^z z5l_y%i>UwnQvB=6rjddFrEr7e`l1`BEB#!a#%_yP8N%7S(@cSZrav}D%rNTxw(QB& z%3RiK-Fm)fJ%K+bsn~Mx9VNACEWJTm^)e~x(~`Zr-?Hq3+jwDC=V$Qbr*$sxS>GsH7-6?1b3xdz^D8!?_~WooAGW&PTbJNd__yOw2s zBiBS<*3s!*jIYjKny}>Y&TsiCx#`yLDYN)~I^x?zTup$L*fndBS19M^PqLEw2xuyj zzvQ$fA$l3^@xlJuXdJ5WtKs_6I(Rhoxqf)xRHZ{9NQK{>`Bo3U{+Kq^sb*b90Ht$r z;}QpOYGu6!%gk=(;Wi0S(PDdBl$d=nc_)`!HN}4$I_L?ee2=f~cb$Rlj;E!{V9_e0 z6G-{|NZRT#$(@<6qjz;EuGy=+9V(nZ6r^q5e}K8pUX}`Pd2DzI&@6YFomBkPEGsIl zNnV)#$p#L7S~OXFQt3X|UkOcyk=Bu96v(A>wS*3kf%*2!9E(JkxEAPi%5}eBJ*OmS2#|L|2 z>qSgBD@Lt*&bDs=6YJah;0$+}w+(P?(?1-KH{Tta{UvhP;O|4q!vk`LezGN9{wfbm zME*2fuh(qKNBGLG&%7At>PZh>PflC2{!6t$`>AfEj-p9_U>JV_i2tCPT>pvU_J31N zR`&nP0{Y*3Nj7G7*8g!Y>157|YqjJWPslxA1X~z`SR}f1N`^^aBo@iUf;k1tf-+z9 zB1bGrF&~MH#8xPJ(=Gmp*o~sc@zVXGyQ{V9^_|@~%YSj%m^V0C94mB4bUgzZ&nPLX z^2?yiE*}ON4GlcvcSO?NeT4y%0xxo~TIc~1GY|?Gu0e#uuYe&SqryT$X1xIDk#)SN zrp8fFFoPg}!GL-c)L2oXdLZLylo2$jMu@TYjt=UMgWM>CJemj{4lH6spe{ZTgD4ka zAeG0*>83txAYiyUBtg(Xaf1Rmpsy2lIY~@G2q=+!M@XRnx?eC3?-am&j24{t_5jWV z0g$;GNZN0$bUO54kkbmGjAKOw>d+`n60=pfyz5eOZw z3J8H>>$=SZ+?_x~6d?lwpnn~G;>PuXxG*_@p~Eru<>#OJYbu~FjDP)1e&~mx<9l5N zBX3-9aLxM@lFH$og#}!Y+^wo@3g^_&y$5Fze!pSpg5kd5KFJFlY{Xpyes?_SbeBX_pgNldhgZ2 z_&rLz4Rl;c?4K8Yx_h%bcK@R>F@eb;Z#YL{8Bo`T2?>-K?*kc+o_7E3|92qQxE_-A zJ8plq^E2y*ixg}C4wxhhcd*X{6fdzN7{+JG=@+y;6x^dG4pdAa$GJ;_4BQ{ zOs~cZVO0hL<=V%W&EH=88S5+GzhNVUlxiK`#O=a#qZ;x%cc>e0$uqfMDXv=hR?V)9 z>EPg?yIb&^!xZv>_X|*O(w&kYTHk|23i(Rl-%T^Mwmo7j7(7fq0V{}2e@kUNy`%zP ziByDH3<#3DO&?xO3=BTJ}yB7PKps5`8s7Bosv9pTu|&z#U@ZBT=yoJ zC!HkIUF=SG$N)z8iLEa?OA(P%4!uLjdCq^~H%iY6@8)2IrGt;5c?|F$F^k_!uMH6K z#5E`%qTdnQIMqdoTMi*VkQ5IYUUk3rsC5?V4gVnp$L^IvYA;60GJe^s3HnWl0P+Sx zjpn{ymoiL|uURNqDEQ%ZTkD?-57W?nRisr8Cx|kb$Bp@wP1&xK>wpz`V=WRAfV+%}hvQBf+VLJ!!U&_UEdno32X$7jqOqbiR-@i-(Kh`-nJG z>f+PILU+VKqcEb+A=dY<;p7a4LWDi$w2brcK*K)#aOT6z7)Ed0GG%U>JiTRC;->>7 zE@fAHlG+rJf=M1YQ7gUImzy@qAl&yom+0U=xmTSc?sH=jMB-EGpH(5`7#Oqw9=Nfi zc;&AGn+i`u6s`tv;(%lL1bN=tKJbzJFx?yZM@yE4+m;$)acc){EtD4Uq5`q%OB)1n zUpadXHW;eXK-8(TEG7Ys?}{T+iME-K(cUVNI^XANI>7@H?Zoj2vFI>`)Rv2 z((i{>IaJG{|Gn#WZ@J4PrFE%9c2TUCC7qRo;ZSjcjqdF{#-WHo_82~Utps`MPi?gLH?EK4DiLPD5lE}cp1l|);e@si6 zTWs6nOB9xleWx=+pF0!2k!;E*8X4mKdo~h~sF#XMcniKFHq6{S4^33FFyPMc%tG^z zks%p^htK|e*~tgvk9xHX=x2QvqS)j0VsN>wX~-6-8ORwpRJeCf39-|e5j0E4xy1c$ zXaZv==uRTy4h`iV#kh2(+Z*$<*R+nyPObPnt{O&|tg|;!#62GG!%JFncVgL}5~yGw zN7f!xCK*PyTpL}ZF3UK<3?T@a2!HihdW%s>DJ>2K;7cCWbD7y7OES}^47K;VNvHEp;!{;Jb4umTXeieNdfrt-IJM^H0^BHEOulk4+gM>MKcX`$K63F$)kgQ(b`|FL zk6f_3c&v;bKJE8!>!WC;Vzhf zP+02r+w^6$-ds|p!Kry2c?RnxTLN_+hQXJB9~+D9l0Dbu)vXdk`BXI`zcs^xd6KA< zyv+~gZffF3BsH_j>_3icZ{W>AE4RUhr9_jSs>`>`ui_nOdqd&!d*JT!zr=fbP(|LNFy)PGEg10(2Npl*Pr1yF3EdK0UeJYNbC zq_V0U0$??jGRnodUls|KC&H96imNuEFAh?c-uG*t-#_qmETQ2-WzQ=<{>9{P53{x3 z3EWmeoFM$h=NWzSsXTU%g!=vGjLIbB6yGavwevM_=u90;wfA@tVlszglAP^cyE;;2m>ZGvy7BMPX<#Zz3qCe zkEC;0HK$SjyHw`=T)j$=0ydm01o4Nwt5-|QXN#h_w@xho zFRkM!+Ne^|k=*wp%N6QLXB~0+KC~J*d932emxE%tF7$?>o$TcpsD5@ts8ICH_D0(0 zs}*%%5FDZ+5;A;^P_sh&mEBe@Mi?q0Rp))`8^NA1eoNo|oje~2w{-%rNN~EPF|Qjq zkv9T56RvF0t5jDeIa-%8p^A#g1{5d*(_-@C-sZmT%atozEAt4zEv5MF(*0qkliX)D_zCVle(AnE+#+iz;RBLR$3qi* zx9PZKoA=OOD7aqQ3?o;}3di{|x=gBtq!MmyezU_uCsX`c5%a2+ZZ|aRy@MK0%w_VN2Q7D4+W zqx8Dtb=LRfI~+V1*8_cR1UQyak!OS7&7^xzTE>2QV3os+|9p;UBCa{r`IXJ$+(`#S zW$vwRYYlwZ)Y-0h{X(N@bvk0N>IO9|TbT;qMiYTHKKcdpOP!2}OyKI?=rfjgOuZ{% zF|mX8+c?S_57kNT^oVb==U&fR&Si|+MU|Lu7hIF& zg;`c;%fm8fQyC%c4zr*UsKpj!alxWHFS4`{NbWsCHZi_wkTbj?B)e-jlAuE0X4T?~YHrp;&DL!c zssDI;fo&ph_javJl>WYD)>ee;XPU;hd1co*BM%9fq((5&SLo^HW2rk6ybsnTHn$cv zIGkzV1HwSrt5d*3b=}c4E|W>RbSyxJ^n1FT&Ryxl<7;#THk-gw?fnJ*Y#i1m42_)NVRbHFyvh7Fphq!&Qm`W~fE(>_# zSWcpo9SfAWA_c0s4hk*KpyPQeKW0{Uy9$l0cL2TH_GjYiVqO*US>C*JsW=kIsv5^0 z7zT(y&F~zv7kG*(oWJ%}owe#&)LG%>GbxMx6+1t7sT3Da0&imZjdY%Wb;o`^bLxVZ zOaIj!edNBT<~`|HpRFI&XSaH+rkf69zHw*c;;#c!b7!=Ufie9yh%d`Cdwn6F!diy6 zQY%-W1)SE7Oa?dW;Zni{ z*zfNEMrYq<%=6>=$9gM=Zphy|_v{rA&*PWKLp})Hqa{~*2{x5GyMKSh(y?f{@|NK* z-K^EhszSaFe-jvmJQNEksNOb$jgrf**8})P`J3yFw%H#UN2wVu&eQ)SznwXdb2e6c zUM%QzF_a9_uyu*Ta@Jscj3#bnR@^!NnJsTtde@NYLj0+b>K*eRdKzApG}$in*xAdv zldI{(&cW$~u_bTqg4$8@wX|S|@J2!$nU3wio>82}oPk=o@bd9|9S-TH$QXF%W%*Tu zre%=5~*0p)JFCY_*!SF5q|Y@ufCL1c9wd+_iNKQZJcWU?ujPRhE(V z*%XPy53O$S*YQ;7-tFdP-zG;Z8oPOGJhgM{9b&^Yu6^OzQ5=t}-n@;tvkaKzs)uow zVe>iL)CX=pj5j&SHqcdZrDy#qS&InG9b1&vsd6Yx4bRVy$|Sf=);s*XXJW%B>OxeQ zJigpKWH$cxMa^R{W6IrwlNnf({|p1`jZ3!dDSpTp8MWnJxu0Ek-?DcHG^L=rj_M-z zEAk0OT>i@LJi{oAdF_HfJV5lJ{nX3a=mM>zFs%K;X)yOZ(Qtg z(hP*o2Xa0Kb%Y(8?8~n=LTQw0ilU~uWY@{l0?*4Jsosq?@WzX+)(G?6;nHHh`HKmq zR=eb^@sYHL32mZc%6n@-+9)vH4`FAicL;WP$hnf|dP1L<%ZJOQypp;Ob0R0~Fy})f z+K1P|3iV`$7ye!N1WpJRNl>M(X_nepff?0NCFI158cd|Lp#Xb>TBBifU8X6=L=*$N zm)`g0RKf@Nhq}?M_!JjZyVU0M8d5mpBg3TWy#9xCE8`W^4fw6loo;d3H z*4}M4ma6Xj(ITBypggC-=v#}!jzCeVj7xvEiJ$)yuvxCxBTG4rxLuF}Q?yc)gm}~? zle{JgiLg;oUL0HLg_Jb3)O2?ch#;C_h4S(0@(_SJ(AZYLlkPb%bxCf^SK`7|SpM)v zlj?LBd8R#JQy+W>uFNtM@~C(&T43%;gTi8_=b>MT_GMwYw39z+a%PfT&GJ}Soyo?5 zYhepl_%AGbW22LY4`=K>cj|67!(L)aQAv`1k8wWD=R_+j*G6@EK+^G!)+_Z`FK6twyPm?WPDXF?A;FasUs9 zl)ql1zWYOcR*c@@FQyBS`w-*dc)p|MAE1LrBHRBo2K_hL{9l%bgdD6)|L1)3e{iea zRuy1>z=+y)qV9o^vH?2mCJBNj-0(o;xMr(bv8+eLv@IE*kND@RS3gcaD9sij|L)<{ za|C~CyEScERP|~`c0giAZ@RtPr8wiyZfoh@HNExZ4(OFW0_2EO4Tt}FzV4fpTORlqWw*Bbme2$800+83kK?$LZM z1iC%{OgC3emM;!%n$5cv#jNkkJ}W=i;zhqk4gl`~fk{4Zuq1T`A9jP5Io zub(x1xGcZ3y9v(^PAu|mjrf{|>Z!LTIzX%K4xsL@&v2?(J+!Ry7f0IM?`^+75zy!mlNuV?l$;W z)++H+5@JU;wj35G`yi8?*fQhB22!q+dqjqCyd>G65>22Gu~41-gEMw?F49E0^2|u1 zuLjxgcOZvTamvvT|KhMq+o}U8vLTQTXu){bl4JwO91G+*5p-q)av(CeV;=}vEYMuw zpu=gAD`@t(s$FqUV$5Sj+~WtM9Ck#(eu8|^V&!v{M#m#b0>{HQ)uB6VPty`>m_X$q z2i+z4&^U>nVEduYqFEnBMZg@$ksOH~3bQ|jMO5M$9;3PL3W&zb;QfR@VSSMo&aRal z7a|{222RAK@^H}Jx5_eRV}ASCsa!f2A?M)~``DqHiV4O28#DMFKm57hV1Dh4$GbdY@dS z>xX3Myi%2S-r~lKoOb=5kK#ky0U0mf=LwnBb&Pqw*6V^n6m^c{cjCMa6t(s^?}^bL z_~_iIJqE$43XT6%HLU+XshY9>T{Zu^YW_d38ZWyFqyJPj|IeTmLJk(L|2JBr8e(!H za!~i*wdS9l=;DsVWBnNjTOcma{IY-R%R^UQW!rX|)aUQzbascceumVuvoq}V^?eOD zfh-R%j&hb)HXmR8YER{gnZe6CwSM#Tt-*L@%SN6xOG6KZ<->eaZjkTVSLYjFIehnC z(pCF90)S_KuZLrB$EhdFtYyT`YKrd7^j&$eSzeacX~wp|;-ZSDZ;&co&!z6H%x1O8 z)gNiFB_17e{JaF4p5#Rr@6yoV?)`x^eXwHNRVcIJEGvf>?yFS!6;W3Ied7`E(SL29 zyq@v&=COrSX27dKuW9SnQt?he7-AvnQZg9ZuiGRQaV?%i*< z{`qRR?%i8;_tsW*%}m=&_tg9J@0{oDe$U5&i7IV`{L;r18S@@zE+rh9pAH`b=rs|F zP#M3lA}<+gbr=^GvD61t)PS;mdxT!0gyH;z66b^O0Hp{8)0jwBkfydvWs^Akz(*C8 z`}y;i2fW()gc3${Zm4gH(BXM0zxtT@qpSuyuXeB+%x+gzYoVkmNzTT;>t^LAF!O5Z z09|dMZmg3&^wZf|t|Dy<`GZhkFfJ)^^rf_Sa7N%}+4Z-DOd~gV`-wE+b^N}Oh57$N zc9pXv?sMB{+!!OqJtj}(!s=`X=;dw177W=TuS@Emu3g6a+PuEa%e5Tx!TEMftRxRF zC&?!Q7Urfv;~S@f1;qw~ut17JURh-H125fsQ?JLOH&qY?$h;!?k#-$jia@*FZuDyo zIk}?|$euP+L)+qF{9N>xjfx?o5lZ}Ap;g(L5;{=Q9G>NdLP>M_7t)kNNn`nsNb>}t zE4PUNC}}*?*`TCB$|!hu%WLvCq~US*yeM{9SwtM?t9v5NY|Yj!328IM6KVSU+R?&w z2UnR-&I%QNry1aHNb~Qfndtv$=0BSGD`);ynqi18)cl=h{`i;xG^b(tKi~}NA2=f_ z1v+0T&mdi6TqF_P*giQ6=SyrREniG3TWC!Sh)Qjp&*HnvOUn~*(E+<8)c(9NPC1!h z7*w6yKkn9r)GaN1UM@(nnw;A3qp(drbuHWPzD+mYmsrMxzcjcyy1e?zdm%i0xqSk`mXG;#TyBv9XLymKqIN@Q8(UJP~7 zuSQT(b^50(;my(MCORCxtw7LtI&e4(EyjO%WoeV;GDCMwgvEQ8VR3dCvAJDw%d~F0 zj7L+U)3rjkdXPk0Wc%LDrON6dt&`#BeRcJ_+e*RrCR+1Zi}7_UoTNZWkY05z?Tn9g z@#7Y5*&Hc#vAs&0f4PFh9^i9U7h52#%N(9Q8;81jldw97$e?o#5E9TO4e{*~%GOuP z8w*C+$;t}S)p4e;ejZQl?KB)T?w#P|?i|&1ekaTzx=@L~H0o;PAPt z#{KVT$)NYj*^YzD1#A^v|QZzOXgyYNq9d8%m-sx<7i@) z(%*@@vAz8{fE!m3r<|@No<`0s)< zjlTwG(|-)k?%48y7a$LJLuOC4(&3K7>i2CHDQlfEYSy&^v!=$L@)KLFANb%Q6%ej7 zGbQ3%*D5|PNY)1$qN#e*Ijupi>Y*=l+t>~$UMUY#jt4k6QK=4EvH)E;`ZqPd0$O$m7~MAJhhD1A0!j zRYOL_=o!ge8;g@%Z#&9qB-UyLj< zuiQ_Z0N3zZ`Q4`*j}AO`(lxKf)a0eXiEP_(5UU%#jS=)+k)=q$VvfK(nE*sMz5E*^4fs&&tdb@v*yy?gbg_XK&MqKl zkm|iIpSoU$!0SUI`ofH?!;eP_ULE`8InsH@Ff5CEG@bpJ-;iKQrsWv8UszEGPIp3c zGm|Tw4EjhrA~S4j$csCa)AxW3-n@RObp{iQvGin}jWJW{7$K-*A*_&HOm!d?nY~Dj z9_jy-ZXW*ZPoe=S%6SPrd1pI>4(omh@ruOPq1S}xin%yPN)8i9+eHDR5nuSZxX+=O z$#_kCv$Qj}EzXefy42H#!$i!FgwI{#$vZn+xe zkIqy&ZE{#JThH)*h~!;H_X1&J=tGTPN88WUSwJ0U9B`9lh>#%qJ>()>#I*Sr%sD}O zlG_H&)?weCP5!S-eX|n^Gh1hKW=|8dt?jljs0&cnlv?^aqqNkTFAZIOzt*-Sjd;Fw zeoJdM93#EOX?j;_G30_zYB8!83;wnO;sWtdP3u!Uo?n|t?d=@~MX#isY)Y-9vy&R@ z`Ju*o&3l<^UUhdGBF@ zpn24wX1kQ9Zrpu%%k}|G0Fj@%#vF#40GAOE{VTn^o1EpmJvn@177}aV*E$Kk@zKW? z0WIL8bMNczr{HY8{zX%9U7bjMxmCLhDNxg)&0w!;NpxoVF^!6B1DslzRB%ewVi<3g zU5iWd9T#@Vi1breE~-x+Ms7zxGHPyzLWe?$7TStaamGoJ-i6tUUi^tG9)u$@jZz-w zJ7y5HLX$e2?sM&Da>9t{AT)wKZ62dC-(+D!*$7v7>_#Ml8@EI%IHF#Kek67aZ(B}P_{`fI9`;}ggJ~#w9{A*X+@7~v;K@cCpctCp` zWxL8duOj%HJnU1Myv>Bce&#&rDr$?^a4{WwK+V-C78e|}?5d1G#P+VjT7YmGF} z8~1LlBFg4_PSbho3PVE;Sf?Y~Jhk|KMnpr4gT*N{91|s`0pA#+y4(&c)Pbe+R2l14TOk?9_;D7+s#H>M*A}127}P_ zTXru!(RDKwBDiKTnQjnBmaOMgag)10*U;1+YJeDxkMv~omg`-YjXbac+^*h+U$!{) z0X56#?}kayjUGdgNwaTc^cLIfNcSJY*u1Nn9hV1uk$ZE71E(){P;Td;gCBk0T0wKo zY4LXcB6Wu^^nQBAdFL|z;OekT+;W7=O6N;Tf75MyHKj38Bo5~pH2qfl8Y5ft<(pBG z@F$ObIB;SdS;e^zE`f#*^aYu~!l`7fS?+?J|Dxn!A5y|bQf`>hTN%4ar%$-9kkJQY z*%9^$r9*0`1Ev7RD9zGr=)HonuW$un-?$S$@SKye(AT%9QXAQ&DF}v4eLdwEygP>lR5A(D3YDI4AHq2a4Yb z_Ki@xkQ^2W8$1@}Nwt%Jg4Bq8D}A`=dgR*CF#thdfQQgd$*~|fM=8OzT=tM^~QIew3kVXJVBPdB_-?b(73^U(#9Y^^J5Y>%a zyQn0zU0K4b;g_MP<3?21m(J7@8?X88fi>HXFO=SMw{mX|hloVXX9-D~RVF5H|xVvdTa9qd!uR9m-Mr|gb= zuD8ChD?UiN;H0ZPlJ&^bC$)oEcteyqiA*T-^oZ|IrWQLh#DB_cwg4xCHoPQoY-B$+ zQWthJ^MuRJ^1pCm&U5t1F0DQc{vmG5x`z{1y)H62)AdHFMX0KvrZB}KxqU~6I893O zhi7CgpUtAkcIm9)$vS@Dhil3nmmljN3J=C=j@B<_0wODUF7AT@UXis9i%(syBJrba|szlhg>eZThehE>U?=UT6f`X$uQ!|Gc@&V3gFC4 zM|?R$?Kt;lSw<#85LP^!7Ly5U>;+}qdIbtDdI1AEE>m>2?jUNA4nQPxRZzTX1UY-> zYvFA*sp-K4;p#;0dN?L%^A>^NG7e?iTRLI03~&PREK%B%Md7gQjpvR*L}o~0pn z{ALiToc3^n2-ra%NP@>MK3Uvi&%v>$KvAYq5NJA_Zf-tVAz~>8+5r8mVt56yC?h22 zdM1JR>Jt9#cD9!SnCMm{l%beZ>5xq%>N{Q^h5j`rxcz%7s~z&87_?p&Tu_epT~siR zSU+bBpMQq94j^SRNSa!9MJ{l!S`EQ)DA^X2mK z4b_-6a_@0}g$vSU=#4hcREWrrm$!y9v6+vqmoGStW^gi1hg@j$xN{3$jLD>8XSj!$ zt=O5eeo)89H6y*LUt^a8_adF`?oYZgn@cM5t6isz+;)yq3=VfP6DE}0lxMX+f8;cs zFtJxDFaV1LAiP9F3J=P!O_-l?;j4n$d9EbG*Y5PK?bb!%-+w2N-}RUO8ww=v^F(>% zgX^z@Rr#D#Dtq0O-7dTK9He^cHfNMoWpwOWsxJ4lt=u110?kaS!Xkrm9BPS8yp{F)_H7CIHsJ<7hLQh zdBS~jX}1qu!V%FwOeripzyWDoQIH(Gu%lP)3VH$_3>=;esv1tU#1K!DAWw)*Yi-=? zN~Z4-W{bD&wRmPN#HJR{EHIlB;?O-Tak#C%9OmVT17Ii=abG07Fn^9MZ8ryx95jiC zi2PDgk=Z1$iz%8+0SutfF@slhRE2i_VN^TQQCKnJnfZ5+&2n`_(>SHWO}24s!8njg zA*I?gov?2ebi8%S#tw=%MIEO1HB*#x6|+m&s(?{tJuggTO;-IGpOd8#^vZ6So0G%x zQdA@*kXV!98(|+^g3kDyn!_+=BMgXptb)1!?aK>4-;FU+C2hs~X!`_0ckMC|2~1be zCy35gc!)BX8M(oj2t;(h;Vqg}p+R%1#rX0~}+t|zM^yl||)e?n- z)#=YTjoL)$rpfrkKIyK9D4v{F|^J)8k40o6Fmv`Heb0#~<#D_{=tTse$rJAGaCP0~gR`Axt*yDmw(t$}8UL zg;*1dR-)DzWff;zcHT9HCpsH1f9;vs2ISlJ(3h0e8MT-1SJdGhtwBEgUpF(ONV0f>NI{6m|_8CPGk{4!v1$4cxRYxCTr2D{lxq z2ha8#8QnN{?<*j5B(zunVlcyG;$NEgAAtM|0dklXUiBM&u>P$+6zpw{os3NE0kmLa zQxy|+Mph;c7A7`MI%H-cCr5Kzds;eXl@E?qCeU|fCqoMpBS+{b5qlE@N9ekD2GCsq zE*~7t0p@OC6MK6TQ#xiD6E_!Idt(P$=y&{S!w>NPgpFOyLT*~M`Q^Vmk7q-`Sc}GqyodcLut4w9L!tgCW)lH~025%rX7j{u}+{S+S zO1aM(*c6m2);C|eARYf4Ht|sWYIaB5kI0C?jGDzd@%|HcrsvA7_qg8lQ)LjfeG0vl|PIk~RuU1@p)1 z=buxZ;dt+mX}G^hQ==8ZV@W^nuT1uNO*F&u^DUpRvy$($dt&yOUFwHzM(!9wA&U-V zqYkoElU|ugtP%F_XiR5Q@gYE1pQuz#@A@ZrNt%1!IG z%^3Yo-_CEspL((66adeQk~X_Lgm!~{;3z-AAV>if5y!Bav<)SJ_V5xfAF7FEyI*~= zAO{jykk^q{6ov+v(iaP-lNm;3QLcC3Q5?HfYzoo#4OHe*IScnq7I1}I#beSB7zxuE zW}CfwEr=ZnezT96+L7f+1CX`fe)LfxRAzQ7IP4Q;r>?HPc+WRf>?NuQ-6~_ zT%tMT&j%nwt$vX6IARZ=X}lXo)Y#T^vQwi?|K%GG2gjkRv%BxU@e`wu4d0;yWW~Ew ziF}Qd$9J%LFSMVw&ygX2Wajsg1P&1J0v?j=nu zov2QCs$i3XoMZ;S7f>bmJZ)c}`}I~et(LawXjSY!CYaLTT%yY9b9Y1-h z_X(5foH{=_`bQ2SD8WOz;WX{05c%|O_^SI(L*6uFv-##poLg_lR`sRf$0QTAjh+k7 zcAmr^re_~#UJHp`&Oslt?UWrIvU_-|O<98-NJ5DB>w*?m7Z= zj}~Q>#GQ5}X=RbK_pqY9(`5Ts496VA{aJ`2w7m;CU{^eT;N+~0!3R+Gm6EDhWeak& zyb5MR$nG)~<}HP{POIxD)^)T~utHwYOjQ-GCafu$`$1CkQhbNW`SG!>bzfVZtO^BUsa=Pf*vbrtW8TdLD)_1TH{P!wj)LDV2y;V_gFnxYl@Dj;7>zJHd zb*V_bMtbvhGAwCnZa~jTn6f2Q+kTKpc%Z-Q2(lWovxsofbLM!aoFyb zH=3_#wCq0n>Y?y-mL|g0ZF+o50tUl60XFUM04R3HzFpjtc^(l|G&Hc~oCjQc6w%yG z)ML({cv2JWw00$5k_R@-=h;}4>fMCS6lE*c_M4w_BT)(T$Y*7mOt*7row;WMhbhZN z+hTxxvw`PhtH%Kes-xJ#MWkAxUh;d%1Q|sap{7arSt?siwrfw4OHD_r$Xh3qO%fG+ zVj2pa?{k;7n29!EG9}4Y{-P9EG)LH#Q2d>eW?1Zpu)DOUm}dB?tS=6{Mq^8s3~Goj z;lloVK=3zYTa{b9qn`O>`Jd9_Yskt4a=7%=#(3;bYte&~#)7{JfMwcKnvUEu+B@+z ztNH1dtoV+fVsIldr?c2qDv(4%Hb+B#N-HB5lP7G zi}G5!HTu77O6SPTi{?4rO=J@#u393@dX!*=j1NX5o5+*rz0Z({1esNod?fNvJpQ7x zn-n@A>!!qYp*9-RR7ASyx})G*)9*V*+=ulpK3!k%8adHo_e1cm6n2EOZ;XvyIJv$jc=$(miGS4O||(0MVp{Pw}$Wz3TMTxshePb{1N<(@B&X4hx#oZK4)eoXNgi z!+rg->>~pN&7bS(*(Ydoj9{YCKepz!d|l$+<#vi2MXWwqHsqF>Em3RL;&r=!n4Z5` zBt_RF`G`w|VdJjmkdMU~?nIUk7;p25Z}<%JfsT4hG- z{q8}~H?PJ{FR@FMTQ=cqdjJ}(8=Wu1{ZMyg#jnD^lDzEQB5=2-c+ zvp(=|&zrjoz{$-D{?eN-pO83V%R_A}flo3(T75oEQYfQfC=2`}=&qJ6ELIH8% zPnm&1YkubbaK=niaP(|vH~&7@tuh1j_$vLEkm~|{s`S(vZ}2C6#%G@I{mw;TEEX)z zIVEk;R)fJXr^CqGdjf}LFzcobyL&AuA8RCAHR9B-SL^rdA-m+Hm$u}c_R;3+v`(?* zHhog9*18a3FRiOooMzXqHHhA0% zdaE!^`d?1miZgQGw^;1$yTj#L%BnKoPI;)l`fVRYV(3v%0gc}X6=v3V zg``a^6vSr8$&Yx0c=$aB7*{Zb&r`OI#00Xkk0oGd8O~ktc-n*`Z3h=V8qFLI@7|?9 zx-V*J&oVC1HMc&V-|ig0(b(kD^oQtI^%D&Ho<5p{IZN9!ue7>8oEp^xdkL*zRzxF( zDOgXUzzQ|CcV|EFNtcTae;JE$PnScOTywP#sO`;BjO32=8f+b_LO^PjQjl!X?#sT* zitLdRofEQ6?^~&#VbbH=S=YipDOlY&v1$YSOcI~*8fQQNW%0}hoq+B5qK@oD7tVH@ zk&t`IG-}AstR@THh%L-FU6m=M*%=~o3(YBKIVYMeryArr@M827B~@{i_K=g7@hsv7 zWCPBVN6*Fmhnj?)T>@TlS}Kz*s`M``P}}1L>^BM9xCH#-G*!+xNYPZgtQp_?b-g@Y zG#i5E#OvL_&3K61Tr&CfA{n{#z}y;mX#QhL(!Q-(e3-kqXnf%dx{dNHHVYk!%4YEh zw!)(5dt}jg0(LJQvc_iNy$dnrf(vWL5=h}Xr*g|$HLuQkinXP*m&mCuGvO;~ga>`9 zQm^2cmFUsgl27?DdvrR}Z1@qIHCY_$J4`qs^M-6wLUn{-5=exd4 z8CUfkd-cWC%eM!oCaiVuptS}9sLP?lO3`R_Cn|+6kq4@E*H}<|G5n%9OKrNFNhw|) zrg4jFc#h9nMITrnOsB}Hr7+z_-aF^S+T^cp>!Yql24uwC!!~S%H`EDGuNV9(6FiL{ z{2tG68Mcz1OjQe@`aakRrMuW)-P%W8l}wcp^Esk0+Pw2=!I0rm$uIlHST^VwWl`;{s)l@g=kle>m&I-5F`5&>e# zts-UlAu3_`MpDo7*|JzZU&^y)U9Iz6vMQP}_ttNS8BJ5on$Mw%Yx9@x+Vq%2W$-em@+m-;XQBOh7Ae~v zL;iJ$s}-e0>g4wms?+U7i?2q3mV(a|v{Qblziay9HdX?3Z_rKp;wD(KFywDpyI}ZE zsdS)vnE)}#n%hHgIp3k9Wxm*NQOk(#z2pT->zRDaZp7nx(${!8y>6HL`tC03HQG@@ z1~Hknk8@h*Hu-94Dhj2#dV2UG@m5$uG2e;A&zTj|O^&v1itXJ!6XYithKAy|XS|Zs5`^Ep9`Kc?Jm?ASv*%+I+0<>5FY^+>5$jr(g+@bj-D;F}e8bFH`zz$%A zE>yC$b%ZW_GHaEd_As@D&ON1)PjjLGEnZsnEV9Iw;r=qNk$C^^D^r$lT1~l+p-7KoxFjMoVUS-o*mD zXn+|0O^(1%r{eWx2WUS4DLB9oanfdnu>RHS?Xd1#L-&(1Lr#9ZH%xV8_}G(S0l4ga zyS z8+%{Eiti#A@$eL|aFVZeA#!(}Zf5keLC9UnF?&aJ+;XrnqCKjhCdZs_9Gtz@{XSS1 zr@iYQkIiPE@rG2z)o17B+J)ZXQ(Q*O*+#^~qUrJ#p$EoZ&kS4Ha& znIuIk_X9bS@2{q#K3g1S;8UPActePb4j*#(l)CO`wR43=?a#I}_BKTOnnfQxvP

9H5{SY7*D=RV;m6*IZ@_zxwTs2Do literal 0 HcmV?d00001 diff --git a/audits/Cypher Stack coins bitcoin August 2023/LICENSE b/audits/Cypher Stack coins bitcoin August 2023/LICENSE new file mode 100644 index 0000000..8e0f8a9 --- /dev/null +++ b/audits/Cypher Stack coins bitcoin August 2023/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Cypher Stack + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/audits/Cypher Stack coins bitcoin August 2023/README.md b/audits/Cypher Stack coins bitcoin August 2023/README.md new file mode 100644 index 0000000..97507bd --- /dev/null +++ b/audits/Cypher Stack coins bitcoin August 2023/README.md @@ -0,0 +1,6 @@ +# Cypher Stack /coins/bitcoin Audit, August 2023 + +This audit was over the /coins/bitcoin folder. It is encompassing up to commit +5121ca75199dff7bd34230880a1fdd793012068c. + +Please see https://github.com/cypherstack/serai-btc-audit for provenance. diff --git a/audits/Cypher Stack crypto March 2023/Audit.pdf b/audits/Cypher Stack crypto March 2023/Audit.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0bc93ccb656c725e92c27005cc2f693d4fbeb5fd GIT binary patch literal 292738 zcmb4~V~{A#x@OzPZriqP+qP}n?%lR++qP}nwtK!iGk4;~+!GULD)L8GW<*8(saj92 ze3L|8Sd@l=mIaDrZfST8iV>e4-_FnyiklmXPT9lW1fNdM&{E0S28vD=pMjnpicZwR z(a9N~osk8KPTIuQ%-I~DfsKJ4|9@WpF>CE?;)qWtYHi?bB5Y!0XKVt+%M0b?>}XfEaz_4>j zB`B{szWpM-*}pQl$Ay(HEL59B<#Ni6beA#!qAZ2 zRA*H8KF7?URQ|LuN6Pqlj(gBC5+tHs{*GC|ru*t~bA|pJ0AUg#5knxZaTA=DAm|iY zF=@=|$ixg5A({TjH#q?!$Wtr_Go(d?CvyI;e(Qz>=y>4G+ToU3X=j2hpJpu`*K_(2B_>p`=p3XsWbdw zNUiu)CQ$O@2DF2dh9H>+iX4aisKxe3u!ZboAhv~)(g3UIWXPgHM(vQ^6A~x%Sl&}I z)Qt@wv-DZXnA|~_l{mO#jU)R3*Vf0XEi<;ZChE189Kr7uE5`&S39@wdIuggEDJqg2 zuL4PBk}Kz7$YC7!(7opWyq(3)XyE<^`p?D4T;mV%t&<++)(+i1dx^mo@wVFJCvJZJ*$or%HZdOWzlKrlI`AuDtrs60@ zbxnnKVefm+Wu+qg-_P5)(VP)~T~Y|;Kd5BId!gyrg)JcxeGt+gg_0@{C!}IijLG1??P<2-)5GN@y8MIK36BkL}YU803CINyVB8GC3|TW_ago zSAE<6DgHbi#r;hRoz;ieJAiglQ?B1hIczf-RlboIJavLGhcD_tqj6I}YaXEv3|RKIbW}Ky=UM znfnRI*kDW^dyU;88ww>}@!VORc}f}-Ae8I!qJ1mu!xGcG4TV}L+$(|*;@vpDBmR6i zPf3Zv#?$?Ltw9Q*-ucSz-63@Y==A`|=XEyTNc2n!81g8AMMa38@Fz=|!pdk5im$Z* zi8n-9D)*xPjx$%{5+u8E^eyh55Qv-cG+u2wnz8`a;afjXV7V7w!Rk;d|1SJ1UOLm_ z_q&v!$T@EPn~000p6fn(#rw)}22WQu$~~9GFFDJqka>W}BjwyLlhnJ~w7ae#D~O0E zCJ!`aZTF^UjbyTo3J4Lo++{%grrdi$+m>=pdN&~0blF~IPIXmzptD9oPiw3;xO zGhuX_Bf3u9u*LCrfJ1ILJWJW+jX!y3;W1VwDQTst`~&rtqhZp_DJCw6HU8zND1+~* z=}&a&bzZifu!@Tmhxl}52lKRn*hxK7YCZ)`pG=3a4dGPl zk`!O*X5idk+fdI*j{)|6sLp_JA6>C25RKCxw^Iezr!$HOoa(XuSV;nlB@PIr^gScV z&8Y3SF9{6ez6vwGe6j(>&<_TLjC>T$d72F+*^LjaCXVZo(Fmmc9bU$ZEz1lgS&Dtm zL+3+fWCS#_<6_QkH@%aQ4WR;9NEyjQp>+1Zh9CgVInPBmRzmyN_S2VkY0zepr|6 z<(qTTZFvTb1zyh@A28<0Zgp|jOeME-#97f>McKrk;9Eaei$1~Wg@M;^e7SSiEK6ra zrJ7`(pE7bcSM0noTHkR_sE{1sr}Y{$y3ZV5Zyp@4^3`-(VZQi$r+(~Tz+G-3FiB1KtVjHra*8-D7^G z4u3=0(_|61tM2{Qm{(w>?S{m~e}TD+^|hJa&$Vbsbf8X(?J~f?JEMr;+@J|v^N{;( z4L?O-b3M8TlSU^_B5Y687q~KLEq&JIqIGH$6kUH4M8w&;wgdYOW!mdB&3!=Gw0qzL z<>t<;bEO6t(I3+SJ*e+`l_i1z8#}E}2;SENk2JAW)c-nTXPWKh1p6|x{&^b|$m(_EJSl?Oo&UgCr6V>-wTuGW!1DP zAisF3lh=iG@^F#6N^U0Kgs3N4IAXBC=PuZJIiluXKZhzPo36l91^b}()OH~nLq4F9~L#OOfNp&D2!%f>6 z1V`X#skyg5TeA@F$nu)M2=pK06|N_3eP5mcd?3W`D&=b4@Or* z<7>??&p_@_vNV#s#mHPJEa~!8kc-RvBM|EDl-5ok)jU_Lqh#5^58V=FvZBUkV!z8w z=YVWJl(w7YAwCzG#e#4dD8;!b9deQDfZYJN<3|kA$K~|V&tS5ygNTwMT@LOSE3}`X1~sp`Xdyih`<<_UumFJY9r>mJYoWpa#l#w(1UwP+UNe%C&sBRt zn<_uec52I0BFw+~#USk<7E=y4;H+evuOO@Jt}+mbKM%BzBn;* zEjIEEH=XmDqm>LAJ*!Ozc7T7yn)Mgq7i(Ntb|CU(vChZ zBU>)r)@c*2`2?mCk_q9}{1B!$_FS+>C#k#m14Xx6tD#pL93iXy;&;7fs+2l#+n;*b zEwgbS#Q0}?ySTl4P+gHlM42WGk`+k42IsL|>7QS&jw=bR@{rv5-BnCq9nMfg%?T&! z##+{g*Kf^c2f*RU!4i}WvLi{9`xs#)(}@)?gKl9JZ+(*dErB`#)ZZ;WBd*?ir6 zbr02G#bN9&w17lRAnhR^_U~X?{=N@TdAy*-l(tL#3wW^oD|oOmG5-7Tm~{OI9^0o> zT}7zM5WM{M>m@F7UJImT3LO@m{uJQ@44@VG>8d{;a5@edfx*~=@6N8d(1|^xROb6|#|1_5r4RCKdsA}at7JsAMF6myI8VEcHaa$;4MbkE) z@t$wrYMK5{+&;GSR(M#{{6@)J_5<*vF$;ssMBSnQ8cn&`JDI}1C%4j|CHJ>k1E51r z?6hxkw&5S`4?N=<|aVnfsb3OiJHn#xNX*Wba&NEEhy{= zbr=P2FO)JpMRj+lK2~LW5yZQDeiDOFrAt1w6g!q0qJUFru6|XRUSQ4TM8<5Zia|3$ zrdTUPIK3{OEychEKEr(h+jSlYx}l-_ zErUr1%Dnbr+TbxDHp0f4TTjYYh**{I)!5+Bw)nVdfjZGEC+UOv{*l1t^e&*EnlZ^; zrd+y~FT-qpfq*Aao^nXyF^G4UJ+9?Rz>H-d^2*HIc5UR6p=LQkZM7_^dKFR8bM3yc zb$F?QjfSIf{;E$CNVUU^D4-rc4Coj-krMOjXrFUJCQYOIj3oZVjGHIu>R&rq!!zeE zEmvDElY-ar>*$iDNlw)v+9!FV5<-+QwK-8S!AVQAkkD9@Zw6{!@3b3}lV6vGVqfZS+$M4{xgc%cw5pR5Cykid>F6$2t~IoAQGCZ6~Sm@?QC9B3F@mK zxX5X(YEU_-7nq#eE(-lPs!0;p|N%k%XJo(+Ei#1pKtTe-^;ntYSeZ*zGk=1ivp$J%)u9Ts^8R@&b5cp zX3-`he-_pGer&qUAPseJk-e8mo@`l#$fI?q3}1Ly=zNoT>x;W>&c&EDF6#yM;GAc7 zqYz!8{XVKy7>D)nF$*&nk;Jivy!lv@SrK=!$XwR`tILecOf1hOrP*RvLpOueu`<}V z3wHSpFySI{cjjF@$7k?7u=&7$U@X*i^FLu$|>C^R(6!q9| zgAmjri&7N?r){pI&37IUrIlziR}d8log^l_7(qv%h=HjoHnzVBQB9%xV-A4=9T94$ zLBs6v6ynQFA{Y|<(qD);UwI;l$SK<7AbMuJG{){+;y=K?r8<<{w+)PxkHcpm{s^gt z{p$p}khnkDm&UD@P#3k~9t71Mf3K=O$=D@E;y?l+1sDrPQnjiL2swxOf6vT_Kfw_uOEl)vW^MR~R(PbKkBCiHvkjSL5P1oPo)cTP-jzZ)%Kq4vz++?~NjWvoh%}!%V_rAJ#w`;MC?I5+#QoO_gKcXAcSzk43HP zE}{)lSbjW;dIZ&df!ZA=CEcGA#E_@Y?~WQnE;Q~f1%;q~BQLY8FL+r%c+f|HqQF3Z zE*xS+43%4S5<93vJald(q6lD1kS)QL8(x!`bX-u_ac@+U92vAHWJKVR29W|$O?@;e z(40QMA4eD{NOda5L`M-$PD7Pc1W*p3Lbbn6RtQq%NjoT1044_%02Yb?3Vhl+X<3kz zR)eo$j;mUOiW*r?NM;^DbEiRUA#F8`A|DFhH?!x%I9TlO9ps>2 ze;6p<%sc(zAlb39*KrCwn|IsEAPQcIciNr7wyu@&RIX|sy$>?oX3uGLpTy>Aqw3G; zi}NyX{n6kpNsA%+MnHn{% zldAH?StV%TS>dY@XR3ktwMzKDDr>HmM;=Fmt=hE83u^Ivxajrn;wM?c%Vat7l9Q)* z*Li25^`^e*Y+4tvLn+BZsz3Azlha~Mg2!%C&JD(0_u++zYVuZy%L;Ao)UjSW`A>$G z+b5a?+k;1WVWJ~3xJrfA5nA<_tW+i$-6mNjD*sy}`qM@sQ`&dXLmu)Izky@h~I z5+kjo${AOViqxLS-!dydElDct;hJ&WxwTQ9n)B-z6#M2u;^*y)uH%cAr~%&8C%@}> zrget3N<}2x)iyp4L)yu@!-t>l+2g&^&XQuyLO1pUJ<0iPV`L@}J8Cx`uVvVS19|`Dg(Tmx=c-N8#06~`sXVr12HlP0h@GoCVw8^i@SWV$j*q?; zG;UYm&I=ZHE3V7DtW}t~uejp2O{6=Q;HsCX&wlFc)R;Ptvt1R;?Q-MYKTA_Guo5r5 zY^CsE?CUseu0ib-YBO%sSGI__#K)K4440XVX%YL95M%CTbgWGbF694(wI949!QJ7G9tIHez-Q_Gooa853= zzIIZ`iGh;9{>;C9U$1v?{C+NfO`3l_8hY?*u?nr09`;7-#p-z**Iy)!XVeUf*+j#1 zq2P2sb>(Mt!SF(5iX)0NdC_`SBqv)oDu)H^2Ogopv9h0MzM=|Ph9XoyP27aem$&jl zq1GB7y&^H*-`LOLljDjSf(bWdV`G^_iJti{-1WF0jBjvpoInJlpF!MzVw6GLUL88- z2}Pw9PDPvY5crM~;Sdr1aY7$w5LZYe0uOPKM*<728di%l3=1<%NFd2zIeD9&ox)0-7vv(>?0$9U{s-qzFVpNt6~gEh0|QAGQ5#S+ zd%&~%A!O6v1rg4wkyyEU^58YJ-%IRDfVRsW&GJpR*-9aY81s)r*MHthsucM-K zgRic0?=Ro4$hMlJYUIp5c&D5;a_1huQ_q@t{Kt^)*n?mSTFdHm-udZ> zvByI~FZLnvU$Z_hK@csIhAD)vzhY1QNTU*UK-6;d(dDSKceo>s>wz@(W1e-$+}P0$ z$>yQ2bhx4@@c@-I!aDAWMG?(cZTt7+@UaCRVW9UJz`W>QiIn%)1p9eM^nB&hkUakV z4LGhIgR?08=ZGnFkEj_}0MnACmu(!P#1h)}D^chny%xpcaf6+b+=I2$1>`)^B|;q+ zd$D6gu8PfHYx{}tf=37MSX#hBKzKsPMT8%C023e@Wba5?rN+UP_(zdRh9p`0tDFWB zAt#NhYq)KKg7P38|Ga`xwG|0uUyB#5F#z?*pp}oit@eSA;7%@4`dg;SQGNf4!(mL_ ziq2C4pO@&z)t~CNq0Nwb#F;y(ZBTWip;Wl0Kcn;UuP|2U7``Sfr0IORaP%RRfOxmq zp_1HUV~g1LF9vI*mT(p}3nz7?i+4+F#4NX9Br}iihdw(zRfVB3`Qu-#ff>0V;wEpW zpyG7N=y0>icsW?0y)a?Jab4R(Wt7Vfhq3f(tOV>a^%^H5 z53v^RWb99_$dlZi(`uIuT~>jX@xC51R_mk7$U!4u2QYG<`!~HO1|V z4_~dU;ojA7a2x+vzGLjX;OB=u2mRQ?jeU}V9M)RBWBB@LRFd4w1v@9q-`nZJ z_OQOemz*|-|Mv{c#6kaGGVp)yD*S(lp=C9x#7$P1o>R3q^4TgB9`f;vYMDd}&cvd{ zD`|Kcc>5WJ2&5J*ZPjSKU>54Mex2X<=YP3#ZDjsQq{D;-Jjon4VL#{k#%5H7ufP^r zIJzeW8Ogdt+BFU2ADBVple0?Uh0PENFtttU_cW>w*eUDdLIyCOh6I6X4eL=lEN4B7 z+pg`BN(f7y7~oozpe*=g`ln zV|oc^__$C`UUeLN7ptB8F4;eXDb-=YNp?4(ndSQ^96tDp0%DC(KBH=102L1)#tFcS zx~?jEhLzWW@t7^;L9%d+m6?v!dei5i%CMEylngnzHw6;hS}GMqwaHR4MMyj!6=9g zN}zVN>y_|Joaj~#O00VfPz32EFl!4niPJnq(s3zc!;rdQFA*uM8=)5(tr}=1{Ak_v zh>i{iv@lCzTIvH8-i`zmK(O*CqYFTMsKy)HedN0_RDFadbMZq^eQ_(!`xkEd6lyLL zK*P~)gd4?!wQ~Kc6hgpuW6%!cr9?MG(t}VEI_?HCKpm+YNeChs{t-#ZSFylXRJivo zEGl-{Km(WW^jC_7qa!DSTLWsnVYXuVm=O;OiMzn}LyFu1W%7ZHhgAmw!#)LpQ{QHI zB5O5Db}U)Bkkv%#5CWts{&Pw;%Y3(a(`GjMJ+o=@wkOa*-hB$cdgXiy-kW;4Z9H9K zmx0?1r*ypg(opZa{PvAZ9e^9w_;d%Gc;OjSOCM=iheHgx`9pjE^8S+bfop?3pB zXC*7v%^_>r<{N3lca@}ICXW>S0yKAH$2FPIeMNsR&+DpRzX@rp_KfE8b%}eK`X_GE z6P+w7dbEyu176C~fvHQvxDy zw8(||D)1W36Ekj|wPE62|BuuSKga19Kbu&DLUf0osRsV(sUAh^T(Y26ol^F4lIbJ( zbjXQCGQUYQ6nZ|eR;>?ovOV;(tF_j-y_)gT%s_ZD2r;sT6Ix|}tb|dfu z6&!zi@x6ahUPkDT!bxYFJNK+ql8AnDUv(T)WPXbkllWB-*P=E1}A{n zg}ptiomjTM9kaer;cfOvI=DFSTwB5G5{Xv-aWfsOg{r+)@-YOg`3AvPFfJaZx8x9p z^VkICLK(^nN7+C>So%#IM7|0*=KRDCy)Uh5ZKjmS$ngX$uU4BFC4l{iTV%{G6ck>} zke!HU#o=0W!kChl0D!x^ZmJwlH?DjB#b`-MvnsqN6}akd)F29om6e!DE${)jrBOPx zp=)`&fc$8p_R&0d>YbgK4B%ATd+@q9muJ%Jys)NP*7g?V&&<+OA+l7QP&QZdYiZ@> zpZrY(U$ZH3@(@LspXHn5;OL*T2-MV)e~J#&xa}A0uzB{DV*K6tXKv@@7}N+=3$oUR zO`;loYN^)$Oh^&KP?k#fI_QR45Rx?dre1zbF`?%NK$2Y@+?(-B5-hP~&zX3^H*Vlgb<0u^TDcP}uZ7bFxup*KBKE@qUSp>7H z5+v1SsuJP}DAxrkOFXH!=(ncikuyy1f#Z^hln_61=d2Nnf=4Zj0xxA#g|$>FSl9q0 zdafnT&`Zz?8m=R@C3}(T5tN@{(;Wshu8I+sOX{vfJArSVVW92h@-(IYG?Z}FXk-F4SghoUmJ41KB+G-DIw)prUa5&G% zp#k?OIivaZ#8xDbO}yBbf#bH0PU0ScTAk`b92Q;mWQV%S$Wy;fkrK{BrJ3J5sbQBw z$S}eAx<9T5^cP-6Vuqb_4XU^3YQ&dv79N%{B2YCi3`oLPYs>dTBnmd^pyrPzbzdb1 zKz}g3MUFsB+@kZ84O!{XH}mnm-n2fVpPnr@PdWHGg@6M_TzO2j6QttdTR6>)N3D6| zi9-WE!HIKzykDISj@Z3knP98A=;e6TD$F;EmKFs0yqdNSheNmwZ4??@y!9eFJ6XSrICx&(J^}DqW`e&r`le0E4lCm}EhuV?a$%w0H0F zp;!=dP<~TRWW+T5x!P)9-C!vH7%DQ98atrBW3^Pl)|#7}Rrq4eMsk>aVi<7%8qD~n zJLPdWPJ)oPDlSc3sIaqhZQtqWhuxG_zq&wfA;1pfBRe zp`B^qr3!-l?%`oe*4yvP67q5W((pn^3f%VrdZ`%}`qEUJk)qjzg9eB8x{OTZW5~`O zg=&@`-;aZ1amEoD74Il1m*c+&{fesSQS3QmU=q~v(tKic<%^gQz0x+3ZUIQ0AlQ|~ z=;h7Abi1=avcv#S$S(++XfoSno>LHtgJ@b=Eob;fA8Q&{`FPhBC;;*7zl?%R-Y1yb%0`br$-qt-z37BXY~`Sqp&NzcCV2#Pzzp4(@7|Kaj8u2+?krPmb4VbwAn_oYvU(W4qemDl@)0btBOpfG!_;VvKQp>Fnsd;+j^ zRC1_FDLOU;dgPmvUn|Bo4Us10P)}2wncgTt6RyrCfXYR_ts5eOPMQGkY;FbjqD35Y z5kM#Vs{(1Kq@0pB{KC#gVZU5MplWXh5OP&X5g)P@E<=cZDKtc0+cps74y+fw2 zr=%9JqvoP6P!qX$$yjAeJE$_>mip}4x#L3f6yw)~h}9#vXEUm1h(3Lzxn4}Zrz{_6 zQ1Q~`4LwnyAlRRq_>K3JQ=hRwoV27SR_ZK6O4E{?W0Dd&kTG*-hXxOFgkD{e*t2cR z3pB+~XIUCD>oT|4`-mHyJ{9ti!DzH8sUBCrX9 z^Rv~G3xR+E039AujXXV*mx6n-h6Mz&hfid17K03kyTHDY6H5YJIf|z<)W7cDF5+n$ z2`t%iW^759M{sa^E-wEI1u|h`AuWTW<9L5K(OZ;>V+3#v3+s%#>&{Rixad>=+^Tlw zohU4pJFA~>hdwdz+uQ#6;QYFYgIeKEQkv^e$vP0&IWf3B+dEPTMM@BZKi0U6MZ?p# zfINe|5^wtK{<_{Fq=S56Ad2_oO2d4g2VHd|K!6L zw)Ho1r0e80Ld~<9wkyHiXtop5`aCe1wr&7)mN8Dhq%_J9(~fNWfSklrd>xS_{b(q@ z_>^hhh2KmUMSkfCn}i#@D`7+;9Icb1eebD#l#esT5*UY z%aqEx#>_*213shn9~H*%Wac;x5UNLi zMKI(Y22xMVGdi;_yx~ZSiK`a%dA3nW!8c7$Qe-Mf50ZTK`e4U{T)V}^kNP|_89B7E zb+AU7c`nRbAPW(Gy7TtjB8{_t(ey*l5;eeLC}QOP9$+w?ljRG+W_!Zo?JB*?XTum; z`cn9}4mU8kL<`7}z~Vc}f?!aUfEN>enp=xNvk@2ur$s^pe+F-Kn6MK&G4i55Ta~3$ z5hZ8tot3xCgA#g%Uq0!tif{U?4?NbnMxo|nE}!=UPK8bbstw!L@(ssEJUDe{w(XX# zEef;E+U`C5&!ZOdZ{W>E6c~d!g>6_Zu%azjF5LNFoQ#kWK~e4^)ST1e@0bI}mGtd0 z_E(>{7!jQ$obxI}6uSoFbG!QB7(L1^*Gr$(FoV(7u~${?oy(XDA`^| z1$9$9B8(f>tADA}U38X$zbb%qCKbnryUHEMeDeM+G)4DCv-K|{oXtj$XR{{B?B(qb}Gdx&qWDI7{rIxbW4$J_$IgqK85@o zQtl57=nw%HL?X+@T))X@GWC4i&HaJ9_~;BTuFuWu;Dt|z7|fm}#EoyLH)xbgW~763 z1OIzjj5#|7A&fxJw}!)RW?>Ge-E2~tQMH5Tz3T?Wp z`cL_swPko#v*|k>I~{a6Si6P1FF@UBivS508)6+m%_1wgYjX)ciWPt_*q8S{-Rd56 zU+c=w;`~?#vhk2qKPn)JVCE{)n;3yZJi@%VOTsY0ZqRRb|GlwS0&`>FpOhe$Op-CK zWg}2D2`DFaJA0!aVC@^n*&v3m{yw`+fNcqSEyy-sM0DboBi`(2*rN)pP+;8Sq8Ch% zo4tEa#ITE?x6=w9gkO6M{`_XbZRQMF2_&JNboHk5lu3zZr;xu)hI&NhXN&Ra2TzO4i9&1Vx%|MHbE8J7EOsouo*~ z(s-PWbarjzI2NF%5-o}<90>EkCOTE#km`Tn3X+vFjriL>91pm1VPhv@g^TF^JUL#~6kw zyL0O2zhR(mAv?N@uvIa6cR*>fS4ko%9K?wzI2-A^BLiwbpd^stVmwMmBETjHxfG%6 z{(%p5&(cEO)`5qBrv@CTbs@g7!Nyj83^I3ll1`uA5oRaWCCXZX(c#BaweRO%%Y?IX z?2k)s_-O#A1{L&f1!qR@Jhu8pvj+-V4pT5eIsroPLe{8zlv>Vgg5!|EAumFQa$pu0 zWCknaU~&< zVZd@yK9m+ZJSyR2r3BPj)mZDPXT{%G0rUDp&ELyVrKN)B0H)Lpp;Rb;G!3z92tf@^ ztw#iuuK|Qq@{>))-8TY)VSiaU3`yS8-IpuVbXMWE6{Hq~);VyhOSu3GFQWw3ZhoBS zy(z#`A0s0p8Zo@2K>!_?iSLvJ5B_p19YkGcNa#hKU6WXymF+5N}m z9EyN&YM|L|Uz0_ZBU!^PQrf+F$Qv|p&Dmnm9iV8?9X4pDPmI#toIdw0rzmp^$1(J4 zE0E)dPZ-;p35xqyXd~pJg@!R?b3p3}IC=J?xnhog?>%C++d3<+=L3_9tV{-+QlxMl z=uT$SsK25k3S^F2nH=_+T#V@OQ*7zdi;ygbj&-*lh?Y}BBe?e%HO_{x$MJ7wDs>mU zTIuwe$hJHf4Ji&)yyK%a|AO|=d8>!C3O}-LC4lGnrV2pMay2D@-aQFT!3HSAsyl=v z@7LSY-Fd)ZvO8DLm-Qb)LxHx*%&=YGS69%f{^EWqw)JlF88CJuRAQiy=bOR7UfiBR zJT%`XXu$roI7pxr;XjFrDemJJY5?xCFy6SO31MLX`0fC&5zsX;Uh33NHNCjJLVH|i zw_GxAF1bBqGUX{MIq~!s9}2JOKPMRr=e|Y~=*&77Jqx>236SmdFZa>lJ_jB;D~^TB zE;OrFJdG(wcr4v3-!lbsRW$GG3r7>Put2PQ^G?ths|9luw9uYR3E3fQ&qYiFW-`6% zUf=rr^y&~)e|?+gaHloO?t=9p&(Gop4!P+$&g}+1!@Bxj$s%ZJli}d2D8`sqNI^Kj zb8q5l39egzQ>o3dKligQPu_Y1uJ3Cl4tt*Ld+%y>429UOn2fk6d zAWBDK`HYQRr@%PsddS(7xzc15Sb8c>Z2ZMYMGs5UEG|Z->ENa+@N7L-}e-2ILoW}^tMJ{?-UJTe`wp4Bz`o8kPMQBJ#@Vzv2smPf?F$# zeppkERu-Zyd=Ds|+!~yT(#fpE3ivKnl1YAt5Bxg+YLgFaxJmuAN?8+soBdpzL^V#? zJ*8^x^4AvpsJWRxTV6jz3%to_XIQO&8KocNeV(@&FveZb|J_7A{N(@+F%QJ8Zuo*P z@4Jm&MANykTm^koN!Mx~s~CYGh=K?YA}~|1Mut1R`o_DiqzL;L5rc*0U-}}Jf7cg9 zt4q}WN5tsWGngV_rC&i3sct6`$2QM9dg^BbnDo<0io zZIx5(cHwH~xqY$p3J@@JaBksYl!I1la3T6#4SH?Kaj!hbDv4>y>HcPuc62H1Ao`Uf zqx!PBwDEGgSeItmac(%gxgE62bZ=CntYAb)XSJ+uC0oRKh3{;ZZB0C$?*62QKAeX) z!<8XJ##pD_(d{}!+o#E+?jA3!m?NR2QsA6u+BqXvObwSFr_O@=zIjbXa0%Ufa`4H{gbKS{+NsuHx(5c}Pg?wmtB3YVQN!H~JdG zUhJbPQ{aMw5EB@1MDd?|pdiFpw<%mGlUx*g|6!UN7MUPJYfbFDaZ$UV;nz+*M=cpE zNo`%gO~EXKV5JYZ#@eI@MG2$MC5KDxVj<#%l!uM=Iv4gw^IaduzZ3(n707?JQe0G0 zLeAKV8)buy6-2+&3kynTk5x?(F#Yg5oNDrOV>CNW$CIDqP@qw-m~v`DX43oWJJ=V( z*nP)Q=d1s@ZNZ%}TbS6lLRY&RYpV#xph(MHY4gnz?sa?(+5-cYg{w#-RVuXj=e!eT`1B%{6p@1~^O-~2k#R&l zXH?!pyb=1Jc|8k0xm)ktG3>p)h!#7VMtSu?9GK*+K9^f7#Oyg%cLSzX)ud2A6yiW? zU=Xf(1)jPM^rprkHEn@)d*BcQ?5R9^`I=kZek&6j1oS$EGxa(^s?+|-vgYpRl22BC zUx#X((ptkpYC;`vFjyV(6)A*W4m@$G5?l8GANlP*|Dip`wm<&-iHR6;pJ+^)>pqP< zVlCa`4{3Gs#~uV2@Z2WMW|KuDf-eaSoL|73-lZzqeUl!??61v7Qh9pY#7~#w@yv|H zaj-D5vLkNj^_A(?PN@|O;-wyh8>?|?ihxmII_1Pc5OZw!<81{#Yk=$3(#W^&e=F45 zk1RlISbC#S6Yddr^$`xq@WT60^Sr#8 za?XQ2Om!Uil8+QpQevM+cc%VmsYT_UDkRVhPdt>(J2(WacKKgAjtOcRr4bnQPr#0{ zGaU$3NER@z#LjZX4-+?j&OG0e6dU;jT6kyGE9*E*rmR@hG1vDTxTh6^n6o3UJidoY zM~|;DvnnGFIt&z3aiS1EEN{@J6v$rC;*{UgURb&TX2v#VUbR+q*nb5`vIk3D_lJ47 zgWGFLx2Vm!Xa?{blhQ&Y1@k>s!BJIbu<`eOlK)c)SdDiA{b5=GNhbS};egz%0Ag?e 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 atrJnalXlE*f8p3l$!UZO+YB} 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�_b0#SB>p>(D!KE7B4Zl^X}EsDLo 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


rrB}%IRak1^&WnpaL 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 z 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", "Vrx "] +edition = "2021" + +[dependencies] +std-shims = { version = "=0.1.4", default-features = false } + +thiserror = { version = "1", optional = true } + +zeroize = { version = "^1.5", default-features = false } +rand_core = { version = "0.6", default-features = false } + +sha2 = { version = "0.10", default-features = false } + +secp256k1 = { version = "0.27", default-features = false } +bitcoin = { version = "0.30", default-features = false, features = ["no-std"] } + +k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] } + +transcript = { package = "flexible-transcript", version = "0.3", features = ["recommended"], optional = true } +frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", features = ["secp256k1"], optional = true } + +hex = { version = "0.4", optional = true } +serde = { version = "1", features = ["derive"], optional = true } +serde_json = { version = "1", optional = true } + +[dev-dependencies] +frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] } + +tokio = { version = "1", features = ["full"] } + +[features] +std = [ + "std-shims/std", + + "thiserror", + + "zeroize/std", + "rand_core/std", + + "sha2/std", + + "secp256k1/std", + "bitcoin/std", + "bitcoin/serde", + + "k256/std", + + "transcript", + "frost", + + "hex", + "serde", + "serde_json", +] +hazmat = [] +default = ["std"] diff --git a/coins/bitcoin/LICENSE b/coins/bitcoin/LICENSE new file mode 100644 index 0000000..6779f0e --- /dev/null +++ b/coins/bitcoin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2023 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/coins/bitcoin/README.md b/coins/bitcoin/README.md new file mode 100644 index 0000000..a0c05d4 --- /dev/null +++ b/coins/bitcoin/README.md @@ -0,0 +1,4 @@ +# bitcoin-serai + +An application of [modular-frost](https://docs.rs/modular-frost) to Bitcoin +transactions, enabling extremely-efficient multisigs. diff --git a/coins/bitcoin/src/crypto.rs b/coins/bitcoin/src/crypto.rs new file mode 100644 index 0000000..9cf0c8c --- /dev/null +++ b/coins/bitcoin/src/crypto.rs @@ -0,0 +1,172 @@ +use k256::{ + elliptic_curve::sec1::{Tag, ToEncodedPoint}, + ProjectivePoint, +}; + +use bitcoin::key::XOnlyPublicKey; + +/// Get the x coordinate of a non-infinity, even point. Panics on invalid input. +pub fn x(key: &ProjectivePoint) -> [u8; 32] { + let encoded = key.to_encoded_point(true); + assert_eq!(encoded.tag(), Tag::CompressedEvenY, "x coordinate of odd key"); + (*encoded.x().expect("point at infinity")).into() +} + +/// Convert a non-infinity even point to a XOnlyPublicKey. Panics on invalid input. +pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey { + XOnlyPublicKey::from_slice(&x(key)).expect("x_only was passed a point which was infinity or odd") +} + +/// Make a point even by adding the generator until it is even. +/// +/// Returns the even point and the amount of additions required. +#[cfg(any(feature = "std", feature = "hazmat"))] +pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) { + let mut c = 0; + while key.to_encoded_point(true).tag() == Tag::CompressedOddY { + key += ProjectivePoint::GENERATOR; + c += 1; + } + (key, c) +} + +#[cfg(feature = "std")] +mod frost_crypto { + use core::fmt::Debug; + use std_shims::{sync::OnceLock, vec::Vec, io}; + + use zeroize::Zeroizing; + use rand_core::{RngCore, CryptoRng}; + + use sha2::{Digest, Sha256}; + use transcript::Transcript; + + use secp256k1::schnorr::Signature; + use k256::{elliptic_curve::ops::Reduce, U256, Scalar}; + + use frost::{ + curve::{Ciphersuite, Secp256k1}, + Participant, ThresholdKeys, ThresholdView, FrostError, + algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr}, + }; + + use super::*; + + /// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm. + /// + /// If passed an odd nonce, it will have the generator added until it is even. + /// + /// If the key is odd, this will panic. + #[derive(Clone, Copy, Debug)] + pub struct Hram; + + static TAG_HASH_CELL: OnceLock<[u8; 32]> = OnceLock::new(); + #[allow(non_snake_case)] + fn TAG_HASH() -> [u8; 32] { + *TAG_HASH_CELL.get_or_init(|| Sha256::digest(b"BIP0340/challenge").into()) + } + + #[allow(non_snake_case)] + impl HramTrait for Hram { + fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { + // Convert the nonce to be even + let (R, _) = make_even(*R); + + let mut data = Sha256::new(); + data.update(TAG_HASH()); + data.update(TAG_HASH()); + data.update(x(&R)); + data.update(x(A)); + data.update(m); + + Scalar::reduce(U256::from_be_slice(&data.finalize())) + } + } + + /// BIP-340 Schnorr signature algorithm. + /// + /// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic. + #[derive(Clone)] + pub struct Schnorr(FrostSchnorr); + impl Schnorr { + /// Construct a Schnorr algorithm continuing the specified transcript. + pub fn new(transcript: T) -> Schnorr { + Schnorr(FrostSchnorr::new(transcript)) + } + } + + impl Algorithm for Schnorr { + type Transcript = T; + type Addendum = (); + type Signature = Signature; + + fn transcript(&mut self) -> &mut Self::Transcript { + self.0.transcript() + } + + fn nonces(&self) -> Vec> { + self.0.nonces() + } + + fn preprocess_addendum( + &mut self, + rng: &mut R, + keys: &ThresholdKeys, + ) { + self.0.preprocess_addendum(rng, keys) + } + + fn read_addendum(&self, reader: &mut R) -> io::Result { + self.0.read_addendum(reader) + } + + fn process_addendum( + &mut self, + view: &ThresholdView, + i: Participant, + addendum: (), + ) -> Result<(), FrostError> { + self.0.process_addendum(view, i, addendum) + } + + fn sign_share( + &mut self, + params: &ThresholdView, + nonce_sums: &[Vec<::G>], + nonces: Vec::F>>, + msg: &[u8], + ) -> ::F { + self.0.sign_share(params, nonce_sums, nonces, msg) + } + + #[must_use] + fn verify( + &self, + group_key: ProjectivePoint, + nonces: &[Vec], + sum: Scalar, + ) -> Option { + self.0.verify(group_key, nonces, sum).map(|mut sig| { + // Make the R of the final signature even + let offset; + (sig.R, offset) = make_even(sig.R); + // s = r + cx. Since we added to the r, add to s + sig.s += Scalar::from(offset); + // Convert to a secp256k1 signature + Signature::from_slice(&sig.serialize()[1 ..]) + .expect("couldn't convert SchnorrSignature to Signature") + }) + } + + fn verify_share( + &self, + verification_share: ProjectivePoint, + nonces: &[Vec], + share: Scalar, + ) -> Result, ()> { + self.0.verify_share(verification_share, nonces, share) + } + } +} +#[cfg(feature = "std")] +pub use frost_crypto::*; diff --git a/coins/bitcoin/src/lib.rs b/coins/bitcoin/src/lib.rs new file mode 100644 index 0000000..c5b1956 --- /dev/null +++ b/coins/bitcoin/src/lib.rs @@ -0,0 +1,21 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(not(feature = "std"))] +extern crate alloc; + +/// The bitcoin Rust library. +pub use bitcoin; + +/// Cryptographic helpers. +#[cfg(feature = "hazmat")] +pub mod crypto; +#[cfg(not(feature = "hazmat"))] +pub(crate) mod crypto; + +/// Wallet functionality to create transactions. +pub mod wallet; + +#[cfg(test)] +mod tests; diff --git a/coins/bitcoin/src/tests/crypto.rs b/coins/bitcoin/src/tests/crypto.rs new file mode 100644 index 0000000..f827591 --- /dev/null +++ b/coins/bitcoin/src/tests/crypto.rs @@ -0,0 +1,47 @@ +use rand_core::OsRng; + +use sha2::{Digest, Sha256}; + +use secp256k1::{Secp256k1 as BContext, Message}; + +use k256::Scalar; +use transcript::{Transcript, RecommendedTranscript}; +use frost::{ + curve::Secp256k1, + Participant, + tests::{algorithm_machines, key_gen, sign}, +}; + +use crate::{ + bitcoin::hashes::{Hash as HashTrait, sha256::Hash}, + crypto::{x_only, make_even, Schnorr}, +}; + +#[test] +fn test_algorithm() { + let mut keys = key_gen::<_, Secp256k1>(&mut OsRng); + const MESSAGE: &[u8] = b"Hello, World!"; + + for (_, keys) in keys.iter_mut() { + let (_, offset) = make_even(keys.group_key()); + *keys = keys.offset(Scalar::from(offset)); + } + + let algo = + Schnorr::::new(RecommendedTranscript::new(b"bitcoin-serai sign test")); + let sig = sign( + &mut OsRng, + algo.clone(), + keys.clone(), + algorithm_machines(&mut OsRng, algo, &keys), + &Sha256::digest(MESSAGE), + ); + + BContext::new() + .verify_schnorr( + &sig, + &Message::from(Hash::hash(MESSAGE)), + &x_only(&keys[&Participant::new(1).unwrap()].group_key()), + ) + .unwrap() +} diff --git a/coins/bitcoin/src/tests/mod.rs b/coins/bitcoin/src/tests/mod.rs new file mode 100644 index 0000000..b3a33b1 --- /dev/null +++ b/coins/bitcoin/src/tests/mod.rs @@ -0,0 +1 @@ +mod crypto; diff --git a/coins/bitcoin/src/wallet/mod.rs b/coins/bitcoin/src/wallet/mod.rs new file mode 100644 index 0000000..abc10ca --- /dev/null +++ b/coins/bitcoin/src/wallet/mod.rs @@ -0,0 +1,185 @@ +use std_shims::{ + vec::Vec, + collections::HashMap, + io::{self, Write}, +}; +#[cfg(feature = "std")] +use std_shims::io::Read; + +use k256::{ + elliptic_curve::sec1::{Tag, ToEncodedPoint}, + Scalar, ProjectivePoint, +}; + +#[cfg(feature = "std")] +use frost::{ + curve::{Ciphersuite, Secp256k1}, + ThresholdKeys, +}; + +use bitcoin::{ + consensus::encode::serialize, key::TweakedPublicKey, address::Payload, OutPoint, ScriptBuf, + TxOut, Transaction, Block, +}; +#[cfg(feature = "std")] +use bitcoin::consensus::encode::Decodable; + +use crate::crypto::x_only; +#[cfg(feature = "std")] +use crate::crypto::make_even; + +#[cfg(feature = "std")] +mod send; +#[cfg(feature = "std")] +pub use send::*; + +/// Tweak keys to ensure they're usable with Bitcoin. +/// +/// Taproot keys, which these keys are used as, must be even. This offsets the keys until they're +/// even. +#[cfg(feature = "std")] +pub fn tweak_keys(keys: &ThresholdKeys) -> ThresholdKeys { + let (_, offset) = make_even(keys.group_key()); + keys.offset(Scalar::from(offset)) +} + +/// Return the Taproot address payload for a public key. +/// +/// If the key is odd, this will return None. +pub fn address_payload(key: ProjectivePoint) -> Option { + if key.to_encoded_point(true).tag() != Tag::CompressedEvenY { + return None; + } + + Some(Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key)))) +} + +/// A spendable output. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct ReceivedOutput { + // The scalar offset to obtain the key usable to spend this output. + offset: Scalar, + // The output to spend. + output: TxOut, + // The TX ID and vout of the output to spend. + outpoint: OutPoint, +} + +impl ReceivedOutput { + /// The offset for this output. + pub fn offset(&self) -> Scalar { + self.offset + } + + /// The outpoint for this output. + pub fn outpoint(&self) -> &OutPoint { + &self.outpoint + } + + /// The value of this output. + pub fn value(&self) -> u64 { + self.output.value + } + + /// Read a ReceivedOutput from a generic satisfying Read. + #[cfg(feature = "std")] + pub fn read(r: &mut R) -> io::Result { + Ok(ReceivedOutput { + offset: Secp256k1::read_F(r)?, + output: TxOut::consensus_decode(r) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid TxOut"))?, + outpoint: OutPoint::consensus_decode(r) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid OutPoint"))?, + }) + } + + /// Write a ReceivedOutput to a generic satisfying Write. + pub fn write(&self, w: &mut W) -> io::Result<()> { + w.write_all(&self.offset.to_bytes())?; + w.write_all(&serialize(&self.output))?; + w.write_all(&serialize(&self.outpoint)) + } + + /// Serialize a ReceivedOutput to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut res = Vec::new(); + self.write(&mut res).unwrap(); + res + } +} + +/// A transaction scanner capable of being used with HDKD schemes. +#[derive(Clone, Debug)] +pub struct Scanner { + key: ProjectivePoint, + scripts: HashMap, +} + +impl Scanner { + /// Construct a Scanner for a key. + /// + /// Returns None if this key can't be scanned for. + pub fn new(key: ProjectivePoint) -> Option { + let mut scripts = HashMap::new(); + scripts.insert(address_payload(key)?.script_pubkey(), Scalar::ZERO); + Some(Scanner { key, scripts }) + } + + /// Register an offset to scan for. + /// + /// Due to Bitcoin's requirement that points are even, not every offset may be used. + /// If an offset isn't usable, it will be incremented until it is. If this offset is already + /// present, None is returned. Else, Some(offset) will be, with the used offset. + /// + /// This means offsets are surjective, not bijective, and the order offsets are registered in + /// may determine the validity of future offsets. + pub fn register_offset(&mut self, mut offset: Scalar) -> Option { + // This loop will terminate as soon as an even point is found, with any point having a ~50% + // chance of being even + // That means this should terminate within a very small amount of iterations + loop { + match address_payload(self.key + (ProjectivePoint::GENERATOR * offset)) { + Some(address) => { + let script = address.script_pubkey(); + if self.scripts.contains_key(&script) { + None?; + } + self.scripts.insert(script, offset); + return Some(offset); + } + None => offset += Scalar::ONE, + } + } + } + + /// Scan a transaction. + pub fn scan_transaction(&self, tx: &Transaction) -> Vec { + let mut res = Vec::new(); + for (vout, output) in tx.output.iter().enumerate() { + // If the vout index exceeds 2**32, stop scanning outputs + let Ok(vout) = u32::try_from(vout) else { break }; + + if let Some(offset) = self.scripts.get(&output.script_pubkey) { + res.push(ReceivedOutput { + offset: *offset, + output: output.clone(), + outpoint: OutPoint::new(tx.txid(), vout), + }); + } + } + res + } + + /// Scan a block. + /// + /// This will also scan the coinbase transaction which is bound by maturity. If received outputs + /// must be immediately spendable, a post-processing pass is needed to remove those outputs. + /// Alternatively, scan_transaction can be called on `block.txdata[1 ..]`. + pub fn scan_block(&self, block: &Block) -> Vec { + let mut res = Vec::new(); + for tx in &block.txdata { + res.extend(self.scan_transaction(tx)); + } + res + } +} diff --git a/coins/bitcoin/src/wallet/send.rs b/coins/bitcoin/src/wallet/send.rs new file mode 100644 index 0000000..a7e4abc --- /dev/null +++ b/coins/bitcoin/src/wallet/send.rs @@ -0,0 +1,420 @@ +use std_shims::{ + io::{self, Read}, + collections::HashMap, +}; + +use thiserror::Error; + +use rand_core::{RngCore, CryptoRng}; + +use transcript::{Transcript, RecommendedTranscript}; + +use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar}; +use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*}; + +use bitcoin::{ + sighash::{TapSighashType, SighashCache, Prevouts}, + absolute::LockTime, + script::{PushBytesBuf, ScriptBuf}, + OutPoint, Sequence, Witness, TxIn, TxOut, Transaction, Address, +}; + +use crate::{ + crypto::Schnorr, + wallet::{ReceivedOutput, address_payload}, +}; + +#[rustfmt::skip] +// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.h#L27 +const MAX_STANDARD_TX_WEIGHT: u64 = 400_000; + +#[rustfmt::skip] +// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.cpp#L26-L63 +// As the above notes, a lower amount may not be considered dust if contained in a SegWit output +// This doesn't bother with delineation due to how marginal these values are, and because it isn't +// worth the complexity to implement differentation +const DUST: u64 = 546; + +#[rustfmt::skip] +// The constant is from: +// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.h#L56-L57 +// It's used here: +// https://github.com/bitcoin/bitcoin/blob/296735f7638749906243c9e203df7bd024493806/src/net_processing.cpp#L5386-L5390 +// Peers won't relay TXs below the filter's fee rate, yet they calculate the fee not against weight yet vsize +// https://github.com/bitcoin/bitcoin/blob/296735f7638749906243c9e203df7bd024493806/src/net_processing.cpp#L5721-L5732 +// And then the fee itself is fee per thousand units, not fee per unit +// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/feerate.cpp#L23-L37 +const MIN_FEE_PER_KILO_VSIZE: u64 = 1000; + +#[derive(Clone, PartialEq, Eq, Debug, Error)] +pub enum TransactionError { + #[error("no inputs were specified")] + NoInputs, + #[error("no outputs were created")] + NoOutputs, + #[error("a specified payment's amount was less than bitcoin's required minimum")] + DustPayment, + #[error("too much data was specified")] + TooMuchData, + #[error("fee was too low to pass the default minimum fee rate")] + TooLowFee, + #[error("not enough funds for these payments")] + NotEnoughFunds, + #[error("transaction was too large")] + TooLargeTransaction, +} + +/// A signable transaction, clone-able across attempts. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct SignableTransaction { + tx: Transaction, + offsets: Vec, + prevouts: Vec, + needed_fee: u64, +} + +impl SignableTransaction { + fn calculate_weight(inputs: usize, payments: &[(Address, u64)], change: Option<&Address>) -> u64 { + // Expand this a full transaction in order to use the bitcoin library's weight function + let mut tx = Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![ + TxIn { + // This is a fixed size + // See https://developer.bitcoin.org/reference/transactions.html#raw-transaction-format + previous_output: OutPoint::default(), + // This is empty for a Taproot spend + script_sig: ScriptBuf::new(), + // This is fixed size, yet we do use Sequence::MAX + sequence: Sequence::MAX, + // Our witnesses contains a single 64-byte signature + witness: Witness::from_slice(&[vec![0; 64]]) + }; + inputs + ], + output: payments + .iter() + // The payment is a fixed size so we don't have to use it here + // The script pub key is not of a fixed size and does have to be used here + .map(|payment| TxOut { value: payment.1, script_pubkey: payment.0.script_pubkey() }) + .collect(), + }; + if let Some(change) = change { + // Use a 0 value since we're currently unsure what the change amount will be, and since + // the value is fixed size (so any value could be used here) + tx.output.push(TxOut { value: 0, script_pubkey: change.script_pubkey() }); + } + u64::try_from(tx.weight()).unwrap() + } + + /// Returns the fee necessary for this transaction to achieve the fee rate specified at + /// construction. + /// + /// The actual fee this transaction will use is `sum(inputs) - sum(outputs)`. + pub fn needed_fee(&self) -> u64 { + self.needed_fee + } + + /// Create a new SignableTransaction. + /// + /// If a change address is specified, any leftover funds will be sent to it if the leftover funds + /// exceed the minimum output amount. If a change address isn't specified, all leftover funds + /// will become part of the paid fee. + /// + /// If data is specified, an OP_RETURN output will be added with it. + pub fn new( + mut inputs: Vec, + payments: &[(Address, u64)], + change: Option
, + data: Option>, + fee_per_weight: u64, + ) -> Result { + if inputs.is_empty() { + Err(TransactionError::NoInputs)?; + } + + if payments.is_empty() && change.is_none() && data.is_none() { + Err(TransactionError::NoOutputs)?; + } + + for (_, amount) in payments { + if *amount < DUST { + Err(TransactionError::DustPayment)?; + } + } + + if data.as_ref().map(|data| data.len()).unwrap_or(0) > 80 { + Err(TransactionError::TooMuchData)?; + } + + let input_sat = inputs.iter().map(|input| input.output.value).sum::(); + let offsets = inputs.iter().map(|input| input.offset).collect(); + let tx_ins = inputs + .iter() + .map(|input| TxIn { + previous_output: input.outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }) + .collect::>(); + + let payment_sat = payments.iter().map(|payment| payment.1).sum::(); + let mut tx_outs = payments + .iter() + .map(|payment| TxOut { value: payment.1, script_pubkey: payment.0.script_pubkey() }) + .collect::>(); + + // Add the OP_RETURN output + if let Some(data) = data { + tx_outs.push(TxOut { + value: 0, + script_pubkey: ScriptBuf::new_op_return( + &PushBytesBuf::try_from(data) + .expect("data didn't fit into PushBytes depsite being checked"), + ), + }) + } + + let mut weight = Self::calculate_weight(tx_ins.len(), payments, None); + let mut needed_fee = fee_per_weight * weight; + + // "Virtual transaction size" is weight ceildiv 4 per + // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki + + // https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/ + // src/policy/policy.cpp#L295-L298 + // implements this as expected + + // Technically, it takes whatever's greater, the weight or the amount of signature operatons + // multiplied by DEFAULT_BYTES_PER_SIGOP (20) + // We only use 1 signature per input, and our inputs have a weight exceeding 20 + // Accordingly, our inputs' weight will always be greater than the cost of the signature ops + let vsize = (weight + 3) / 4; + // Technically, if there isn't change, this TX may still pay enough of a fee to pass the + // minimum fee. Such edge cases aren't worth programming when they go against intent, as the + // specified fee rate is too low to be valid + if needed_fee < ((MIN_FEE_PER_KILO_VSIZE * vsize) / 1000) { + Err(TransactionError::TooLowFee)?; + } + + if input_sat < (payment_sat + needed_fee) { + Err(TransactionError::NotEnoughFunds)?; + } + + // If there's a change address, check if there's change to give it + if let Some(change) = change.as_ref() { + let weight_with_change = Self::calculate_weight(tx_ins.len(), payments, Some(change)); + let fee_with_change = fee_per_weight * weight_with_change; + if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) { + if value >= DUST { + tx_outs.push(TxOut { value, script_pubkey: change.script_pubkey() }); + weight = weight_with_change; + needed_fee = fee_with_change; + } + } + } + + if tx_outs.is_empty() { + Err(TransactionError::NoOutputs)?; + } + + if weight > MAX_STANDARD_TX_WEIGHT { + Err(TransactionError::TooLargeTransaction)?; + } + + Ok(SignableTransaction { + tx: Transaction { version: 2, lock_time: LockTime::ZERO, input: tx_ins, output: tx_outs }, + offsets, + prevouts: inputs.drain(..).map(|input| input.output).collect(), + needed_fee, + }) + } + + /// Create a multisig machine for this transaction. + /// + /// Returns None if the wrong keys are used. + pub fn multisig( + self, + keys: ThresholdKeys, + mut transcript: RecommendedTranscript, + ) -> Option { + transcript.domain_separate(b"bitcoin_transaction"); + transcript.append_message(b"root_key", keys.group_key().to_encoded_point(true).as_bytes()); + + // Transcript the inputs and outputs + let tx = &self.tx; + for input in &tx.input { + transcript.append_message(b"input_hash", input.previous_output.txid); + transcript.append_message(b"input_output_index", input.previous_output.vout.to_le_bytes()); + } + for payment in &tx.output { + transcript.append_message(b"output_script", payment.script_pubkey.as_bytes()); + transcript.append_message(b"output_amount", payment.value.to_le_bytes()); + } + + let mut sigs = vec![]; + for i in 0 .. tx.input.len() { + let mut transcript = transcript.clone(); + // This unwrap is safe since any transaction with this many inputs violates the maximum + // size allowed under standards, which this lib will error on creation of + transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes()); + + let offset = keys.clone().offset(self.offsets[i]); + if address_payload(offset.group_key())?.script_pubkey() != self.prevouts[i].script_pubkey { + None?; + } + + sigs.push(AlgorithmMachine::new( + Schnorr::new(transcript), + keys.clone().offset(self.offsets[i]), + )); + } + + Some(TransactionMachine { tx: self, sigs }) + } +} + +/// A FROST signing machine to produce a Bitcoin transaction. +/// +/// This does not support caching its preprocess. When sign is called, the message must be empty. +/// This will panic if either `cache` is called or the message isn't empty. +pub struct TransactionMachine { + tx: SignableTransaction, + sigs: Vec>>, +} + +impl PreprocessMachine for TransactionMachine { + type Preprocess = Vec>; + type Signature = Transaction; + type SignMachine = TransactionSignMachine; + + fn preprocess( + mut self, + rng: &mut R, + ) -> (Self::SignMachine, Self::Preprocess) { + let mut preprocesses = Vec::with_capacity(self.sigs.len()); + let sigs = self + .sigs + .drain(..) + .map(|sig| { + let (sig, preprocess) = sig.preprocess(rng); + preprocesses.push(preprocess); + sig + }) + .collect(); + + (TransactionSignMachine { tx: self.tx, sigs }, preprocesses) + } +} + +pub struct TransactionSignMachine { + tx: SignableTransaction, + sigs: Vec>>, +} + +impl SignMachine for TransactionSignMachine { + type Params = (); + type Keys = ThresholdKeys; + type Preprocess = Vec>; + type SignatureShare = Vec>; + type SignatureMachine = TransactionSignatureMachine; + + fn cache(self) -> CachedPreprocess { + unimplemented!( + "Bitcoin transactions don't support caching their preprocesses due to {}", + "being already bound to a specific transaction" + ); + } + + fn from_cache( + _: (), + _: ThresholdKeys, + _: CachedPreprocess, + ) -> Result { + unimplemented!( + "Bitcoin transactions don't support caching their preprocesses due to {}", + "being already bound to a specific transaction" + ); + } + + fn read_preprocess(&self, reader: &mut R) -> io::Result { + self.sigs.iter().map(|sig| sig.read_preprocess(reader)).collect() + } + + fn sign( + mut self, + commitments: HashMap, + msg: &[u8], + ) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> { + if !msg.is_empty() { + panic!("message was passed to the TransactionMachine when it generates its own"); + } + + let commitments = (0 .. self.sigs.len()) + .map(|c| { + commitments + .iter() + .map(|(l, commitments)| (*l, commitments[c].clone())) + .collect::>() + }) + .collect::>(); + + let mut cache = SighashCache::new(&self.tx.tx); + // Sign committing to all inputs + let prevouts = Prevouts::All(&self.tx.prevouts); + + let mut shares = Vec::with_capacity(self.sigs.len()); + let sigs = self + .sigs + .drain(..) + .enumerate() + .map(|(i, sig)| { + let (sig, share) = sig.sign( + commitments[i].clone(), + cache + .taproot_key_spend_signature_hash(i, &prevouts, TapSighashType::Default) + // This should never happen since the inputs align with the TX the cache was + // constructed with, and because i is always < prevouts.len() + .expect("taproot_key_spend_signature_hash failed to return a hash") + .as_ref(), + )?; + shares.push(share); + Ok(sig) + }) + .collect::>()?; + + Ok((TransactionSignatureMachine { tx: self.tx.tx, sigs }, shares)) + } +} + +pub struct TransactionSignatureMachine { + tx: Transaction, + sigs: Vec>>, +} + +impl SignatureMachine for TransactionSignatureMachine { + type SignatureShare = Vec>; + + fn read_share(&self, reader: &mut R) -> io::Result { + self.sigs.iter().map(|sig| sig.read_share(reader)).collect() + } + + fn complete( + mut self, + mut shares: HashMap, + ) -> Result { + for (input, schnorr) in self.tx.input.iter_mut().zip(self.sigs.drain(..)) { + let sig = schnorr.complete( + shares.iter_mut().map(|(l, shares)| (*l, shares.remove(0))).collect::>(), + )?; + + let mut witness = Witness::new(); + witness.push(sig.as_ref()); + input.witness = witness; + } + + Ok(self.tx) + } +} diff --git a/coins/bitcoin/tests/runner.rs b/coins/bitcoin/tests/runner.rs new file mode 100644 index 0000000..6491d53 --- /dev/null +++ b/coins/bitcoin/tests/runner.rs @@ -0,0 +1,48 @@ +use std::sync::OnceLock; + +use bitcoin_serai::rpc::Rpc; + +use tokio::sync::Mutex; + +static SEQUENTIAL_CELL: OnceLock> = OnceLock::new(); +#[allow(non_snake_case)] +pub fn SEQUENTIAL() -> &'static Mutex<()> { + SEQUENTIAL_CELL.get_or_init(|| Mutex::new(())) +} + +#[allow(dead_code)] +pub(crate) async fn rpc() -> Rpc { + let rpc = Rpc::new("http://serai:seraidex@127.0.0.1:18443".to_string()).await.unwrap(); + + // If this node has already been interacted with, clear its chain + if rpc.get_latest_block_number().await.unwrap() > 0 { + rpc + .rpc_call( + "invalidateblock", + serde_json::json!([hex::encode(rpc.get_block_hash(1).await.unwrap())]), + ) + .await + .unwrap() + } + + rpc +} + +#[macro_export] +macro_rules! async_sequential { + ($(async fn $name: ident() $body: block)*) => { + $( + #[tokio::test] + async fn $name() { + let guard = runner::SEQUENTIAL().lock().await; + let local = tokio::task::LocalSet::new(); + local.run_until(async move { + if let Err(err) = tokio::task::spawn_local(async move { $body }).await { + drop(guard); + Err(err).unwrap() + } + }).await; + } + )* + } +} diff --git a/coins/bitcoin/tests/wallet.rs b/coins/bitcoin/tests/wallet.rs new file mode 100644 index 0000000..1b7bfe7 --- /dev/null +++ b/coins/bitcoin/tests/wallet.rs @@ -0,0 +1,92 @@ +use std::collections::HashMap; + +use rand_core::{RngCore, OsRng}; + +use transcript::{Transcript, RecommendedTranscript}; + +use k256::{ + elliptic_curve::{ + group::{ff::Field, Group}, + sec1::{Tag, ToEncodedPoint}, + }, + Scalar, ProjectivePoint, +}; +use frost::{ + curve::Secp256k1, + Participant, ThresholdKeys, + tests::{THRESHOLD, key_gen, sign_without_caching}, +}; + +use bitcoin_serai::{ + bitcoin::{ + hashes::Hash as HashTrait, + blockdata::opcodes::all::OP_RETURN, + script::{PushBytesBuf, Instruction, Instructions, Script}, + address::NetworkChecked, + OutPoint, TxOut, Transaction, Network, Address, + }, + wallet::{ + tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError, SignableTransaction, + }, +}; + +const FEE: u64 = 20; + +fn is_even(key: ProjectivePoint) -> bool { + key.to_encoded_point(true).tag() == Tag::CompressedEvenY +} + +fn keys() -> (HashMap>, ProjectivePoint) { + let mut keys = key_gen(&mut OsRng); + for (_, keys) in keys.iter_mut() { + *keys = tweak_keys(keys); + } + let key = keys.values().next().unwrap().group_key(); + (keys, key) +} + +fn sign( + keys: &HashMap>, + tx: SignableTransaction, +) -> Transaction { + let mut machines = HashMap::new(); + for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) { + machines.insert( + i, + tx.clone() + .multisig(keys[&i].clone(), RecommendedTranscript::new(b"bitcoin-serai Test Transaction")) + .unwrap(), + ); + } + sign_without_caching(&mut OsRng, machines, &[]) +} + +#[test] +fn test_tweak_keys() { + let mut even = false; + let mut odd = false; + + // Generate keys until we get an even set and an odd set + while !(even && odd) { + let mut keys = key_gen(&mut OsRng).drain().next().unwrap().1; + if is_even(keys.group_key()) { + // Tweaking should do nothing + assert_eq!(tweak_keys(&keys).group_key(), keys.group_key()); + + even = true; + } else { + let tweaked = tweak_keys(&keys).group_key(); + assert_ne!(tweaked, keys.group_key()); + // Tweaking should produce an even key + assert!(is_even(tweaked)); + + // Verify it uses the smallest possible offset + while keys.group_key().to_encoded_point(true).tag() == Tag::CompressedOddY { + keys = keys.offset(Scalar::ONE); + } + assert_eq!(tweaked, keys.group_key()); + + odd = true; + } + } +} diff --git a/crypto/dkg/Cargo.toml b/crypto/dkg/Cargo.toml new file mode 100644 index 0000000..883d529 --- /dev/null +++ b/crypto/dkg/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "dkg" +version = "0.5.1" +description = "Distributed key generation over ff/group" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg" +authors = ["Luke Parker "] +keywords = ["dkg", "multisig", "threshold", "ff", "group"] +edition = "2021" +rust-version = "1.70" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +thiserror = { version = "1", default-features = false, optional = true } + +rand_core = { version = "0.6", default-features = false } + +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } + +std-shims = { version = "0.1", default-features = false } + +serde = { version = "1", features = ["derive"], optional = true } + +transcript = { package = "flexible-transcript", version = "^0.3.2", default-features = false, features = ["recommended"] } +chacha20 = { version = "0.9", default-features = false, features = ["zeroize"] } + +ciphersuite = { version = "^0.4.1", default-features = false } +multiexp = { version = "0.4", default-features = false } + +schnorr = { package = "schnorr-signatures", version = "^0.5.1", default-features = false } +dleq = { version = "^0.4.1", default-features = false } + +[dev-dependencies] +rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } +ciphersuite = { version = "0.4", default-features = false, features = ["ristretto"] } + +[features] +std = [ + "thiserror", + "rand_core/std", + + "std-shims/std", + + "ciphersuite/std", + + "multiexp/batch", + "multiexp/std", + + "schnorr/std", + "dleq/serialize" +] +serde = ["dep:serde"] +tests = ["rand_core/getrandom"] +default = ["std"] diff --git a/crypto/dkg/LICENSE b/crypto/dkg/LICENSE new file mode 100644 index 0000000..be67c32 --- /dev/null +++ b/crypto/dkg/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-2023 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crypto/dkg/README.md b/crypto/dkg/README.md new file mode 100644 index 0000000..27e3412 --- /dev/null +++ b/crypto/dkg/README.md @@ -0,0 +1,16 @@ +# Distributed Key Generation + +A collection of implementations of various distributed key generation protocols. + +All included protocols resolve into the provided `Threshold` types, intended to +enable their modularity. Additional utilities around these types, such as +promotion from one generator to another, are also provided. + +Currently, the only included protocol is the two-round protocol from the +[FROST paper](https://eprint.iacr.org/2020/852). + +This library was +[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf), +culminating in commit +[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06). +Any subsequent changes have not undergone auditing. diff --git a/crypto/dkg/src/encryption.rs b/crypto/dkg/src/encryption.rs new file mode 100644 index 0000000..1393290 --- /dev/null +++ b/crypto/dkg/src/encryption.rs @@ -0,0 +1,487 @@ +use core::{ops::Deref, fmt}; +use std::{io, collections::HashMap}; + +use thiserror::Error; + +use zeroize::{Zeroize, Zeroizing}; +use rand_core::{RngCore, CryptoRng}; + +use chacha20::{ + cipher::{crypto_common::KeyIvInit, StreamCipher}, + Key as Cc20Key, Nonce as Cc20Iv, ChaCha20, +}; + +use transcript::{Transcript, RecommendedTranscript}; + +#[cfg(test)] +use ciphersuite::group::ff::Field; +use ciphersuite::{group::GroupEncoding, Ciphersuite}; +use multiexp::BatchVerifier; + +use schnorr::SchnorrSignature; +use dleq::DLEqProof; + +use crate::{Participant, ThresholdParams}; + +mod sealed { + use super::*; + + pub trait ReadWrite: Sized { + fn read(reader: &mut R, params: ThresholdParams) -> io::Result; + fn write(&self, writer: &mut W) -> io::Result<()>; + + fn serialize(&self) -> Vec { + let mut buf = vec![]; + self.write(&mut buf).unwrap(); + buf + } + } + + impl ReadWrite for () { + fn read(_: &mut R, _: ThresholdParams) -> io::Result { + Ok(()) + } + fn write(&self, _: &mut W) -> io::Result<()> { + Ok(()) + } + } + + pub trait Message: Clone + PartialEq + Eq + fmt::Debug + Zeroize + ReadWrite {} + impl Message for M {} + + pub trait Encryptable: Clone + AsRef<[u8]> + AsMut<[u8]> + Zeroize + ReadWrite {} + impl + AsMut<[u8]> + Zeroize + ReadWrite> Encryptable for E {} +} +pub(crate) use sealed::*; + +/// Wraps a message with a key to use for encryption in the future. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct EncryptionKeyMessage { + msg: M, + enc_key: C::G, +} + +// Doesn't impl ReadWrite so that doesn't need to be imported +impl EncryptionKeyMessage { + pub fn read(reader: &mut R, params: ThresholdParams) -> io::Result { + Ok(Self { msg: M::read(reader, params)?, enc_key: C::read_G(reader)? }) + } + + pub fn write(&self, writer: &mut W) -> io::Result<()> { + self.msg.write(writer)?; + writer.write_all(self.enc_key.to_bytes().as_ref()) + } + + pub fn serialize(&self) -> Vec { + let mut buf = vec![]; + self.write(&mut buf).unwrap(); + buf + } + + #[cfg(any(test, feature = "tests"))] + pub(crate) fn enc_key(&self) -> C::G { + self.enc_key + } +} + +/// An encrypted message, with a per-message encryption key enabling revealing specific messages +/// without side effects. +#[derive(Clone, Zeroize)] +pub struct EncryptedMessage { + key: C::G, + // Also include a proof-of-possession for the key. + // If this proof-of-possession wasn't here, Eve could observe Alice encrypt to Bob with key X, + // then send Bob a message also claiming to use X. + // While Eve's message would fail to meaningfully decrypt, Bob would then use this to create a + // blame argument against Eve. When they do, they'd reveal bX, revealing Alice's message to Bob. + // This is a massive side effect which could break some protocols, in the worst case. + // While Eve can still reuse their own keys, causing Bob to leak all messages by revealing for + // any single one, that's effectively Eve revealing themselves, and not considered relevant. + pop: SchnorrSignature, + msg: Zeroizing, +} + +fn ecdh(private: &Zeroizing, public: C::G) -> Zeroizing { + Zeroizing::new(public * private.deref()) +} + +// Each ecdh must be distinct. Reuse of an ecdh for multiple ciphers will cause the messages to be +// leaked. +fn cipher(context: &str, ecdh: &Zeroizing) -> ChaCha20 { + // Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly + // TODO: https://github.com/serai-dex/serai/issues/151 + let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0.2"); + transcript.append_message(b"context", context.as_bytes()); + + transcript.domain_separate(b"encryption_key"); + + let mut ecdh = ecdh.to_bytes(); + transcript.append_message(b"shared_key", ecdh.as_ref()); + ecdh.as_mut().zeroize(); + + let zeroize = |buf: &mut [u8]| buf.zeroize(); + + let mut key = Cc20Key::default(); + let mut challenge = transcript.challenge(b"key"); + key.copy_from_slice(&challenge[.. 32]); + zeroize(challenge.as_mut()); + + // Since the key is single-use, it doesn't matter what we use for the IV + // The isssue is key + IV reuse. If we never reuse the key, we can't have the opportunity to + // reuse a nonce + // Use a static IV in acknowledgement of this + let mut iv = Cc20Iv::default(); + // The \0 is to satisfy the length requirement (12), not to be null terminated + iv.copy_from_slice(b"DKG IV v0.2\0"); + + // ChaCha20 has the same commentary as the transcript regarding ZAlloc + // TODO: https://github.com/serai-dex/serai/issues/151 + let res = ChaCha20::new(&key, &iv); + zeroize(key.as_mut()); + res +} + +fn encrypt( + rng: &mut R, + context: &str, + from: Participant, + to: C::G, + mut msg: Zeroizing, +) -> EncryptedMessage { + /* + The following code could be used to replace the requirement on an RNG here. + It's just currently not an issue to require taking in an RNG here. + let last = self.last_enc_key.to_bytes(); + self.last_enc_key = C::hash_to_F(b"encryption_base", last.as_ref()); + let key = C::hash_to_F(b"encryption_key", last.as_ref()); + last.as_mut().zeroize(); + */ + + // Generate a new key for this message, satisfying cipher's requirement of distinct keys per + // message, and enabling revealing this message without revealing any others + let key = Zeroizing::new(C::random_nonzero_F(rng)); + cipher::(context, &ecdh::(&key, to)).apply_keystream(msg.as_mut().as_mut()); + + let pub_key = C::generator() * key.deref(); + let nonce = Zeroizing::new(C::random_nonzero_F(rng)); + let pub_nonce = C::generator() * nonce.deref(); + EncryptedMessage { + key: pub_key, + pop: SchnorrSignature::sign( + &key, + nonce, + pop_challenge::(context, pub_nonce, pub_key, from, msg.deref().as_ref()), + ), + msg, + } +} + +impl EncryptedMessage { + pub fn read(reader: &mut R, params: ThresholdParams) -> io::Result { + Ok(Self { + key: C::read_G(reader)?, + pop: SchnorrSignature::::read(reader)?, + msg: Zeroizing::new(E::read(reader, params)?), + }) + } + + pub fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.key.to_bytes().as_ref())?; + self.pop.write(writer)?; + self.msg.write(writer) + } + + pub fn serialize(&self) -> Vec { + let mut buf = vec![]; + self.write(&mut buf).unwrap(); + buf + } + + #[cfg(test)] + pub(crate) fn invalidate_pop(&mut self) { + self.pop.s += C::F::ONE; + } + + #[cfg(test)] + pub(crate) fn invalidate_msg( + &mut self, + rng: &mut R, + context: &str, + from: Participant, + ) { + // Invalidate the message by specifying a new key/Schnorr PoP + // This will cause all initial checks to pass, yet a decrypt to gibberish + let key = Zeroizing::new(C::random_nonzero_F(rng)); + let pub_key = C::generator() * key.deref(); + let nonce = Zeroizing::new(C::random_nonzero_F(rng)); + let pub_nonce = C::generator() * nonce.deref(); + self.key = pub_key; + self.pop = SchnorrSignature::sign( + &key, + nonce, + pop_challenge::(context, pub_nonce, pub_key, from, self.msg.deref().as_ref()), + ); + } + + // Assumes the encrypted message is a secret share. + #[cfg(test)] + pub(crate) fn invalidate_share_serialization( + &mut self, + rng: &mut R, + context: &str, + from: Participant, + to: C::G, + ) { + use ciphersuite::group::ff::PrimeField; + + let mut repr = ::Repr::default(); + for b in repr.as_mut().iter_mut() { + *b = 255; + } + // Tries to guarantee the above assumption. + assert_eq!(repr.as_ref().len(), self.msg.as_ref().len()); + // Checks that this isn't over a field where this is somehow valid + assert!(!bool::from(C::F::from_repr(repr).is_some())); + + self.msg.as_mut().as_mut().copy_from_slice(repr.as_ref()); + *self = encrypt(rng, context, from, to, self.msg.clone()); + } + + // Assumes the encrypted message is a secret share. + #[cfg(test)] + pub(crate) fn invalidate_share_value( + &mut self, + rng: &mut R, + context: &str, + from: Participant, + to: C::G, + ) { + use ciphersuite::group::ff::PrimeField; + + // Assumes the share isn't randomly 1 + let repr = C::F::ONE.to_repr(); + self.msg.as_mut().as_mut().copy_from_slice(repr.as_ref()); + *self = encrypt(rng, context, from, to, self.msg.clone()); + } +} + +/// A proof that the provided encryption key is a legitimately derived shared key for some message. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct EncryptionKeyProof { + key: Zeroizing, + dleq: DLEqProof, +} + +impl EncryptionKeyProof { + pub fn read(reader: &mut R) -> io::Result { + Ok(Self { key: Zeroizing::new(C::read_G(reader)?), dleq: DLEqProof::read(reader)? }) + } + + pub fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.key.to_bytes().as_ref())?; + self.dleq.write(writer) + } + + pub fn serialize(&self) -> Vec { + let mut buf = vec![]; + self.write(&mut buf).unwrap(); + buf + } + + #[cfg(test)] + pub(crate) fn invalidate_key(&mut self) { + *self.key += C::generator(); + } + + #[cfg(test)] + pub(crate) fn invalidate_dleq(&mut self) { + let mut buf = vec![]; + self.dleq.write(&mut buf).unwrap(); + // Adds one to c since this is serialized c, s + // Adding one to c will leave a validly serialized c + // Adding one to s may leave an invalidly serialized s + buf[0] = buf[0].wrapping_add(1); + self.dleq = DLEqProof::read::<&[u8]>(&mut buf.as_ref()).unwrap(); + } +} + +// This doesn't need to take the msg. It just doesn't hurt as an extra layer. +// This still doesn't mean the DKG offers an authenticated channel. The per-message keys have no +// root of trust other than their existence in the assumed-to-exist external authenticated channel. +fn pop_challenge( + context: &str, + nonce: C::G, + key: C::G, + sender: Participant, + msg: &[u8], +) -> C::F { + let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Proof of Possession v0.2"); + transcript.append_message(b"context", context.as_bytes()); + + transcript.domain_separate(b"proof_of_possession"); + + transcript.append_message(b"nonce", nonce.to_bytes()); + transcript.append_message(b"key", key.to_bytes()); + // This is sufficient to prevent the attack this is meant to stop + transcript.append_message(b"sender", sender.to_bytes()); + // This, as written above, doesn't hurt + transcript.append_message(b"message", msg); + // While this is a PoK and a PoP, it's called a PoP here since the important part is its owner + // Elsewhere, where we use the term PoK, the important part is that it isn't some inverse, with + // an unknown to anyone discrete log, breaking the system + C::hash_to_F(b"DKG-encryption-proof_of_possession", &transcript.challenge(b"schnorr")) +} + +fn encryption_key_transcript(context: &str) -> RecommendedTranscript { + let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2"); + transcript.append_message(b"context", context.as_bytes()); + transcript +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)] +pub(crate) enum DecryptionError { + #[error("accused provided an invalid signature")] + InvalidSignature, + #[error("accuser provided an invalid decryption key")] + InvalidProof, +} + +// A simple box for managing encryption. +#[derive(Clone)] +pub(crate) struct Encryption { + context: String, + i: Participant, + enc_key: Zeroizing, + enc_pub_key: C::G, + enc_keys: HashMap, +} + +impl fmt::Debug for Encryption { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt + .debug_struct("Encryption") + .field("context", &self.context) + .field("i", &self.i) + .field("enc_pub_key", &self.enc_pub_key) + .field("enc_keys", &self.enc_keys) + .finish_non_exhaustive() + } +} + +impl Zeroize for Encryption { + fn zeroize(&mut self) { + self.enc_key.zeroize(); + self.enc_pub_key.zeroize(); + for (_, mut value) in self.enc_keys.drain() { + value.zeroize(); + } + } +} + +impl Encryption { + pub(crate) fn new(context: String, i: Participant, rng: &mut R) -> Self { + let enc_key = Zeroizing::new(C::random_nonzero_F(rng)); + Self { + context, + i, + enc_pub_key: C::generator() * enc_key.deref(), + enc_key, + enc_keys: HashMap::new(), + } + } + + pub(crate) fn registration(&self, msg: M) -> EncryptionKeyMessage { + EncryptionKeyMessage { msg, enc_key: self.enc_pub_key } + } + + pub(crate) fn register( + &mut self, + participant: Participant, + msg: EncryptionKeyMessage, + ) -> M { + assert!( + !self.enc_keys.contains_key(&participant), + "Re-registering encryption key for a participant" + ); + self.enc_keys.insert(participant, msg.enc_key); + msg.msg + } + + pub(crate) fn encrypt( + &self, + rng: &mut R, + participant: Participant, + msg: Zeroizing, + ) -> EncryptedMessage { + encrypt(rng, &self.context, self.i, self.enc_keys[&participant], msg) + } + + pub(crate) fn decrypt( + &self, + rng: &mut R, + batch: &mut BatchVerifier, + // Uses a distinct batch ID so if this batch verifier is reused, we know its the PoP aspect + // which failed, and therefore to use None for the blame + batch_id: I, + from: Participant, + mut msg: EncryptedMessage, + ) -> (Zeroizing, EncryptionKeyProof) { + msg.pop.batch_verify( + rng, + batch, + batch_id, + msg.key, + pop_challenge::(&self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()), + ); + + let key = ecdh::(&self.enc_key, msg.key); + cipher::(&self.context, &key).apply_keystream(msg.msg.as_mut().as_mut()); + ( + msg.msg, + EncryptionKeyProof { + key, + dleq: DLEqProof::prove( + rng, + &mut encryption_key_transcript(&self.context), + &[C::generator(), msg.key], + &self.enc_key, + ), + }, + ) + } + + // Given a message, and the intended decryptor, and a proof for its key, decrypt the message. + // Returns None if the key was wrong. + pub(crate) fn decrypt_with_proof( + &self, + from: Participant, + decryptor: Participant, + mut msg: EncryptedMessage, + // There's no encryption key proof if the accusation is of an invalid signature + proof: Option>, + ) -> Result, DecryptionError> { + if !msg.pop.verify( + msg.key, + pop_challenge::(&self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()), + ) { + Err(DecryptionError::InvalidSignature)?; + } + + if let Some(proof) = proof { + // Verify this is the decryption key for this message + proof + .dleq + .verify( + &mut encryption_key_transcript(&self.context), + &[C::generator(), msg.key], + &[self.enc_keys[&decryptor], *proof.key], + ) + .map_err(|_| DecryptionError::InvalidProof)?; + + cipher::(&self.context, &proof.key).apply_keystream(msg.msg.as_mut().as_mut()); + Ok(msg.msg) + } else { + Err(DecryptionError::InvalidProof) + } + } +} diff --git a/crypto/dkg/src/frost.rs b/crypto/dkg/src/frost.rs new file mode 100644 index 0000000..64c465f --- /dev/null +++ b/crypto/dkg/src/frost.rs @@ -0,0 +1,607 @@ +use core::{marker::PhantomData, ops::Deref, fmt}; +use std::{ + io::{self, Read, Write}, + collections::HashMap, +}; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +use transcript::{Transcript, RecommendedTranscript}; + +use ciphersuite::{ + group::{ + ff::{Field, PrimeField}, + Group, GroupEncoding, + }, + Ciphersuite, +}; +use multiexp::{multiexp_vartime, BatchVerifier}; + +use schnorr::SchnorrSignature; + +use crate::{ + Participant, DkgError, ThresholdParams, ThresholdCore, validate_map, + encryption::{ + ReadWrite, EncryptionKeyMessage, EncryptedMessage, Encryption, EncryptionKeyProof, + DecryptionError, + }, +}; + +type FrostError = DkgError>; + +#[allow(non_snake_case)] +fn challenge(context: &str, l: Participant, R: &[u8], Am: &[u8]) -> C::F { + let mut transcript = RecommendedTranscript::new(b"DKG FROST v0.2"); + transcript.domain_separate(b"schnorr_proof_of_knowledge"); + transcript.append_message(b"context", context.as_bytes()); + transcript.append_message(b"participant", l.to_bytes()); + transcript.append_message(b"nonce", R); + transcript.append_message(b"commitments", Am); + C::hash_to_F(b"DKG-FROST-proof_of_knowledge-0", &transcript.challenge(b"schnorr")) +} + +/// The commitments message, intended to be broadcast to all other parties. +/// +/// Every participant should only provide one set of commitments to all parties. If any +/// participant sends multiple sets of commitments, they are faulty and should be presumed +/// malicious. As this library does not handle networking, it is unable to detect if any +/// participant is so faulty. That responsibility lies with the caller. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct Commitments { + commitments: Vec, + cached_msg: Vec, + sig: SchnorrSignature, +} + +impl ReadWrite for Commitments { + fn read(reader: &mut R, params: ThresholdParams) -> io::Result { + let mut commitments = Vec::with_capacity(params.t().into()); + let mut cached_msg = vec![]; + + #[allow(non_snake_case)] + let mut read_G = || -> io::Result { + let mut buf = ::Repr::default(); + reader.read_exact(buf.as_mut())?; + let point = C::read_G(&mut buf.as_ref())?; + cached_msg.extend(buf.as_ref()); + Ok(point) + }; + + for _ in 0 .. params.t() { + commitments.push(read_G()?); + } + + Ok(Commitments { commitments, cached_msg, sig: SchnorrSignature::read(reader)? }) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&self.cached_msg)?; + self.sig.write(writer) + } +} + +/// State machine to begin the key generation protocol. +#[derive(Debug, Zeroize)] +pub struct KeyGenMachine { + params: ThresholdParams, + context: String, + _curve: PhantomData, +} + +impl KeyGenMachine { + /// Create a new machine to generate a key. + /// + /// The context string should be unique among multisigs. + pub fn new(params: ThresholdParams, context: String) -> KeyGenMachine { + KeyGenMachine { params, context, _curve: PhantomData } + } + + /// Start generating a key according to the FROST DKG spec. + /// + /// Returns a commitments message to be sent to all parties over an authenticated channel. If any + /// party submits multiple sets of commitments, they MUST be treated as malicious. + pub fn generate_coefficients( + self, + rng: &mut R, + ) -> (SecretShareMachine, EncryptionKeyMessage>) { + let t = usize::from(self.params.t); + let mut coefficients = Vec::with_capacity(t); + let mut commitments = Vec::with_capacity(t); + let mut cached_msg = vec![]; + + for i in 0 .. t { + // Step 1: Generate t random values to form a polynomial with + coefficients.push(Zeroizing::new(C::random_nonzero_F(&mut *rng))); + // Step 3: Generate public commitments + commitments.push(C::generator() * coefficients[i].deref()); + cached_msg.extend(commitments[i].to_bytes().as_ref()); + } + + // Step 2: Provide a proof of knowledge + let r = Zeroizing::new(C::random_nonzero_F(rng)); + let nonce = C::generator() * r.deref(); + let sig = SchnorrSignature::::sign( + &coefficients[0], + // This could be deterministic as the PoK is a singleton never opened up to cooperative + // discussion + // There's no reason to spend the time and effort to make this deterministic besides a + // general obsession with canonicity and determinism though + r, + challenge::(&self.context, self.params.i(), nonce.to_bytes().as_ref(), &cached_msg), + ); + + // Additionally create an encryption mechanism to protect the secret shares + let encryption = Encryption::new(self.context.clone(), self.params.i, rng); + + // Step 4: Broadcast + let msg = + encryption.registration(Commitments { commitments: commitments.clone(), cached_msg, sig }); + ( + SecretShareMachine { + params: self.params, + context: self.context, + coefficients, + our_commitments: commitments, + encryption, + }, + msg, + ) + } +} + +fn polynomial( + coefficients: &[Zeroizing], + l: Participant, +) -> Zeroizing { + let l = F::from(u64::from(u16::from(l))); + // This should never be reached since Participant is explicitly non-zero + assert!(l != F::ZERO, "zero participant passed to polynomial"); + let mut share = Zeroizing::new(F::ZERO); + for (idx, coefficient) in coefficients.iter().rev().enumerate() { + *share += coefficient.deref(); + if idx != (coefficients.len() - 1) { + *share *= l; + } + } + share +} + +/// The secret share message, to be sent to the party it's intended for over an authenticated +/// channel. +/// +/// If any participant sends multiple secret shares to another participant, they are faulty. +// This should presumably be written as SecretShare(Zeroizing). +// It's unfortunately not possible as F::Repr doesn't have Zeroize as a bound. +// The encryption system also explicitly uses Zeroizing so it can ensure anything being +// encrypted is within Zeroizing. Accordingly, internally having Zeroizing would be redundant. +#[derive(Clone, PartialEq, Eq)] +pub struct SecretShare(F::Repr); +impl AsRef<[u8]> for SecretShare { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} +impl AsMut<[u8]> for SecretShare { + fn as_mut(&mut self) -> &mut [u8] { + self.0.as_mut() + } +} +impl fmt::Debug for SecretShare { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("SecretShare").finish_non_exhaustive() + } +} +impl Zeroize for SecretShare { + fn zeroize(&mut self) { + self.0.as_mut().zeroize() + } +} +// Still manually implement ZeroizeOnDrop to ensure these don't stick around. +// We could replace Zeroizing with a bound M: ZeroizeOnDrop. +// Doing so would potentially fail to highlight the expected behavior with these and remove a layer +// of depth. +impl Drop for SecretShare { + fn drop(&mut self) { + self.zeroize(); + } +} +impl ZeroizeOnDrop for SecretShare {} + +impl ReadWrite for SecretShare { + fn read(reader: &mut R, _: ThresholdParams) -> io::Result { + let mut repr = F::Repr::default(); + reader.read_exact(repr.as_mut())?; + Ok(SecretShare(repr)) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.0.as_ref()) + } +} + +/// Advancement of the key generation state machine. +#[derive(Zeroize)] +pub struct SecretShareMachine { + params: ThresholdParams, + context: String, + coefficients: Vec>, + our_commitments: Vec, + encryption: Encryption, +} + +impl fmt::Debug for SecretShareMachine { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt + .debug_struct("SecretShareMachine") + .field("params", &self.params) + .field("context", &self.context) + .field("our_commitments", &self.our_commitments) + .field("encryption", &self.encryption) + .finish_non_exhaustive() + } +} + +impl SecretShareMachine { + /// Verify the data from the previous round (canonicity, PoKs, message authenticity) + #[allow(clippy::type_complexity)] + fn verify_r1( + &mut self, + rng: &mut R, + mut commitments: HashMap>>, + ) -> Result>, FrostError> { + validate_map( + &commitments, + &(1 ..= self.params.n()).map(Participant).collect::>(), + Some(self.params.i()), + )?; + + let mut batch = BatchVerifier::::new(commitments.len()); + let mut commitments = commitments + .drain() + .map(|(l, msg)| { + let mut msg = self.encryption.register(l, msg); + + // Step 5: Validate each proof of knowledge + // This is solely the prep step for the latter batch verification + msg.sig.batch_verify( + rng, + &mut batch, + l, + msg.commitments[0], + challenge::(&self.context, l, msg.sig.R.to_bytes().as_ref(), &msg.cached_msg), + ); + + (l, msg.commitments.drain(..).collect::>()) + }) + .collect::>(); + + batch.verify_vartime_with_vartime_blame().map_err(FrostError::InvalidProofOfKnowledge)?; + + commitments.insert(self.params.i, self.our_commitments.drain(..).collect()); + Ok(commitments) + } + + /// Continue generating a key. + /// + /// Takes in everyone else's commitments. Returns a HashMap of encrypted secret shares to be sent + /// over authenticated channels to their relevant counterparties. + /// + /// If any participant sends multiple secret shares to another participant, they are faulty. + #[allow(clippy::type_complexity)] + pub fn generate_secret_shares( + mut self, + rng: &mut R, + commitments: HashMap>>, + ) -> Result< + (KeyMachine, HashMap>>), + FrostError, + > { + let commitments = self.verify_r1(&mut *rng, commitments)?; + + // Step 1: Generate secret shares for all other parties + let mut res = HashMap::new(); + for l in (1 ..= self.params.n()).map(Participant) { + // Don't insert our own shares to the byte buffer which is meant to be sent around + // An app developer could accidentally send it. Best to keep this black boxed + if l == self.params.i() { + continue; + } + + let mut share = polynomial(&self.coefficients, l); + let share_bytes = Zeroizing::new(SecretShare::(share.to_repr())); + share.zeroize(); + res.insert(l, self.encryption.encrypt(rng, l, share_bytes)); + } + + // Calculate our own share + let share = polynomial(&self.coefficients, self.params.i()); + self.coefficients.zeroize(); + + Ok(( + KeyMachine { params: self.params, secret: share, commitments, encryption: self.encryption }, + res, + )) + } +} + +/// Advancement of the the secret share state machine. +/// +/// This machine will 'complete' the protocol, by a local perspective. In order to be secure, +/// the parties must confirm having successfully completed the protocol (an effort out of scope to +/// this library), yet this is modeled by one more state transition (BlameMachine). +pub struct KeyMachine { + params: ThresholdParams, + secret: Zeroizing, + commitments: HashMap>, + encryption: Encryption, +} + +impl fmt::Debug for KeyMachine { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt + .debug_struct("KeyMachine") + .field("params", &self.params) + .field("commitments", &self.commitments) + .field("encryption", &self.encryption) + .finish_non_exhaustive() + } +} + +impl Zeroize for KeyMachine { + fn zeroize(&mut self) { + self.params.zeroize(); + self.secret.zeroize(); + for commitments in self.commitments.values_mut() { + commitments.zeroize(); + } + self.encryption.zeroize(); + } +} + +// Calculate the exponent for a given participant and apply it to a series of commitments +// Initially used with the actual commitments to verify the secret share, later used with +// stripes to generate the verification shares +fn exponential(i: Participant, values: &[C::G]) -> Vec<(C::F, C::G)> { + let i = C::F::from(u16::from(i).into()); + let mut res = Vec::with_capacity(values.len()); + (0 .. values.len()).fold(C::F::ONE, |exp, l| { + res.push((exp, values[l])); + exp * i + }); + res +} + +fn share_verification_statements( + target: Participant, + commitments: &[C::G], + mut share: Zeroizing, +) -> Vec<(C::F, C::G)> { + // This can be insecurely linearized from n * t to just n using the below sums for a given + // stripe. Doing so uses naive addition which is subject to malleability. The only way to + // ensure that malleability isn't present is to use this n * t algorithm, which runs + // per sender and not as an aggregate of all senders, which also enables blame + let mut values = exponential::(target, commitments); + + // Perform the share multiplication outside of the multiexp to minimize stack copying + // While the multiexp BatchVerifier does zeroize its flattened multiexp, and itself, it still + // converts whatever we give to an iterator and then builds a Vec internally, welcoming copies + let neg_share_pub = C::generator() * -*share; + share.zeroize(); + values.push((C::F::ONE, neg_share_pub)); + + values +} + +#[derive(Clone, Copy, Hash, Debug, Zeroize)] +enum BatchId { + Decryption(Participant), + Share(Participant), +} + +impl KeyMachine { + /// Calculate our share given the shares sent to us. + /// + /// Returns a BlameMachine usable to determine if faults in the protocol occurred. + /// + /// This will error on, and return a blame proof for, the first-observed case of faulty behavior. + pub fn calculate_share( + mut self, + rng: &mut R, + mut shares: HashMap>>, + ) -> Result, FrostError> { + validate_map( + &shares, + &(1 ..= self.params.n()).map(Participant).collect::>(), + Some(self.params.i()), + )?; + + let mut batch = BatchVerifier::new(shares.len()); + let mut blames = HashMap::new(); + for (l, share_bytes) in shares.drain() { + let (mut share_bytes, blame) = + self.encryption.decrypt(rng, &mut batch, BatchId::Decryption(l), l, share_bytes); + let share = + Zeroizing::new(Option::::from(C::F::from_repr(share_bytes.0)).ok_or_else(|| { + FrostError::InvalidShare { participant: l, blame: Some(blame.clone()) } + })?); + share_bytes.zeroize(); + *self.secret += share.deref(); + + blames.insert(l, blame); + batch.queue( + rng, + BatchId::Share(l), + share_verification_statements::(self.params.i(), &self.commitments[&l], share), + ); + } + batch.verify_with_vartime_blame().map_err(|id| { + let (l, blame) = match id { + BatchId::Decryption(l) => (l, None), + BatchId::Share(l) => (l, Some(blames.remove(&l).unwrap())), + }; + FrostError::InvalidShare { participant: l, blame } + })?; + + // Stripe commitments per t and sum them in advance. Calculating verification shares relies on + // these sums so preprocessing them is a massive speedup + // If these weren't just sums, yet the tables used in multiexp, this would be further optimized + // As of right now, each multiexp will regenerate them + let mut stripes = Vec::with_capacity(usize::from(self.params.t())); + for t in 0 .. usize::from(self.params.t()) { + stripes.push(self.commitments.values().map(|commitments| commitments[t]).sum()); + } + + // Calculate each user's verification share + let mut verification_shares = HashMap::new(); + for i in (1 ..= self.params.n()).map(Participant) { + verification_shares.insert( + i, + if i == self.params.i() { + C::generator() * self.secret.deref() + } else { + multiexp_vartime(&exponential::(i, &stripes)) + }, + ); + } + + let KeyMachine { commitments, encryption, params, secret } = self; + Ok(BlameMachine { + commitments, + encryption, + result: ThresholdCore { + params, + secret_share: secret, + group_key: stripes[0], + verification_shares, + }, + }) + } +} + +/// A machine capable of handling blame proofs. +pub struct BlameMachine { + commitments: HashMap>, + encryption: Encryption, + result: ThresholdCore, +} + +impl fmt::Debug for BlameMachine { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt + .debug_struct("BlameMachine") + .field("commitments", &self.commitments) + .field("encryption", &self.encryption) + .finish_non_exhaustive() + } +} + +impl Zeroize for BlameMachine { + fn zeroize(&mut self) { + for commitments in self.commitments.values_mut() { + commitments.zeroize(); + } + self.encryption.zeroize(); + self.result.zeroize(); + } +} + +impl BlameMachine { + /// Mark the protocol as having been successfully completed, returning the generated keys. + /// This should only be called after having confirmed, with all participants, successful + /// completion. + /// + /// Confirming successful completion is not necessarily as simple as everyone reporting their + /// completion. Everyone must also receive everyone's report of completion, entering into the + /// territory of consensus protocols. This library does not handle that nor does it provide any + /// tooling to do so. This function is solely intended to force users to acknowledge they're + /// completing the protocol, not processing any blame. + pub fn complete(self) -> ThresholdCore { + self.result + } + + fn blame_internal( + &self, + sender: Participant, + recipient: Participant, + msg: EncryptedMessage>, + proof: Option>, + ) -> Participant { + let share_bytes = match self.encryption.decrypt_with_proof(sender, recipient, msg, proof) { + Ok(share_bytes) => share_bytes, + // If there's an invalid signature, the sender did not send a properly formed message + Err(DecryptionError::InvalidSignature) => return sender, + // Decryption will fail if the provided ECDH key wasn't correct for the given message + Err(DecryptionError::InvalidProof) => return recipient, + }; + + let Some(share) = Option::::from(C::F::from_repr(share_bytes.0)) else { + // If this isn't a valid scalar, the sender is faulty + return sender; + }; + + // If this isn't a valid share, the sender is faulty + if !bool::from( + multiexp_vartime(&share_verification_statements::( + recipient, + &self.commitments[&sender], + Zeroizing::new(share), + )) + .is_identity(), + ) { + return sender; + } + + // The share was canonical and valid + recipient + } + + /// Given an accusation of fault, determine the faulty party (either the sender, who sent an + /// invalid secret share, or the receiver, who claimed a valid secret share was invalid). No + /// matter which, prevent completion of the machine, forcing an abort of the protocol. + /// + /// The message should be a copy of the encrypted secret share from the accused sender to the + /// accusing recipient. This message must have been authenticated as actually having come from + /// the sender in question. + /// + /// In order to enable detecting multiple faults, an `AdditionalBlameMachine` is returned, which + /// can be used to determine further blame. These machines will process the same blame statements + /// multiple times, always identifying blame. It is the caller's job to ensure they're unique in + /// order to prevent multiple instances of blame over a single incident. + pub fn blame( + self, + sender: Participant, + recipient: Participant, + msg: EncryptedMessage>, + proof: Option>, + ) -> (AdditionalBlameMachine, Participant) { + let faulty = self.blame_internal(sender, recipient, msg, proof); + (AdditionalBlameMachine(self), faulty) + } +} + +/// A machine capable of handling an arbitrary amount of additional blame proofs. +#[derive(Debug, Zeroize)] +pub struct AdditionalBlameMachine(BlameMachine); +impl AdditionalBlameMachine { + /// Given an accusation of fault, determine the faulty party (either the sender, who sent an + /// invalid secret share, or the receiver, who claimed a valid secret share was invalid). + /// + /// The message should be a copy of the encrypted secret share from the accused sender to the + /// accusing recipient. This message must have been authenticated as actually having come from + /// the sender in question. + /// + /// This will process the same blame statement multiple times, always identifying blame. It is + /// the caller's job to ensure they're unique in order to prevent multiple instances of blame + /// over a single incident. + pub fn blame( + self, + sender: Participant, + recipient: Participant, + msg: EncryptedMessage>, + proof: Option>, + ) -> Participant { + self.0.blame_internal(sender, recipient, msg, proof) + } +} diff --git a/crypto/dkg/src/lib.rs b/crypto/dkg/src/lib.rs new file mode 100644 index 0000000..d4b5d86 --- /dev/null +++ b/crypto/dkg/src/lib.rs @@ -0,0 +1,536 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![cfg_attr(not(feature = "std"), no_std)] + +use core::fmt::{self, Debug}; + +#[cfg(feature = "std")] +use thiserror::Error; + +use zeroize::Zeroize; + +/// MuSig-style key aggregation. +pub mod musig; + +/// Encryption types and utilities used to secure DKG messages. +#[cfg(feature = "std")] +pub mod encryption; + +/// The distributed key generation protocol described in the +/// [FROST paper](https://eprint.iacr.org/2020/852). +#[cfg(feature = "std")] +pub mod frost; + +/// Promote keys between ciphersuites. +#[cfg(feature = "std")] +pub mod promote; + +/// Tests for application-provided curves and algorithms. +#[cfg(any(test, feature = "tests"))] +pub mod tests; + +#[cfg(feature = "std")] +pub mod resharing; + +/// The ID of a participant, defined as a non-zero u16. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Zeroize)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Participant(pub(crate) u16); +impl Participant { + /// Create a new Participant identifier from a u16. + pub fn new(i: u16) -> Option { + if i == 0 { + None + } else { + Some(Participant(i)) + } + } + + /// Convert a Participant identifier to bytes. + #[allow(clippy::wrong_self_convention)] + pub fn to_bytes(&self) -> [u8; 2] { + self.0.to_le_bytes() + } +} + +impl From for u16 { + fn from(participant: Participant) -> u16 { + participant.0 + } +} + +impl fmt::Display for Participant { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Various errors possible during key generation. +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(Error))] +pub enum DkgError { + /// A parameter was zero. + #[cfg_attr(feature = "std", error("a parameter was 0 (threshold {0}, participants {1})"))] + ZeroParameter(u16, u16), + /// The threshold exceeded the amount of participants. + #[cfg_attr(feature = "std", error("invalid threshold (max {1}, got {0})"))] + InvalidThreshold(u16, u16), + /// Invalid participant identifier. + #[cfg_attr( + feature = "std", + error("invalid participant (0 < participant <= {0}, yet participant is {1})") + )] + InvalidParticipant(u16, Participant), + + /// Invalid signing set. + #[cfg_attr(feature = "std", error("invalid signing set"))] + InvalidSigningSet, + /// Invalid amount of participants. + #[cfg_attr(feature = "std", error("invalid participant quantity (expected {0}, got {1})"))] + InvalidParticipantQuantity(usize, usize), + /// A participant was duplicated. + #[cfg_attr(feature = "std", error("duplicated participant ({0})"))] + DuplicatedParticipant(Participant), + /// A participant was missing. + #[cfg_attr(feature = "std", error("missing participant {0}"))] + MissingParticipant(Participant), + + /// An invalid proof of knowledge was provided. + #[cfg_attr(feature = "std", error("invalid proof of knowledge (participant {0})"))] + InvalidProofOfKnowledge(Participant), + /// An invalid DKG share was provided. + #[cfg_attr(feature = "std", error("invalid share (participant {participant}, blame {blame})"))] + InvalidShare { participant: Participant, blame: Option }, +} + +#[cfg(feature = "std")] +mod lib { + pub use super::*; + + use core::ops::Deref; + use std::{io, sync::Arc, collections::HashMap}; + + use zeroize::Zeroizing; + + use ciphersuite::{ + group::{ + ff::{Field, PrimeField}, + GroupEncoding, + }, + Ciphersuite, + }; + + // Validate a map of values to have the expected included participants + pub(crate) fn validate_map( + map: &HashMap, + included: &[Participant], + ours: Option, + ) -> Result<(), DkgError> { + if (map.len() + usize::from(u8::from(ours.is_some()))) != included.len() { + Err(DkgError::InvalidParticipantQuantity(included.len(), map.len() + 1))?; + } + + for included in included { + if Some(*included) == ours { + if map.contains_key(included) { + Err(DkgError::DuplicatedParticipant(*included))?; + } + continue; + } + + if !map.contains_key(included) { + Err(DkgError::MissingParticipant(*included))?; + } + } + + Ok(()) + } + + /// Parameters for a multisig. + // These fields should not be made public as they should be static + #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub struct ThresholdParams { + /// Participants needed to sign on behalf of the group. + pub(crate) t: u16, + /// Amount of participants. + pub(crate) n: u16, + /// Index of the participant being acted for. + pub(crate) i: Participant, + } + + impl ThresholdParams { + /// Create a new set of parameters. + pub fn new(t: u16, n: u16, i: Participant) -> Result> { + if (t == 0) || (n == 0) { + Err(DkgError::ZeroParameter(t, n))?; + } + + if t > n { + Err(DkgError::InvalidThreshold(t, n))?; + } + if u16::from(i) > n { + Err(DkgError::InvalidParticipant(n, i))?; + } + + Ok(ThresholdParams { t, n, i }) + } + + /// Return the threshold for a multisig with these parameters. + pub fn t(&self) -> u16 { + self.t + } + /// Return the amount of participants for a multisig with these parameters. + pub fn n(&self) -> u16 { + self.n + } + /// Return the participant index of the share with these parameters. + pub fn i(&self) -> Participant { + self.i + } + } + + /// Calculate the lagrange coefficient for a signing set. + pub fn lagrange(i: Participant, included: &[Participant]) -> F { + let i_f = F::from(u64::from(u16::from(i))); + + let mut num = F::ONE; + let mut denom = F::ONE; + for l in included { + if i == *l { + continue; + } + + let share = F::from(u64::from(u16::from(*l))); + num *= share; + denom *= share - i_f; + } + + // Safe as this will only be 0 if we're part of the above loop + // (which we have an if case to avoid) + num * denom.invert().unwrap() + } + + /// Keys and verification shares generated by a DKG. + /// Called core as they're expected to be wrapped into an Arc before usage in various operations. + #[derive(Clone, PartialEq, Eq)] + pub struct ThresholdCore { + /// Threshold Parameters. + pub(crate) params: ThresholdParams, + + /// Secret share key. + pub(crate) secret_share: Zeroizing, + /// Group key. + pub(crate) group_key: C::G, + /// Verification shares. + pub(crate) verification_shares: HashMap, + } + + impl fmt::Debug for ThresholdCore { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt + .debug_struct("ThresholdCore") + .field("params", &self.params) + .field("group_key", &self.group_key) + .field("verification_shares", &self.verification_shares) + .finish_non_exhaustive() + } + } + + impl Zeroize for ThresholdCore { + fn zeroize(&mut self) { + self.params.zeroize(); + self.secret_share.zeroize(); + self.group_key.zeroize(); + for (_, share) in self.verification_shares.iter_mut() { + share.zeroize(); + } + } + } + + impl ThresholdCore { + pub(crate) fn new( + params: ThresholdParams, + secret_share: Zeroizing, + verification_shares: HashMap, + ) -> ThresholdCore { + let t = (1 ..= params.t()).map(Participant).collect::>(); + ThresholdCore { + params, + secret_share, + group_key: t.iter().map(|i| verification_shares[i] * lagrange::(*i, &t)).sum(), + verification_shares, + } + } + + /// Parameters for these keys. + pub fn params(&self) -> ThresholdParams { + self.params + } + + /// Secret share for these keys. + pub fn secret_share(&self) -> &Zeroizing { + &self.secret_share + } + + /// Group key for these keys. + pub fn group_key(&self) -> C::G { + self.group_key + } + + pub fn verification_shares(&self) -> HashMap { + self.verification_shares.clone() + } + + /// Write these keys to a type satisfying std::io::Write. + pub fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&u32::try_from(C::ID.len()).unwrap().to_le_bytes())?; + writer.write_all(C::ID)?; + writer.write_all(&self.params.t.to_le_bytes())?; + writer.write_all(&self.params.n.to_le_bytes())?; + writer.write_all(&self.params.i.to_bytes())?; + let mut share_bytes = self.secret_share.to_repr(); + writer.write_all(share_bytes.as_ref())?; + share_bytes.as_mut().zeroize(); + for l in 1 ..= self.params.n { + writer + .write_all(self.verification_shares[&Participant::new(l).unwrap()].to_bytes().as_ref())?; + } + Ok(()) + } + + /// Serialize these keys to a `Vec`. + pub fn serialize(&self) -> Zeroizing> { + let mut serialized = Zeroizing::new(vec![]); + self.write::>(serialized.as_mut()).unwrap(); + serialized + } + + /// Read keys from a type satisfying std::io::Read. + pub fn read(reader: &mut R) -> io::Result> { + { + let different = + || io::Error::new(io::ErrorKind::Other, "deserializing ThresholdCore for another curve"); + + let mut id_len = [0; 4]; + reader.read_exact(&mut id_len)?; + if u32::try_from(C::ID.len()).unwrap().to_le_bytes() != id_len { + Err(different())?; + } + + let mut id = vec![0; C::ID.len()]; + reader.read_exact(&mut id)?; + if id != C::ID { + Err(different())?; + } + } + + let (t, n, i) = { + let mut read_u16 = || -> io::Result { + let mut value = [0; 2]; + reader.read_exact(&mut value)?; + Ok(u16::from_le_bytes(value)) + }; + ( + read_u16()?, + read_u16()?, + Participant::new(read_u16()?) + .ok_or(io::Error::new(io::ErrorKind::Other, "invalid participant index"))?, + ) + }; + + let secret_share = Zeroizing::new(C::read_F(reader)?); + + let mut verification_shares = HashMap::new(); + for l in (1 ..= n).map(Participant) { + verification_shares.insert(l, ::read_G(reader)?); + } + + Ok(ThresholdCore::new( + ThresholdParams::new(t, n, i) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid parameters"))?, + secret_share, + verification_shares, + )) + } + } + + /// Threshold keys usable for signing. + #[derive(Clone, Debug, Zeroize)] + pub struct ThresholdKeys { + // Core keys. + // If this is the last reference, the underlying keys will be dropped. When that happens, the + // private key present within it will be zeroed out (as it's within Zeroizing). + #[zeroize(skip)] + pub(crate) core: Arc>, + + // Offset applied to these keys. + pub(crate) offset: Option, + } + + /// View of keys, interpolated and offset for usage. + #[derive(Clone)] + pub struct ThresholdView { + offset: C::F, + group_key: C::G, + included: Vec, + secret_share: Zeroizing, + original_verification_shares: HashMap, + verification_shares: HashMap, + } + + impl fmt::Debug for ThresholdView { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt + .debug_struct("ThresholdView") + .field("offset", &self.offset) + .field("group_key", &self.group_key) + .field("included", &self.included) + .field("original_verification_shares", &self.original_verification_shares) + .field("verification_shares", &self.verification_shares) + .finish_non_exhaustive() + } + } + + impl Zeroize for ThresholdView { + fn zeroize(&mut self) { + self.offset.zeroize(); + self.group_key.zeroize(); + self.included.zeroize(); + self.secret_share.zeroize(); + for (_, share) in self.original_verification_shares.iter_mut() { + share.zeroize(); + } + for (_, share) in self.verification_shares.iter_mut() { + share.zeroize(); + } + } + } + + impl ThresholdKeys { + /// Create a new set of ThresholdKeys from a ThresholdCore. + pub fn new(core: ThresholdCore) -> ThresholdKeys { + ThresholdKeys { core: Arc::new(core), offset: None } + } + + /// Offset the keys by a given scalar to allow for various account and privacy schemes. + /// + /// This offset is ephemeral and will not be included when these keys are serialized. It also + /// accumulates, so calling offset multiple times will produce a offset of the offsets' sum. + #[must_use] + pub fn offset(&self, offset: C::F) -> ThresholdKeys { + let mut res = self.clone(); + // Carry any existing offset + // Enables schemes like Monero's subaddresses which have a per-subaddress offset and then a + // one-time-key offset + res.offset = Some(offset + res.offset.unwrap_or(C::F::ZERO)); + res + } + + /// Return the current offset in-use for these keys. + pub fn current_offset(&self) -> Option { + self.offset + } + + /// Return the parameters for these keys. + pub fn params(&self) -> ThresholdParams { + self.core.params + } + + /// Return the secret share for these keys. + pub fn secret_share(&self) -> &Zeroizing { + &self.core.secret_share + } + + /// Return the group key, with any offset applied. + pub fn group_key(&self) -> C::G { + self.core.group_key + (C::generator() * self.offset.unwrap_or(C::F::ZERO)) + } + + /// Return all participants' verification shares without any offsetting. + pub fn verification_shares(&self) -> HashMap { + self.core.verification_shares() + } + + /// Serialize these keys to a `Vec`. + pub fn serialize(&self) -> Zeroizing> { + self.core.serialize() + } + + /// Obtain a view of these keys, with any offset applied, interpolated for the specified signing + /// set. + pub fn view(&self, mut included: Vec) -> Result, DkgError<()>> { + if (included.len() < self.params().t.into()) || + (usize::from(self.params().n()) < included.len()) + { + Err(DkgError::InvalidSigningSet)?; + } + included.sort(); + + let mut secret_share = Zeroizing::new( + lagrange::(self.params().i(), &included) * self.secret_share().deref(), + ); + + let mut verification_shares = self.verification_shares(); + for (i, share) in verification_shares.iter_mut() { + *share *= lagrange::(*i, &included); + } + + // The offset is included by adding it to the participant with the lowest ID + let offset = self.offset.unwrap_or(C::F::ZERO); + if included[0] == self.params().i() { + *secret_share += offset; + } + *verification_shares.get_mut(&included[0]).unwrap() += C::generator() * offset; + + Ok(ThresholdView { + offset, + group_key: self.group_key(), + secret_share, + original_verification_shares: self.verification_shares(), + verification_shares, + included, + }) + } + } + + impl From> for ThresholdKeys { + fn from(keys: ThresholdCore) -> ThresholdKeys { + ThresholdKeys::new(keys) + } + } + + impl ThresholdView { + /// Return the offset for this view. + pub fn offset(&self) -> C::F { + self.offset + } + + /// Return the group key. + pub fn group_key(&self) -> C::G { + self.group_key + } + + /// Return the included signers. + pub fn included(&self) -> &[Participant] { + &self.included + } + + /// Return the interpolated, offset secret share. + pub fn secret_share(&self) -> &Zeroizing { + &self.secret_share + } + + /// Return the original verification share for the specified participant. + pub fn original_verification_share(&self, l: Participant) -> C::G { + self.original_verification_shares[&l] + } + + /// Return the interpolated, offset verification share for the specified participant. + pub fn verification_share(&self, l: Participant) -> C::G { + self.verification_shares[&l] + } + } +} +#[cfg(feature = "std")] +pub use lib::*; diff --git a/crypto/dkg/src/musig.rs b/crypto/dkg/src/musig.rs new file mode 100644 index 0000000..4b11532 --- /dev/null +++ b/crypto/dkg/src/musig.rs @@ -0,0 +1,138 @@ +#[cfg(feature = "std")] +use core::ops::Deref; +use std_shims::collections::HashSet; +#[cfg(feature = "std")] +use std_shims::collections::HashMap; + +#[cfg(feature = "std")] +use zeroize::Zeroizing; + +use transcript::{Transcript, RecommendedTranscript}; + +#[cfg(feature = "std")] +use ciphersuite::group::ff::Field; +use ciphersuite::{ + group::{Group, GroupEncoding}, + Ciphersuite, +}; + +use crate::DkgError; +#[cfg(feature = "std")] +use crate::{Participant, ThresholdParams, ThresholdCore, lagrange}; + +fn check_keys(keys: &[C::G]) -> Result> { + if keys.is_empty() { + Err(DkgError::InvalidSigningSet)?; + } + // Too many signers + let keys_len = u16::try_from(keys.len()).map_err(|_| DkgError::InvalidSigningSet)?; + + // Duplicated public keys + if keys.iter().map(|key| key.to_bytes().as_ref().to_vec()).collect::>().len() != + keys.len() + { + Err(DkgError::InvalidSigningSet)?; + } + + Ok(keys_len) +} + +fn binding_factor_transcript( + context: &[u8], + keys: &[C::G], +) -> RecommendedTranscript { + let mut transcript = RecommendedTranscript::new(b"DKG MuSig v0.5"); + transcript.append_message(b"context", context); + transcript.domain_separate(b"keys"); + for key in keys { + transcript.append_message(b"key", key.to_bytes()); + } + transcript +} + +fn binding_factor(mut transcript: RecommendedTranscript, i: u16) -> C::F { + transcript.domain_separate(b"participant"); + transcript.append_message(b"participant", i.to_le_bytes()); + C::hash_to_F(b"DKG-MuSig-binding_factor", &transcript.challenge(b"binding_factor")) +} + +/// The group key resulting from using this library's MuSig key gen. +/// +/// Creating an aggregate key with a list containing duplicated public keys returns an error. +pub fn musig_key(context: &[u8], keys: &[C::G]) -> Result> { + let keys_len = check_keys::(keys)?; + let transcript = binding_factor_transcript::(context, keys); + let mut res = C::G::identity(); + for i in 1 ..= keys_len { + res += keys[usize::from(i - 1)] * binding_factor::(transcript.clone(), i); + } + Ok(res) +} + +/// A n-of-n non-interactive DKG which does not guarantee the usability of the resulting key. +/// +/// Creating an aggregate key with a list containing duplicated public keys returns an error. +#[cfg(feature = "std")] +pub fn musig( + context: &[u8], + private_key: &Zeroizing, + keys: &[C::G], +) -> Result, DkgError<()>> { + let keys_len = check_keys::(keys)?; + + let our_pub_key = C::generator() * private_key.deref(); + let Some(pos) = keys.iter().position(|key| *key == our_pub_key) else { + // Not present in signing set + Err(DkgError::InvalidSigningSet)? + }; + let params = ThresholdParams::new( + keys_len, + keys_len, + // These errors shouldn't be possible, as pos is bounded to len - 1 + // Since len is prior guaranteed to be within u16::MAX, pos + 1 must also be + Participant::new((pos + 1).try_into().map_err(|_| DkgError::InvalidSigningSet)?) + .ok_or(DkgError::InvalidSigningSet)?, + )?; + + // Calculate the binding factor per-key + let transcript = binding_factor_transcript::(context, keys); + let mut binding = Vec::with_capacity(keys.len()); + for i in 1 ..= keys_len { + binding.push(binding_factor::(transcript.clone(), i)); + } + + // Multiply our private key by our binding factor + let mut secret_share = private_key.clone(); + *secret_share *= binding[pos]; + + // Calculate verification shares + let mut verification_shares = HashMap::new(); + // When this library offers a ThresholdView for a specific signing set, it applies the lagrange + // factor + // Since this is a n-of-n scheme, there's only one possible signing set, and one possible + // lagrange factor + // In the name of simplicity, we define the group key as the sum of all bound keys + // Accordingly, the secret share must be multiplied by the inverse of the lagrange factor, along + // with all verification shares + // This is less performant than simply defining the group key as the sum of all post-lagrange + // bound keys, yet the simplicity is preferred + let included = (1 ..= keys_len) + // This error also shouldn't be possible, for the same reasons as documented above + .map(|l| Participant::new(l).ok_or(DkgError::InvalidSigningSet)) + .collect::, _>>()?; + let mut group_key = C::G::identity(); + for (l, p) in included.iter().enumerate() { + let bound = keys[l] * binding[l]; + group_key += bound; + + let lagrange_inv = lagrange::(*p, &included).invert().unwrap(); + if params.i() == *p { + *secret_share *= lagrange_inv; + } + verification_shares.insert(*p, bound * lagrange_inv); + } + debug_assert_eq!(C::generator() * secret_share.deref(), verification_shares[¶ms.i()]); + debug_assert_eq!(musig_key::(context, keys).unwrap(), group_key); + + Ok(ThresholdCore { params, secret_share, group_key, verification_shares }) +} diff --git a/crypto/dkg/src/promote.rs b/crypto/dkg/src/promote.rs new file mode 100644 index 0000000..0c944d2 --- /dev/null +++ b/crypto/dkg/src/promote.rs @@ -0,0 +1,125 @@ +use core::{marker::PhantomData, ops::Deref}; +use std::{ + io::{self, Read, Write}, + sync::Arc, + collections::HashMap, +}; + +use rand_core::{RngCore, CryptoRng}; + +use ciphersuite::{group::GroupEncoding, Ciphersuite}; + +use transcript::{Transcript, RecommendedTranscript}; +use dleq::DLEqProof; + +use crate::{Participant, DkgError, ThresholdCore, ThresholdKeys, validate_map}; + +/// Promote a set of keys to another Ciphersuite definition. +pub trait CiphersuitePromote { + fn promote(self) -> ThresholdKeys; +} + +fn transcript(key: G, i: Participant) -> RecommendedTranscript { + let mut transcript = RecommendedTranscript::new(b"DKG Generator Promotion v0.2"); + transcript.append_message(b"group_key", key.to_bytes()); + transcript.append_message(b"participant", i.to_bytes()); + transcript +} + +/// Proof of valid promotion to another generator. +#[derive(Clone, Copy)] +pub struct GeneratorProof { + share: C::G, + proof: DLEqProof, +} + +impl GeneratorProof { + pub fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.share.to_bytes().as_ref())?; + self.proof.write(writer) + } + + pub fn read(reader: &mut R) -> io::Result> { + Ok(GeneratorProof { + share: ::read_G(reader)?, + proof: DLEqProof::read(reader)?, + }) + } + + pub fn serialize(&self) -> Vec { + let mut buf = vec![]; + self.write(&mut buf).unwrap(); + buf + } +} + +/// Promote a set of keys from one generator to another, where the elliptic curve is the same. +/// +/// Since the Ciphersuite trait additionally specifies a generator, this provides an O(n) way to +/// update the generator used with keys. This outperforms the key generation protocol which is +/// exponential. +pub struct GeneratorPromotion { + base: ThresholdKeys, + proof: GeneratorProof, + _c2: PhantomData, +} + +impl GeneratorPromotion +where + C2: Ciphersuite, +{ + /// Begin promoting keys from one generator to another. Returns a proof this share was properly + /// promoted. + pub fn promote( + rng: &mut R, + base: ThresholdKeys, + ) -> (GeneratorPromotion, GeneratorProof) { + // Do a DLEqProof for the new generator + let proof = GeneratorProof { + share: C2::generator() * base.secret_share().deref(), + proof: DLEqProof::prove( + rng, + &mut transcript(base.core.group_key(), base.params().i), + &[C1::generator(), C2::generator()], + base.secret_share(), + ), + }; + + (GeneratorPromotion { base, proof, _c2: PhantomData:: }, proof) + } + + /// Complete promotion by taking in the proofs from all other participants. + pub fn complete( + self, + proofs: &HashMap>, + ) -> Result, DkgError<()>> { + let params = self.base.params(); + validate_map(proofs, &(1 ..= params.n).map(Participant).collect::>(), Some(params.i))?; + + let original_shares = self.base.verification_shares(); + + let mut verification_shares = HashMap::new(); + verification_shares.insert(params.i, self.proof.share); + for (i, proof) in proofs { + let i = *i; + proof + .proof + .verify( + &mut transcript(self.base.core.group_key(), i), + &[C1::generator(), C2::generator()], + &[original_shares[&i], proof.share], + ) + .map_err(|_| DkgError::InvalidProofOfKnowledge(i))?; + verification_shares.insert(i, proof.share); + } + + Ok(ThresholdKeys { + core: Arc::new(ThresholdCore::new( + params, + self.base.secret_share().clone(), + verification_shares, + )), + offset: None, + }) + } +} diff --git a/crypto/dkg/src/resharing/common.rs b/crypto/dkg/src/resharing/common.rs new file mode 100644 index 0000000..54be46f --- /dev/null +++ b/crypto/dkg/src/resharing/common.rs @@ -0,0 +1,137 @@ +use core::{ops::Deref, fmt}; +use std::io::{self, Read, Write}; + +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +use transcript::{Transcript, RecommendedTranscript}; + +use ciphersuite::{ + group::{ff::PrimeField, GroupEncoding}, + Ciphersuite, +}; + +use schnorr::SchnorrSignature; + +use crate::{Participant, DkgError, ThresholdParams, encryption::ReadWrite}; + +pub type ReshareError = DkgError<()>; + +#[allow(non_snake_case)] +pub(crate) fn challenge(context: &str, R: &[u8], Am: &[u8]) -> C::F { + let mut transcript = RecommendedTranscript::new(b"DKG FROST v0.2"); + transcript.domain_separate(b"schnorr_proof_of_knowledge"); + transcript.append_message(b"context", context.as_bytes()); + transcript.append_message(b"nonce", R); + transcript.append_message(b"commitments", Am); + C::hash_to_F(b"DKG-FROST-proof_of_knowledge-0", &transcript.challenge(b"schnorr")) +} + +/// The commitments message, intended to be broadcast to all other parties. +/// +/// Every participant should only provide one set of commitments to all parties. If any +/// participant sends multiple sets of commitments, they are faulty and should be presumed +/// malicious. As this library does not handle networking, it is unable to detect if any +/// participant is so faulty. That responsibility lies with the caller. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct Commitments { + pub(crate) commitments: Vec, + pub(crate) cached_msg: Vec, + pub(crate) sig: SchnorrSignature, +} + +impl ReadWrite for Commitments { + fn read(reader: &mut R, params: ThresholdParams) -> io::Result { + let mut commitments = Vec::with_capacity(params.t().into()); + let mut cached_msg = vec![]; + + #[allow(non_snake_case)] + let mut read_G = || -> io::Result { + let mut buf = ::Repr::default(); + reader.read_exact(buf.as_mut())?; + let point = C::read_G(&mut buf.as_ref())?; + cached_msg.extend(buf.as_ref()); + Ok(point) + }; + + for _ in 0 .. params.t() { + commitments.push(read_G()?); + } + + Ok(Commitments { commitments, cached_msg, sig: SchnorrSignature::read(reader)? }) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(&self.cached_msg)?; + self.sig.write(writer) + } +} + +pub(crate) fn polynomial( + coefficients: &[Zeroizing], + l: Participant, +) -> Zeroizing { + let l = F::from(u64::from(u16::from(l))); + // This should never be reached since Participant is explicitly non-zero + assert!(l != F::ZERO, "zero participant passed to polynomial"); + let mut share = Zeroizing::new(F::ZERO); + for (idx, coefficient) in coefficients.iter().rev().enumerate() { + *share += coefficient.deref(); + if idx != (coefficients.len() - 1) { + *share *= l; + } + } + share +} + +/// The secret share message, to be sent to the party it's intended for over an authenticated +/// channel. +/// +/// If any participant sends multiple secret shares to another participant, they are faulty. +// This should presumably be written as SecretShare(Zeroizing). +// It's unfortunately not possible as F::Repr doesn't have Zeroize as a bound. +// The encryption system also explicitly uses Zeroizing so it can ensure anything being +// encrypted is within Zeroizing. Accordingly, internally having Zeroizing would be redundant. +#[derive(Clone, PartialEq, Eq)] +pub struct SecretShare(pub(crate) F::Repr); +impl AsRef<[u8]> for SecretShare { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} +impl AsMut<[u8]> for SecretShare { + fn as_mut(&mut self) -> &mut [u8] { + self.0.as_mut() + } +} +impl fmt::Debug for SecretShare { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("SecretShare").finish_non_exhaustive() + } +} +impl Zeroize for SecretShare { + fn zeroize(&mut self) { + self.0.as_mut().zeroize() + } +} +// Still manually implement ZeroizeOnDrop to ensure these don't stick around. +// We could replace Zeroizing with a bound M: ZeroizeOnDrop. +// Doing so would potentially fail to highlight the expected behavior with these and remove a layer +// of depth. +impl Drop for SecretShare { + fn drop(&mut self) { + self.zeroize(); + } +} +impl ZeroizeOnDrop for SecretShare {} + +impl ReadWrite for SecretShare { + fn read(reader: &mut R, _: ThresholdParams) -> io::Result { + let mut repr = F::Repr::default(); + reader.read_exact(repr.as_mut())?; + Ok(SecretShare(repr)) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.0.as_ref()) + } +} diff --git a/crypto/dkg/src/resharing/mod.rs b/crypto/dkg/src/resharing/mod.rs new file mode 100644 index 0000000..9ab6527 --- /dev/null +++ b/crypto/dkg/src/resharing/mod.rs @@ -0,0 +1,7 @@ +pub mod common; + +mod resharer; +pub use resharer::*; + +mod reshared; +pub use reshared::*; diff --git a/crypto/dkg/src/resharing/reshared.rs b/crypto/dkg/src/resharing/reshared.rs new file mode 100644 index 0000000..0eda02e --- /dev/null +++ b/crypto/dkg/src/resharing/reshared.rs @@ -0,0 +1,180 @@ +use core::{ops::Deref, fmt}; +use std::{collections::HashMap}; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::{Zeroize, Zeroizing}; + +use ciphersuite::{ + group::{ + ff::{Field, PrimeField}, + GroupEncoding, + }, + Ciphersuite, +}; +use multiexp::{multiexp_vartime, BatchVerifier}; + +use crate::{ + Participant, ThresholdParams, ThresholdCore, + encryption::{EncryptionKeyMessage, EncryptedMessage, Encryption}, + resharing::common::*, +}; + +// Calculate the exponent for a given participant and apply it to a series of commitments +// Initially used with the actual commitments to verify the secret share, later used with +// stripes to generate the verification shares +fn exponential(i: Participant, values: &[C::G]) -> Vec<(C::F, C::G)> { + let i = C::F::from(u16::from(i).into()); + let mut res = Vec::with_capacity(values.len()); + (0 .. values.len()).fold(C::F::ONE, |exp, l| { + res.push((exp, values[l])); + exp * i + }); + res +} + +fn share_verification_statements( + target: Participant, + commitments: &[C::G], + mut share: Zeroizing, +) -> Vec<(C::F, C::G)> { + // This can be insecurely linearized from n * t to just n using the below sums for a given + // stripe. Doing so uses naive addition which is subject to malleability. The only way to + // ensure that malleability isn't present is to use this n * t algorithm, which runs + // per sender and not as an aggregate of all senders, which also enables blame + let mut values = exponential::(target, commitments); + + // Perform the share multiplication outside of the multiexp to minimize stack copying + // While the multiexp BatchVerifier does zeroize its flattened multiexp, and itself, it still + // converts whatever we give to an iterator and then builds a Vec internally, welcoming copies + let neg_share_pub = C::generator() * -*share; + share.zeroize(); + values.push((C::F::ONE, neg_share_pub)); + + values +} + +/// A machine used to recieve a reshared key. +#[derive(Zeroize)] +pub struct ResharedMachine { + params: ThresholdParams, + commitments: Vec>, + encryption: Encryption, +} + +impl fmt::Debug for ResharedMachine { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt + .debug_struct("ResharedMachine") + .field("params", &self.params) + .field("commitments", &self.commitments) + .field("encryption", &self.encryption) + .finish() + } +} + +impl ResharedMachine { + /// Create a new machine to receive a reshared key. + /// + /// This machine is not capable of accurately blaming faulty parts. Any blame statements + /// generated must signify a fault, yet not be trusted to identify the faulty party. + pub fn new( + rng: &mut R, + resharers: u16, + params: ThresholdParams, + context: String, + commitments: Vec>>, + ) -> Result<(ResharedMachine, EncryptionKeyMessage), ReshareError> { + let mut encryption = Encryption::new(context.clone(), params.i, rng); + let msg = encryption.registration(()); + + if usize::from(resharers) != commitments.len() { + Err(ReshareError::InvalidParticipantQuantity(resharers.into(), commitments.len()))?; + } + + let mut batch = BatchVerifier::::new(commitments.len()); + let commitments = commitments + .into_iter() + .enumerate() + .map(|(l, msg)| { + let mut msg = encryption.register(Participant((l + 1).try_into().unwrap()), msg); + + // Step 5: Validate each proof of knowledge + // This is solely the prep step for the latter batch verification + msg.sig.batch_verify( + rng, + &mut batch, + Participant(0), + msg.commitments[0], + challenge::(&context, msg.sig.R.to_bytes().as_ref(), &msg.cached_msg), + ); + + msg.commitments.drain(..).collect::>() + }) + .collect::>(); + + // TODO: Document how this blame will not be valid + batch.verify_vartime_with_vartime_blame().map_err(ReshareError::InvalidProofOfKnowledge)?; + + Ok((ResharedMachine { params, commitments, encryption }, msg)) + } + + /// Accept private key shares from the resharers. + /// + /// The order of the shares must have the same order as the prior specified commitments. + /// + /// This will yield a ThresholdCore yet it cannot be trusted until all parties confirm successful + /// completion of the protocol. + pub fn accept_shares( + self, + rng: &mut R, + shares: Vec>>, + ) -> Result, ReshareError> { + let ResharedMachine { params, commitments, encryption } = self; + if commitments.len() != shares.len() { + Err(ReshareError::InvalidParticipantQuantity(commitments.len(), shares.len()))?; + } + + let mut secret = Zeroizing::new(C::F::ZERO); + let mut batch = BatchVerifier::new(shares.len()); + for (l, share_bytes) in shares.into_iter().enumerate() { + let (mut share_bytes, _) = + encryption.decrypt(rng, &mut batch, (), Participant(0), share_bytes); + let share = Zeroizing::new( + Option::::from(C::F::from_repr(share_bytes.0)) + .ok_or(ReshareError::InvalidShare { participant: Participant(0), blame: None })?, + ); + share_bytes.zeroize(); + *secret += share.deref(); + + batch.queue(rng, (), share_verification_statements::(params.i(), &commitments[l], share)); + } + batch + .verify_with_vartime_blame() + .map_err(|_| ReshareError::InvalidShare { participant: Participant(0), blame: None })?; + + // Stripe commitments per t and sum them in advance. Calculating verification shares relies on + // these sums so preprocessing them is a massive speedup + // If these weren't just sums, yet the tables used in multiexp, this would be further optimized + // As of right now, each multiexp will regenerate them + let mut stripes = Vec::with_capacity(usize::from(params.t())); + for t in 0 .. usize::from(params.t()) { + stripes.push(commitments.iter().map(|commitments| commitments[t]).sum()); + } + + // Calculate each user's verification share + let mut verification_shares = HashMap::new(); + for i in (1 ..= params.n()).map(Participant) { + verification_shares.insert( + i, + if i == params.i() { + C::generator() * secret.deref() + } else { + multiexp_vartime(&exponential::(i, &stripes)) + }, + ); + } + + Ok(ThresholdCore { params, secret_share: secret, group_key: stripes[0], verification_shares }) + } +} diff --git a/crypto/dkg/src/resharing/resharer.rs b/crypto/dkg/src/resharing/resharer.rs new file mode 100644 index 0000000..d154fef --- /dev/null +++ b/crypto/dkg/src/resharing/resharer.rs @@ -0,0 +1,180 @@ +use core::{marker::PhantomData, ops::Deref, fmt}; +use std::collections::{HashSet, HashMap}; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::{Zeroize, Zeroizing}; + +use ciphersuite::{ + group::{ff::PrimeField, GroupEncoding}, + Ciphersuite, +}; + +use schnorr::SchnorrSignature; + +use crate::{ + Participant, ThresholdParams, ThresholdKeys, validate_map, lagrange, + encryption::{EncryptionKeyMessage, EncryptedMessage, Encryption}, + resharing::common::*, +}; + +/// State machine to begin the resharing protocol. +#[derive(Debug, Zeroize)] +pub struct ResharingMachine { + share: Zeroizing, + params: ThresholdParams, + context: String, + _curve: PhantomData, +} + +impl ResharingMachine { + /// Create a new machine to reshare a key. + /// + /// Returns None if the keys have an offset or if the specified resharers are invalid. + /// + /// The context string should be unique among multisigs. + // TODO: Return an eerror + pub fn new( + existing_keys: ThresholdKeys, + mut resharers: Vec, + params: ThresholdParams, + context: String, + ) -> Option> { + if existing_keys.current_offset().is_some() { + return None; + } + + if resharers.len() < existing_keys.params().t.into() { + return None; + } + + resharers.sort_unstable(); + // TODO: Use a sort, check equality test rather than a HashSet + if resharers.iter().cloned().collect::>().len() != resharers.len() { + return None; + } + + for participant in &resharers { + if u16::from(*participant) > existing_keys.params().n { + return None; + } + } + + let share = Zeroizing::new( + lagrange::(existing_keys.params().i, &resharers) * existing_keys.secret_share().deref(), + ); + Some(ResharingMachine { share, params, context, _curve: PhantomData }) + } + + /// Returns a commitments message to be sent to all parties over an authenticated channel. If any + /// party submits multiple sets of commitments, they MUST be treated as malicious. + pub fn generate_coefficients( + self, + rng: &mut R, + ) -> (ResharingSecretMachine, EncryptionKeyMessage>) { + let t = usize::from(self.params.t); + let mut coefficients = Vec::with_capacity(t); + let mut commitments = Vec::with_capacity(t); + let mut cached_msg = vec![]; + + coefficients.push(self.share); + commitments.push(C::generator() * coefficients[0].deref()); + cached_msg.extend(commitments[0].to_bytes().as_ref()); + for i in 1 .. t { + // Step 1: Generate t random values to form a polynomial with + coefficients.push(Zeroizing::new(C::random_nonzero_F(&mut *rng))); + // Step 3: Generate public commitments + commitments.push(C::generator() * coefficients[i].deref()); + cached_msg.extend(commitments[i].to_bytes().as_ref()); + } + + // Step 2: Provide a proof of knowledge + let r = Zeroizing::new(C::random_nonzero_F(rng)); + let nonce = C::generator() * r.deref(); + let sig = SchnorrSignature::::sign( + &coefficients[0], + // This could be deterministic as the PoK is a singleton never opened up to cooperative + // discussion + // There's no reason to spend the time and effort to make this deterministic besides a + // general obsession with canonicity and determinism though + r, + challenge::(&self.context, nonce.to_bytes().as_ref(), &cached_msg), + ); + + // Additionally create an encryption mechanism to protect the secret shares + // Use 0 for our participant ID since we don't necessarily have a participant ID in the new + // multisig + // Reduces the integrity of the proofs of possession used by encryption, yet shouldn't + // invalidate them + // It doesn't matter either way since resharing doesn't expose a blame API + let encryption = Encryption::new(self.context.clone(), Participant(0), rng); + + // Step 4: Broadcast + let msg = + encryption.registration(Commitments { commitments: commitments.clone(), cached_msg, sig }); + ( + ResharingSecretMachine { + params: self.params, + context: self.context, + coefficients, + encryption, + }, + msg, + ) + } +} + +#[derive(Zeroize)] +pub struct ResharingSecretMachine { + params: ThresholdParams, + context: String, + coefficients: Vec>, + encryption: Encryption, +} + +impl fmt::Debug for ResharingSecretMachine { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt + .debug_struct("ResharingSecretMachine") + .field("params", &self.params) + .field("context", &self.context) + .field("encryption", &self.encryption) + .finish_non_exhaustive() + } +} + +impl ResharingSecretMachine { + /// Generate the secret shares for this resharing. + /// + /// Takes in everyone the reshare's targets' encryption keys. Returns a HashMap of encrypted + /// secret shares to be sent over authenticated channels to their relevant counterparties. + /// + /// If any participant sends multiple secret shares to another participant, they are faulty. + #[allow(clippy::type_complexity)] + pub fn generate_secret_shares( + mut self, + rng: &mut R, + mut encryption_keys: HashMap>, + ) -> Result>>, ReshareError> { + validate_map( + &encryption_keys, + &(1 ..= self.params.n).map(Participant).collect::>(), + None, + )?; + + // Step 1: Generate secret shares for all other parties + let mut res = HashMap::new(); + for l in (1 ..= self.params.n()).map(Participant) { + // Safe unwrap due to validate_map + self.encryption.register(l, encryption_keys.remove(&l).unwrap()); + + let mut share = polynomial(&self.coefficients, l); + let share_bytes = Zeroizing::new(SecretShare::(share.to_repr())); + share.zeroize(); + res.insert(l, self.encryption.encrypt(rng, l, share_bytes)); + } + self.coefficients.zeroize(); + self.encryption.zeroize(); + Ok(res) + } +} diff --git a/crypto/dkg/src/tests/frost.rs b/crypto/dkg/src/tests/frost.rs new file mode 100644 index 0000000..50a8e90 --- /dev/null +++ b/crypto/dkg/src/tests/frost.rs @@ -0,0 +1,315 @@ +use std::collections::HashMap; + +use rand_core::{RngCore, CryptoRng}; + +use ciphersuite::Ciphersuite; + +use crate::{ + Participant, ThresholdParams, ThresholdCore, + frost::{KeyGenMachine, SecretShare, KeyMachine}, + encryption::{EncryptionKeyMessage, EncryptedMessage}, + tests::{THRESHOLD, PARTICIPANTS, clone_without}, +}; + +// Needed so rustfmt doesn't fail to format on line length issues +type FrostEncryptedMessage = EncryptedMessage::F>>; +type FrostSecretShares = HashMap>; + +const CONTEXT: &str = "DKG Test Key Generation"; + +// Commit, then return enc key and shares +#[allow(clippy::type_complexity)] +fn commit_enc_keys_and_shares( + rng: &mut R, +) -> ( + HashMap>, + HashMap, + HashMap>, +) { + let mut machines = HashMap::new(); + let mut commitments = HashMap::new(); + let mut enc_keys = HashMap::new(); + for i in (1 ..= PARTICIPANTS).map(Participant) { + let params = ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap(); + let machine = KeyGenMachine::::new(params, CONTEXT.to_string()); + let (machine, these_commitments) = machine.generate_coefficients(rng); + machines.insert(i, machine); + + commitments.insert( + i, + EncryptionKeyMessage::read::<&[u8]>(&mut these_commitments.serialize().as_ref(), params) + .unwrap(), + ); + enc_keys.insert(i, commitments[&i].enc_key()); + } + + let mut secret_shares = HashMap::new(); + let machines = machines + .drain() + .map(|(l, machine)| { + let (machine, mut shares) = + machine.generate_secret_shares(rng, clone_without(&commitments, &l)).unwrap(); + let shares = shares + .drain() + .map(|(l, share)| { + ( + l, + EncryptedMessage::read::<&[u8]>( + &mut share.serialize().as_ref(), + // Only t/n actually matters, so hardcode i to 1 here + ThresholdParams { t: THRESHOLD, n: PARTICIPANTS, i: Participant(1) }, + ) + .unwrap(), + ) + }) + .collect::>(); + secret_shares.insert(l, shares); + (l, machine) + }) + .collect::>(); + + (machines, enc_keys, secret_shares) +} + +fn generate_secret_shares( + shares: &HashMap>, + recipient: Participant, +) -> FrostSecretShares { + let mut our_secret_shares = HashMap::new(); + for (i, shares) in shares { + if recipient == *i { + continue; + } + our_secret_shares.insert(*i, shares[&recipient].clone()); + } + our_secret_shares +} + +/// Fully perform the FROST key generation algorithm. +pub fn frost_gen( + rng: &mut R, +) -> HashMap> { + let (mut machines, _, secret_shares) = commit_enc_keys_and_shares::<_, C>(rng); + + let mut verification_shares = None; + let mut group_key = None; + machines + .drain() + .map(|(i, machine)| { + let our_secret_shares = generate_secret_shares(&secret_shares, i); + let these_keys = machine.calculate_share(rng, our_secret_shares).unwrap().complete(); + + // Verify the verification_shares are agreed upon + if verification_shares.is_none() { + verification_shares = Some(these_keys.verification_shares()); + } + assert_eq!(verification_shares.as_ref().unwrap(), &these_keys.verification_shares()); + + // Verify the group keys are agreed upon + if group_key.is_none() { + group_key = Some(these_keys.group_key()); + } + assert_eq!(group_key.unwrap(), these_keys.group_key()); + + (i, these_keys) + }) + .collect::>() +} + +#[cfg(test)] +mod literal { + use rand_core::OsRng; + + use ciphersuite::Ristretto; + + use crate::{DkgError, encryption::EncryptionKeyProof, frost::BlameMachine}; + + use super::*; + + const ONE: Participant = Participant(1); + const TWO: Participant = Participant(2); + + fn test_blame( + machines: Vec>, + msg: FrostEncryptedMessage, + blame: Option>, + ) { + for machine in machines { + let (additional, blamed) = machine.blame(ONE, TWO, msg.clone(), blame.clone()); + assert_eq!(blamed, ONE); + // Verify additional blame also works + assert_eq!(additional.blame(ONE, TWO, msg.clone(), blame.clone()), ONE); + } + } + + // TODO: Write a macro which expands to the following + #[test] + fn invalid_encryption_pop_blame() { + let (mut machines, _, mut secret_shares) = + commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng); + + // Mutate the PoP of the encrypted message from 1 to 2 + secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_pop(); + + let mut blame = None; + let machines = machines + .drain() + .filter_map(|(i, machine)| { + let our_secret_shares = generate_secret_shares(&secret_shares, i); + let machine = machine.calculate_share(&mut OsRng, our_secret_shares); + if i == TWO { + assert_eq!(machine.err(), Some(DkgError::InvalidShare { participant: ONE, blame: None })); + // Explicitly declare we have a blame object, which happens to be None since invalid PoP + // is self-explainable + blame = Some(None); + None + } else { + Some(machine.unwrap()) + } + }) + .collect::>(); + + test_blame(machines, secret_shares[&ONE][&TWO].clone(), blame.unwrap()); + } + + #[test] + fn invalid_ecdh_blame() { + let (mut machines, _, mut secret_shares) = + commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng); + + // Mutate the share to trigger a blame event + // Mutates from 2 to 1, as 1 is expected to end up malicious for test_blame to pass + // While here, 2 is malicious, this is so 1 creates the blame proof + // We then malleate 1's blame proof, so 1 ends up malicious + // Doesn't simply invalidate the PoP as that won't have a blame statement + // By mutating the encrypted data, we do ensure a blame statement is created + secret_shares + .get_mut(&TWO) + .unwrap() + .get_mut(&ONE) + .unwrap() + .invalidate_msg(&mut OsRng, CONTEXT, TWO); + + let mut blame = None; + let machines = machines + .drain() + .filter_map(|(i, machine)| { + let our_secret_shares = generate_secret_shares(&secret_shares, i); + let machine = machine.calculate_share(&mut OsRng, our_secret_shares); + if i == ONE { + blame = Some(match machine.err() { + Some(DkgError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame), + _ => panic!(), + }); + None + } else { + Some(machine.unwrap()) + } + }) + .collect::>(); + + blame.as_mut().unwrap().as_mut().unwrap().invalidate_key(); + test_blame(machines, secret_shares[&TWO][&ONE].clone(), blame.unwrap()); + } + + // This should be largely equivalent to the prior test + #[test] + fn invalid_dleq_blame() { + let (mut machines, _, mut secret_shares) = + commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng); + + secret_shares + .get_mut(&TWO) + .unwrap() + .get_mut(&ONE) + .unwrap() + .invalidate_msg(&mut OsRng, CONTEXT, TWO); + + let mut blame = None; + let machines = machines + .drain() + .filter_map(|(i, machine)| { + let our_secret_shares = generate_secret_shares(&secret_shares, i); + let machine = machine.calculate_share(&mut OsRng, our_secret_shares); + if i == ONE { + blame = Some(match machine.err() { + Some(DkgError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame), + _ => panic!(), + }); + None + } else { + Some(machine.unwrap()) + } + }) + .collect::>(); + + blame.as_mut().unwrap().as_mut().unwrap().invalidate_dleq(); + test_blame(machines, secret_shares[&TWO][&ONE].clone(), blame.unwrap()); + } + + #[test] + fn invalid_share_serialization_blame() { + let (mut machines, enc_keys, mut secret_shares) = + commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng); + + secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_serialization( + &mut OsRng, + CONTEXT, + ONE, + enc_keys[&TWO], + ); + + let mut blame = None; + let machines = machines + .drain() + .filter_map(|(i, machine)| { + let our_secret_shares = generate_secret_shares(&secret_shares, i); + let machine = machine.calculate_share(&mut OsRng, our_secret_shares); + if i == TWO { + blame = Some(match machine.err() { + Some(DkgError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame), + _ => panic!(), + }); + None + } else { + Some(machine.unwrap()) + } + }) + .collect::>(); + + test_blame(machines, secret_shares[&ONE][&TWO].clone(), blame.unwrap()); + } + + #[test] + fn invalid_share_value_blame() { + let (mut machines, enc_keys, mut secret_shares) = + commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng); + + secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_value( + &mut OsRng, + CONTEXT, + ONE, + enc_keys[&TWO], + ); + + let mut blame = None; + let machines = machines + .drain() + .filter_map(|(i, machine)| { + let our_secret_shares = generate_secret_shares(&secret_shares, i); + let machine = machine.calculate_share(&mut OsRng, our_secret_shares); + if i == TWO { + blame = Some(match machine.err() { + Some(DkgError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame), + _ => panic!(), + }); + None + } else { + Some(machine.unwrap()) + } + }) + .collect::>(); + + test_blame(machines, secret_shares[&ONE][&TWO].clone(), blame.unwrap()); + } +} diff --git a/crypto/dkg/src/tests/mod.rs b/crypto/dkg/src/tests/mod.rs new file mode 100644 index 0000000..99c8c80 --- /dev/null +++ b/crypto/dkg/src/tests/mod.rs @@ -0,0 +1,103 @@ +use core::ops::Deref; +use std::collections::HashMap; + +use zeroize::Zeroizing; +use rand_core::{RngCore, CryptoRng}; + +use ciphersuite::{group::ff::Field, Ciphersuite}; + +use crate::{Participant, ThresholdCore, ThresholdKeys, lagrange, musig::musig as musig_fn}; + +mod musig; +pub use musig::test_musig; + +/// FROST key generation testing utility. +pub mod frost; +use frost::frost_gen; + +mod resharing; + +// Promotion test. +mod promote; +use promote::test_generator_promotion; + +/// Constant amount of participants to use when testing. +pub const PARTICIPANTS: u16 = 5; +/// Constant threshold of participants to use when testing. +pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1; + +/// Clone a map without a specific value. +pub fn clone_without( + map: &HashMap, + without: &K, +) -> HashMap { + let mut res = map.clone(); + res.remove(without).unwrap(); + res +} + +/// Recover the secret from a collection of keys. +/// +/// This will panic if no keys, an insufficient amount of keys, or the wrong keys are provided. +pub fn recover_key(keys: &HashMap>) -> C::F { + let first = keys.values().next().expect("no keys provided"); + assert!(keys.len() >= first.params().t().into(), "not enough keys provided"); + let included = keys.keys().copied().collect::>(); + + let group_private = keys.iter().fold(C::F::ZERO, |accum, (i, keys)| { + accum + (lagrange::(*i, &included) * keys.secret_share().deref()) + }); + assert_eq!(C::generator() * group_private, first.group_key(), "failed to recover keys"); + group_private +} + +/// Generate threshold keys for tests. +pub fn key_gen( + rng: &mut R, +) -> HashMap> { + let res = frost_gen(rng) + .drain() + .map(|(i, core)| { + assert_eq!( + &ThresholdCore::::read::<&[u8]>(&mut core.serialize().as_ref()).unwrap(), + &core + ); + (i, ThresholdKeys::new(core)) + }) + .collect(); + assert_eq!(C::generator() * recover_key(&res), res[&Participant(1)].group_key()); + res +} + +/// Generate MuSig keys for tests. +pub fn musig_key_gen( + rng: &mut R, +) -> HashMap> { + let mut keys = vec![]; + let mut pub_keys = vec![]; + for _ in 0 .. PARTICIPANTS { + let key = Zeroizing::new(C::F::random(&mut *rng)); + pub_keys.push(C::generator() * *key); + keys.push(key); + } + + let mut res = HashMap::new(); + for key in keys { + let these_keys = musig_fn::(b"Test MuSig Key Gen", &key, &pub_keys).unwrap(); + res.insert(these_keys.params().i(), ThresholdKeys::new(these_keys)); + } + + assert_eq!(C::generator() * recover_key(&res), res[&Participant(1)].group_key()); + res +} + +/// Run the test suite on a ciphersuite. +pub fn test_ciphersuite(rng: &mut R) { + key_gen::<_, C>(rng); + test_generator_promotion::<_, C>(rng); +} + +#[test] +fn test_with_ristretto() { + test_ciphersuite::<_, ciphersuite::Ristretto>(&mut rand_core::OsRng); +} diff --git a/crypto/dkg/src/tests/musig.rs b/crypto/dkg/src/tests/musig.rs new file mode 100644 index 0000000..086b26b --- /dev/null +++ b/crypto/dkg/src/tests/musig.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; + +use zeroize::Zeroizing; +use rand_core::{RngCore, CryptoRng}; + +use ciphersuite::{group::ff::Field, Ciphersuite}; + +use crate::{ + ThresholdKeys, + musig::{musig_key, musig}, + tests::{PARTICIPANTS, recover_key}, +}; + +/// Tests MuSig key generation. +pub fn test_musig(rng: &mut R) { + let mut keys = vec![]; + let mut pub_keys = vec![]; + for _ in 0 .. PARTICIPANTS { + let key = Zeroizing::new(C::F::random(&mut *rng)); + pub_keys.push(C::generator() * *key); + keys.push(key); + } + + const CONTEXT: &[u8] = b"MuSig Test"; + + // Empty signing set + musig::(CONTEXT, &Zeroizing::new(C::F::ZERO), &[]).unwrap_err(); + // Signing set we're not part of + musig::(CONTEXT, &Zeroizing::new(C::F::ZERO), &[C::generator()]).unwrap_err(); + + // Test with n keys + { + let mut created_keys = HashMap::new(); + let mut verification_shares = HashMap::new(); + let group_key = musig_key::(CONTEXT, &pub_keys).unwrap(); + for (i, key) in keys.iter().enumerate() { + let these_keys = musig::(CONTEXT, key, &pub_keys).unwrap(); + assert_eq!(these_keys.params().t(), PARTICIPANTS); + assert_eq!(these_keys.params().n(), PARTICIPANTS); + assert_eq!(usize::from(these_keys.params().i().0), i + 1); + + verification_shares + .insert(these_keys.params().i(), C::generator() * **these_keys.secret_share()); + + assert_eq!(these_keys.group_key(), group_key); + + created_keys.insert(these_keys.params().i(), ThresholdKeys::new(these_keys)); + } + + for keys in created_keys.values() { + assert_eq!(keys.verification_shares(), verification_shares); + } + + assert_eq!(C::generator() * recover_key(&created_keys), group_key); + } +} + +#[test] +fn musig_literal() { + test_musig::<_, ciphersuite::Ristretto>(&mut rand_core::OsRng) +} diff --git a/crypto/dkg/src/tests/promote.rs b/crypto/dkg/src/tests/promote.rs new file mode 100644 index 0000000..99c0043 --- /dev/null +++ b/crypto/dkg/src/tests/promote.rs @@ -0,0 +1,62 @@ +use core::{marker::PhantomData, ops::Deref}; +use std::collections::HashMap; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::Zeroize; + +use ciphersuite::{group::Group, Ciphersuite}; + +use crate::{ + promote::{GeneratorPromotion, GeneratorProof}, + tests::{clone_without, key_gen, recover_key}, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +struct AltGenerator { + _curve: PhantomData, +} + +impl Ciphersuite for AltGenerator { + type F = C::F; + type G = C::G; + type H = C::H; + + const ID: &'static [u8] = b"Alternate Ciphersuite"; + + fn generator() -> Self::G { + C::G::generator() * ::hash_to_F(b"DKG Promotion Test", b"generator") + } + + fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F { + ::hash_to_F(dst, data) + } +} + +// Test promotion of threshold keys to another generator +pub(crate) fn test_generator_promotion(rng: &mut R) { + let keys = key_gen::<_, C>(&mut *rng); + + let mut promotions = HashMap::new(); + let mut proofs = HashMap::new(); + for (i, keys) in &keys { + let (promotion, proof) = + GeneratorPromotion::<_, AltGenerator>::promote(&mut *rng, keys.clone()); + promotions.insert(*i, promotion); + proofs.insert(*i, GeneratorProof::::read::<&[u8]>(&mut proof.serialize().as_ref()).unwrap()); + } + + let new_group_key = AltGenerator::::generator() * recover_key(&keys); + for (i, promoting) in promotions.drain() { + let promoted = promoting.complete(&clone_without(&proofs, &i)).unwrap(); + assert_eq!(keys[&i].params(), promoted.params()); + assert_eq!(keys[&i].secret_share(), promoted.secret_share()); + assert_eq!(new_group_key, promoted.group_key()); + for (l, verification_share) in promoted.verification_shares() { + assert_eq!( + AltGenerator::::generator() * keys[&l].secret_share().deref(), + verification_share + ); + } + } +} diff --git a/crypto/dkg/src/tests/resharing.rs b/crypto/dkg/src/tests/resharing.rs new file mode 100644 index 0000000..d119a98 --- /dev/null +++ b/crypto/dkg/src/tests/resharing.rs @@ -0,0 +1,87 @@ +#[cfg(test)] +mod literal { + use std::collections::HashMap; + + use rand_core::OsRng; + + use ciphersuite::{Ciphersuite, Ristretto}; + + use crate::{ + Participant, ThresholdParams, + resharing::*, + tests::{recover_key, frost::frost_gen}, + }; + + const CONTEXT: &str = "DKG Test Resharing"; + + #[test] + fn resharing() { + let keys = frost_gen::<_, Ristretto>(&mut OsRng); + + let first_keys = keys.values().next().unwrap().clone(); + let resharers = (1 ..= first_keys.params().t()).map(Participant).collect::>(); + let mut resharer_machines = vec![]; + let mut commitments = vec![]; + for i in &resharers { + let (machine, msg) = ResharingMachine::new( + keys[i].clone().into(), + resharers.clone(), + ThresholdParams::new(2, 6, Participant::new(1).unwrap()).unwrap(), + CONTEXT.to_string(), + ) + .unwrap() + .generate_coefficients(&mut OsRng); + resharer_machines.push(machine); + commitments.push(msg); + } + + let mut reshareds = HashMap::new(); + let mut keys = HashMap::new(); + for i in 1 ..= 6u16 { + let i = Participant::new(i).unwrap(); + let new_params = ThresholdParams::new(2, 6, i).unwrap(); + let (machine, key) = ResharedMachine::new( + &mut OsRng, + resharers.len().try_into().unwrap(), + new_params, + CONTEXT.to_string(), + commitments.clone(), + ) + .unwrap(); + reshareds.insert(i, machine); + keys.insert(i, key); + } + + let mut shares = vec![]; + for resharer in resharer_machines { + shares.push(resharer.generate_secret_shares(&mut OsRng, keys.clone()).unwrap()); + } + + let mut verification_shares = None; + let mut keys = HashMap::new(); + for i in 1 ..= 6u16 { + let i = Participant::new(i).unwrap(); + let these_keys = reshareds + .remove(&i) + .unwrap() + .accept_shares( + &mut OsRng, + shares.iter().map(|shares| shares[&i].clone()).collect::>(), + ) + .unwrap(); + + // Verify the verification_shares are agreed upon + if verification_shares.is_none() { + verification_shares = Some(these_keys.verification_shares()); + } + assert_eq!(verification_shares.as_ref().unwrap(), &these_keys.verification_shares()); + + // Verify the group keys are agreed upon + assert_eq!(these_keys.group_key(), first_keys.group_key()); + + keys.insert(i, these_keys.into()); + } + + assert_eq!(Ristretto::generator() * recover_key(&keys), first_keys.group_key()); + } +} diff --git a/crypto/frost/Cargo.toml b/crypto/frost/Cargo.toml new file mode 100644 index 0000000..9b0c32c --- /dev/null +++ b/crypto/frost/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "modular-frost" +version = "0.8.1" +description = "Modular implementation of FROST over ff/group" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/crypto/frost" +authors = ["Luke Parker "] +keywords = ["frost", "multisig", "threshold"] +edition = "2021" +rust-version = "1.70" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +thiserror = "1" + +rand_core = "0.6" +rand_chacha = "0.3" + +zeroize = { version = "^1.5", features = ["zeroize_derive"] } +subtle = "^2.4" + +hex = { version = "0.4", optional = true } + +digest = "0.10" +transcript = { package = "flexible-transcript", version = "^0.3.2", features = ["recommended"] } + +dalek-ff-group = { version = "0.4", optional = true } +minimal-ed448 = { version = "0.4", optional = true } + +ciphersuite = { version = "^0.4.1", features = ["std"] } + +multiexp = { version = "0.4", features = ["batch"] } + +schnorr = { package = "schnorr-signatures", version = "^0.5.1" } +dleq = { version = "^0.4.1", features = ["serialize"] } + +dkg = { path = "../dkg", version = "^0.5.1" } + +[dev-dependencies] +hex = "0.4" +serde_json = "1" + +dkg = { path = "../dkg", features = ["tests"] } + +[features] +ed25519 = ["dalek-ff-group", "ciphersuite/ed25519"] +ristretto = ["dalek-ff-group", "ciphersuite/ristretto"] + +secp256k1 = ["ciphersuite/secp256k1"] +p256 = ["ciphersuite/p256"] + +ed448 = ["minimal-ed448", "ciphersuite/ed448"] + +tests = ["hex", "rand_core/getrandom", "dkg/tests"] diff --git a/crypto/frost/LICENSE b/crypto/frost/LICENSE new file mode 100644 index 0000000..be67c32 --- /dev/null +++ b/crypto/frost/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-2023 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crypto/frost/README.md b/crypto/frost/README.md new file mode 100644 index 0000000..2784584 --- /dev/null +++ b/crypto/frost/README.md @@ -0,0 +1,22 @@ +# Modular FROST + +A modular implementation of FROST for any curve with a ff/group API. +Additionally, custom algorithms may be specified so any signature reducible to +Schnorr-like may be used with FROST. + +A Schnorr algorithm is provided, of the form (R, s) where `s = r + cx`, which +allows specifying the challenge format. This is intended to easily allow +integrating with existing systems. + +This library offers ciphersuites compatible with the +[IETF draft](https://github.com/cfrg/draft-irtf-cfrg-frost). Currently, version +11 is supported. + +This library was +[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf), +culminating in commit +[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06). +Any subsequent changes have not undergone auditing. While this audit included +FROST's definition of Ed448, the underlying Ed448 ciphersuite (offered by the +ciphersuite crate) was not audited, nor was the minimal-ed448 crate implementing +the curve itself. diff --git a/crypto/frost/src/algorithm.rs b/crypto/frost/src/algorithm.rs new file mode 100644 index 0000000..073b483 --- /dev/null +++ b/crypto/frost/src/algorithm.rs @@ -0,0 +1,222 @@ +use core::{marker::PhantomData, fmt::Debug}; +use std::io::{self, Read, Write}; + +use zeroize::Zeroizing; +use rand_core::{RngCore, CryptoRng}; + +use transcript::Transcript; + +use crate::{Participant, ThresholdKeys, ThresholdView, Curve, FrostError}; +pub use schnorr::SchnorrSignature; + +/// Write an addendum to a writer. +pub trait WriteAddendum { + fn write(&self, writer: &mut W) -> io::Result<()>; +} + +impl WriteAddendum for () { + fn write(&self, _: &mut W) -> io::Result<()> { + Ok(()) + } +} + +/// Trait alias for the requirements to be used as an addendum. +pub trait Addendum: Send + Sync + Clone + PartialEq + Debug + WriteAddendum {} +impl Addendum for A {} + +/// Algorithm trait usable by the FROST signing machine to produce signatures.. +pub trait Algorithm: Send + Sync + Clone { + /// The transcript format this algorithm uses. This likely should NOT be the IETF-compatible + /// transcript included in this crate. + type Transcript: Sync + Clone + Debug + Transcript; + /// Serializable addendum, used in algorithms requiring more data than just the nonces. + type Addendum: Addendum; + /// The resulting type of the signatures this algorithm will produce. + type Signature: Clone + PartialEq + Debug; + + /// Obtain a mutable borrow of the underlying transcript. + fn transcript(&mut self) -> &mut Self::Transcript; + + /// Obtain the list of nonces to generate, as specified by the generators to create commitments + /// against per-nonce. + fn nonces(&self) -> Vec>; + + /// Generate an addendum to FROST"s preprocessing stage. + fn preprocess_addendum( + &mut self, + rng: &mut R, + keys: &ThresholdKeys, + ) -> Self::Addendum; + + /// Read an addendum from a reader. + fn read_addendum(&self, reader: &mut R) -> io::Result; + + /// Proccess the addendum for the specified participant. Guaranteed to be called in order. + fn process_addendum( + &mut self, + params: &ThresholdView, + l: Participant, + reader: Self::Addendum, + ) -> Result<(), FrostError>; + + /// Sign a share with the given secret/nonce. + /// The secret will already have been its lagrange coefficient applied so it is the necessary + /// key share. + /// The nonce will already have been processed into the combined form d + (e * p). + fn sign_share( + &mut self, + params: &ThresholdView, + nonce_sums: &[Vec], + nonces: Vec>, + msg: &[u8], + ) -> C::F; + + /// Verify a signature. + #[must_use] + fn verify(&self, group_key: C::G, nonces: &[Vec], sum: C::F) -> Option; + + /// Verify a specific share given as a response. + /// This function should return a series of pairs whose products should sum to zero for a valid + /// share. Any error raised is treated as the share being invalid. + #[allow(clippy::type_complexity, clippy::result_unit_err)] + fn verify_share( + &self, + verification_share: C::G, + nonces: &[Vec], + share: C::F, + ) -> Result, ()>; +} + +mod sealed { + pub use super::*; + + /// IETF-compliant transcript. This is incredibly naive and should not be used within larger + /// protocols. + #[derive(Clone, Debug)] + pub struct IetfTranscript(pub(crate) Vec); + impl Transcript for IetfTranscript { + type Challenge = Vec; + + fn new(_: &'static [u8]) -> IetfTranscript { + IetfTranscript(vec![]) + } + + fn domain_separate(&mut self, _: &[u8]) {} + + fn append_message>(&mut self, _: &'static [u8], message: M) { + self.0.extend(message.as_ref()); + } + + fn challenge(&mut self, _: &'static [u8]) -> Vec { + self.0.clone() + } + + // FROST won't use this and this shouldn't be used outside of FROST + fn rng_seed(&mut self, _: &'static [u8]) -> [u8; 32] { + unimplemented!() + } + } +} +pub(crate) use sealed::IetfTranscript; + +/// HRAm usable by the included Schnorr signature algorithm to generate challenges. +pub trait Hram: Send + Sync + Clone { + /// HRAm function to generate a challenge. + /// H2 from the IETF draft, despite having a different argument set (not being pre-formatted). + #[allow(non_snake_case)] + fn hram(R: &C::G, A: &C::G, m: &[u8]) -> C::F; +} + +/// Schnorr signature algorithm ((R, s) where s = r + cx). +#[derive(Clone)] +pub struct Schnorr> { + transcript: T, + c: Option, + _hram: PhantomData, +} + +/// IETF-compliant Schnorr signature algorithm. +/// +/// This algorithm specifically uses the transcript format defined in the FROST IETF draft. +/// It's a naive transcript format not viable for usage in larger protocols, yet is presented here +/// in order to provide compatibility. +/// +/// Usage of this with key offsets will break the intended compatibility as the IETF draft does not +/// specify a protocol for offsets. +pub type IetfSchnorr = Schnorr; + +impl> Schnorr { + /// Construct a Schnorr algorithm continuing the specified transcript. + pub fn new(transcript: T) -> Schnorr { + Schnorr { transcript, c: None, _hram: PhantomData } + } +} + +impl> IetfSchnorr { + /// Construct a IETF-compatible Schnorr algorithm. + /// + /// Please see the `IetfSchnorr` documentation for the full details of this. + pub fn ietf() -> IetfSchnorr { + Schnorr::new(IetfTranscript(vec![])) + } +} + +impl> Algorithm for Schnorr { + type Transcript = T; + type Addendum = (); + type Signature = SchnorrSignature; + + fn transcript(&mut self) -> &mut Self::Transcript { + &mut self.transcript + } + + fn nonces(&self) -> Vec> { + vec![vec![C::generator()]] + } + + fn preprocess_addendum(&mut self, _: &mut R, _: &ThresholdKeys) {} + + fn read_addendum(&self, _: &mut R) -> io::Result { + Ok(()) + } + + fn process_addendum( + &mut self, + _: &ThresholdView, + _: Participant, + _: (), + ) -> Result<(), FrostError> { + Ok(()) + } + + fn sign_share( + &mut self, + params: &ThresholdView, + nonce_sums: &[Vec], + mut nonces: Vec>, + msg: &[u8], + ) -> C::F { + let c = H::hram(&nonce_sums[0][0], ¶ms.group_key(), msg); + self.c = Some(c); + SchnorrSignature::::sign(params.secret_share(), nonces.swap_remove(0), c).s + } + + #[must_use] + fn verify(&self, group_key: C::G, nonces: &[Vec], sum: C::F) -> Option { + let sig = SchnorrSignature { R: nonces[0][0], s: sum }; + Some(sig).filter(|sig| sig.verify(group_key, self.c.unwrap())) + } + + fn verify_share( + &self, + verification_share: C::G, + nonces: &[Vec], + share: C::F, + ) -> Result, ()> { + Ok( + SchnorrSignature:: { R: nonces[0][0], s: share } + .batch_statements(verification_share, self.c.unwrap()) + .to_vec(), + ) + } +} diff --git a/crypto/frost/src/curve/dalek.rs b/crypto/frost/src/curve/dalek.rs new file mode 100644 index 0000000..094e200 --- /dev/null +++ b/crypto/frost/src/curve/dalek.rs @@ -0,0 +1,47 @@ +use digest::Digest; + +use dalek_ff_group::Scalar; + +use ciphersuite::Ciphersuite; + +use crate::{curve::Curve, algorithm::Hram}; + +macro_rules! dalek_curve { + ( + $feature: literal, + + $Curve: ident, + $Hram: ident, + + $CONTEXT: literal, + $chal: literal + ) => { + pub use ciphersuite::$Curve; + + impl Curve for $Curve { + const CONTEXT: &'static [u8] = $CONTEXT; + } + + /// The challenge function for this ciphersuite. + #[derive(Copy, Clone)] + pub struct $Hram; + impl Hram<$Curve> for $Hram { + #[allow(non_snake_case)] + fn hram(R: &<$Curve as Ciphersuite>::G, A: &<$Curve as Ciphersuite>::G, m: &[u8]) -> Scalar { + let mut hash = <$Curve as Ciphersuite>::H::new(); + if $chal.len() != 0 { + hash.update(&[$CONTEXT.as_ref(), $chal].concat()); + } + Scalar::from_hash( + hash.chain_update(&[&R.compress().to_bytes(), &A.compress().to_bytes(), m].concat()), + ) + } + } + }; +} + +#[cfg(feature = "ristretto")] +dalek_curve!("ristretto", Ristretto, IetfRistrettoHram, b"FROST-RISTRETTO255-SHA512-v1", b"chal"); + +#[cfg(feature = "ed25519")] +dalek_curve!("ed25519", Ed25519, IetfEd25519Hram, b"FROST-ED25519-SHA512-v1", b""); diff --git a/crypto/frost/src/curve/ed448.rs b/crypto/frost/src/curve/ed448.rs new file mode 100644 index 0000000..0a5e448 --- /dev/null +++ b/crypto/frost/src/curve/ed448.rs @@ -0,0 +1,44 @@ +use digest::Digest; + +use minimal_ed448::{Scalar, Point}; +pub use ciphersuite::{group::GroupEncoding, Shake256_114, Ed448}; + +use crate::{curve::Curve, algorithm::Hram}; + +const CONTEXT: &[u8] = b"FROST-ED448-SHAKE256-v1"; + +impl Curve for Ed448 { + const CONTEXT: &'static [u8] = CONTEXT; +} + +// The RFC-8032 Ed448 challenge function. +#[derive(Copy, Clone)] +pub(crate) struct Ietf8032Ed448Hram; +impl Ietf8032Ed448Hram { + #[allow(non_snake_case)] + pub(crate) fn hram(context: &[u8], R: &Point, A: &Point, m: &[u8]) -> Scalar { + Scalar::wide_reduce( + Shake256_114::digest( + [ + &[b"SigEd448".as_ref(), &[0, u8::try_from(context.len()).unwrap()]].concat(), + context, + &[R.to_bytes().as_ref(), A.to_bytes().as_ref(), m].concat(), + ] + .concat(), + ) + .as_ref() + .try_into() + .unwrap(), + ) + } +} + +/// The challenge function for FROST's Ed448 ciphersuite. +#[derive(Copy, Clone)] +pub struct IetfEd448Hram; +impl Hram for IetfEd448Hram { + #[allow(non_snake_case)] + fn hram(R: &Point, A: &Point, m: &[u8]) -> Scalar { + Ietf8032Ed448Hram::hram(&[], R, A, m) + } +} diff --git a/crypto/frost/src/curve/kp256.rs b/crypto/frost/src/curve/kp256.rs new file mode 100644 index 0000000..81ccc2e --- /dev/null +++ b/crypto/frost/src/curve/kp256.rs @@ -0,0 +1,43 @@ +use ciphersuite::{group::GroupEncoding, Ciphersuite}; + +use crate::{curve::Curve, algorithm::Hram}; + +macro_rules! kp_curve { + ( + $feature: literal, + + $Curve: ident, + $Hram: ident, + + $CONTEXT: literal + ) => { + pub use ciphersuite::$Curve; + + impl Curve for $Curve { + const CONTEXT: &'static [u8] = $CONTEXT; + } + + /// The challenge function for this ciphersuite. + #[derive(Clone)] + pub struct $Hram; + impl Hram<$Curve> for $Hram { + #[allow(non_snake_case)] + fn hram( + R: &<$Curve as Ciphersuite>::G, + A: &<$Curve as Ciphersuite>::G, + m: &[u8], + ) -> <$Curve as Ciphersuite>::F { + <$Curve as Curve>::hash_to_F( + b"chal", + &[R.to_bytes().as_ref(), A.to_bytes().as_ref(), m].concat(), + ) + } + } + }; +} + +#[cfg(feature = "p256")] +kp_curve!("p256", P256, IetfP256Hram, b"FROST-P256-SHA256-v1"); + +#[cfg(feature = "secp256k1")] +kp_curve!("secp256k1", Secp256k1, IetfSecp256k1Hram, b"FROST-secp256k1-SHA256-v1"); diff --git a/crypto/frost/src/curve/mod.rs b/crypto/frost/src/curve/mod.rs new file mode 100644 index 0000000..1fe0c22 --- /dev/null +++ b/crypto/frost/src/curve/mod.rs @@ -0,0 +1,114 @@ +use core::ops::Deref; +use std::io::{self, Read}; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::{Zeroize, Zeroizing}; +use subtle::ConstantTimeEq; + +use digest::{Digest, Output}; + +pub use ciphersuite::{ + group::{ + ff::{Field, PrimeField}, + Group, + }, + Ciphersuite, +}; + +#[cfg(any(feature = "ristretto", feature = "ed25519"))] +mod dalek; +#[cfg(feature = "ristretto")] +pub use dalek::{Ristretto, IetfRistrettoHram}; +#[cfg(feature = "ed25519")] +pub use dalek::{Ed25519, IetfEd25519Hram}; + +#[cfg(any(feature = "secp256k1", feature = "p256"))] +mod kp256; +#[cfg(feature = "secp256k1")] +pub use kp256::{Secp256k1, IetfSecp256k1Hram}; +#[cfg(feature = "p256")] +pub use kp256::{P256, IetfP256Hram}; + +#[cfg(feature = "ed448")] +mod ed448; +#[cfg(feature = "ed448")] +pub use ed448::{Ed448, IetfEd448Hram}; +#[cfg(all(test, feature = "ed448"))] +pub(crate) use ed448::Ietf8032Ed448Hram; + +/// FROST Ciphersuite. +/// +/// This exclude the signing algorithm specific H2, making this solely the curve, its associated +/// hash function, and the functions derived from it. +pub trait Curve: Ciphersuite { + /// Context string for this curve. + const CONTEXT: &'static [u8]; + + /// Hash the given dst and data to a byte vector. Used to instantiate H4 and H5. + fn hash(dst: &[u8], data: &[u8]) -> Output { + Self::H::digest([Self::CONTEXT, dst, data].concat()) + } + + /// Field element from hash. Used during key gen and by other crates under Serai as a general + /// utility. Used to instantiate H1 and H3. + #[allow(non_snake_case)] + fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F { + ::hash_to_F(&[Self::CONTEXT, dst].concat(), msg) + } + + /// Hash the message for the binding factor. H4 from the IETF draft. + fn hash_msg(msg: &[u8]) -> Output { + Self::hash(b"msg", msg) + } + + /// Hash the commitments for the binding factor. H5 from the IETF draft. + fn hash_commitments(commitments: &[u8]) -> Output { + Self::hash(b"com", commitments) + } + + /// Hash the commitments and message to calculate the binding factor. H1 from the IETF draft. + fn hash_binding_factor(binding: &[u8]) -> Self::F { + ::hash_to_F(b"rho", binding) + } + + /// Securely generate a random nonce. H3 from the IETF draft. + fn random_nonce( + secret: &Zeroizing, + rng: &mut R, + ) -> Zeroizing { + let mut seed = Zeroizing::new(vec![0; 32]); + rng.fill_bytes(seed.as_mut()); + + let mut repr = secret.to_repr(); + + // Perform rejection sampling until we reach a non-zero nonce + // While the IETF spec doesn't explicitly require this, generating a zero nonce will produce + // commitments which will be rejected for being zero (and if they were used, leak the secret + // share) + // Rejection sampling here will prevent an honest participant from ever generating 'malicious' + // values and ensure safety + let mut res; + while { + seed.extend(repr.as_ref()); + res = Zeroizing::new(::hash_to_F(b"nonce", seed.deref())); + res.ct_eq(&Self::F::ZERO).into() + } { + seed = Zeroizing::new(vec![0; 32]); + rng.fill_bytes(&mut seed); + } + repr.as_mut().zeroize(); + + res + } + + /// Read a point from a reader, rejecting identity. + #[allow(non_snake_case)] + fn read_G(reader: &mut R) -> io::Result { + let res = ::read_G(reader)?; + if res.is_identity().into() { + Err(io::Error::new(io::ErrorKind::Other, "identity point"))?; + } + Ok(res) + } +} diff --git a/crypto/frost/src/lib.rs b/crypto/frost/src/lib.rs new file mode 100644 index 0000000..47862c6 --- /dev/null +++ b/crypto/frost/src/lib.rs @@ -0,0 +1,73 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] + +use core::fmt::Debug; +use std::collections::HashMap; + +use thiserror::Error; + +/// Distributed key generation protocol. +pub use dkg::{self, Participant, ThresholdParams, ThresholdCore, ThresholdKeys, ThresholdView}; + +/// Curve trait and provided curves/HRAMs, forming various ciphersuites. +pub mod curve; +use curve::Curve; + +/// Algorithm for the signing process. +pub mod algorithm; +mod nonce; +/// Threshold signing protocol. +pub mod sign; + +/// Tests for application-provided curves and algorithms. +#[cfg(any(test, feature = "tests"))] +pub mod tests; + +/// Various errors possible during signing. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)] +pub enum FrostError { + #[error("internal error: {0}")] + InternalError(&'static str), + + #[error("invalid participant (0 < participant <= {0}, yet participant is {1})")] + InvalidParticipant(u16, Participant), + #[error("invalid signing set ({0})")] + InvalidSigningSet(&'static str), + #[error("invalid participant quantity (expected {0}, got {1})")] + InvalidParticipantQuantity(usize, usize), + #[error("duplicated participant ({0})")] + DuplicatedParticipant(Participant), + #[error("missing participant {0}")] + MissingParticipant(Participant), + + #[error("invalid preprocess (participant {0})")] + InvalidPreprocess(Participant), + #[error("invalid share (participant {0})")] + InvalidShare(Participant), +} + +/// Validate a map of values to have the expected participants. +pub fn validate_map( + map: &HashMap, + included: &[Participant], + ours: Participant, +) -> Result<(), FrostError> { + if (map.len() + 1) != included.len() { + Err(FrostError::InvalidParticipantQuantity(included.len(), map.len() + 1))?; + } + + for included in included { + if *included == ours { + if map.contains_key(included) { + Err(FrostError::DuplicatedParticipant(*included))?; + } + continue; + } + + if !map.contains_key(included) { + Err(FrostError::MissingParticipant(*included))?; + } + } + + Ok(()) +} diff --git a/crypto/frost/src/nonce.rs b/crypto/frost/src/nonce.rs new file mode 100644 index 0000000..89b9524 --- /dev/null +++ b/crypto/frost/src/nonce.rs @@ -0,0 +1,310 @@ +// FROST defines its nonce as sum(Di, Ei * bi) +// Monero needs not just the nonce over G however, yet also over H +// Then there is a signature (a modified Chaum Pedersen proof) using multiple nonces at once +// +// Accordingly, in order for this library to be robust, it supports generating an arbitrary amount +// of nonces, each against an arbitrary list of generators +// +// Each nonce remains of the form (d, e) and made into a proper nonce with d + (e * b) +// When representations across multiple generators are provided, a DLEq proof is also provided to +// confirm their integrity + +use core::ops::Deref; +use std::{ + io::{self, Read, Write}, + collections::HashMap, +}; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::{Zeroize, Zeroizing}; + +use transcript::Transcript; + +use ciphersuite::group::{ff::PrimeField, Group, GroupEncoding}; +use multiexp::multiexp_vartime; + +use dleq::MultiDLEqProof; + +use crate::{curve::Curve, Participant}; + +// Transcript used to aggregate binomial nonces for usage within a single DLEq proof. +fn aggregation_transcript(context: &[u8]) -> T { + let mut transcript = T::new(b"FROST DLEq Aggregation v0.5"); + transcript.append_message(b"context", context); + transcript +} + +// Every participant proves for their commitments at the start of the protocol +// These proofs are verified sequentially, requiring independent transcripts +// In order to make these transcripts more robust, the FROST transcript (at time of preprocess) is +// challenged in order to create a commitment to it, carried in each independent transcript +// (effectively forking the original transcript) +// +// For FROST, as defined by the IETF, this will do nothing (and this transcript will never even be +// constructed). For higher level protocols, the transcript may have contextual info these proofs +// will then be bound to +fn dleq_transcript(context: &[u8]) -> T { + let mut transcript = T::new(b"FROST Commitments DLEq v0.5"); + transcript.append_message(b"context", context); + transcript +} + +// Each nonce is actually a pair of random scalars, notated as d, e under the FROST paper +// This is considered a single nonce as r = d + be +#[derive(Clone, Zeroize)] +pub(crate) struct Nonce(pub(crate) [Zeroizing; 2]); + +// Commitments to a specific generator for this binomial nonce +#[derive(Copy, Clone, PartialEq, Eq)] +pub(crate) struct GeneratorCommitments(pub(crate) [C::G; 2]); +impl GeneratorCommitments { + fn read(reader: &mut R) -> io::Result> { + Ok(GeneratorCommitments([::read_G(reader)?, ::read_G(reader)?])) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.0[0].to_bytes().as_ref())?; + writer.write_all(self.0[1].to_bytes().as_ref()) + } +} + +// A single nonce's commitments and relevant proofs +#[derive(Clone, PartialEq, Eq)] +pub(crate) struct NonceCommitments { + // Called generators as these commitments are indexed by generator later on + // So to get the commitments for the first generator, it'd be commitments.generators[0] + pub(crate) generators: Vec>, +} + +impl NonceCommitments { + pub(crate) fn new( + rng: &mut R, + secret_share: &Zeroizing, + generators: &[C::G], + ) -> (Nonce, NonceCommitments) { + let nonce = Nonce::([ + C::random_nonce(secret_share, &mut *rng), + C::random_nonce(secret_share, &mut *rng), + ]); + + let mut commitments = Vec::with_capacity(generators.len()); + for generator in generators { + commitments.push(GeneratorCommitments([ + *generator * nonce.0[0].deref(), + *generator * nonce.0[1].deref(), + ])); + } + + (nonce, NonceCommitments { generators: commitments }) + } + + fn read(reader: &mut R, generators: &[C::G]) -> io::Result> { + Ok(NonceCommitments { + generators: (0 .. generators.len()) + .map(|_| GeneratorCommitments::read(reader)) + .collect::>()?, + }) + } + + fn write(&self, writer: &mut W) -> io::Result<()> { + for generator in &self.generators { + generator.write(writer)?; + } + Ok(()) + } + + fn transcript(&self, t: &mut T) { + t.domain_separate(b"nonce"); + for commitments in &self.generators { + t.append_message(b"commitment_D", commitments.0[0].to_bytes()); + t.append_message(b"commitment_E", commitments.0[1].to_bytes()); + } + } + + fn aggregation_factor(&self, context: &[u8]) -> C::F { + let mut transcript = aggregation_transcript::(context); + self.transcript(&mut transcript); + ::hash_to_F(b"dleq_aggregation", transcript.challenge(b"binding").as_ref()) + } +} + +/// Commitments for all the nonces across all their generators. +#[derive(Clone, PartialEq, Eq)] +pub(crate) struct Commitments { + // Called nonces as these commitments are indexed by nonce + // So to get the commitments for the first nonce, it'd be commitments.nonces[0] + pub(crate) nonces: Vec>, + // DLEq Proof proving that each set of commitments were generated using a single pair of discrete + // logarithms + pub(crate) dleq: Option>, +} + +impl Commitments { + pub(crate) fn new( + rng: &mut R, + secret_share: &Zeroizing, + planned_nonces: &[Vec], + context: &[u8], + ) -> (Vec>, Commitments) { + let mut nonces = vec![]; + let mut commitments = vec![]; + + let mut dleq_generators = vec![]; + let mut dleq_nonces = vec![]; + for generators in planned_nonces { + let (nonce, these_commitments): (Nonce, _) = + NonceCommitments::new(&mut *rng, secret_share, generators); + + if generators.len() > 1 { + dleq_generators.push(generators.clone()); + dleq_nonces.push(Zeroizing::new( + (these_commitments.aggregation_factor::(context) * nonce.0[1].deref()) + + nonce.0[0].deref(), + )); + } + + nonces.push(nonce); + commitments.push(these_commitments); + } + + let dleq = if !dleq_generators.is_empty() { + Some(MultiDLEqProof::prove( + rng, + &mut dleq_transcript::(context), + &dleq_generators, + &dleq_nonces, + )) + } else { + None + }; + + (nonces, Commitments { nonces: commitments, dleq }) + } + + pub(crate) fn transcript(&self, t: &mut T) { + t.domain_separate(b"commitments"); + for nonce in &self.nonces { + nonce.transcript(t); + } + + // Transcripting the DLEqs implicitly transcripts the exact generators used for the nonces in + // an exact order + // This means it shouldn't be possible for variadic generators to cause conflicts + if let Some(dleq) = &self.dleq { + t.append_message(b"dleq", dleq.serialize()); + } + } + + pub(crate) fn read( + reader: &mut R, + generators: &[Vec], + context: &[u8], + ) -> io::Result { + let nonces = (0 .. generators.len()) + .map(|i| NonceCommitments::read(reader, &generators[i])) + .collect::>, _>>()?; + + let mut dleq_generators = vec![]; + let mut dleq_nonces = vec![]; + for (generators, nonce) in generators.iter().cloned().zip(&nonces) { + if generators.len() > 1 { + let binding = nonce.aggregation_factor::(context); + let mut aggregated = vec![]; + for commitments in &nonce.generators { + aggregated.push(commitments.0[0] + (commitments.0[1] * binding)); + } + dleq_generators.push(generators); + dleq_nonces.push(aggregated); + } + } + + let dleq = if !dleq_generators.is_empty() { + let dleq = MultiDLEqProof::read(reader, dleq_generators.len())?; + dleq + .verify(&mut dleq_transcript::(context), &dleq_generators, &dleq_nonces) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "invalid DLEq proof"))?; + Some(dleq) + } else { + None + }; + + Ok(Commitments { nonces, dleq }) + } + + pub(crate) fn write(&self, writer: &mut W) -> io::Result<()> { + for nonce in &self.nonces { + nonce.write(writer)?; + } + if let Some(dleq) = &self.dleq { + dleq.write(writer)?; + } + Ok(()) + } +} + +pub(crate) struct IndividualBinding { + commitments: Commitments, + binding_factors: Option>, +} + +pub(crate) struct BindingFactor(pub(crate) HashMap>); + +impl BindingFactor { + pub(crate) fn insert(&mut self, i: Participant, commitments: Commitments) { + self.0.insert(i, IndividualBinding { commitments, binding_factors: None }); + } + + pub(crate) fn calculate_binding_factors(&mut self, transcript: &T) { + for (l, binding) in self.0.iter_mut() { + let mut transcript = transcript.clone(); + transcript.append_message(b"participant", C::F::from(u64::from(u16::from(*l))).to_repr()); + // It *should* be perfectly fine to reuse a binding factor for multiple nonces + // This generates a binding factor per nonce just to ensure it never comes up as a question + binding.binding_factors = Some( + (0 .. binding.commitments.nonces.len()) + .map(|_| C::hash_binding_factor(transcript.challenge(b"rho").as_ref())) + .collect(), + ); + } + } + + pub(crate) fn binding_factors(&self, i: Participant) -> &[C::F] { + self.0[&i].binding_factors.as_ref().unwrap() + } + + // Get the bound nonces for a specific party + pub(crate) fn bound(&self, l: Participant) -> Vec> { + let mut res = vec![]; + for (i, (nonce, rho)) in + self.0[&l].commitments.nonces.iter().zip(self.binding_factors(l).iter()).enumerate() + { + res.push(vec![]); + for generator in &nonce.generators { + res[i].push(generator.0[0] + (generator.0[1] * rho)); + } + } + res + } + + // Get the nonces for this signing session + pub(crate) fn nonces(&self, planned_nonces: &[Vec]) -> Vec> { + let mut nonces = Vec::with_capacity(planned_nonces.len()); + for n in 0 .. planned_nonces.len() { + nonces.push(Vec::with_capacity(planned_nonces[n].len())); + for g in 0 .. planned_nonces[n].len() { + #[allow(non_snake_case)] + let mut D = C::G::identity(); + let mut statements = Vec::with_capacity(self.0.len()); + #[allow(non_snake_case)] + for IndividualBinding { commitments, binding_factors } in self.0.values() { + D += commitments.nonces[n].generators[g].0[0]; + statements + .push((binding_factors.as_ref().unwrap()[n], commitments.nonces[n].generators[g].0[1])); + } + nonces[n].push(D + multiexp_vartime(&statements)); + } + } + nonces + } +} diff --git a/crypto/frost/src/sign.rs b/crypto/frost/src/sign.rs new file mode 100644 index 0000000..ac4c53a --- /dev/null +++ b/crypto/frost/src/sign.rs @@ -0,0 +1,516 @@ +use core::{ops::Deref, fmt::Debug}; +use std::{ + io::{self, Read, Write}, + collections::HashMap, +}; + +use rand_core::{RngCore, CryptoRng, SeedableRng}; +use rand_chacha::ChaCha20Rng; + +use zeroize::{Zeroize, Zeroizing}; + +use transcript::Transcript; + +use ciphersuite::group::{ + ff::{Field, PrimeField}, + GroupEncoding, +}; +use multiexp::BatchVerifier; + +use crate::{ + curve::Curve, + Participant, FrostError, ThresholdParams, ThresholdKeys, ThresholdView, + algorithm::{WriteAddendum, Addendum, Algorithm}, + validate_map, +}; + +pub(crate) use crate::nonce::*; + +/// Trait enabling writing preprocesses and signature shares. +pub trait Writable { + fn write(&self, writer: &mut W) -> io::Result<()>; + + fn serialize(&self) -> Vec { + let mut buf = vec![]; + self.write(&mut buf).unwrap(); + buf + } +} + +impl Writable for Vec { + fn write(&self, writer: &mut W) -> io::Result<()> { + for w in self { + w.write(writer)?; + } + Ok(()) + } +} + +// Pairing of an Algorithm with a ThresholdKeys instance. +#[derive(Clone, Zeroize)] +struct Params> { + // Skips the algorithm due to being too large a bound to feasibly enforce on users + #[zeroize(skip)] + algorithm: A, + keys: ThresholdKeys, +} + +impl> Params { + fn new(algorithm: A, keys: ThresholdKeys) -> Params { + Params { algorithm, keys } + } + + fn multisig_params(&self) -> ThresholdParams { + self.keys.params() + } +} + +/// Preprocess for an instance of the FROST signing protocol. +#[derive(Clone, PartialEq, Eq)] +pub struct Preprocess { + pub(crate) commitments: Commitments, + /// The addendum used by the algorithm. + pub addendum: A, +} + +impl Writable for Preprocess { + fn write(&self, writer: &mut W) -> io::Result<()> { + self.commitments.write(writer)?; + self.addendum.write(writer) + } +} + +/// A cached preprocess. +/// +/// A preprocess MUST only be used once. Reuse will enable third-party recovery of your private +/// key share. Additionally, this MUST be handled with the same security as your private key share, +/// as knowledge of it also enables recovery. +// Directly exposes the [u8; 32] member to void needing to route through std::io interfaces. +// Still uses Zeroizing internally so when users grab it, they have a higher likelihood of +// appreciating how to handle it and don't immediately start copying it just by grabbing it. +#[derive(Zeroize)] +pub struct CachedPreprocess(pub Zeroizing<[u8; 32]>); + +/// Trait for the initial state machine of a two-round signing protocol. +pub trait PreprocessMachine: Send { + /// Preprocess message for this machine. + type Preprocess: Clone + PartialEq + Writable; + /// Signature produced by this machine. + type Signature: Clone + PartialEq + Debug; + /// SignMachine this PreprocessMachine turns into. + type SignMachine: SignMachine; + + /// Perform the preprocessing round required in order to sign. + /// Returns a preprocess message to be broadcast to all participants, over an authenticated + /// channel. + fn preprocess(self, rng: &mut R) + -> (Self::SignMachine, Self::Preprocess); +} + +/// State machine which manages signing for an arbitrary signature algorithm. +pub struct AlgorithmMachine> { + params: Params, +} + +impl> AlgorithmMachine { + /// Creates a new machine to generate a signature with the specified keys. + pub fn new(algorithm: A, keys: ThresholdKeys) -> AlgorithmMachine { + AlgorithmMachine { params: Params::new(algorithm, keys) } + } + + fn seeded_preprocess( + self, + seed: CachedPreprocess, + ) -> (AlgorithmSignMachine, Preprocess) { + let mut params = self.params; + + let mut rng = ChaCha20Rng::from_seed(*seed.0); + // Get a challenge to the existing transcript for use when proving for the commitments + let commitments_challenge = params.algorithm.transcript().challenge(b"commitments"); + let (nonces, commitments) = Commitments::new::<_, A::Transcript>( + &mut rng, + params.keys.secret_share(), + ¶ms.algorithm.nonces(), + commitments_challenge.as_ref(), + ); + let addendum = params.algorithm.preprocess_addendum(&mut rng, ¶ms.keys); + + let preprocess = Preprocess { commitments, addendum }; + + // Also obtain entropy to randomly sort the included participants if we need to identify blame + let mut blame_entropy = [0; 32]; + rng.fill_bytes(&mut blame_entropy); + ( + AlgorithmSignMachine { + params, + seed, + commitments_challenge, + nonces, + preprocess: preprocess.clone(), + blame_entropy, + }, + preprocess, + ) + } + + #[cfg(any(test, feature = "tests"))] + pub(crate) fn unsafe_override_preprocess( + mut self, + nonces: Vec>, + preprocess: Preprocess, + ) -> AlgorithmSignMachine { + AlgorithmSignMachine { + commitments_challenge: self.params.algorithm.transcript().challenge(b"commitments"), + + params: self.params, + seed: CachedPreprocess(Zeroizing::new([0; 32])), + + nonces, + preprocess, + // Uses 0s since this is just used to protect against a malicious participant from + // deliberately increasing the amount of time needed to identify them (and is accordingly + // not necessary to function) + blame_entropy: [0; 32], + } + } +} + +impl> PreprocessMachine for AlgorithmMachine { + type Preprocess = Preprocess; + type Signature = A::Signature; + type SignMachine = AlgorithmSignMachine; + + fn preprocess( + self, + rng: &mut R, + ) -> (Self::SignMachine, Preprocess) { + let mut seed = CachedPreprocess(Zeroizing::new([0; 32])); + rng.fill_bytes(seed.0.as_mut()); + self.seeded_preprocess(seed) + } +} + +/// Share of a signature produced via FROST. +#[derive(Clone, PartialEq, Eq)] +pub struct SignatureShare(C::F); +impl Writable for SignatureShare { + fn write(&self, writer: &mut W) -> io::Result<()> { + writer.write_all(self.0.to_repr().as_ref()) + } +} +#[cfg(any(test, feature = "tests"))] +impl SignatureShare { + pub(crate) fn invalidate(&mut self) { + self.0 += C::F::ONE; + } +} + +/// Trait for the second machine of a two-round signing protocol. +pub trait SignMachine: Send + Sync + Sized { + /// Params used to instantiate this machine which can be used to rebuild from a cache. + type Params: Clone; + /// Keys used for signing operations. + type Keys; + /// Preprocess message for this machine. + type Preprocess: Clone + PartialEq + Writable; + /// SignatureShare message for this machine. + type SignatureShare: Clone + PartialEq + Writable; + /// SignatureMachine this SignMachine turns into. + type SignatureMachine: SignatureMachine; + + /// Cache this preprocess for usage later. This cached preprocess MUST only be used once. Reuse + /// of it enables recovery of your private key share. Third-party recovery of a cached preprocess + /// also enables recovery of your private key share, so this MUST be treated with the same + /// security as your private key share. + fn cache(self) -> CachedPreprocess; + + /// Create a sign machine from a cached preprocess. After this, the preprocess must be deleted so + /// it's never reused. Any reuse would cause the signer to leak their secret share. + fn from_cache( + params: Self::Params, + keys: Self::Keys, + cache: CachedPreprocess, + ) -> Result; + + /// Read a Preprocess message. Despite taking self, this does not save the preprocess. + /// It must be externally cached and passed into sign. + fn read_preprocess(&self, reader: &mut R) -> io::Result; + + /// Sign a message. + /// Takes in the participants' preprocess messages. Returns the signature share to be broadcast + /// to all participants, over an authenticated channel. The parties who participate here will + /// become the signing set for this session. + fn sign( + self, + commitments: HashMap, + msg: &[u8], + ) -> Result<(Self::SignatureMachine, Self::SignatureShare), FrostError>; +} + +/// Next step of the state machine for the signing process. +#[derive(Zeroize)] +pub struct AlgorithmSignMachine> { + params: Params, + seed: CachedPreprocess, + + #[zeroize(skip)] + commitments_challenge: ::Challenge, + pub(crate) nonces: Vec>, + // Skips the preprocess due to being too large a bound to feasibly enforce on users + #[zeroize(skip)] + pub(crate) preprocess: Preprocess, + pub(crate) blame_entropy: [u8; 32], +} + +impl> SignMachine for AlgorithmSignMachine { + type Params = A; + type Keys = ThresholdKeys; + type Preprocess = Preprocess; + type SignatureShare = SignatureShare; + type SignatureMachine = AlgorithmSignatureMachine; + + fn cache(self) -> CachedPreprocess { + self.seed + } + + fn from_cache( + algorithm: A, + keys: ThresholdKeys, + cache: CachedPreprocess, + ) -> Result { + let (machine, _) = AlgorithmMachine::new(algorithm, keys).seeded_preprocess(cache); + Ok(machine) + } + + fn read_preprocess(&self, reader: &mut R) -> io::Result { + Ok(Preprocess { + commitments: Commitments::read::<_, A::Transcript>( + reader, + &self.params.algorithm.nonces(), + self.commitments_challenge.as_ref(), + )?, + addendum: self.params.algorithm.read_addendum(reader)?, + }) + } + + fn sign( + mut self, + mut preprocesses: HashMap>, + msg: &[u8], + ) -> Result<(Self::SignatureMachine, SignatureShare), FrostError> { + let multisig_params = self.params.multisig_params(); + + let mut included = Vec::with_capacity(preprocesses.len() + 1); + included.push(multisig_params.i()); + for l in preprocesses.keys() { + included.push(*l); + } + included.sort_unstable(); + + // Included < threshold + if included.len() < usize::from(multisig_params.t()) { + Err(FrostError::InvalidSigningSet("not enough signers"))?; + } + // OOB index + if u16::from(included[included.len() - 1]) > multisig_params.n() { + Err(FrostError::InvalidParticipant(multisig_params.n(), included[included.len() - 1]))?; + } + // Same signer included multiple times + for i in 0 .. (included.len() - 1) { + if included[i] == included[i + 1] { + Err(FrostError::DuplicatedParticipant(included[i]))?; + } + } + + let view = self.params.keys.view(included.clone()).unwrap(); + validate_map(&preprocesses, &included, multisig_params.i())?; + + { + // Domain separate FROST + self.params.algorithm.transcript().domain_separate(b"FROST"); + } + + let nonces = self.params.algorithm.nonces(); + #[allow(non_snake_case)] + let mut B = BindingFactor(HashMap::::with_capacity(included.len())); + { + // Parse the preprocesses + for l in &included { + { + self + .params + .algorithm + .transcript() + .append_message(b"participant", C::F::from(u64::from(u16::from(*l))).to_repr()); + } + + if *l == self.params.keys.params().i() { + let commitments = self.preprocess.commitments.clone(); + commitments.transcript(self.params.algorithm.transcript()); + + let addendum = self.preprocess.addendum.clone(); + { + let mut buf = vec![]; + addendum.write(&mut buf).unwrap(); + self.params.algorithm.transcript().append_message(b"addendum", buf); + } + + B.insert(*l, commitments); + self.params.algorithm.process_addendum(&view, *l, addendum)?; + } else { + let preprocess = preprocesses.remove(l).unwrap(); + preprocess.commitments.transcript(self.params.algorithm.transcript()); + { + let mut buf = vec![]; + preprocess.addendum.write(&mut buf).unwrap(); + self.params.algorithm.transcript().append_message(b"addendum", buf); + } + + B.insert(*l, preprocess.commitments); + self.params.algorithm.process_addendum(&view, *l, preprocess.addendum)?; + } + } + + // Re-format into the FROST-expected rho transcript + let mut rho_transcript = A::Transcript::new(b"FROST_rho"); + rho_transcript.append_message( + b"group_key", + (self.params.keys.group_key() + + (C::generator() * self.params.keys.current_offset().unwrap_or(C::F::ZERO))) + .to_bytes(), + ); + rho_transcript.append_message(b"message", C::hash_msg(msg)); + rho_transcript.append_message( + b"preprocesses", + &C::hash_commitments( + self.params.algorithm.transcript().challenge(b"preprocesses").as_ref(), + ), + ); + + // Generate the per-signer binding factors + B.calculate_binding_factors(&rho_transcript); + + // Merge the rho transcript back into the global one to ensure its advanced, while + // simultaneously committing to everything + self + .params + .algorithm + .transcript() + .append_message(b"rho_transcript", rho_transcript.challenge(b"merge")); + } + + #[allow(non_snake_case)] + let Rs = B.nonces(&nonces); + + let our_binding_factors = B.binding_factors(multisig_params.i()); + let nonces = self + .nonces + .drain(..) + .enumerate() + .map(|(n, nonces)| { + let [base, mut actual] = nonces.0; + *actual *= our_binding_factors[n]; + *actual += base.deref(); + actual + }) + .collect::>(); + + let share = self.params.algorithm.sign_share(&view, &Rs, nonces, msg); + + Ok(( + AlgorithmSignatureMachine { + params: self.params.clone(), + view, + B, + Rs, + share, + blame_entropy: self.blame_entropy, + }, + SignatureShare(share), + )) + } +} + +/// Trait for the final machine of a two-round signing protocol. +pub trait SignatureMachine: Send + Sync { + /// SignatureShare message for this machine. + type SignatureShare: Clone + PartialEq + Writable; + + /// Read a Signature Share message. + fn read_share(&self, reader: &mut R) -> io::Result; + + /// Complete signing. + /// Takes in everyone elses' shares. Returns the signature. + fn complete(self, shares: HashMap) -> Result; +} + +/// Final step of the state machine for the signing process. +/// +/// This may panic if an invalid algorithm is provided. +#[allow(non_snake_case)] +pub struct AlgorithmSignatureMachine> { + params: Params, + view: ThresholdView, + B: BindingFactor, + Rs: Vec>, + share: C::F, + blame_entropy: [u8; 32], +} + +impl> SignatureMachine for AlgorithmSignatureMachine { + type SignatureShare = SignatureShare; + + fn read_share(&self, reader: &mut R) -> io::Result> { + Ok(SignatureShare(C::read_F(reader)?)) + } + + fn complete( + self, + mut shares: HashMap>, + ) -> Result { + let params = self.params.multisig_params(); + validate_map(&shares, self.view.included(), params.i())?; + + let mut responses = HashMap::new(); + responses.insert(params.i(), self.share); + let mut sum = self.share; + for (l, share) in shares.drain() { + responses.insert(l, share.0); + sum += share.0; + } + + // Perform signature validation instead of individual share validation + // For the success route, which should be much more frequent, this should be faster + // It also acts as an integrity check of this library's signing function + if let Some(sig) = self.params.algorithm.verify(self.view.group_key(), &self.Rs, sum) { + return Ok(sig); + } + + // We could remove blame_entropy by taking in an RNG here + // Considering we don't need any RNG for a valid signature, and we only use the RNG here for + // performance reasons, it doesn't feel worthwhile to include as an argument to every + // implementor of the trait + let mut rng = ChaCha20Rng::from_seed(self.blame_entropy); + let mut batch = BatchVerifier::new(self.view.included().len()); + for l in self.view.included() { + if let Ok(statements) = self.params.algorithm.verify_share( + self.view.verification_share(*l), + &self.B.bound(*l), + responses[l], + ) { + batch.queue(&mut rng, *l, statements); + } else { + Err(FrostError::InvalidShare(*l))?; + } + } + + if let Err(l) = batch.verify_vartime_with_vartime_blame() { + Err(FrostError::InvalidShare(l))?; + } + + // If everyone has a valid share, and there were enough participants, this should've worked + // The only known way to cause this, for valid parameters/algorithms, is to deserialize a + // semantically invalid FrostKeys + Err(FrostError::InternalError("everyone had a valid share yet the signature was still invalid")) + } +} diff --git a/crypto/frost/src/tests/literal/dalek.rs b/crypto/frost/src/tests/literal/dalek.rs new file mode 100644 index 0000000..9a11c5d --- /dev/null +++ b/crypto/frost/src/tests/literal/dalek.rs @@ -0,0 +1,32 @@ +use rand_core::OsRng; + +use crate::{ + curve, + tests::vectors::{Vectors, test_with_vectors}, +}; + +#[cfg(feature = "ristretto")] +#[test] +fn ristretto_vectors() { + test_with_vectors::<_, curve::Ristretto, curve::IetfRistrettoHram>( + &mut OsRng, + Vectors::from( + serde_json::from_str::(include_str!( + "vectors/frost-ristretto255-sha512.json" + )) + .unwrap(), + ), + ); +} + +#[cfg(feature = "ed25519")] +#[test] +fn ed25519_vectors() { + test_with_vectors::<_, curve::Ed25519, curve::IetfEd25519Hram>( + &mut OsRng, + Vectors::from( + serde_json::from_str::(include_str!("vectors/frost-ed25519-sha512.json")) + .unwrap(), + ), + ); +} diff --git a/crypto/frost/src/tests/literal/ed448.rs b/crypto/frost/src/tests/literal/ed448.rs new file mode 100644 index 0000000..79b7679 --- /dev/null +++ b/crypto/frost/src/tests/literal/ed448.rs @@ -0,0 +1,65 @@ +use rand_core::OsRng; + +use ciphersuite::Ciphersuite; + +use schnorr::SchnorrSignature; + +use crate::{ + curve::{Ed448, Ietf8032Ed448Hram, IetfEd448Hram}, + tests::vectors::{Vectors, test_with_vectors}, +}; + +// This is a vector from RFC 8032 to sanity check the HRAM is properly implemented +// The RFC 8032 Ed448 HRAM is much more complex than the other HRAMs, hence why it's helpful to +// have additional testing for it +// Additionally, FROST, despite being supposed to use the RFC 8032 HRAMs, originally applied +// Ed25519's HRAM to both Ed25519 and Ed448 +// This test was useful when proposing the corrections to the spec to demonstrate the correctness +// the new algorithm/vectors +// While we could test all Ed448 vectors here, this is sufficient for sanity +#[test] +fn ed448_8032_vector() { + let context = hex::decode("666f6f").unwrap(); + + #[allow(non_snake_case)] + let A = Ed448::read_G::<&[u8]>( + &mut hex::decode( + "43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c".to_owned() + + "6798c0866aea01eb00742802b8438ea4cb82169c235160627b4c3a94" + + "80", + ) + .unwrap() + .as_ref(), + ) + .unwrap(); + + let msg = hex::decode("03").unwrap(); + + let sig = hex::decode( + "d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b3".to_owned() + + "2a89f7d2151f7647f11d8ca2ae279fb842d607217fce6e042f6815ea" + + "00" + + "0c85741de5c8da1144a6a1aba7f96de42505d7a7298524fda538fccb" + + "bb754f578c1cad10d54d0d5428407e85dcbc98a49155c13764e66c3c" + + "00", + ) + .unwrap(); + #[allow(non_snake_case)] + let R = Ed448::read_G::<&[u8]>(&mut sig.as_ref()).unwrap(); + let s = Ed448::read_F::<&[u8]>(&mut &sig[57 ..]).unwrap(); + + assert!( + SchnorrSignature:: { R, s }.verify(A, Ietf8032Ed448Hram::hram(&context, &R, &A, &msg)) + ); +} + +#[test] +fn ed448_vectors() { + test_with_vectors::<_, Ed448, IetfEd448Hram>( + &mut OsRng, + Vectors::from( + serde_json::from_str::(include_str!("vectors/frost-ed448-shake256.json")) + .unwrap(), + ), + ); +} diff --git a/crypto/frost/src/tests/literal/kp256.rs b/crypto/frost/src/tests/literal/kp256.rs new file mode 100644 index 0000000..175039e --- /dev/null +++ b/crypto/frost/src/tests/literal/kp256.rs @@ -0,0 +1,35 @@ +use rand_core::OsRng; + +use crate::tests::vectors::{Vectors, test_with_vectors}; + +#[cfg(feature = "secp256k1")] +use crate::curve::{Secp256k1, IetfSecp256k1Hram}; + +#[cfg(feature = "p256")] +use crate::curve::{P256, IetfP256Hram}; + +#[cfg(feature = "secp256k1")] +#[test] +fn secp256k1_vectors() { + test_with_vectors::<_, Secp256k1, IetfSecp256k1Hram>( + &mut OsRng, + Vectors::from( + serde_json::from_str::(include_str!( + "vectors/frost-secp256k1-sha256.json" + )) + .unwrap(), + ), + ); +} + +#[cfg(feature = "p256")] +#[test] +fn p256_vectors() { + test_with_vectors::<_, P256, IetfP256Hram>( + &mut OsRng, + Vectors::from( + serde_json::from_str::(include_str!("vectors/frost-p256-sha256.json")) + .unwrap(), + ), + ); +} diff --git a/crypto/frost/src/tests/literal/mod.rs b/crypto/frost/src/tests/literal/mod.rs new file mode 100644 index 0000000..9650571 --- /dev/null +++ b/crypto/frost/src/tests/literal/mod.rs @@ -0,0 +1,6 @@ +#[cfg(any(feature = "ristretto", feature = "ed25519"))] +mod dalek; +#[cfg(any(feature = "secp256k1", feature = "p256"))] +mod kp256; +#[cfg(feature = "ed448")] +mod ed448; diff --git a/crypto/frost/src/tests/literal/vectors/frost-ed25519-sha512.json b/crypto/frost/src/tests/literal/vectors/frost-ed25519-sha512.json new file mode 100644 index 0000000..9bf89d8 --- /dev/null +++ b/crypto/frost/src/tests/literal/vectors/frost-ed25519-sha512.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(Ed25519, SHA-512)", + "group": "ed25519", + "hash": "SHA-512" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "7b1c33d3f5291d85de664833beb1ad469f7fb6025a0ec78b3a790c6e13a98304", + "group_public_key": "15d21ccd7ee42959562fc8aa63224c8851fb3ec85a3faf66040d380fb9738673", + "message": "74657374", + "share_polynomial_coefficients": [ + "178199860edd8c62f5212ee91eff1295d0d670ab4ed4506866bae57e7030b204" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "929dcc590407aae7d388761cddb0c0db6f5627aea8e217f4a033f2ec83d93509" + }, + { + "identifier": 2, + "participant_share": "a91e66e012e4364ac9aaa405fcafd370402d9859f7b6685c07eed76bf409e80d" + }, + { + "identifier": 3, + "participant_share": "d3cb090a075eb154e82fdb4b3cb507f110040905468bb9c46da8bdea643a9a02" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "0fd2e39e111cdc266f6c0f4d0fd45c947761f1f5d3cb583dfcb9bbaf8d4c9fec", + "binding_nonce_randomness": "69cd85f631d5f7f2721ed5e40519b1366f340a87c2f6856363dbdcda348a7501", + "hiding_nonce": "812d6104142944d5a55924de6d49940956206909f2acaeedecda2b726e630407", + "binding_nonce": "b1110165fc2334149750b28dd813a39244f315cff14d4e89e6142f262ed83301", + "hiding_nonce_commitment": "b5aa8ab305882a6fc69cbee9327e5a45e54c08af61ae77cb8207be3d2ce13de3", + "binding_nonce_commitment": "67e98ab55aa310c3120418e5050c9cf76cf387cb20ac9e4b6fdb6f82a469f932", + "binding_factor_input": "15d21ccd7ee42959562fc8aa63224c8851fb3ec85a3faf66040d380fb9738673504df914fa965023fb75c25ded4bb260f417de6d32e5c442c6ba313791cc9a4948d6273e8d3511f93348ea7a708a9b862bc73ba2a79cfdfe07729a193751cbc973af46d8ac3440e518d4ce440a0e7d4ad5f62ca8940f32de6d8dc00fc12c660b817d587d82f856d277ce6473cae6d2f5763f7da2e8b4d799a3f3e725d4522ec70100000000000000000000000000000000000000000000000000000000000000", + "binding_factor": "f2cb9d7dd9beff688da6fcc83fa89046b3479417f47f55600b106760eb3b5603" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "86d64a260059e495d0fb4fcc17ea3da7452391baa494d4b00321098ed2a0062f", + "binding_nonce_randomness": "13e6b25afb2eba51716a9a7d44130c0dbae0004a9ef8d7b5550c8a0e07c61775", + "hiding_nonce": "c256de65476204095ebdc01bd11dc10e57b36bc96284595b8215222374f99c0e", + "binding_nonce": "243d71944d929063bc51205714ae3c2218bd3451d0214dfb5aeec2a90c35180d", + "hiding_nonce_commitment": "cfbdb165bd8aad6eb79deb8d287bcc0ab6658ae57fdcc98ed12c0669e90aec91", + "binding_nonce_commitment": "7487bc41a6e712eea2f2af24681b58b1cf1da278ea11fe4e8b78398965f13552", + "binding_factor_input": "15d21ccd7ee42959562fc8aa63224c8851fb3ec85a3faf66040d380fb9738673504df914fa965023fb75c25ded4bb260f417de6d32e5c442c6ba313791cc9a4948d6273e8d3511f93348ea7a708a9b862bc73ba2a79cfdfe07729a193751cbc973af46d8ac3440e518d4ce440a0e7d4ad5f62ca8940f32de6d8dc00fc12c660b817d587d82f856d277ce6473cae6d2f5763f7da2e8b4d799a3f3e725d4522ec70300000000000000000000000000000000000000000000000000000000000000", + "binding_factor": "b087686bf35a13f3dc78e780a34b0fe8a77fef1b9938c563f5573d71d8d7890f" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "001719ab5a53ee1a12095cd088fd149702c0720ce5fd2f29dbecf24b7281b603" + }, + { + "identifier": 3, + "sig_share": "bd86125de990acc5e1f13781d8e32c03a9bbd4c53539bbc106058bfd14326007" + } + ] + }, + "final_output": { + "sig": "36282629c383bb820a88b71cae937d41f2f2adfcc3d02e55507e2fb9e2dd3cbebd9d2b0844e49ae0f3fa935161e1419aab7b47d21a37ebeae1f17d4987b3160b" + } +} \ No newline at end of file diff --git a/crypto/frost/src/tests/literal/vectors/frost-ed448-shake256.json b/crypto/frost/src/tests/literal/vectors/frost-ed448-shake256.json new file mode 100644 index 0000000..9b4ec50 --- /dev/null +++ b/crypto/frost/src/tests/literal/vectors/frost-ed448-shake256.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(Ed448, SHAKE256)", + "group": "ed448", + "hash": "SHAKE256" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "6298e1eef3c379392caaed061ed8a31033c9e9e3420726f23b404158a401cd9df24632adfe6b418dc942d8a091817dd8bd70e1c72ba52f3c00", + "group_public_key": "3832f82fda00ff5365b0376df705675b63d2a93c24c6e81d40801ba265632be10f443f95968fadb70d10786827f30dc001c8d0f9b7c1d1b000", + "message": "74657374", + "share_polynomial_coefficients": [ + "dbd7a514f7a731976620f0436bd135fe8dddc3fadd6e0d13dbd58a1981e587d377d48e0b7ce4e0092967c5e85884d0275a7a740b6abdcd0500" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "4a2b2f5858a932ad3d3b18bd16e76ced3070d72fd79ae4402df201f525e754716a1bc1b87a502297f2a99d89ea054e0018eb55d39562fd0100" + }, + { + "identifier": 2, + "participant_share": "2503d56c4f516444a45b080182b8a2ebbe4d9b2ab509f25308c88c0ea7ccdc44e2ef4fc4f63403a11b116372438a1e287265cadeff1fcb0700" + }, + { + "identifier": 3, + "participant_share": "00db7a8146f995db0a7cf844ed89d8e94c2b5f259378ff66e39d172828b264185ac4decf7219e4aa4478285b9c0eef4fccdf3eea69dd980d00" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "9cda90c98863ef3141b75f09375757286b4bc323dd61aeb45c07de45e4937bbd", + "binding_nonce_randomness": "781bf4881ffe1aa06f9341a747179f07a49745f8cd37d4696f226aa065683c0a", + "hiding_nonce": "f922beb51a5ac88d1e862278d89e12c05263b945147db04b9566acb2b5b0f7422ccea4f9286f4f80e6b646e72143eeaecc0e5988f8b2b93100", + "binding_nonce": "1890f16a120cdeac092df29955a29c7cf29c13f6f7be60e63d63f3824f2d37e9c3a002dfefc232972dc08658a8c37c3ec06a0c5dc146150500", + "hiding_nonce_commitment": "3518c2246c874569e54ab254cb1da666ca30f7879605cc43b4d2c47a521f8b5716080ab723d3a0cd04b7e41f3cc1d3031c94ccf3829b23fe80", + "binding_nonce_commitment": "11b3d5220c57d02057497de3c4eebab384900206592d877059b0a5f1d5250d002682f0e22dff096c46bb81b46d60fcfe7752ed47cea76c3900", + "binding_factor_input": "3832f82fda00ff5365b0376df705675b63d2a93c24c6e81d40801ba265632be10f443f95968fadb70d10786827f30dc001c8d0f9b7c1d1b000e9a0f30b97fe77ef751b08d4e252a3719ae9135e7f7926f7e3b7dd6656b27089ca354997fe5a633aa0946c89f022462e7e9d50fd6ef313f72d956ea4571089427daa1862f623a41625177d91e4a8f350ce9c8bd3bc7c766515dc1dd3a0eab93777526b616cccb148fe1e5992dc1ae705c8ba2f97ca8983328d41d375ed1e5fde5c9d672121c9e8f177f4a1a9b2575961531b33f054451363c8f27618382cd66ce14ad93b68dac6a09f5edcbccc813906b3fc50b8fef1cc09757b06646f38ceed1674cd6ced28a59c93851b325c6a9ef6a4b3b88860b7138ee246034561c7460db0b3fae5010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "binding_factor": "71966390dfdbed73cf9b79486f3b70e23b243e6c40638fb55998642a60109daecbfcb879eed9fe7dbbed8d9e47317715a5740f772173342e00" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "b3adf97ceea770e703ab295babf311d77e956a20d3452b4b3344aa89a828e6df", + "binding_nonce_randomness": "81dbe7742b0920930299197322b255734e52bbb91f50cfe8ce689f56fadbce31", + "hiding_nonce": "ccb5c1e82f23e0a4b966b824dbc7b0ef1cc5f56eeac2a4126e2b2143c5f3a4d890c52d27803abcf94927faf3fc405c0b2123a57a93cefa3b00", + "binding_nonce": "e089df9bf311cf711e2a24ea27af53e07b846d09692fe11035a1112f04d8b7462a62f34d8c01493a22b57a1cbf1f0a46c77d64d46449a90100", + "hiding_nonce_commitment": "1254546d7d104c04e4fbcf29e05747e2edd392f6787d05a6216f3713ef859efe573d180d291e48411e5e3006e9f90ee986ccc26b7a42490b80", + "binding_nonce_commitment": "3ef0cec20be15e56b3ddcb6f7b956fca0c8f71990f45316b537b4f64c5e8763e6629d7262ff7cd0235d0781f23be97bf8fa8817643ea19cd00", + "binding_factor_input": "3832f82fda00ff5365b0376df705675b63d2a93c24c6e81d40801ba265632be10f443f95968fadb70d10786827f30dc001c8d0f9b7c1d1b000e9a0f30b97fe77ef751b08d4e252a3719ae9135e7f7926f7e3b7dd6656b27089ca354997fe5a633aa0946c89f022462e7e9d50fd6ef313f72d956ea4571089427daa1862f623a41625177d91e4a8f350ce9c8bd3bc7c766515dc1dd3a0eab93777526b616cccb148fe1e5992dc1ae705c8ba2f97ca8983328d41d375ed1e5fde5c9d672121c9e8f177f4a1a9b2575961531b33f054451363c8f27618382cd66ce14ad93b68dac6a09f5edcbccc813906b3fc50b8fef1cc09757b06646f38ceed1674cd6ced28a59c93851b325c6a9ef6a4b3b88860b7138ee246034561c7460db0b3fae5030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "binding_factor": "236a6f7239ac2019334bad21323ec93bef2fead37bd55114356419f3fc1fb59f797f44079f28b1a64f51dd0a113f90f2c3a1c27d2faa4f1300" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "e1eb9bfbef792776b7103891032788406c070c5c315e3bf5d64acd46ea8855e85b53146150a09149665cbfec71626810b575e6f4dbe9ba3700" + }, + { + "identifier": 3, + "sig_share": "815434eb0b9f9242d54b8baf2141fe28976cabe5f441ccfcd5ee7cdb4b52185b02b99e6de28e2ab086c7764068c5a01b5300986b9f084f3e00" + } + ] + }, + "final_output": { + "sig": "cd642cba59c449dad8e896a78a60e8edfcbd9040df524370891ff8077d47ce721d683874483795f0d85efcbd642c4510614328605a19c6ed806ffb773b6956419537cdfdb2b2a51948733de192dcc4b82dc31580a536db6d435e0cb3ce322fbcf9ec23362dda27092c08767e607bf2093600" + } +} \ No newline at end of file diff --git a/crypto/frost/src/tests/literal/vectors/frost-p256-sha256.json b/crypto/frost/src/tests/literal/vectors/frost-p256-sha256.json new file mode 100644 index 0000000..fdad941 --- /dev/null +++ b/crypto/frost/src/tests/literal/vectors/frost-p256-sha256.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(P-256, SHA-256)", + "group": "P-256", + "hash": "SHA-256" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "8ba9bba2e0fd8c4767154d35a0b7562244a4aaf6f36c8fb8735fa48b301bd8de", + "group_public_key": "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70", + "message": "74657374", + "share_polynomial_coefficients": [ + "80f25e6c0709353e46bfbe882a11bdbb1f8097e46340eb8673b7e14556e6c3a4" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "0c9c1a0fe806c184add50bbdcac913dda73e482daf95dcb9f35dbb0d8a9f7731" + }, + { + "identifier": 2, + "participant_share": "8d8e787bef0ff6c2f494ca45f4dad198c6bee01212d6c84067159c52e1863ad5" + }, + { + "identifier": 3, + "participant_share": "0e80d6e8f6192c003b5488ce1eec8f5429587d48cf001541e713b2d53c09d928" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "ec4c891c85fee802a9d757a67d1252e7f4e5efb8a538991ac18fbd0e06fb6fd3", + "binding_nonce_randomness": "9334e29d09061223f69a09421715a347e4e6deba77444c8f42b0c833f80f4ef9", + "hiding_nonce": "9f0542a5ba879a58f255c09f06da7102ef6a2dec6279700c656d58394d8facd4", + "binding_nonce": "6513dfe7429aa2fc972c69bb495b27118c45bbc6e654bb9dc9be55385b55c0d7", + "hiding_nonce_commitment": "0213b3e6298bf8ad46fd5e9389519a8665d63d98f4ec6a1fcca434e809d2d8070e", + "binding_nonce_commitment": "02188ff1390bf69374d7b272e454b1878ef10a6b6ea3ff36f114b300b4dbd5233b", + "binding_factor_input": "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70825371853e974bc30ac5b947b216d70461919666584c70c51f9f56f117736c5d178dd0b521ad9c1abe98048419cbdec81504c85e12eb40e3bcb6ec73d3fc4afd0000000000000000000000000000000000000000000000000000000000000001", + "binding_factor": "7925f0d4693f204e6e59233e92227c7124664a99739d2c06b81cf64ddf90559e" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "c0451c5a0a5480d6c1f860e5db7d655233dca2669fd90ff048454b8ce983367b", + "binding_nonce_randomness": "2ba5f7793ae700e40e78937a82f407dd35e847e33d1e607b5c7eb6ed2a8ed799", + "hiding_nonce": "f73444a8972bcda9e506bbca3d2b1c083c10facdf4bb5d47fef7c2dc1d9f2a0d", + "binding_nonce": "44c6a29075d6e7e4f8b97796205f9e22062e7835141470afe9417fd317c1c303", + "hiding_nonce_commitment": "033ac9a5fe4a8b57316ba1c34e8a6de453033b750e8984924a984eb67a11e73a3f", + "binding_nonce_commitment": "03a7a2480ee16199262e648aea3acab628a53e9b8c1945078f2ddfbdc98b7df369", + "binding_factor_input": "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70825371853e974bc30ac5b947b216d70461919666584c70c51f9f56f117736c5d178dd0b521ad9c1abe98048419cbdec81504c85e12eb40e3bcb6ec73d3fc4afd0000000000000000000000000000000000000000000000000000000000000003", + "binding_factor": "e10d24a8a403723bcb6f9bb4c537f316593683b472f7a89f166630dde11822c4" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "400308eaed7a2ddee02a265abe6a1cfe04d946ee8720768899619cfabe7a3aeb" + }, + { + "identifier": 3, + "sig_share": "561da3c179edbb0502d941bb3e3ace3c37d122aaa46fb54499f15f3a3331de44" + } + ] + }, + "final_output": { + "sig": "026d8d434874f87bdb7bc0dfd239b2c00639044f9dcb195e9a04426f70bfa4b70d9620acac6767e8e3e3036815fca4eb3a3caa69992b902bcd3352fc34f1ac192f" + } +} \ No newline at end of file diff --git a/crypto/frost/src/tests/literal/vectors/frost-ristretto255-sha512.json b/crypto/frost/src/tests/literal/vectors/frost-ristretto255-sha512.json new file mode 100644 index 0000000..5e39a55 --- /dev/null +++ b/crypto/frost/src/tests/literal/vectors/frost-ristretto255-sha512.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(ristretto255, SHA-512)", + "group": "ristretto255", + "hash": "SHA-512" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "1b25a55e463cfd15cf14a5d3acc3d15053f08da49c8afcf3ab265f2ebc4f970b", + "group_public_key": "e2a62f39eede11269e3bd5a7d97554f5ca384f9f6d3dd9c3c0d05083c7254f57", + "message": "74657374", + "share_polynomial_coefficients": [ + "410f8b744b19325891d73736923525a4f596c805d060dfb9c98009d34e3fec02" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "5c3430d391552f6e60ecdc093ff9f6f4488756aa6cebdbad75a768010b8f830e" + }, + { + "identifier": 2, + "participant_share": "b06fc5eac20b4f6e1b271d9df2343d843e1e1fb03c4cbb673f2872d459ce6f01" + }, + { + "identifier": 3, + "participant_share": "f17e505f0e2581c6acfe54d3846a622834b5e7b50cad9a2109a97ba7a80d5c04" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "f595a133b4d95c6e1f79887220c8b275ce6277e7f68a6640e1e7140f9be2fb5c", + "binding_nonce_randomness": "34dd1001360e3513cb37bebfabe7be4a32c5bb91ba19fbd4360d039111f0fbdc", + "hiding_nonce": "214f2cabb86ed71427ea7ad4283b0fae26b6746c801ce824b83ceb2b99278c03", + "binding_nonce": "c9b8f5e16770d15603f744f8694c44e335e8faef00dad182b8d7a34a62552f0c", + "hiding_nonce_commitment": "965def4d0958398391fc06d8c2d72932608b1e6255226de4fb8d972dac15fd57", + "binding_nonce_commitment": "ec5170920660820007ae9e1d363936659ef622f99879898db86e5bf1d5bf2a14", + "binding_factor_input": "e2a62f39eede11269e3bd5a7d97554f5ca384f9f6d3dd9c3c0d05083c7254f572889dde2854e26377a16caf77dfee5f6be8fe5b4c80318da84698a4161021b033911db5ef8205362701bc9ecd983027814abee94f46d094943a2f4b79a6e4d4603e52c435d8344554942a0a472d8ad84320585b8da3ae5b9ce31cd1903f795c1af66de22af1a45f652cd05ee446b1b4091aaccc91e2471cd18a85a659cecd11f0100000000000000000000000000000000000000000000000000000000000000", + "binding_factor": "8967fd70fa06a58e5912603317fa94c77626395a695a0e4e4efc4476662eba0c" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "daa0cf42a32617786d390e0c7edfbf2efbd428037069357b5173ae61d6dd5d5e", + "binding_nonce_randomness": "b4387e72b2e4108ce4168931cc2c7fcce5f345a5297368952c18b5fc8473f050", + "hiding_nonce": "3f7927872b0f9051dd98dd73eb2b91494173bbe0feb65a3e7e58d3e2318fa40f", + "binding_nonce": "ffd79445fb8030f0a3ddd3861aa4b42b618759282bfe24f1f9304c7009728305", + "hiding_nonce_commitment": "480e06e3de182bf83489c45d7441879932fd7b434a26af41455756264fbd5d6e", + "binding_nonce_commitment": "3064746dfd3c1862ef58fc68c706da287dd925066865ceacc816b3a28c7b363b", + "binding_factor_input": "e2a62f39eede11269e3bd5a7d97554f5ca384f9f6d3dd9c3c0d05083c7254f572889dde2854e26377a16caf77dfee5f6be8fe5b4c80318da84698a4161021b033911db5ef8205362701bc9ecd983027814abee94f46d094943a2f4b79a6e4d4603e52c435d8344554942a0a472d8ad84320585b8da3ae5b9ce31cd1903f795c1af66de22af1a45f652cd05ee446b1b4091aaccc91e2471cd18a85a659cecd11f0300000000000000000000000000000000000000000000000000000000000000", + "binding_factor": "f2c1bb7c33a10511158c2f1766a4a5fadf9f86f2a92692ed333128277cc31006" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "9285f875923ce7e0c491a592e9ea1865ec1b823ead4854b48c8a46287749ee09" + }, + { + "identifier": 3, + "sig_share": "7cb211fe0e3d59d25db6e36b3fb32344794139602a7b24f1ae0dc4e26ad7b908" + } + ] + }, + "final_output": { + "sig": "fc45655fbc66bbffad654ea4ce5fdae253a49a64ace25d9adb62010dd9fb25552164141787162e5b4cab915b4aa45d94655dbb9ed7c378a53b980a0be220a802" + } +} \ No newline at end of file diff --git a/crypto/frost/src/tests/literal/vectors/frost-secp256k1-sha256.json b/crypto/frost/src/tests/literal/vectors/frost-secp256k1-sha256.json new file mode 100644 index 0000000..b361db5 --- /dev/null +++ b/crypto/frost/src/tests/literal/vectors/frost-secp256k1-sha256.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(secp256k1, SHA-256)", + "group": "secp256k1", + "hash": "SHA-256" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "0d004150d27c3bf2a42f312683d35fac7394b1e9e318249c1bfe7f0795a83114", + "group_public_key": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4f", + "message": "74657374", + "share_polynomial_coefficients": [ + "fbf85eadae3058ea14f19148bb72b45e4399c0b16028acaf0395c9b03c823579" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c" + }, + { + "identifier": 2, + "participant_share": "04f0feac2edcedc6ce1253b7fab8c86b856a797f44d83d82a385554e6e401984" + }, + { + "identifier": 3, + "participant_share": "00e95d59dd0d46b0e303e500b62b7ccb0e555d49f5b849f5e748c071da8c0dbc" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "7ea5ed09af19f6ff21040c07ec2d2adbd35b759da5a401d4c99dd26b82391cb2", + "binding_nonce_randomness": "47acab018f116020c10cb9b9abdc7ac10aae1b48ca6e36dc15acb6ec9be5cdc5", + "hiding_nonce": "841d3a6450d7580b4da83c8e618414d0f024391f2aeb511d7579224420aa81f0", + "binding_nonce": "8d2624f532af631377f33cf44b5ac5f849067cae2eacb88680a31e77c79b5a80", + "hiding_nonce_commitment": "03c699af97d26bb4d3f05232ec5e1938c12f1e6ae97643c8f8f11c9820303f1904", + "binding_nonce_commitment": "02fa2aaccd51b948c9dc1a325d77226e98a5a3fe65fe9ba213761a60123040a45e", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fff9b5210ffbb3c07a73a7c8935be4a8c62cf015f6cf7ade6efac09a6513540fc3f5a816aaebc2114a811a415d7a55db7c5cbc1cf27183e79dd9def941b5d48010000000000000000000000000000000000000000000000000000000000000001", + "binding_factor": "3e08fe561e075c653cbfd46908a10e7637c70c74f0a77d5fd45d1a750c739ec6" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "e6cc56ccbd0502b3f6f831d91e2ebd01c4de0479e0191b66895a4ffd9b68d544", + "binding_nonce_randomness": "7203d55eb82a5ca0d7d83674541ab55f6e76f1b85391d2c13706a89a064fd5b9", + "hiding_nonce": "2b19b13f193f4ce83a399362a90cdc1e0ddcd83e57089a7af0bdca71d47869b2", + "binding_nonce": "7a443bde83dc63ef52dda354005225ba0e553243402a4705ce28ffaafe0f5b98", + "hiding_nonce_commitment": "03077507ba327fc074d2793955ef3410ee3f03b82b4cdc2370f71d865beb926ef6", + "binding_nonce_commitment": "02ad53031ddfbbacfc5fbda3d3b0c2445c8e3e99cbc4ca2db2aa283fa68525b135", + "binding_factor_input": "02f37c34b66ced1fb51c34a90bdae006901f10625cc06c4f64663b0eae87d87b4fff9b5210ffbb3c07a73a7c8935be4a8c62cf015f6cf7ade6efac09a6513540fc3f5a816aaebc2114a811a415d7a55db7c5cbc1cf27183e79dd9def941b5d48010000000000000000000000000000000000000000000000000000000000000003", + "binding_factor": "93f79041bb3fd266105be251adaeb5fd7f8b104fb554a4ba9a0becea48ddbfd7" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "c4fce1775a1e141fb579944166eab0d65eefe7b98d480a569bbbfcb14f91c197" + }, + { + "identifier": 3, + "sig_share": "0160fd0d388932f4826d2ebcd6b9eaba734f7c71cf25b4279a4ca2581e47b18d" + } + ] + }, + "final_output": { + "sig": "0205b6d04d3774c8929413e3c76024d54149c372d57aae62574ed74319b5ea14d0c65dde8492a7471437e6c2fe3da49b90d23f642b5c6dbe7e36089f096dd97324" + } +} \ No newline at end of file diff --git a/crypto/frost/src/tests/mod.rs b/crypto/frost/src/tests/mod.rs new file mode 100644 index 0000000..e36bd71 --- /dev/null +++ b/crypto/frost/src/tests/mod.rs @@ -0,0 +1,272 @@ +use std::collections::HashMap; + +use rand_core::{RngCore, CryptoRng}; + +pub use dkg::tests::{key_gen, musig_key_gen, recover_key}; + +use crate::{ + Curve, Participant, ThresholdKeys, FrostError, + algorithm::{Algorithm, Hram, IetfSchnorr}, + sign::{Writable, PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine}, +}; + +/// Tests for the nonce handling code. +pub mod nonces; +use nonces::{test_multi_nonce, test_invalid_commitment, test_invalid_dleq_proof}; + +/// Vectorized test suite to ensure consistency. +pub mod vectors; + +// Literal test definitions to run during `cargo test` +#[cfg(test)] +mod literal; + +/// Constant amount of participants to use when testing. +pub const PARTICIPANTS: u16 = 5; +/// Constant threshold of participants to use when signing. +pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1; + +/// Clone a map without a specific value. +pub fn clone_without( + map: &HashMap, + without: &K, +) -> HashMap { + let mut res = map.clone(); + res.remove(without).unwrap(); + res +} + +/// Spawn algorithm machines for a random selection of signers, each executing the given algorithm. +pub fn algorithm_machines>( + rng: &mut R, + algorithm: A, + keys: &HashMap>, +) -> HashMap> { + let mut included = vec![]; + while included.len() < usize::from(keys[&Participant::new(1).unwrap()].params().t()) { + let n = Participant::new( + u16::try_from((rng.next_u64() % u64::try_from(keys.len()).unwrap()) + 1).unwrap(), + ) + .unwrap(); + if included.contains(&n) { + continue; + } + included.push(n); + } + + keys + .iter() + .filter_map(|(i, keys)| { + if included.contains(i) { + Some((*i, AlgorithmMachine::new(algorithm.clone(), keys.clone()))) + } else { + None + } + }) + .collect() +} + +// Run the preprocess step +pub(crate) fn preprocess< + R: RngCore + CryptoRng, + M: PreprocessMachine, + F: FnMut(&mut R, &mut HashMap), +>( + rng: &mut R, + mut machines: HashMap, + mut cache: F, +) -> (HashMap, HashMap) { + let mut commitments = HashMap::new(); + let mut machines = machines + .drain() + .map(|(i, machine)| { + let (machine, preprocess) = machine.preprocess(rng); + commitments.insert(i, { + let mut buf = vec![]; + preprocess.write(&mut buf).unwrap(); + machine.read_preprocess::<&[u8]>(&mut buf.as_ref()).unwrap() + }); + (i, machine) + }) + .collect::>(); + + cache(rng, &mut machines); + + (machines, commitments) +} + +// Run the preprocess and generate signature shares +#[allow(clippy::type_complexity)] +pub(crate) fn preprocess_and_shares< + R: RngCore + CryptoRng, + M: PreprocessMachine, + F: FnMut(&mut R, &mut HashMap), +>( + rng: &mut R, + machines: HashMap, + cache: F, + msg: &[u8], +) -> ( + HashMap>::SignatureMachine>, + HashMap>::SignatureShare>, +) { + let (mut machines, commitments) = preprocess(rng, machines, cache); + + let mut shares = HashMap::new(); + let machines = machines + .drain() + .map(|(i, machine)| { + let (machine, share) = machine.sign(clone_without(&commitments, &i), msg).unwrap(); + shares.insert(i, { + let mut buf = vec![]; + share.write(&mut buf).unwrap(); + machine.read_share::<&[u8]>(&mut buf.as_ref()).unwrap() + }); + (i, machine) + }) + .collect::>(); + + (machines, shares) +} + +fn sign_internal< + R: RngCore + CryptoRng, + M: PreprocessMachine, + F: FnMut(&mut R, &mut HashMap), +>( + rng: &mut R, + machines: HashMap, + cache: F, + msg: &[u8], +) -> M::Signature { + let (mut machines, shares) = preprocess_and_shares(rng, machines, cache, msg); + + let mut signature = None; + for (i, machine) in machines.drain() { + let sig = machine.complete(clone_without(&shares, &i)).unwrap(); + if signature.is_none() { + signature = Some(sig.clone()); + } + assert_eq!(&sig, signature.as_ref().unwrap()); + } + signature.unwrap() +} + +/// Execute the signing protocol, without caching any machines. This isn't as comprehensive at +/// testing as sign, and accordingly isn't preferred, yet is usable for machines not supporting +/// caching. +pub fn sign_without_caching( + rng: &mut R, + machines: HashMap, + msg: &[u8], +) -> M::Signature { + sign_internal(rng, machines, |_, _| {}, msg) +} + +/// Execute the signing protocol, randomly caching various machines to ensure they can cache +/// successfully. +pub fn sign( + rng: &mut R, + params: >::Params, + mut keys: HashMap>::Keys>, + machines: HashMap, + msg: &[u8], +) -> M::Signature { + sign_internal( + rng, + machines, + |rng, machines| { + // Cache and rebuild half of the machines + let included = machines.keys().cloned().collect::>(); + for i in included { + if (rng.next_u64() % 2) == 0 { + let cache = machines.remove(&i).unwrap().cache(); + machines.insert( + i, + M::SignMachine::from_cache(params.clone(), keys.remove(&i).unwrap(), cache).unwrap(), + ); + } + } + }, + msg, + ) +} + +/// Test a basic Schnorr signature with the provided keys. +pub fn test_schnorr_with_keys>( + rng: &mut R, + keys: HashMap>, +) { + const MSG: &[u8] = b"Hello, World!"; + + let machines = algorithm_machines(&mut *rng, IetfSchnorr::::ietf(), &keys); + let sig = sign(&mut *rng, IetfSchnorr::::ietf(), keys.clone(), machines, MSG); + let group_key = keys[&Participant::new(1).unwrap()].group_key(); + assert!(sig.verify(group_key, H::hram(&sig.R, &group_key, MSG))); +} + +/// Test a basic Schnorr signature. +pub fn test_schnorr>(rng: &mut R) { + let keys = key_gen(&mut *rng); + test_schnorr_with_keys::<_, _, H>(&mut *rng, keys) +} + +/// Test a basic Schnorr signature, yet with MuSig. +pub fn test_musig_schnorr>(rng: &mut R) { + let keys = musig_key_gen(&mut *rng); + test_schnorr_with_keys::<_, _, H>(&mut *rng, keys) +} + +/// Test an offset Schnorr signature. +pub fn test_offset_schnorr>(rng: &mut R) { + const MSG: &[u8] = b"Hello, World!"; + + let mut keys = key_gen(&mut *rng); + let group_key = keys[&Participant::new(1).unwrap()].group_key(); + + let offset = C::F::from(5); + let offset_key = group_key + (C::generator() * offset); + for keys in keys.values_mut() { + *keys = keys.offset(offset); + assert_eq!(keys.group_key(), offset_key); + } + + let machines = algorithm_machines(&mut *rng, IetfSchnorr::::ietf(), &keys); + let sig = sign(&mut *rng, IetfSchnorr::::ietf(), keys.clone(), machines, MSG); + let group_key = keys[&Participant::new(1).unwrap()].group_key(); + assert!(sig.verify(offset_key, H::hram(&sig.R, &group_key, MSG))); +} + +/// Test blame for an invalid Schnorr signature share. +pub fn test_schnorr_blame>(rng: &mut R) { + const MSG: &[u8] = b"Hello, World!"; + + let keys = key_gen(&mut *rng); + let machines = algorithm_machines(&mut *rng, IetfSchnorr::::ietf(), &keys); + + let (mut machines, shares) = preprocess_and_shares(&mut *rng, machines, |_, _| {}, MSG); + + for (i, machine) in machines.drain() { + let mut shares = clone_without(&shares, &i); + + // Select a random participant to give an invalid share + let participants = shares.keys().collect::>(); + let faulty = *participants + [usize::try_from(rng.next_u64() % u64::try_from(participants.len()).unwrap()).unwrap()]; + shares.get_mut(&faulty).unwrap().invalidate(); + + assert_eq!(machine.complete(shares).err(), Some(FrostError::InvalidShare(faulty))); + } +} + +/// Run a variety of tests against a ciphersuite. +pub fn test_ciphersuite>(rng: &mut R) { + test_schnorr::(rng); + test_musig_schnorr::(rng); + test_offset_schnorr::(rng); + test_schnorr_blame::(rng); + + test_multi_nonce::(rng); + test_invalid_commitment::(rng); + test_invalid_dleq_proof::(rng); +} diff --git a/crypto/frost/src/tests/nonces.rs b/crypto/frost/src/tests/nonces.rs new file mode 100644 index 0000000..8031013 --- /dev/null +++ b/crypto/frost/src/tests/nonces.rs @@ -0,0 +1,231 @@ +use std::io::{self, Read}; + +use zeroize::Zeroizing; + +use rand_core::{RngCore, CryptoRng, SeedableRng}; +use rand_chacha::ChaCha20Rng; + +use transcript::{Transcript, RecommendedTranscript}; + +use ciphersuite::group::{ff::Field, Group, GroupEncoding}; + +use dleq::MultiDLEqProof; +pub use dkg::tests::{key_gen, recover_key}; + +use crate::{ + Curve, Participant, ThresholdView, ThresholdKeys, FrostError, + algorithm::Algorithm, + sign::{Writable, SignMachine}, + tests::{algorithm_machines, preprocess, sign}, +}; + +#[derive(Clone)] +struct MultiNonce { + transcript: RecommendedTranscript, + nonces: Option>>, +} + +impl MultiNonce { + fn new() -> MultiNonce { + MultiNonce { + transcript: RecommendedTranscript::new(b"FROST MultiNonce Algorithm Test"), + nonces: None, + } + } +} + +fn nonces() -> Vec> { + vec![ + vec![C::generator(), C::generator().double()], + vec![C::generator(), C::generator() * C::F::from(3), C::generator() * C::F::from(4)], + ] +} + +fn verify_nonces(nonces: &[Vec]) { + assert_eq!(nonces.len(), 2); + + // Each nonce should be a series of commitments, over some generators, which share a discrete log + // Since they share a discrete log, their only distinction should be the generator + // Above, the generators were created with a known relationship + // Accordingly, we can check here that relationship holds to make sure these commitments are well + // formed + assert_eq!(nonces[0].len(), 2); + assert_eq!(nonces[0][0].double(), nonces[0][1]); + + assert_eq!(nonces[1].len(), 3); + assert_eq!(nonces[1][0] * C::F::from(3), nonces[1][1]); + assert_eq!(nonces[1][0] * C::F::from(4), nonces[1][2]); + + assert!(nonces[0][0] != nonces[1][0]); +} + +impl Algorithm for MultiNonce { + type Transcript = RecommendedTranscript; + type Addendum = (); + type Signature = (); + + fn transcript(&mut self) -> &mut Self::Transcript { + &mut self.transcript + } + + fn nonces(&self) -> Vec> { + nonces::() + } + + fn preprocess_addendum(&mut self, _: &mut R, _: &ThresholdKeys) {} + + fn read_addendum(&self, _: &mut R) -> io::Result { + Ok(()) + } + + fn process_addendum( + &mut self, + _: &ThresholdView, + _: Participant, + _: (), + ) -> Result<(), FrostError> { + Ok(()) + } + + fn sign_share( + &mut self, + _: &ThresholdView, + nonce_sums: &[Vec], + nonces: Vec>, + _: &[u8], + ) -> C::F { + // Verify the nonce sums are as expected + verify_nonces::(nonce_sums); + + // Verify we actually have two nonces and that they're distinct + assert_eq!(nonces.len(), 2); + assert!(nonces[0] != nonces[1]); + + // Save the nonce sums for later so we can check they're consistent with the call to verify + assert!(self.nonces.is_none()); + self.nonces = Some(nonce_sums.to_vec()); + + // Sum the nonces so we can later check they actually have a relationship to nonce_sums + let mut res = C::F::ZERO; + + // Weight each nonce + // This is probably overkill, since their unweighted forms would practically still require + // some level of crafting to pass a naive sum via malleability, yet this makes it more robust + for nonce in nonce_sums { + self.transcript.domain_separate(b"nonce"); + for commitment in nonce { + self.transcript.append_message(b"commitment", commitment.to_bytes()); + } + } + let mut rng = ChaCha20Rng::from_seed(self.transcript.clone().rng_seed(b"weight")); + + for nonce in nonces { + res += *nonce * C::F::random(&mut rng); + } + res + } + + #[must_use] + fn verify(&self, _: C::G, nonces: &[Vec], sum: C::F) -> Option { + verify_nonces::(nonces); + assert_eq!(&self.nonces.clone().unwrap(), nonces); + + // Make sure the nonce sums actually relate to the nonces + let mut res = C::G::identity(); + let mut rng = ChaCha20Rng::from_seed(self.transcript.clone().rng_seed(b"weight")); + for nonce in nonces { + res += nonce[0] * C::F::random(&mut rng); + } + assert_eq!(res, C::generator() * sum); + + Some(()) + } + + fn verify_share(&self, _: C::G, _: &[Vec], _: C::F) -> Result, ()> { + panic!("share verification triggered"); + } +} + +/// Test a multi-nonce, multi-generator algorithm. +// Specifically verifies this library can: +// 1) Generate multiple nonces +// 2) Provide the group nonces (nonce_sums) across multiple generators, still with the same +// discrete log +// 3) Provide algorithms with nonces which match the group nonces +pub fn test_multi_nonce(rng: &mut R) { + let keys = key_gen::(&mut *rng); + let machines = algorithm_machines(&mut *rng, MultiNonce::::new(), &keys); + sign(&mut *rng, MultiNonce::::new(), keys.clone(), machines, &[]); +} + +/// Test malleating a commitment for a nonce across generators causes the preprocess to error. +pub fn test_invalid_commitment(rng: &mut R) { + let keys = key_gen::(&mut *rng); + let machines = algorithm_machines(&mut *rng, MultiNonce::::new(), &keys); + let (machines, mut preprocesses) = preprocess(&mut *rng, machines, |_, _| {}); + + // Select a random participant to give an invalid commitment + let participants = preprocesses.keys().collect::>(); + let faulty = *participants + [usize::try_from(rng.next_u64() % u64::try_from(participants.len()).unwrap()).unwrap()]; + + // Grab their preprocess + let mut preprocess = preprocesses.remove(&faulty).unwrap(); + + // Mutate one of the commitments + let nonce = + preprocess.commitments.nonces.get_mut(usize::try_from(rng.next_u64()).unwrap() % 2).unwrap(); + let generators_len = nonce.generators.len(); + nonce.generators[usize::try_from(rng.next_u64()).unwrap() % generators_len].0 + [usize::try_from(rng.next_u64()).unwrap() % 2] = C::G::random(&mut *rng); + + // The commitments are validated at time of deserialization (read_preprocess) + // Accordingly, serialize it and read it again to make sure that errors + assert!(machines + .iter() + .next() + .unwrap() + .1 + .read_preprocess::<&[u8]>(&mut preprocess.serialize().as_ref()) + .is_err()); +} + +/// Test malleating the DLEq proof for a preprocess causes it to error. +pub fn test_invalid_dleq_proof(rng: &mut R) { + let keys = key_gen::(&mut *rng); + let machines = algorithm_machines(&mut *rng, MultiNonce::::new(), &keys); + let (machines, mut preprocesses) = preprocess(&mut *rng, machines, |_, _| {}); + + // Select a random participant to give an invalid DLEq proof + let participants = preprocesses.keys().collect::>(); + let faulty = *participants + [usize::try_from(rng.next_u64() % u64::try_from(participants.len()).unwrap()).unwrap()]; + + // Invalidate it by replacing it with a completely different proof + let dlogs = [Zeroizing::new(C::F::random(&mut *rng)), Zeroizing::new(C::F::random(&mut *rng))]; + let mut preprocess = preprocesses.remove(&faulty).unwrap(); + preprocess.commitments.dleq = Some(MultiDLEqProof::prove( + &mut *rng, + &mut RecommendedTranscript::new(b"Invalid DLEq Proof"), + &nonces::(), + &dlogs, + )); + + assert!(machines + .iter() + .next() + .unwrap() + .1 + .read_preprocess::<&[u8]>(&mut preprocess.serialize().as_ref()) + .is_err()); + + // Also test None for a proof will cause an error + preprocess.commitments.dleq = None; + assert!(machines + .iter() + .next() + .unwrap() + .1 + .read_preprocess::<&[u8]>(&mut preprocess.serialize().as_ref()) + .is_err()); +} diff --git a/crypto/frost/src/tests/vectors.rs b/crypto/frost/src/tests/vectors.rs new file mode 100644 index 0000000..f653513 --- /dev/null +++ b/crypto/frost/src/tests/vectors.rs @@ -0,0 +1,369 @@ +use core::ops::Deref; + +use std::collections::HashMap; +#[cfg(test)] +use std::str::FromStr; + +use zeroize::Zeroizing; + +use rand_core::{RngCore, CryptoRng, SeedableRng}; +use rand_chacha::ChaCha20Rng; + +use ciphersuite::group::{ff::PrimeField, GroupEncoding}; + +use crate::{ + curve::Curve, + Participant, ThresholdCore, ThresholdKeys, + algorithm::{IetfTranscript, Hram, IetfSchnorr}, + sign::{ + Writable, Nonce, GeneratorCommitments, NonceCommitments, Commitments, Preprocess, + PreprocessMachine, SignMachine, SignatureMachine, AlgorithmMachine, + }, + tests::{clone_without, recover_key, test_ciphersuite}, +}; + +/// Vectors for a ciphersuite. +pub struct Vectors { + pub threshold: u16, + + pub group_secret: String, + pub group_key: String, + pub shares: Vec, + + pub msg: String, + pub included: Vec, + + pub nonce_randomness: Vec<[String; 2]>, + pub nonces: Vec<[String; 2]>, + pub commitments: Vec<[String; 2]>, + + pub sig_shares: Vec, + + pub sig: String, +} + +// Vectors are expected to be formatted per the IETF proof of concept +// The included vectors are direcly from +// https://github.com/cfrg/draft-irtf-cfrg-frost/tree/draft-irtf-cfrg-frost-14/poc +#[cfg(test)] +impl From for Vectors { + fn from(value: serde_json::Value) -> Vectors { + let to_str = |value: &serde_json::Value| value.as_str().unwrap().to_string(); + Vectors { + threshold: u16::from_str(value["config"]["NUM_PARTICIPANTS"].as_str().unwrap()).unwrap(), + + group_secret: to_str(&value["inputs"]["group_secret_key"]), + group_key: to_str(&value["inputs"]["group_public_key"]), + shares: value["inputs"]["participant_shares"] + .as_array() + .unwrap() + .iter() + .map(|share| to_str(&share["participant_share"])) + .collect(), + + msg: to_str(&value["inputs"]["message"]), + included: value["inputs"]["participant_list"] + .as_array() + .unwrap() + .iter() + .map(|i| Participant::new(u16::try_from(i.as_u64().unwrap()).unwrap()).unwrap()) + .collect(), + + nonce_randomness: value["round_one_outputs"]["outputs"] + .as_array() + .unwrap() + .iter() + .map(|value| { + [to_str(&value["hiding_nonce_randomness"]), to_str(&value["binding_nonce_randomness"])] + }) + .collect(), + nonces: value["round_one_outputs"]["outputs"] + .as_array() + .unwrap() + .iter() + .map(|value| [to_str(&value["hiding_nonce"]), to_str(&value["binding_nonce"])]) + .collect(), + commitments: value["round_one_outputs"]["outputs"] + .as_array() + .unwrap() + .iter() + .map(|value| { + [to_str(&value["hiding_nonce_commitment"]), to_str(&value["binding_nonce_commitment"])] + }) + .collect(), + + sig_shares: value["round_two_outputs"]["outputs"] + .as_array() + .unwrap() + .iter() + .map(|value| to_str(&value["sig_share"])) + .collect(), + + sig: to_str(&value["final_output"]["sig"]), + } + } +} + +// Load these vectors into ThresholdKeys using a custom serialization it'll deserialize +fn vectors_to_multisig_keys(vectors: &Vectors) -> HashMap> { + let shares = vectors + .shares + .iter() + .map(|secret| C::read_F::<&[u8]>(&mut hex::decode(secret).unwrap().as_ref()).unwrap()) + .collect::>(); + let verification_shares = shares.iter().map(|secret| C::generator() * secret).collect::>(); + + let mut keys = HashMap::new(); + for i in 1 ..= u16::try_from(shares.len()).unwrap() { + // Manually re-implement the serialization for ThresholdCore to import this data + let mut serialized = vec![]; + serialized.extend(u32::try_from(C::ID.len()).unwrap().to_le_bytes()); + serialized.extend(C::ID); + serialized.extend(vectors.threshold.to_le_bytes()); + serialized.extend(u16::try_from(shares.len()).unwrap().to_le_bytes()); + serialized.extend(i.to_le_bytes()); + serialized.extend(shares[usize::from(i) - 1].to_repr().as_ref()); + for share in &verification_shares { + serialized.extend(share.to_bytes().as_ref()); + } + + let these_keys = ThresholdCore::::read::<&[u8]>(&mut serialized.as_ref()).unwrap(); + assert_eq!(these_keys.params().t(), vectors.threshold); + assert_eq!(usize::from(these_keys.params().n()), shares.len()); + let participant = Participant::new(i).unwrap(); + assert_eq!(these_keys.params().i(), participant); + assert_eq!(these_keys.secret_share().deref(), &shares[usize::from(i - 1)]); + assert_eq!(hex::encode(these_keys.group_key().to_bytes().as_ref()), vectors.group_key); + keys.insert(participant, ThresholdKeys::new(these_keys)); + } + + keys +} + +/// Test a Ciphersuite with its vectors. +pub fn test_with_vectors>( + rng: &mut R, + vectors: Vectors, +) { + test_ciphersuite::(rng); + + // Test against the vectors + let keys = vectors_to_multisig_keys::(&vectors); + { + let group_key = + ::read_G::<&[u8]>(&mut hex::decode(&vectors.group_key).unwrap().as_ref()) + .unwrap(); + let secret = + C::read_F::<&[u8]>(&mut hex::decode(&vectors.group_secret).unwrap().as_ref()).unwrap(); + assert_eq!(C::generator() * secret, group_key); + assert_eq!(recover_key(&keys), secret); + + let mut machines = vec![]; + for i in &vectors.included { + machines.push((i, AlgorithmMachine::new(IetfSchnorr::::ietf(), keys[i].clone()))); + } + + let mut commitments = HashMap::new(); + let machines = machines + .into_iter() + .enumerate() + .map(|(c, (i, machine))| { + let nonce = |i| { + Zeroizing::new( + C::read_F::<&[u8]>(&mut hex::decode(&vectors.nonces[c][i]).unwrap().as_ref()).unwrap(), + ) + }; + let nonces = [nonce(0), nonce(1)]; + let these_commitments = + [C::generator() * nonces[0].deref(), C::generator() * nonces[1].deref()]; + + assert_eq!( + these_commitments[0].to_bytes().as_ref(), + hex::decode(&vectors.commitments[c][0]).unwrap() + ); + assert_eq!( + these_commitments[1].to_bytes().as_ref(), + hex::decode(&vectors.commitments[c][1]).unwrap() + ); + + let preprocess = Preprocess { + commitments: Commitments { + nonces: vec![NonceCommitments { + generators: vec![GeneratorCommitments(these_commitments)], + }], + dleq: None, + }, + addendum: (), + }; + // FROST doesn't specify how to serialize these together, yet this is sane + // (and the simplest option) + assert_eq!( + preprocess.serialize(), + hex::decode(vectors.commitments[c][0].clone() + &vectors.commitments[c][1]).unwrap() + ); + + let machine = machine.unsafe_override_preprocess(vec![Nonce(nonces)], preprocess); + + commitments.insert( + *i, + machine + .read_preprocess::<&[u8]>( + &mut [ + these_commitments[0].to_bytes().as_ref(), + these_commitments[1].to_bytes().as_ref(), + ] + .concat() + .as_ref(), + ) + .unwrap(), + ); + (i, machine) + }) + .collect::>(); + + let mut shares = HashMap::new(); + let machines = machines + .into_iter() + .enumerate() + .map(|(c, (i, machine))| { + let (machine, share) = machine + .sign(clone_without(&commitments, i), &hex::decode(&vectors.msg).unwrap()) + .unwrap(); + + let share = { + let mut buf = vec![]; + share.write(&mut buf).unwrap(); + buf + }; + assert_eq!(share, hex::decode(&vectors.sig_shares[c]).unwrap()); + + shares.insert(*i, machine.read_share::<&[u8]>(&mut share.as_ref()).unwrap()); + (i, machine) + }) + .collect::>(); + + for (i, machine) in machines { + let sig = machine.complete(clone_without(&shares, i)).unwrap(); + let mut serialized = sig.R.to_bytes().as_ref().to_vec(); + serialized.extend(sig.s.to_repr().as_ref()); + assert_eq!(hex::encode(serialized), vectors.sig); + } + } + + // The above code didn't test the nonce generation due to the infeasibility of doing so against + // the current codebase + + // A transparent RNG which has a fixed output + struct TransparentRng(Vec<[u8; 32]>); + impl RngCore for TransparentRng { + fn next_u32(&mut self) -> u32 { + unimplemented!() + } + fn next_u64(&mut self) -> u64 { + unimplemented!() + } + fn fill_bytes(&mut self, dest: &mut [u8]) { + dest.copy_from_slice(&self.0.remove(0)) + } + fn try_fill_bytes(&mut self, _: &mut [u8]) -> Result<(), rand_core::Error> { + unimplemented!() + } + } + // CryptoRng requires the output not reveal any info about any other outputs + // Since this only will produce one output, this is actually met, even though it'd be fine to + // fake it as this is a test + impl CryptoRng for TransparentRng {} + + // Test C::random_nonce matches the expected vectors + for (i, l) in vectors.included.iter().enumerate() { + let l = usize::from(u16::from(*l)); + + // Shares are a zero-indexed array of all participants, hence l - 1 + let share = Zeroizing::new( + C::read_F::<&[u8]>(&mut hex::decode(&vectors.shares[l - 1]).unwrap().as_ref()).unwrap(), + ); + + let randomness = vectors.nonce_randomness[i] + .iter() + .map(|randomness| hex::decode(randomness).unwrap().try_into().unwrap()) + .collect::>(); + + let nonces = vectors.nonces[i] + .iter() + .map(|nonce| { + Zeroizing::new(C::read_F::<&[u8]>(&mut hex::decode(nonce).unwrap().as_ref()).unwrap()) + }) + .collect::>(); + + for (randomness, nonce) in randomness.iter().zip(&nonces) { + // Nonces are only present for participating signers, hence i + assert_eq!(C::random_nonce(&share, &mut TransparentRng(vec![*randomness])), *nonce); + } + + // Also test it at the Commitments level + let (generated_nonces, commitments) = Commitments::::new::<_, IetfTranscript>( + &mut TransparentRng(randomness), + &share, + &[vec![C::generator()]], + &[], + ); + + assert_eq!(generated_nonces.len(), 1); + assert_eq!(generated_nonces[0].0, [nonces[0].clone(), nonces[1].clone()]); + + let mut commitments_bytes = vec![]; + commitments.write(&mut commitments_bytes).unwrap(); + assert_eq!( + commitments_bytes, + hex::decode(vectors.commitments[i][0].clone() + &vectors.commitments[i][1]).unwrap() + ); + } + + // This doesn't verify C::random_nonce is called correctly, where the code should call it with + // the output from a ChaCha20 stream + // Create a known ChaCha20 stream to verify it ends up at random_nonce properly + + { + let mut chacha_seed = [0; 32]; + rng.fill_bytes(&mut chacha_seed); + let mut ours = ChaCha20Rng::from_seed(chacha_seed); + let frosts = ours.clone(); + + // The machines should geenerate a seed, and then use that seed in a ChaCha20 RNG for nonces + let mut preprocess_seed = [0; 32]; + ours.fill_bytes(&mut preprocess_seed); + let mut ours = ChaCha20Rng::from_seed(preprocess_seed); + + // Get the randomness which will be used + let mut randomness = ([0; 32], [0; 32]); + ours.fill_bytes(&mut randomness.0); + ours.fill_bytes(&mut randomness.1); + + // Create the machines + let mut machines = vec![]; + for i in &vectors.included { + machines.push((i, AlgorithmMachine::new(IetfSchnorr::::ietf(), keys[i].clone()))); + } + + for (i, machine) in machines { + let (_, preprocess) = machine.preprocess(&mut frosts.clone()); + + // Calculate the expected nonces + let mut expected = (C::generator() * + C::random_nonce(keys[i].secret_share(), &mut TransparentRng(vec![randomness.0])).deref()) + .to_bytes() + .as_ref() + .to_vec(); + expected.extend( + (C::generator() * + C::random_nonce(keys[i].secret_share(), &mut TransparentRng(vec![randomness.1])) + .deref()) + .to_bytes() + .as_ref(), + ); + + // Ensure they match + assert_eq!(preprocess.serialize(), expected); + } + } +} diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..a480b08 --- /dev/null +++ b/deny.toml @@ -0,0 +1,61 @@ +[advisories] +db-path = "~/.cargo/advisory-db" +db-urls = ["https://github.com/rustsec/advisory-db"] + +vulnerability = "deny" +yanked = "deny" +notice = "warn" +unmaintained = "warn" + +ignore = [ + "RUSTSEC-2020-0071", # https://github.com/chronotope/chrono/issues/602 + "RUSTSEC-2021-0139", # https://github.com/serai-dex/serai/228 + "RUSTSEC-2022-0061", # https://github.com/serai-dex/serai/227 +] + +[licenses] +unlicensed = "deny" + +allow = [ + # Effective public domain + "CC0-1.0", + "Unlicense", + + # Attribution required + "MIT", + "MITNFA", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-3.0", + "OpenSSL", + + # Non-invasive copyleft + "MPL-2.0", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "GPL-3.0 WITH Classpath-exception-2.0", +] + +copyleft = "deny" +allow-osi-fsf-free = "neither" +default = "deny" + +[[licenses.clarify]] +name = "ring" +version = "*" +expression = "MIT AND ISC AND OpenSSL" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 } +] + +[bans] +multiple-versions = "warn" +wildcards = "warn" +highlight = "all" +deny = [ { name = "serde_derive", version = ">=1.0.172, <1.0.185" } ] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] diff --git a/hrf/Cargo.toml b/hrf/Cargo.toml new file mode 100644 index 0000000..3629e2e --- /dev/null +++ b/hrf/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "hrf-api" +version = "0.1.0" +description = "An API for wallet functionality per the HRF" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/coins/hrf" +authors = ["Luke Parker "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib"] + +[dependencies] +zeroize = "^1.5" + +rand_core = "0.6" +rand_chacha = "0.3" + +sha2 = "0.10" + +k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic", "bits"] } +transcript = { package = "flexible-transcript", version = "0.3", features = ["recommended"] } +ciphersuite = { version = "0.4", features = ["secp256k1"] } +frost = { package = "modular-frost", path = "../crypto/frost", version = "0.8", features = ["secp256k1"] } + +bitcoin-serai = { path = "../coins/bitcoin" } + +hex = "0.4" +base64ct = { version = "1", features = ["alloc"] } +tiny-bip39 = "1" + +serde = "1" +bincode = "1" diff --git a/hrf/LICENSE b/hrf/LICENSE new file mode 100644 index 0000000..e6bff13 --- /dev/null +++ b/hrf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/hrf/cbindgen.toml b/hrf/cbindgen.toml new file mode 100644 index 0000000..a5e4279 --- /dev/null +++ b/hrf/cbindgen.toml @@ -0,0 +1,6 @@ +language = "C" +pragma_once = true +cpp_compat = true + +[export.rename] +"String" = "RustString" diff --git a/hrf/header.h b/hrf/header.h new file mode 100644 index 0000000..e9e24a0 --- /dev/null +++ b/hrf/header.h @@ -0,0 +1,428 @@ +#pragma once + +#include +#include +#include +#include + +#define LANGUAGE_ENGLISH 1 + +#define LANGUAGE_CHINESE_SIMPLIFIED 2 + +#define LANGUAGE_CHINESE_TRADITIONAL 3 + +#define LANGUAGE_FRENCH 4 + +#define LANGUAGE_ITALIAN 5 + +#define LANGUAGE_JAPANESE 6 + +#define LANGUAGE_KOREAN 7 + +#define LANGUAGE_SPANISH 8 + +#define UNKNOWN_ERROR 21 + +#define INVALID_ENCODING_ERROR 22 + +#define INVALID_PARTICIPANT_ERROR 23 + +#define INVALID_SHARE_ERROR 24 + +#define ZERO_PARAMETER_ERROR 41 + +#define INVALID_THRESHOLD_ERROR 42 + +#define INVALID_NAME_ERROR 43 + +#define UNKNOWN_LANGUAGE_ERROR 44 + +#define INVALID_SEED_ERROR 45 + +#define INVALID_AMOUNT_OF_COMMITMENTS_ERROR 46 + +#define INVALID_COMMITMENTS_ERROR 47 + +#define INVALID_AMOUNT_OF_SHARES_ERROR 48 + +#define INVALID_OUTPUT_ERROR 61 + +#define INVALID_ADDRESS_ERROR 62 + +#define INVALID_NETWORK_ERROR 63 + +#define NO_INPUTS_ERROR 64 + +#define NO_OUTPUTS_ERROR 65 + +#define DUST_ERROR 66 + +#define NOT_ENOUGH_FUNDS_ERROR 67 + +#define TOO_LARGE_TRANSACTION_ERROR 68 + +#define WRONG_KEYS_ERROR 69 + +#define INVALID_PREPROCESS_ERROR 70 + +#define FEE_ERROR 71 + +#define INVALID_PARTICIPANTS_AMOUNT_ERROR 81 + +#define DUPLICATED_PARTICIPANT_ERROR 82 + +#define NOT_ENOUGH_RESHARERS_ERROR 83 + +#define INVALID_RESHARED_MSG_ERROR 84 + +#define INVALID_RESHARER_MSG_ERROR 85 + +typedef enum Network { + Mainnet, + Testnet, + Regtest, +} Network; + +typedef struct KeyMachineWrapper KeyMachineWrapper; + +typedef struct MultisigConfig MultisigConfig; + +typedef struct OpaqueResharedMachine OpaqueResharedMachine; + +typedef struct OpaqueResharingMachine OpaqueResharingMachine; + +typedef struct OwnedPortableOutput OwnedPortableOutput; + +typedef struct ResharerConfig ResharerConfig; + +typedef struct SecretShareMachineWrapper SecretShareMachineWrapper; + +typedef struct SignConfig SignConfig; + +typedef struct RustString RustString; + +typedef struct ThresholdKeysWrapper ThresholdKeysWrapper; + +typedef struct TransactionSignMachineWrapper TransactionSignMachineWrapper; + +typedef struct TransactionSignatureMachineWrapper TransactionSignatureMachineWrapper; + +typedef struct Vec_u8 Vec_u8; + +typedef struct OwnedString { + struct RustString *str_box; + const uint8_t *ptr; + uintptr_t len; +} OwnedString; + +typedef struct StringView { + const uint8_t *ptr; + uintptr_t len; +} StringView; + +typedef struct MultisigConfigWithName { + struct MultisigConfig *config; + struct RustString *my_name; +} MultisigConfigWithName; + +typedef struct MultisigConfigRes { + struct MultisigConfig *config; + struct OwnedString encoded; +} MultisigConfigRes; + +typedef struct CResult_MultisigConfigRes { + struct MultisigConfigRes *value; + uint8_t err; +} CResult_MultisigConfigRes; + +typedef struct CResult_MultisigConfig { + struct MultisigConfig *value; + uint8_t err; +} CResult_MultisigConfig; + +typedef struct StartKeyGenRes { + struct OwnedString seed; + struct MultisigConfigWithName *config; + struct SecretShareMachineWrapper *machine; + struct OwnedString commitments; +} StartKeyGenRes; + +typedef struct CResult_StartKeyGenRes { + struct StartKeyGenRes *value; + uint8_t err; +} CResult_StartKeyGenRes; + +typedef struct SecretSharesRes { + struct KeyMachineWrapper *machine; + struct Vec_u8 *internal_commitments; + struct OwnedString shares; +} SecretSharesRes; + +typedef struct CResult_SecretSharesRes { + struct SecretSharesRes *value; + uint8_t err; +} CResult_SecretSharesRes; + +typedef struct KeyGenRes { + uint8_t multisig_id[32]; + struct ThresholdKeysWrapper *keys; + struct OwnedString recovery; +} KeyGenRes; + +typedef struct CResult_KeyGenRes { + struct KeyGenRes *value; + uint8_t err; +} CResult_KeyGenRes; + +typedef struct CResult_ThresholdKeysWrapper { + struct ThresholdKeysWrapper *value; + uint8_t err; +} CResult_ThresholdKeysWrapper; + +typedef struct SignConfigRes { + struct SignConfig *config; + struct OwnedString encoded; +} SignConfigRes; + +typedef struct CResult_SignConfigRes { + struct SignConfigRes *value; + uint8_t err; +} CResult_SignConfigRes; + +typedef struct PortableOutput { + uint8_t hash[32]; + uint32_t vout; + uint64_t value; + const uint8_t *script_pubkey; + uintptr_t script_pubkey_len; + uint32_t account; + uint32_t address; + bool change; + bool secure; +} PortableOutput; + +typedef struct CResult_SignConfig { + struct SignConfig *value; + uint8_t err; +} CResult_SignConfig; + +typedef struct AttemptSignRes { + struct TransactionSignMachineWrapper *machine; + struct OwnedString preprocess; +} AttemptSignRes; + +typedef struct CResult_AttemptSignRes { + struct AttemptSignRes *value; + uint8_t err; +} CResult_AttemptSignRes; + +typedef struct ContinueSignRes { + struct TransactionSignatureMachineWrapper *machine; + struct OwnedString preprocess; +} ContinueSignRes; + +typedef struct CResult_ContinueSignRes { + struct ContinueSignRes *value; + uint8_t err; +} CResult_ContinueSignRes; + +typedef struct CResult_OwnedString { + struct OwnedString *value; + uint8_t err; +} CResult_OwnedString; + +typedef struct ResharerConfigRes { + struct ResharerConfig *config; + struct OwnedString encoded; +} ResharerConfigRes; + +typedef struct CResult_ResharerConfigRes { + struct ResharerConfigRes *value; + uint8_t err; +} CResult_ResharerConfigRes; + +typedef struct CResult_ResharerConfig { + struct ResharerConfig *value; + uint8_t err; +} CResult_ResharerConfig; + +typedef struct StartResharerRes { + uintptr_t new_participants_len; + struct OpaqueResharingMachine *machine; + struct OwnedString encoded; +} StartResharerRes; + +typedef struct CResult_StartResharerRes { + struct StartResharerRes *value; + uint8_t err; +} CResult_StartResharerRes; + +typedef struct StartResharedRes { + uintptr_t resharers_len; + struct MultisigConfig *multisig_config; + uint16_t my_i; + struct OpaqueResharedMachine *machine; + struct OwnedString encoded; +} StartResharedRes; + +typedef struct CResult_StartResharedRes { + struct StartResharedRes *value; + uint8_t err; +} CResult_StartResharedRes; + +typedef struct ResharedRes { + struct MultisigConfig *config; + struct OwnedString id; + struct ThresholdKeysWrapper *keys; +} ResharedRes; + +typedef struct CResult_ResharedRes { + struct ResharedRes *value; + uint8_t err; +} CResult_ResharedRes; + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +void free_owned_string(struct OwnedString self); + +struct StringView multisig_name(const struct MultisigConfig *self); + +uint16_t multisig_threshold(const struct MultisigConfig *self); + +uintptr_t multisig_participants(const struct MultisigConfig *self); + +struct StringView multisig_participant(const struct MultisigConfig *self, uintptr_t i); + +const uint8_t *multisig_salt(const struct MultisigConfig *self); + +struct OwnedString encode_multisig_config(const struct MultisigConfig *self); + +const struct MultisigConfig *multisig_config(const struct MultisigConfigWithName *self); + +struct StringView multisig_my_name(const struct MultisigConfigWithName *self); + +struct CResult_MultisigConfigRes new_multisig_config(const uint8_t *multisig_name, + uintptr_t multisig_name_len, + uint16_t threshold, + const struct StringView *participants, + uint16_t participants_len); + +struct CResult_MultisigConfig decode_multisig_config(struct StringView config); + +struct CResult_StartKeyGenRes start_key_gen(struct MultisigConfig *config, + struct StringView my_name, + uint8_t language); + +struct CResult_SecretSharesRes get_secret_shares(const struct MultisigConfigWithName *config, + uint8_t language, + struct StringView seed, + struct SecretShareMachineWrapper *machine, + const struct StringView *commitments, + uintptr_t commitments_len); + +struct CResult_KeyGenRes complete_key_gen(const struct MultisigConfigWithName *config, + struct SecretSharesRes machine_and_commitments, + const struct StringView *shares, + uintptr_t shares_len); + +uint16_t keys_threshold(const struct ThresholdKeysWrapper *keys); + +uint16_t keys_participants(const struct ThresholdKeysWrapper *keys); + +uint16_t keys_index(const struct ThresholdKeysWrapper *keys); + +struct OwnedString serialize_keys(const struct ThresholdKeysWrapper *keys); + +struct CResult_ThresholdKeysWrapper deserialize_keys(struct StringView keys); + +struct CResult_OwnedString address_for_keys(enum Network network, const struct ThresholdKeysWrapper *keys, uint32_t account, uint32_t address, bool change, bool secure); + +struct OwnedString script_pubkey_for_keys(const struct ThresholdKeysWrapper *keys, uint32_t account, uint32_t address, bool change, bool secure); + +const uint8_t *output_hash(const struct OwnedPortableOutput *self); + +uint32_t output_vout(const struct OwnedPortableOutput *self); + +uint64_t output_value(const struct OwnedPortableOutput *self); + +uintptr_t output_script_pubkey_len(const struct OwnedPortableOutput *self); + +const uint8_t *output_script_pubkey(const struct OwnedPortableOutput *self); + +uintptr_t sign_inputs(const struct SignConfig *self); + +const struct OwnedPortableOutput *sign_input(const struct SignConfig *self, uintptr_t i); + +uintptr_t sign_payments(const struct SignConfig *self); + +struct StringView sign_payment_address(const struct SignConfig *self, uintptr_t i); + +uint64_t sign_payment_amount(const struct SignConfig *self, uintptr_t i); + +struct StringView sign_change(const struct SignConfig *self); + +uint64_t sign_fee_per_weight(const struct SignConfig *self); + +struct CResult_SignConfigRes new_sign_config(const struct ThresholdKeysWrapper *keys, + enum Network network, + const struct PortableOutput *outputs, + uintptr_t outputs_len, + uintptr_t payments, + const struct StringView *payment_addresses, + const uint64_t *payment_amounts, + struct StringView change, + uint64_t fee_per_weight); + +struct CResult_SignConfig decode_sign_config(const struct ThresholdKeysWrapper *keys, + enum Network network, struct StringView encoded); + +struct CResult_AttemptSignRes attempt_sign(const struct ThresholdKeysWrapper *keys, + const struct SignConfig *config); + +struct CResult_ContinueSignRes continue_sign(struct TransactionSignMachineWrapper *machine, + const struct StringView *preprocesses, + uintptr_t preprocesses_len); + +struct CResult_OwnedString complete_sign(struct TransactionSignatureMachineWrapper *machine, + const struct StringView *shares, + uintptr_t shares_len); + +uint16_t resharer_new_threshold(const struct ResharerConfig *self); + +uintptr_t resharer_resharers(const struct ResharerConfig *self); + +uint16_t resharer_resharer(const struct ResharerConfig *self, uintptr_t i); + +uintptr_t resharer_new_participants(const struct ResharerConfig *self); + +struct StringView resharer_new_participant(const struct ResharerConfig *self, uintptr_t i); + +const uint8_t *resharer_salt(const struct ResharerConfig *self); + +struct CResult_ResharerConfigRes new_resharer_config(uint16_t new_threshold, + const uint16_t *resharers, + uint16_t resharers_len, + const struct StringView *new_participants, + uint16_t new_participants_len); + +struct CResult_ResharerConfig decode_resharer_config(struct StringView config); + +struct CResult_StartResharerRes start_resharer(const struct ThresholdKeysWrapper *keys, + struct ResharerConfig *config); + +struct CResult_StartResharedRes start_reshared(struct StringView multisig_name, + struct ResharerConfig *resharer_config, + struct StringView my_name, + const struct StringView *resharer_starts); + +struct CResult_OwnedString complete_resharer(struct StartResharerRes machine, + const struct StringView *encryption_keys_of_reshared_to); + +struct CResult_ResharedRes complete_reshared(struct StartResharedRes prior, + const struct StringView *resharer_completes); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus diff --git a/hrf/src/key_gen.rs b/hrf/src/key_gen.rs new file mode 100644 index 0000000..261cbc7 --- /dev/null +++ b/hrf/src/key_gen.rs @@ -0,0 +1,525 @@ +use std::collections::HashMap; + +use zeroize::Zeroizing; + +use rand_core::{RngCore, SeedableRng, OsRng}; +use rand_chacha::ChaCha20Rng; + +use transcript::{Transcript, RecommendedTranscript}; + +use ciphersuite::{Ciphersuite, Secp256k1}; +use ::frost::dkg::{*, encryption::*, frost::*}; + +use base64ct::{Encoding, Base64}; +use serde::{Serialize, Deserialize}; + +use bip39::{Language, Mnemonic}; + +use crate::*; + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct MultisigConfig { + pub(crate) multisig_name: String, + pub(crate) threshold: u16, + pub(crate) participants: Vec, + // TODO: The wallet MUST check this salt hasn't been prior observed in any prior protocol. + // It's probably fine in practice since every honest user will use a distinct seed entirely, + // and malicious parties are already accounted for in the threshold selection. + // It's still best practice for the wallet to save prior seen salts and check + pub(crate) salt: [u8; 32], +} + +impl MultisigConfig { + #[no_mangle] + pub extern "C" fn multisig_name(&self) -> StringView { + StringView::new(&self.multisig_name) + } + + #[no_mangle] + pub extern "C" fn multisig_threshold(&self) -> u16 { + self.threshold + } + + #[no_mangle] + pub extern "C" fn multisig_participants(&self) -> usize { + self.participants.len() + } + + #[no_mangle] + pub extern "C" fn multisig_participant(&self, i: usize) -> StringView { + StringView::new(&self.participants[i]) + } + + #[no_mangle] + pub extern "C" fn multisig_salt(&self) -> *const u8 { + self.salt.as_ptr() + } + + fn context(&self) -> String { + let mut context = RecommendedTranscript::new(b"HRF Multisig Context String"); + context.append_message(b"name", self.multisig_name.as_bytes()); + context.append_message(b"threshold", self.threshold.to_le_bytes()); + for participant in &self.participants { + context.append_message(b"participant", participant.as_bytes()); + } + context.append_message(b"salt", self.salt); + hex::encode(context.challenge(b"challenge")) + } + + #[no_mangle] + pub extern "C" fn encode_multisig_config(&self) -> OwnedString { + OwnedString::new(Base64::encode_string(&bincode::serialize(self).unwrap())) + } +} + +#[repr(C)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct MultisigConfigWithName { + config: Box, + my_name: Box, +} + +impl MultisigConfigWithName { + #[no_mangle] + pub extern "C" fn multisig_config(&self) -> &MultisigConfig { + &self.config + } + + #[no_mangle] + pub extern "C" fn multisig_my_name(&self) -> StringView { + StringView::new(&self.my_name) + } + + fn params(&self) -> Result { + let mut my_index = 0; + while (my_index < self.config.participants.len()) && + (self.config.participants[my_index] != *self.my_name) + { + my_index += 1; + } + if my_index == self.config.participants.len() { + Err(INVALID_PARTICIPANT_ERROR)? + } + + let i = Participant::new(u16::try_from(my_index).unwrap() + 1).unwrap(); + Ok( + ThresholdParams::new( + self.config.threshold, + u16::try_from(self.config.participants.len()).unwrap(), + i, + ) + .unwrap(), + ) + } +} + +fn check_t_n(threshold: u16, participants: u16) -> Result<(), u8> { + match ThresholdParams::new(threshold, participants, Participant::new(1).unwrap()) { + Err(DkgError::ZeroParameter(..)) => Err(ZERO_PARAMETER_ERROR)?, + Err(DkgError::InvalidThreshold(..)) => Err(INVALID_THRESHOLD_ERROR)?, + Err(DkgError::InvalidParticipant(..)) => Err(INVALID_PARTICIPANT_ERROR)?, + Err(_) => Err(UNKNOWN_ERROR)?, + Ok(_) => Ok(()), + } +} + +#[repr(C)] +pub struct MultisigConfigRes { + config: Box, + encoded: OwnedString, +} + +#[no_mangle] +pub unsafe extern "C" fn new_multisig_config( + multisig_name: *const u8, + multisig_name_len: usize, + threshold: u16, + participants: *const StringView, + participants_len: u16, +) -> CResult { + CResult::new(new_multisig_config_rust( + multisig_name, + multisig_name_len, + threshold, + participants, + participants_len, + )) +} + +unsafe fn new_multisig_config_rust( + multisig_name: *const u8, + multisig_name_len: usize, + threshold: u16, + participants: *const StringView, + participants_len: u16, +) -> Result { + check_t_n(threshold, participants_len)?; + + if multisig_name_len == 0 { + Err(INVALID_NAME_ERROR)?; + } + + let multisig_name = unsafe { std::slice::from_raw_parts(multisig_name, multisig_name_len) }; + let participants = unsafe { std::slice::from_raw_parts(participants, participants_len.into()) }; + + let Ok(multisig_name) = String::from_utf8(multisig_name.to_vec()) else { + Err(INVALID_NAME_ERROR)? + }; + let mut participants_res = vec![]; + for participant in participants { + if participant.len == 0 { + Err(INVALID_NAME_ERROR)?; + } + let Some(participant) = participant.to_string() else { Err(INVALID_NAME_ERROR)? }; + participants_res.push(participant); + } + + // All multisigs should have a unique context string + // Have the multisig proposer choose a random salt so the only way a conflict will occur is if + // the proposer is malicious + // Then, the only way this may be an issue is if a participant doesn't check if they've seen a + // salt before + let mut salt = [0; 32]; + OsRng.fill_bytes(&mut salt); + + let config = MultisigConfig { multisig_name, threshold, participants: participants_res, salt }; + let encoded = OwnedString::new(Base64::encode_string(&bincode::serialize(&config).unwrap())); + Ok(MultisigConfigRes { config: config.into(), encoded }) +} + +#[no_mangle] +pub extern "C" fn decode_multisig_config(config: StringView) -> CResult { + CResult::new(decode_multisig_config_rust(config)) +} + +fn decode_multisig_config_rust(config: StringView) -> Result { + let Ok(config) = Base64::decode_vec(&config.to_string().ok_or(INVALID_ENCODING_ERROR)?) else { + Err(INVALID_ENCODING_ERROR)? + }; + let Ok(config) = bincode::deserialize::(&config) else { + Err(INVALID_ENCODING_ERROR)? + }; + + let Ok(participants_len) = u16::try_from(config.participants.len()) else { + Err(INVALID_PARTICIPANT_ERROR)? + }; + check_t_n(config.threshold, participants_len)?; + + Ok(config) +} + +fn inner_key_gen( + config: Box, + my_name: StringView, + seed: &[u8; 16], +) -> Result<(MultisigConfigWithName, SecretShareMachine, OwnedString), u8> { + let config = MultisigConfigWithName { + config, + my_name: my_name.to_string().ok_or(INVALID_NAME_ERROR)?.into(), + }; + + let context = config.config.context(); + + let mut coefficients_rng = RecommendedTranscript::new(b"HRF Key Gen Coefficients RNG"); + coefficients_rng.append_message(b"seed", seed); + coefficients_rng.append_message(b"context", context.as_bytes()); + let mut coefficients_rng = ChaCha20Rng::from_seed(coefficients_rng.rng_seed(b"rng")); + + let (machine, commitments) = KeyGenMachine::::new(config.params()?, context) + .generate_coefficients(&mut coefficients_rng); + + Ok((config, machine, OwnedString::new(Base64::encode_string(&commitments.serialize())))) +} + +pub struct SecretShareMachineWrapper(SecretShareMachine); + +#[repr(C)] +pub struct StartKeyGenRes { + seed: OwnedString, + config: Box, + machine: Box, + commitments: OwnedString, +} + +#[no_mangle] +pub extern "C" fn start_key_gen( + config: Box, + my_name: StringView, + language: u8, +) -> CResult { + CResult::new(start_key_gen_rust(config, my_name, language)) +} + +fn start_key_gen_rust( + config: Box, + my_name: StringView, + language: u8, +) -> Result { + // 128-bits of entropy for a 12-word seed + let mut seed = Zeroizing::new([0; 16]); + OsRng.fill_bytes(seed.as_mut()); + + let (config, machine, commitments) = inner_key_gen(config, my_name, &seed)?; + + // TODO: Screen where the seed is converted to a Bitcoin seed and displayed for backup + // TODO: Screen where the commitments are displayed for transmision to everyone else + + Ok(StartKeyGenRes { + seed: OwnedString::new( + Mnemonic::from_entropy( + seed.as_ref(), + match language { + LANGUAGE_ENGLISH => Language::English, + LANGUAGE_CHINESE_SIMPLIFIED => Language::ChineseSimplified, + LANGUAGE_CHINESE_TRADITIONAL => Language::ChineseTraditional, + LANGUAGE_FRENCH => Language::French, + LANGUAGE_ITALIAN => Language::Italian, + LANGUAGE_JAPANESE => Language::Japanese, + LANGUAGE_KOREAN => Language::Korean, + LANGUAGE_SPANISH => Language::Spanish, + _ => Err(UNKNOWN_LANGUAGE_ERROR)?, + }, + ) + .unwrap() + .to_string(), + ), + config: config.into(), + machine: SecretShareMachineWrapper(machine).into(), + commitments, + }) +} + +struct KeyMachineWrapper(KeyMachine); + +#[repr(C)] +pub struct SecretSharesRes { + machine: Box, + internal_commitments: Box>, + shares: OwnedString, +} + +#[no_mangle] +pub unsafe extern "C" fn get_secret_shares( + config: &MultisigConfigWithName, + language: u8, + seed: StringView, + machine: Box, + commitments: *const StringView, + commitments_len: usize, +) -> CResult { + CResult::new(get_secret_shares_rust( + config, + language, + seed, + machine, + commitments, + commitments_len, + )) +} + +fn get_secret_shares_rust( + config: &MultisigConfigWithName, + language: u8, + seed: StringView, + machine: Box, + commitments: *const StringView, + commitments_len: usize, +) -> Result { + let mut secret_shares_rng = RecommendedTranscript::new(b"HRF Key Gen Secret Shares RNG"); + let Ok(mnemonic) = Mnemonic::from_phrase( + &seed.to_string().ok_or(INVALID_SEED_ERROR)?, + match language { + LANGUAGE_ENGLISH => Language::English, + LANGUAGE_CHINESE_SIMPLIFIED => Language::ChineseSimplified, + LANGUAGE_CHINESE_TRADITIONAL => Language::ChineseTraditional, + LANGUAGE_FRENCH => Language::French, + LANGUAGE_ITALIAN => Language::Italian, + LANGUAGE_JAPANESE => Language::Japanese, + LANGUAGE_KOREAN => Language::Korean, + LANGUAGE_SPANISH => Language::Spanish, + _ => Err(UNKNOWN_LANGUAGE_ERROR)?, + }, + ) else { + Err(INVALID_SEED_ERROR)? + }; + secret_shares_rng.append_message(b"seed", mnemonic.entropy()); + secret_shares_rng.append_message(b"context", config.config.context().as_bytes()); + let mut secret_shares_rng = ChaCha20Rng::from_seed(secret_shares_rng.rng_seed(b"rng")); + + let params = config.params().unwrap(); + + let commitments = unsafe { std::slice::from_raw_parts(commitments, commitments_len) }; + if commitments.len() != config.config.participants.len() { + Err(INVALID_AMOUNT_OF_COMMITMENTS_ERROR)?; + } + let mut new_commitments = vec![]; + let mut commitments_map = HashMap::new(); + for (i, commitments) in commitments.iter().enumerate() { + let i = Participant::new(u16::try_from(i).unwrap() + 1).unwrap(); + let Ok(commitments) = + Base64::decode_vec(&commitments.to_string().ok_or(INVALID_ENCODING_ERROR)?) + else { + Err(INVALID_ENCODING_ERROR)? + }; + let Ok(message) = EncryptionKeyMessage::>::read( + &mut commitments.as_slice(), + params, + ) else { + Err(INVALID_ENCODING_ERROR)? + }; + commitments_map.insert(i, message); + + new_commitments.push(commitments); + } + let commitments = new_commitments; + + commitments_map.remove(¶ms.i()); + let Ok((machine, mut shares)) = + machine.0.generate_secret_shares(&mut secret_shares_rng, commitments_map) + else { + Err(INVALID_COMMITMENTS_ERROR)? + }; + + let mut linearized_shares = vec![]; + for l in 1 ..= params.n() { + let l = Participant::new(l).unwrap(); + if l != params.i() { + shares.remove(&l).unwrap().write(&mut linearized_shares).unwrap(); + } + } + + Ok(SecretSharesRes { + machine: Box::new(KeyMachineWrapper(machine)), + internal_commitments: Box::new(bincode::serialize(&commitments).unwrap()), + shares: OwnedString::new(Base64::encode_string(&linearized_shares)), + }) + + // TODO: Display commitments to be sent to everyone +} + +#[repr(C)] +pub struct KeyGenRes { + multisig_id: [u8; 32], + keys: Box, + recovery: OwnedString, +} + +#[no_mangle] +pub unsafe extern "C" fn complete_key_gen( + config: &MultisigConfigWithName, + machine_and_commitments: SecretSharesRes, + shares: *const StringView, + shares_len: usize, +) -> CResult { + CResult::new(complete_key_gen_rust(config, machine_and_commitments, shares, shares_len)) +} + +unsafe fn complete_key_gen_rust( + config: &MultisigConfigWithName, + machine_and_commitments: SecretSharesRes, + shares: *const StringView, + shares_len: usize, +) -> Result { + let params = config.params().unwrap(); + let SecretSharesRes { machine, internal_commitments: commitments, .. } = machine_and_commitments; + + if shares_len != config.config.participants.len() { + Err(INVALID_AMOUNT_OF_SHARES_ERROR)?; + } + let shares = unsafe { std::slice::from_raw_parts(shares, shares_len) }; + + let mut new_shares = vec![]; + let mut shares_map = HashMap::new(); + for (i, shares) in shares.iter().enumerate() { + let i = Participant::new(u16::try_from(i).unwrap() + 1).unwrap(); + let Ok(linearized_shares) = + Base64::decode_vec(&shares.to_string().ok_or(INVALID_ENCODING_ERROR)?) + else { + Err(INVALID_ENCODING_ERROR)? + }; + let mut reader_slice = linearized_shares.as_slice(); + let reader = &mut reader_slice; + + let mut shares_from_i = HashMap::new(); + for l in 0 .. config.config.participants.len() { + let l = Participant::new(u16::try_from(l).unwrap() + 1).unwrap(); + if l == i { + continue; + } + let Ok(message) = + EncryptedMessage::::F>>::read( + reader, params, + ) + else { + Err(INVALID_ENCODING_ERROR)? + }; + shares_from_i.insert(l, message); + } + shares_map.insert(i, shares_from_i); + + new_shares.push(linearized_shares); + } + let shares = new_shares; + shares_map.remove(¶ms.i()); + + let Some(my_shares) = shares_map + .into_iter() + .map(|(l, mut shares)| shares.remove(¶ms.i()).map(|s| (l, s))) + .collect::>>() + else { + Err(INVALID_SHARE_ERROR)? + }; + + // Doesn't use a seeded RNG since this only uses the RNG for batch verification + let Ok(machine) = machine.0.calculate_share(&mut OsRng, my_shares) else { + Err(INVALID_SHARE_ERROR)? + }; + let keys = machine.complete(); + + let mut recovery = bincode::serialize(&config.config).unwrap(); + recovery.extend(*commitments); + recovery.extend(bincode::serialize(&shares).unwrap()); + + let mut id = RecommendedTranscript::new(b"HRF Multisig ID"); + id.append_message(b"recovery", &recovery); + let id = id.challenge(b"id"); + + Ok(KeyGenRes { + multisig_id: id.as_slice()[.. 32].try_into().unwrap(), + keys: Box::new(ThresholdKeysWrapper(keys.into())), + recovery: OwnedString::new(Base64::encode_string(&recovery)), + }) + + // TODO: Have everyone confirm they have the same 32-byte ID + // TODO: Give everyone the option to save the recovery string +} + +#[no_mangle] +pub unsafe extern "C" fn keys_threshold(keys: &ThresholdKeysWrapper) -> u16 { + keys.0.params().t() +} + +#[no_mangle] +pub unsafe extern "C" fn keys_participants(keys: &ThresholdKeysWrapper) -> u16 { + keys.0.params().n() +} + +#[no_mangle] +pub unsafe extern "C" fn keys_index(keys: &ThresholdKeysWrapper) -> u16 { + u16::from(keys.0.params().i()) - 1 +} + +#[no_mangle] +pub unsafe extern "C" fn serialize_keys(keys: &ThresholdKeysWrapper) -> OwnedString { + OwnedString::new(hex::encode(&keys.0.serialize())) +} + +#[no_mangle] +pub unsafe extern "C" fn deserialize_keys(keys: StringView) -> CResult { + let Some(string) = keys.to_string() else { return CResult::new(Err(INVALID_ENCODING_ERROR)) }; + let Ok(bytes) = hex::decode(string) else { return CResult::new(Err(INVALID_ENCODING_ERROR)) }; + let Ok(keys) = ThresholdCore::::read(&mut bytes.as_slice()) else { + return CResult::new(Err(UNKNOWN_ERROR)); + }; + CResult::new(Ok(ThresholdKeysWrapper(keys.into()))) +} diff --git a/hrf/src/lib.rs b/hrf/src/lib.rs new file mode 100644 index 0000000..564a36e --- /dev/null +++ b/hrf/src/lib.rs @@ -0,0 +1,101 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(clippy::missing_safety_doc)] +#![allow(clippy::borrowed_box, clippy::box_collection, clippy::boxed_local)] + +pub mod key_gen; +pub mod sign; +pub mod resharing; + +// Seed languages +pub const LANGUAGE_ENGLISH: u8 = 1; +pub const LANGUAGE_CHINESE_SIMPLIFIED: u8 = 2; +pub const LANGUAGE_CHINESE_TRADITIONAL: u8 = 3; +pub const LANGUAGE_FRENCH: u8 = 4; +pub const LANGUAGE_ITALIAN: u8 = 5; +pub const LANGUAGE_JAPANESE: u8 = 6; +pub const LANGUAGE_KOREAN: u8 = 7; +pub const LANGUAGE_SPANISH: u8 = 8; + +// Common errors +pub const UNKNOWN_ERROR: u8 = 21; +pub const INVALID_ENCODING_ERROR: u8 = 22; +pub const INVALID_PARTICIPANT_ERROR: u8 = 23; +pub const INVALID_SHARE_ERROR: u8 = 24; + +// Key gen errors +pub const ZERO_PARAMETER_ERROR: u8 = 41; +pub const INVALID_THRESHOLD_ERROR: u8 = 42; +pub const INVALID_NAME_ERROR: u8 = 43; +pub const UNKNOWN_LANGUAGE_ERROR: u8 = 44; +pub const INVALID_SEED_ERROR: u8 = 45; +pub const INVALID_AMOUNT_OF_COMMITMENTS_ERROR: u8 = 46; +pub const INVALID_COMMITMENTS_ERROR: u8 = 47; +pub const INVALID_AMOUNT_OF_SHARES_ERROR: u8 = 48; + +// Sign errors +pub const INVALID_OUTPUT_ERROR: u8 = 61; +pub const INVALID_ADDRESS_ERROR: u8 = 62; +pub const INVALID_NETWORK_ERROR: u8 = 63; +pub const NO_INPUTS_ERROR: u8 = 64; +pub const NO_OUTPUTS_ERROR: u8 = 65; +pub const DUST_ERROR: u8 = 66; +pub const NOT_ENOUGH_FUNDS_ERROR: u8 = 67; +pub const TOO_LARGE_TRANSACTION_ERROR: u8 = 68; +pub const WRONG_KEYS_ERROR: u8 = 69; +pub const INVALID_PREPROCESS_ERROR: u8 = 70; +pub const FEE_ERROR: u8 = 71; +pub const INVALID_DERIVATION_ERROR: u8 = 72; + +// Resharing errors +pub const INVALID_PARTICIPANTS_AMOUNT_ERROR: u8 = 81; +pub const DUPLICATED_PARTICIPANT_ERROR: u8 = 82; +pub const NOT_ENOUGH_RESHARERS_ERROR: u8 = 83; +pub const INVALID_RESHARED_MSG_ERROR: u8 = 84; +pub const INVALID_RESHARER_MSG_ERROR: u8 = 85; + +#[repr(C)] +pub struct StringView { + pub ptr: *const u8, + pub len: usize, +} +impl StringView { + pub(crate) fn new(to_view: &str) -> StringView { + StringView { ptr: to_view.as_ptr(), len: to_view.len() } + } + pub(crate) fn to_string(&self) -> Option { + let slice = unsafe { std::slice::from_raw_parts(self.ptr, self.len) }; + String::from_utf8(slice.to_vec()).ok() + } +} + +#[repr(C)] +pub struct OwnedString { + str_box: *mut String, + pub ptr: *const u8, + pub len: usize, +} +impl OwnedString { + pub(crate) fn new(str: String) -> OwnedString { + OwnedString { ptr: str.as_ptr(), len: str.len(), str_box: Box::into_raw(Box::new(str)) } + } + #[no_mangle] + pub extern "C" fn free_owned_string(self) { + drop(unsafe { Box::from_raw(self.str_box) }); + } +} + +#[repr(C)] +pub struct CResult { + value: Option>, + err: u8, +} +impl CResult { + pub(crate) fn new(res: Result) -> Self { + match res { + Ok(value) => CResult { value: Some(value.into()), err: 0 }, + Err(e) => CResult { value: None, err: e }, + } + } +} + +pub struct ThresholdKeysWrapper(frost::dkg::ThresholdKeys); diff --git a/hrf/src/resharing.rs b/hrf/src/resharing.rs new file mode 100644 index 0000000..39d987b --- /dev/null +++ b/hrf/src/resharing.rs @@ -0,0 +1,420 @@ +use std::collections::{HashSet, HashMap}; + +use rand_core::{RngCore, OsRng}; + +use transcript::{Transcript, RecommendedTranscript}; + +use ciphersuite::{group::GroupEncoding, Ciphersuite, Secp256k1}; +use ::frost::dkg::{ + *, + encryption::*, + resharing::{*, common::*}, +}; + +use base64ct::{Encoding, Base64}; +use serde::{Serialize, Deserialize}; + +use crate::{*, key_gen::*}; + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct ResharerConfig { + new_threshold: u16, + resharers: Vec, + new_participants: Vec, + salt: [u8; 32], +} + +impl ResharerConfig { + #[no_mangle] + pub extern "C" fn resharer_new_threshold(&self) -> u16 { + self.new_threshold + } + + #[no_mangle] + pub extern "C" fn resharer_resharers(&self) -> usize { + self.resharers.len() + } + + #[no_mangle] + pub extern "C" fn resharer_resharer(&self, i: usize) -> u16 { + self.resharers[i] + } + + #[no_mangle] + pub extern "C" fn resharer_new_participants(&self) -> usize { + self.new_participants.len() + } + + #[no_mangle] + pub extern "C" fn resharer_new_participant(&self, i: usize) -> StringView { + StringView::new(&self.new_participants[i]) + } + + #[no_mangle] + pub extern "C" fn resharer_salt(&self) -> *const u8 { + self.salt.as_ptr() + } + + fn context(&self) -> String { + let mut context = RecommendedTranscript::new(b"HRF Resharing Context String"); + context.append_message(b"new_threshold", self.new_threshold.to_le_bytes()); + for resharer in &self.resharers { + context.append_message(b"resharer", resharer.to_le_bytes()); + } + for new_participant in &self.new_participants { + context.append_message(b"new_participant", new_participant.as_bytes()); + } + context.append_message(b"salt", self.salt); + hex::encode(context.challenge(b"challenge")) + } +} + +fn check_t_n(threshold: u16, participants: u16) -> Result<(), u8> { + match ThresholdParams::new(threshold, participants, Participant::new(1).unwrap()) { + Err(DkgError::ZeroParameter(..)) => Err(ZERO_PARAMETER_ERROR)?, + Err(DkgError::InvalidThreshold(..)) => Err(INVALID_THRESHOLD_ERROR)?, + Err(DkgError::InvalidParticipant(..)) => Err(INVALID_PARTICIPANT_ERROR)?, + Err(_) => Err(UNKNOWN_ERROR)?, + Ok(_) => Ok(()), + } +} + +#[repr(C)] +pub struct ResharerConfigRes { + config: Box, + encoded: OwnedString, +} + +#[no_mangle] +pub unsafe extern "C" fn new_resharer_config( + new_threshold: u16, + resharers: *const u16, + resharers_len: u16, + new_participants: *const StringView, + new_participants_len: u16, +) -> CResult { + CResult::new(new_resharer_config_rust( + new_threshold, + resharers, + resharers_len, + new_participants, + new_participants_len, + )) +} + +unsafe fn new_resharer_config_rust( + new_threshold: u16, + resharers_ptr: *const u16, + resharers_len: u16, + new_participants: *const StringView, + new_participants_len: u16, +) -> Result { + check_t_n(new_threshold, new_participants_len)?; + if new_participants_len == u16::MAX { + Err(INVALID_PARTICIPANTS_AMOUNT_ERROR)?; + } + + let mut resharers = HashSet::new(); + for resharer in unsafe { std::slice::from_raw_parts(resharers_ptr, resharers_len.into()) } { + resharers.insert(*resharer); + } + if resharers.len() != usize::from(resharers_len) { + Err(DUPLICATED_PARTICIPANT_ERROR)?; + } + + let mut resharers = resharers.into_iter().collect::>(); + resharers.sort(); + if let Some(last) = resharers.last() { + if *last == u16::MAX { + Err(INVALID_PARTICIPANT_ERROR)?; + } + } else { + Err(NOT_ENOUGH_RESHARERS_ERROR)?; + } + + let new_participants = + unsafe { std::slice::from_raw_parts(new_participants, new_participants_len.into()) }; + let mut new_participants_res = vec![]; + for participant in new_participants { + if participant.len == 0 { + Err(INVALID_NAME_ERROR)?; + } + let Some(participant) = participant.to_string() else { Err(INVALID_NAME_ERROR)? }; + new_participants_res.push(participant); + } + + let mut salt = [0; 32]; + OsRng.fill_bytes(&mut salt); + + let config = + ResharerConfig { new_threshold, resharers, new_participants: new_participants_res, salt }; + let encoded = OwnedString::new(Base64::encode_string(&bincode::serialize(&config).unwrap())); + Ok(ResharerConfigRes { config: config.into(), encoded }) +} + +#[no_mangle] +pub extern "C" fn decode_resharer_config(config: StringView) -> CResult { + CResult::new(decode_resharer_config_rust(config)) +} + +fn decode_resharer_config_rust(config: StringView) -> Result { + let Ok(config) = Base64::decode_vec(&config.to_string().ok_or(INVALID_ENCODING_ERROR)?) else { + Err(INVALID_ENCODING_ERROR)? + }; + let Ok(mut config) = bincode::deserialize::(&config) else { + Err(INVALID_ENCODING_ERROR)? + }; + + let Ok(participants_len) = u16::try_from(config.new_participants.len()) else { + Err(INVALID_PARTICIPANT_ERROR)? + }; + check_t_n(config.new_threshold, participants_len)?; + + config.resharers = + config.resharers.into_iter().collect::>().into_iter().collect::>(); + config.resharers.sort(); + if let Some(last) = config.resharers.last() { + if *last == u16::MAX { + Err(INVALID_PARTICIPANT_ERROR)?; + } + } else { + Err(NOT_ENOUGH_RESHARERS_ERROR)?; + } + + Ok(config) +} + +struct OpaqueResharingMachine(ResharingSecretMachine); + +#[repr(C)] +pub struct StartResharerRes { + new_participants_len: usize, + machine: Box, + encoded: OwnedString, +} + +#[no_mangle] +pub unsafe extern "C" fn start_resharer( + keys: &ThresholdKeysWrapper, + config: Box, +) -> CResult { + CResult::new(start_resharer_rust(keys, config)) +} + +fn start_resharer_rust( + keys: &ThresholdKeysWrapper, + config: Box, +) -> Result { + if config.resharers.len() < keys.0.params().t().into() { + Err(NOT_ENOUGH_RESHARERS_ERROR)?; + } + let (machine, message) = ResharingMachine::new( + keys.0.clone(), + config.resharers.iter().map(|i| Participant::new(*i + 1).unwrap()).collect(), + ThresholdParams::new( + config.new_threshold, + u16::try_from(config.new_participants.len()).unwrap(), + Participant::new(1).unwrap(), + ) + .map_err(|_| UNKNOWN_ERROR)?, + config.context(), + ) + .ok_or(UNKNOWN_ERROR)? + .generate_coefficients(&mut OsRng); + Ok(StartResharerRes { + new_participants_len: config.new_participants.len(), + machine: Box::new(OpaqueResharingMachine(machine)), + encoded: OwnedString::new(Base64::encode_string(&message.serialize())), + }) +} + +struct OpaqueResharedMachine(ResharedMachine); + +#[repr(C)] +pub struct StartResharedRes { + resharers_len: usize, + multisig_config: Box, + my_i: u16, + machine: Box, + encoded: OwnedString, +} + +#[no_mangle] +pub unsafe extern "C" fn start_reshared( + multisig_name: StringView, + resharer_config: Box, + my_name: StringView, + resharer_starts: *const StringView, +) -> CResult { + CResult::new(start_reshared_rust(multisig_name, resharer_config, my_name, resharer_starts)) +} + +fn start_reshared_rust( + multisig_name: StringView, + resharer_config: Box, + my_name: StringView, + resharer_starts: *const StringView, +) -> Result { + let mut msgs = vec![]; + for view in + (unsafe { std::slice::from_raw_parts(resharer_starts, resharer_config.resharers.len()) }).iter() + { + let bytes = Base64::decode_vec(&view.to_string().ok_or(INVALID_ENCODING_ERROR)?) + .map_err(|_| INVALID_ENCODING_ERROR)?; + let msg = EncryptionKeyMessage::>::read( + &mut bytes.as_slice(), + ThresholdParams::new( + resharer_config.new_threshold, + resharer_config.new_participants.len().try_into().unwrap(), + Participant::new(1).unwrap(), + ) + .unwrap(), + ) + .map_err(|_| INVALID_RESHARER_MSG_ERROR)?; + msgs.push(msg); + } + + let my_name = my_name.to_string().ok_or(INVALID_NAME_ERROR)?; + let my_i = u16::try_from( + resharer_config + .new_participants + .iter() + .position(|name| name == &my_name) + .ok_or(INVALID_PARTICIPANT_ERROR)?, + ) + .unwrap() + + 1; + let Ok((machine, msg)) = ResharedMachine::new( + &mut OsRng, + u16::try_from(resharer_config.resharers.len()).unwrap(), + ThresholdParams::new( + resharer_config.new_threshold, + u16::try_from(resharer_config.new_participants.len()).unwrap(), + Participant::new(my_i).unwrap(), + ) + .unwrap(), + resharer_config.context(), + msgs, + ) else { + Err(INVALID_RESHARER_MSG_ERROR)? + }; + + Ok(StartResharedRes { + multisig_config: Box::new(MultisigConfig { + multisig_name: multisig_name.to_string().ok_or(INVALID_NAME_ERROR)?, + threshold: resharer_config.new_threshold, + participants: resharer_config.new_participants.clone(), + salt: resharer_config.salt, + }), + resharers_len: resharer_config.resharers.len(), + my_i, + machine: Box::new(OpaqueResharedMachine(machine)), + encoded: OwnedString::new(Base64::encode_string(&msg.serialize())), + }) +} + +#[no_mangle] +pub unsafe extern "C" fn complete_resharer( + machine: StartResharerRes, + encryption_keys_of_reshared_to: *const StringView, +) -> CResult { + CResult::new(complete_resharer_rust(machine, encryption_keys_of_reshared_to)) +} + +fn complete_resharer_rust( + prior: StartResharerRes, + encryption_keys_of_reshared_to: *const StringView, +) -> Result { + let mut msgs = HashMap::new(); + for (i, view) in (unsafe { + std::slice::from_raw_parts(encryption_keys_of_reshared_to, prior.new_participants_len) + }) + .iter() + .enumerate() + { + let bytes = Base64::decode_vec(&view.to_string().ok_or(INVALID_ENCODING_ERROR)?) + .map_err(|_| INVALID_ENCODING_ERROR)?; + let msg = EncryptionKeyMessage::::read( + &mut bytes.as_slice(), + ThresholdParams::new(1, 1, Participant::new(1).unwrap()).unwrap(), + ) + .map_err(|_| INVALID_RESHARED_MSG_ERROR)?; + msgs.insert(Participant::new((i + 1).try_into().unwrap()).unwrap(), msg); + } + Ok(OwnedString::new(Base64::encode_string( + &bincode::serialize( + &prior + .machine + .0 + .generate_secret_shares(&mut OsRng, msgs) + .map_err(|_| INVALID_RESHARED_MSG_ERROR)? + .into_iter() + .map(|(key, value)| (u16::from(key), value.serialize())) + .collect::>(), + ) + .unwrap(), + ))) +} + +#[repr(C)] +pub struct ResharedRes { + config: Box, + id: OwnedString, + keys: Box, +} + +#[no_mangle] +pub unsafe extern "C" fn complete_reshared( + prior: StartResharedRes, + resharer_completes: *const StringView, +) -> CResult { + CResult::new(complete_reshared_rust(prior, resharer_completes)) +} + +fn complete_reshared_rust( + prior: StartResharedRes, + resharer_completes: *const StringView, +) -> Result { + let mut msgs = vec![]; + for view in + (unsafe { std::slice::from_raw_parts(resharer_completes, prior.resharers_len) }).iter() + { + let bytes = Base64::decode_vec(&view.to_string().ok_or(INVALID_ENCODING_ERROR)?) + .map_err(|_| INVALID_ENCODING_ERROR)?; + let mut map: HashMap> = + bincode::deserialize(&bytes).map_err(|_| INVALID_ENCODING_ERROR)?; + let msg = EncryptedMessage::::F>>::read( + &mut map.remove(&prior.my_i).ok_or(INVALID_RESHARER_MSG_ERROR)?.as_slice(), + ThresholdParams::new(1, 1, Participant::new(1).unwrap()).unwrap(), + ) + .map_err(|_| INVALID_RESHARER_MSG_ERROR)?; + msgs.push(msg); + } + + let keys = ThresholdKeys::new( + prior.machine.0.accept_shares(&mut OsRng, msgs).map_err(|_| INVALID_RESHARER_MSG_ERROR)?, + ); + + let mut id = RecommendedTranscript::new(b"HRF Reshared Multisig ID"); + id.append_message(b"threshold", prior.multisig_config.threshold.to_le_bytes()); + id.append_message( + b"participants", + bincode::serialize(&prior.multisig_config.participants).unwrap(), + ); + id.append_message(b"salt", prior.multisig_config.salt); + id.append_message(b"group_key", keys.group_key().to_bytes()); + for i in 0 .. prior.multisig_config.participants.len() { + id.append_message( + b"verification_share", + keys.verification_shares()[&Participant::new((i + 1).try_into().unwrap()).unwrap()] + .to_bytes(), + ); + } + let id = id.challenge(b"id"); + + Ok(ResharedRes { + config: prior.multisig_config, + id: OwnedString::new(hex::encode(id)), + keys: Box::new(ThresholdKeysWrapper(keys)), + }) +} diff --git a/hrf/src/sign.rs b/hrf/src/sign.rs new file mode 100644 index 0000000..a107ea6 --- /dev/null +++ b/hrf/src/sign.rs @@ -0,0 +1,498 @@ +use std::{str::FromStr, collections::HashMap}; + +use rand_core::OsRng; + +use transcript::{Transcript, RecommendedTranscript}; +use ciphersuite::{ + group::{ + ff::{Field, PrimeField}, + GroupEncoding, + }, + Ciphersuite, Secp256k1, +}; +use frost::{*, sign::*}; + +use bitcoin_serai::{ + bitcoin::{ + consensus::Encodable, + network::constants::Network as BNetwork, + address::{NetworkUnchecked, Address}, + }, + wallet::*, +}; + +use base64ct::{Encoding, Base64}; +use serde::{Serialize, Deserialize}; + +use crate::*; + +#[repr(C)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Network { + Mainnet, + Testnet, + Regtest, +} + +impl Network { + fn to_bitcoin(self) -> BNetwork { + match self { + Self::Mainnet => BNetwork::Bitcoin, + Self::Testnet => BNetwork::Testnet, + Self::Regtest => BNetwork::Regtest, + } + } +} + +fn offset_keys( + keys: &ThresholdKeysWrapper, + account: u32, + address: u32, + change: bool, + secure: bool, +) -> Option> { + let offset = if (account == 0) && (address == 0) && (change == false) { + <::F as Field>::ZERO + } else { + let key_bytes = keys.0.group_key().to_bytes(); + Secp256k1::hash_to_F( + b"derivation", + &[ + if secure { key_bytes.as_slice() } else { &[] }, + &account.to_le_bytes(), + &address.to_le_bytes(), + &[u8::from(change)], + ] + .concat(), + ) + }; + let keys = tweak_keys(&keys.0).offset(offset); + if keys.group_key() == tweak_keys(&keys).group_key() { + Some(keys) + } else { + None + } +} + +#[no_mangle] +pub unsafe extern "C" fn address_for_keys( + network: Network, + keys: &ThresholdKeysWrapper, + account: u32, + address: u32, + change: bool, + secure: bool, +) -> CResult { + CResult::new(if let Some(keys) = offset_keys(keys, account, address, change, secure) { + Ok(OwnedString::new( + Address::new( + network.to_bitcoin(), + address_payload(keys.group_key()).expect("tweaked keys didn't have an address"), + ) + .to_string(), + )) + } else { + Err(INVALID_DERIVATION_ERROR) + }) +} + +#[no_mangle] +pub unsafe extern "C" fn script_pubkey_for_keys( + keys: &ThresholdKeysWrapper, + account: u32, + address: u32, + change: bool, + secure: bool, +) -> CResult { + CResult::new(if let Some(keys) = offset_keys(keys, account, address, change, secure) { + Ok(OwnedString::new(hex::encode( + address_payload(keys.group_key()) + .expect("tweaked keys didn't have an address") + .script_pubkey(), + ))) + } else { + Err(INVALID_DERIVATION_ERROR) + }) +} + +#[repr(C)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct PortableOutput { + pub hash: [u8; 32], + pub vout: u32, + pub value: u64, + pub script_pubkey: *const u8, + pub script_pubkey_len: usize, + pub account: u32, + pub address: u32, + pub change: bool, + pub secure: bool, +} + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct OwnedPortableOutput { + hash: [u8; 32], + vout: u32, + value: u64, + script_pubkey: Vec, + account: u32, + address: u32, + change: bool, + secure: bool, +} + +impl OwnedPortableOutput { + #[no_mangle] + pub extern "C" fn output_hash(&self) -> *const u8 { + self.hash.as_ptr() + } + #[no_mangle] + pub extern "C" fn output_vout(&self) -> u32 { + self.vout + } + #[no_mangle] + pub extern "C" fn output_value(&self) -> u64 { + self.value + } + #[no_mangle] + pub extern "C" fn output_script_pubkey_len(&self) -> usize { + self.script_pubkey.len() + } + #[no_mangle] + pub extern "C" fn output_script_pubkey(&self) -> *const u8 { + self.script_pubkey.as_ptr() + } +} + +fn try_into( + keys: &ThresholdKeysWrapper, + output: OwnedPortableOutput, +) -> Result { + let offset = if (output.account == 0) && (output.address == 0) && (output.change == false) { + <::F as Field>::ZERO + } else { + let key_bytes = keys.0.group_key().to_bytes(); + Secp256k1::hash_to_F( + b"derivation", + &[ + if output.secure { key_bytes.as_slice() } else { &[] }, + &output.account.to_le_bytes(), + &output.address.to_le_bytes(), + &[u8::from(output.change)], + ] + .concat(), + ) + }; + let mut buf = offset.to_repr().to_vec(); + buf.extend(&output.value.to_le_bytes()); + buf.push(u8::try_from(output.script_pubkey.len()).map_err(|_| ())?); + buf.extend(output.script_pubkey); + for i in (0 .. 32).rev() { + buf.push(output.hash[i]); + } + buf.extend(&output.vout.to_le_bytes()); + ReceivedOutput::read(&mut buf.as_slice()).map_err(|_| ()) +} + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct SignConfig { + network: BNetwork, + #[allow(clippy::vec_box)] + inputs: Vec>, + payments: Vec<(String, u64)>, + change: String, + fee_per_weight: u64, +} + +impl SignConfig { + #[no_mangle] + pub extern "C" fn sign_inputs(&self) -> usize { + self.inputs.len() + } + #[allow(clippy::borrowed_box)] + #[no_mangle] + pub extern "C" fn sign_input(&self, i: usize) -> &OwnedPortableOutput { + &self.inputs[i] + } + #[no_mangle] + pub extern "C" fn sign_payments(&self) -> usize { + self.payments.len() + } + #[no_mangle] + pub extern "C" fn sign_payment_address(&self, i: usize) -> StringView { + StringView::new(&self.payments[i].0) + } + #[no_mangle] + pub extern "C" fn sign_payment_amount(&self, i: usize) -> u64 { + self.payments[i].1 + } + #[no_mangle] + pub extern "C" fn sign_change(&self) -> StringView { + StringView::new(&self.change) + } + #[no_mangle] + pub extern "C" fn sign_fee_per_weight(&self) -> u64 { + self.fee_per_weight + } +} + +fn sign_config_to_tx( + keys: &ThresholdKeysWrapper, + network: BNetwork, + config: &SignConfig, +) -> Result { + SignableTransaction::new( + config + .inputs + .iter() + .cloned() + .map(|input| try_into(keys, *input)) + .collect::>() + .map_err(|_| INVALID_OUTPUT_ERROR)?, + &config + .payments + .iter() + .map(|(address, amount)| { + Ok(( + Address::::from_str(address) + .map_err(|_| INVALID_ADDRESS_ERROR)? + .require_network(network) + .map_err(|_| INVALID_NETWORK_ERROR)?, + *amount, + )) + }) + .collect::, u8>>()?, + Some( + Address::::from_str(&config.change) + .map_err(|_| INVALID_ADDRESS_ERROR)? + .require_network(network) + .map_err(|_| INVALID_NETWORK_ERROR)?, + ), + None, + config.fee_per_weight, + ) + .map_err(|e| match e { + TransactionError::NoInputs => NO_INPUTS_ERROR, + TransactionError::NoOutputs => NO_OUTPUTS_ERROR, + TransactionError::DustPayment => DUST_ERROR, + TransactionError::TooMuchData => unreachable!(), + TransactionError::NotEnoughFunds => NOT_ENOUGH_FUNDS_ERROR, + TransactionError::TooLargeTransaction => TOO_LARGE_TRANSACTION_ERROR, + TransactionError::TooLowFee => FEE_ERROR, + }) +} + +#[repr(C)] +pub struct SignConfigRes { + config: Box, + encoded: OwnedString, +} + +#[no_mangle] +pub unsafe extern "C" fn new_sign_config( + keys: &ThresholdKeysWrapper, + network: Network, + outputs: *const PortableOutput, + outputs_len: usize, + payments: usize, + payment_addresses: *const StringView, + payment_amounts: *const u64, + change: StringView, + fee_per_weight: u64, +) -> CResult { + CResult::new(new_sign_config_rust( + keys, + network, + unsafe { std::slice::from_raw_parts(outputs, outputs_len) }, + unsafe { std::slice::from_raw_parts(payment_addresses, payments) }, + unsafe { std::slice::from_raw_parts(payment_amounts, payments) }, + change, + fee_per_weight, + )) +} + +fn new_sign_config_rust( + keys: &ThresholdKeysWrapper, + network: Network, + outputs: &[PortableOutput], + payment_addresses: &[StringView], + payment_amounts: &[u64], + change: StringView, + fee_per_weight: u64, +) -> Result { + let network = network.to_bitcoin(); + let config = SignConfig { + network, + inputs: outputs + .iter() + .map(|output| { + Box::new(OwnedPortableOutput { + hash: output.hash, + vout: output.vout, + value: output.value, + script_pubkey: (unsafe { + std::slice::from_raw_parts(output.script_pubkey, output.script_pubkey_len) + }) + .to_vec(), + account: output.account, + address: output.address, + change: output.change, + secure: output.secure, + }) + }) + .collect(), + payments: payment_addresses + .iter() + .zip(payment_amounts) + .map(|(address, amount)| { + address.to_string().ok_or(INVALID_ADDRESS_ERROR).map(|address| (address, *amount)) + }) + .collect::>()?, + change: change.to_string().ok_or(INVALID_ADDRESS_ERROR)?, + fee_per_weight, + }; + + sign_config_to_tx(keys, network, &config)?; + + let res = Base64::encode_string(&bincode::serialize(&config).unwrap()); + Ok(SignConfigRes { config: config.into(), encoded: OwnedString::new(res) }) +} + +#[no_mangle] +pub extern "C" fn decode_sign_config( + keys: &ThresholdKeysWrapper, + network: Network, + encoded: StringView, +) -> CResult { + CResult::new(decode_sign_config_rust(keys, network, encoded)) +} + +fn decode_sign_config_rust( + keys: &ThresholdKeysWrapper, + network: Network, + encoded: StringView, +) -> Result { + let network = network.to_bitcoin(); + let decoded = bincode::deserialize::( + &Base64::decode_vec(&encoded.to_string().ok_or(INVALID_ENCODING_ERROR)?) + .map_err(|_| INVALID_ENCODING_ERROR)?, + ) + .map_err(|_| INVALID_ENCODING_ERROR)?; + if decoded.network != network { + Err(INVALID_NETWORK_ERROR)?; + } + sign_config_to_tx(keys, network, &decoded)?; + Ok(decoded) +} + +pub struct TransactionSignMachineWrapper(TransactionSignMachine); +pub struct TransactionSignatureMachineWrapper(TransactionSignatureMachine); + +#[repr(C)] +pub struct AttemptSignRes { + machine: Box, + preprocess: OwnedString, +} + +#[no_mangle] +pub extern "C" fn attempt_sign( + keys: &ThresholdKeysWrapper, + config: &SignConfig, +) -> CResult { + CResult::new(attempt_sign_rust(keys, config)) +} + +fn attempt_sign_rust( + keys: &ThresholdKeysWrapper, + config: &SignConfig, +) -> Result { + let (machine, preprocesses) = sign_config_to_tx(keys, config.network, config) + .expect("created a SignConfig which couldn't create a TX") + .multisig(tweak_keys(&keys.0), RecommendedTranscript::new(b"HRF Sign Transaction")) + .ok_or(WRONG_KEYS_ERROR)? + .preprocess(&mut OsRng); + Ok(AttemptSignRes { + machine: TransactionSignMachineWrapper(machine).into(), + preprocess: OwnedString::new(Base64::encode_string(&preprocesses.serialize())), + }) +} + +#[repr(C)] +pub struct ContinueSignRes { + machine: Box, + preprocess: OwnedString, +} + +#[no_mangle] +pub unsafe extern "C" fn continue_sign( + machine: Box, + preprocesses: *const StringView, + preprocesses_len: usize, +) -> CResult { + CResult::new(continue_sign_rust(machine, preprocesses, preprocesses_len)) +} + +fn continue_sign_rust( + machine: Box, + preprocesses: *const StringView, + preprocesses_len: usize, +) -> Result { + let preprocesses = unsafe { std::slice::from_raw_parts(preprocesses, preprocesses_len) }; + + let mut map = HashMap::new(); + for (i, preprocess) in preprocesses.iter().enumerate() { + let preprocess = preprocess.to_string().ok_or(INVALID_ENCODING_ERROR)?; + if preprocess.is_empty() { + continue; + } + map.insert( + Participant::new(u16::try_from(i + 1).map_err(|_| INVALID_PARTICIPANT_ERROR)?).unwrap(), + machine + .0 + .read_preprocess( + &mut Base64::decode_vec(&preprocess).map_err(|_| INVALID_ENCODING_ERROR)?.as_slice(), + ) + .map_err(|_| INVALID_ENCODING_ERROR)?, + ); + } + let (machine, share) = machine.0.sign(map, &[]).map_err(|_| INVALID_PREPROCESS_ERROR)?; + Ok(ContinueSignRes { + machine: TransactionSignatureMachineWrapper(machine).into(), + preprocess: OwnedString::new(Base64::encode_string(&share.serialize())), + }) +} + +#[no_mangle] +pub unsafe extern "C" fn complete_sign( + machine: Box, + shares: *const StringView, + shares_len: usize, +) -> CResult { + CResult::new(complete_sign_rust(machine, unsafe { + std::slice::from_raw_parts(shares, shares_len) + })) +} + +fn complete_sign_rust( + machine: Box, + shares: &[StringView], +) -> Result { + let mut map = HashMap::new(); + for (i, share) in shares.iter().enumerate() { + let share = share.to_string().ok_or(INVALID_ENCODING_ERROR)?; + if share.is_empty() { + continue; + } + map.insert( + Participant::new(u16::try_from(i + 1).map_err(|_| INVALID_PARTICIPANT_ERROR)?).unwrap(), + machine + .0 + .read_share(&mut Base64::decode_vec(&share).map_err(|_| INVALID_ENCODING_ERROR)?.as_slice()) + .map_err(|_| INVALID_ENCODING_ERROR)?, + ); + } + let tx = machine.0.complete(map).map_err(|_| INVALID_SHARE_ERROR)?; + let mut buf = Vec::with_capacity(1024); + tx.consensus_encode(&mut buf).unwrap(); + Ok(OwnedString::new(hex::encode(buf))) +} diff --git a/tests/no-std/Cargo.toml b/tests/no-std/Cargo.toml new file mode 100644 index 0000000..2180c8b --- /dev/null +++ b/tests/no-std/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "serai-no-std-tests" +version = "0.1.0" +description = "A crate to test no-std builds of Serai crates work" +license = "MIT" +repository = "https://github.com/kayabaNerve/serai/tree/develop/tests/no-std" +authors = ["Luke Parker "] +keywords = ["nostd", "no_std", "alloc"] +edition = "2021" +publish = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +zeroize = { version = "1", default-features = false, features = ["alloc"] } + +dkg = { path = "../../crypto/dkg", default-features = false } +# modular-frost = { path = "../../crypto/frost", default-features = false } + +bitcoin-serai = { path = "../../coins/bitcoin", default-features = false, features = ["hazmat"] } diff --git a/tests/no-std/LICENSE b/tests/no-std/LICENSE new file mode 100644 index 0000000..e6bff13 --- /dev/null +++ b/tests/no-std/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/no-std/README.md b/tests/no-std/README.md new file mode 100644 index 0000000..ac5e4e5 --- /dev/null +++ b/tests/no-std/README.md @@ -0,0 +1,3 @@ +# no-std tests + +A crate usable to test building various Serai crates in a no-std environment. diff --git a/tests/no-std/src/lib.rs b/tests/no-std/src/lib.rs new file mode 100644 index 0000000..f2b4a3b --- /dev/null +++ b/tests/no-std/src/lib.rs @@ -0,0 +1,8 @@ +#![no_std] + +pub use dkg; +/* +pub use modular_frost; +*/ + +pub use bitcoin_serai; From d3d048ed635a1fcea1c7889e235755c444fae06d Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 9 Sep 2026 10:34:13 -0600 Subject: [PATCH 3/6] chore: clean docs --- .github/workflows/release.yml | 12 ------------ README.md | 1 - 2 files changed, 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 927ea24..156bd9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,8 +14,6 @@ jobs: image: stackwallet/stackwallet-ci:latest steps: - uses: actions/checkout@v6 - with: - submodules: recursive - uses: Swatinem/rust-cache@v2 with: @@ -41,8 +39,6 @@ jobs: asset: frostdart-linux-aarch64.so steps: - uses: actions/checkout@v6 - with: - submodules: recursive - name: Add Rust target run: rustup +1.71.0 target add ${{ matrix.target }} @@ -82,8 +78,6 @@ jobs: abi: x86_64 steps: - uses: actions/checkout@v6 - with: - submodules: recursive - name: Add Rust target run: rustup +1.71.0 target add ${{ matrix.target }} @@ -115,8 +109,6 @@ jobs: image: stackwallet/stackwallet-ci:latest steps: - uses: actions/checkout@v6 - with: - submodules: recursive - name: Add Rust target run: rustup +1.71.0 target add x86_64-pc-windows-gnu @@ -142,8 +134,6 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v6 - with: - submodules: recursive - uses: dtolnay/rust-toolchain@master with: @@ -174,8 +164,6 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v6 - with: - submodules: recursive - uses: dtolnay/rust-toolchain@master with: diff --git a/README.md b/README.md index 842e6ce..85d3fe1 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ Dart interface to the Serai FROST multisig implementation ``` git clone https://www.github.com/cypherstack/frostdart cd frostdart -git submodule update --init --recursive cd scripts/[platform] ./build_all.sh ``` From aff2e04031d59b63c3f3fabb7dc53aa6a411fe25 Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 9 Sep 2026 13:31:46 -0600 Subject: [PATCH 4/6] simple test --- pubspec.yaml | 1 + test/keys_test.dart | 41 ++++++++++++++++++++++++++++++++ test/support/load_framework.dart | 18 ++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 test/keys_test.dart create mode 100644 test/support/load_framework.dart diff --git a/pubspec.yaml b/pubspec.yaml index 56e5696..acb0df7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: plugin_platform_interface: ^2.0.2 dev_dependencies: + test: ^1.26.3 ffigen: ^9.0.1 flutter_test: sdk: flutter diff --git a/test/keys_test.dart b/test/keys_test.dart new file mode 100644 index 0000000..373e93f --- /dev/null +++ b/test/keys_test.dart @@ -0,0 +1,41 @@ +import 'package:frostdart/frostdart.dart'; +import 'package:frostdart/frostdart_bindings_generated.dart'; +import 'package:frostdart/util.dart'; +import 'package:test/test.dart'; + +import 'support/load_framework.dart'; + +// Participant 1 from the Rust FROST secp256k1 vector: +// src/serai/crypto/frost/src/tests/literal/vectors/frost-secp256k1-sha256.json +// Serialized by ThresholdCore using vectors_to_multisig_keys in +// src/serai/crypto/frost/src/tests/vectors.rs; Rust checked the group public key. +const _keys = '09000000736563703235366b31020003000100' + '08f89ffe80ac94dcb920c26f3f46140bfc7f95b493f8310f5fc1ea2b01f4254c' + '026baee4bf7d4b9c4567dfff6f3c2c76df5c082e9320cd8187d6ab5965bc5a119a' + '03dacc9463e5186f3c81ae1b314f7b09001a22b28bb56ad0abd3f376818f9604ab' + '031404710e938032db0d4f6a4cd20ae37384be98ba9fe05b42d139361202b391e6'; + +void main() { + setUpAll(loadFramework); + + test('Rust vector keys survive a serialization round-trip', () { + expect(serializeKeys(keys: deserializeKeys(keys: _keys)), _keys); + }); + + test('Rust vector key metadata is preserved', () { + expect(getThresholdFromKeys(serializedKeys: _keys), 2); + expect(getParticipantsCountFromKeys(serializedKeys: _keys), 3); + expect(getParticipantIndexFromKeys(serializedKeys: _keys), 0); + }); + + test('invalid key encoding reports the native error', () { + expect( + () => deserializeKeys(keys: 'not hex'), + throwsA(isA().having( + (error) => error.errorCode, + 'errorCode', + INVALID_ENCODING_ERROR, + )), + ); + }); +} diff --git a/test/support/load_framework.dart b/test/support/load_framework.dart new file mode 100644 index 0000000..8f3d8ff --- /dev/null +++ b/test/support/load_framework.dart @@ -0,0 +1,18 @@ +import 'dart:ffi'; +import 'dart:io'; + +void loadFramework() { + if (!Platform.isMacOS) { + throw UnsupportedError('These baseline tests require macOS.'); + } + + final binary = Platform.environment['FROSTDART_FRAMEWORK_BINARY']; + + if (binary == null) { + throw StateError( + 'Set FROSTDART_FRAMEWORK_BINARY to the native binary inside the ' + 'macOS framework (for example, Frostdart.framework/Frostdart).', + ); + } + DynamicLibrary.open(File(binary).absolute.path); +} From e3062e88adb31c0086baf12b2533e0889bbc3672 Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 9 Sep 2026 13:33:27 -0600 Subject: [PATCH 5/6] native assets --- .github/workflows/release.yml | 215 +- .gitignore | 5 - .metadata | 34 +- CHANGELOG.md | 5 + README.md | 21 +- android/.gitignore | 9 - android/build.gradle | 70 - android/settings.gradle | 1 - android/src/main/AndroidManifest.xml | 3 - .../com/example/frostdart/FrostdartPlugin.kt | 35 - cargokit/.github/workflows/check_and_lint.yml | 26 - .../workflows/test_example_plugin_build.yml | 86 - cargokit/.gitignore | 4 - cargokit/LICENSE | 39 - cargokit/README | 8 - cargokit/build_pod.sh | 58 - cargokit/build_tool/README.md | 2 - cargokit/build_tool/analysis_options.yaml | 31 - cargokit/build_tool/bin/build_tool.dart | 5 - cargokit/build_tool/lib/build_tool.dart | 5 - .../lib/src/android_environment.dart | 192 -- .../lib/src/artifacts_provider.dart | 263 --- cargokit/build_tool/lib/src/build_cmake.dart | 37 - cargokit/build_tool/lib/src/build_gradle.dart | 46 - cargokit/build_tool/lib/src/build_pod.dart | 86 - cargokit/build_tool/lib/src/build_tool.dart | 268 --- cargokit/build_tool/lib/src/builder.dart | 195 -- cargokit/build_tool/lib/src/cargo.dart | 45 - cargokit/build_tool/lib/src/crate_hash.dart | 121 -- cargokit/build_tool/lib/src/environment.dart | 65 - cargokit/build_tool/lib/src/logging.dart | 49 - cargokit/build_tool/lib/src/options.dart | 306 --- .../lib/src/precompile_binaries.dart | 199 -- cargokit/build_tool/lib/src/rustup.dart | 133 -- cargokit/build_tool/lib/src/target.dart | 137 -- cargokit/build_tool/lib/src/util.dart | 169 -- .../build_tool/lib/src/verify_binaries.dart | 81 - cargokit/build_tool/pubspec.lock | 453 ---- cargokit/build_tool/pubspec.yaml | 30 - cargokit/build_tool/test/builder_test.dart | 28 - cargokit/build_tool/test/cargo_test.dart | 28 - cargokit/build_tool/test/options_test.dart | 75 - cargokit/build_tool/test/rustup_test.dart | 66 - cargokit/cmake/cargokit.cmake | 99 - cargokit/cmake/resolve_symlinks.ps1 | 27 - cargokit/docs/architecture.md | 104 - cargokit/docs/precompiled_binaries.md | 95 - cargokit/gradle/plugin.gradle | 176 -- cargokit/run_build_tool.cmd | 91 - cargokit/run_build_tool.sh | 99 - example/analysis_options.yaml | 8 + example/android/app/build.gradle | 72 - example/android/app/build.gradle.kts | 49 + .../android/app/src/debug/AndroidManifest.xml | 3 +- .../android/app/src/main/AndroidManifest.xml | 3 +- .../app/src/profile/AndroidManifest.xml | 3 +- example/android/build.gradle | 31 - example/android/build.gradle.kts | 24 + example/android/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 11 - example/android/settings.gradle.kts | 26 + example/ios/Flutter/AppFrameworkInfo.plist | 2 - example/ios/Podfile | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 110 +- .../xcshareddata/xcschemes/Runner.xcscheme | 3 + example/ios/Runner/AppDelegate.swift | 9 +- example/ios/Runner/Info.plist | 29 +- example/linux/CMakeLists.txt | 2 +- example/linux/flutter/generated_plugins.cmake | 1 - example/macos/Podfile | 2 +- example/macos/Podfile.lock | 22 - .../macos/Runner.xcodeproj/project.pbxproj | 101 +- example/pubspec.lock | 92 +- example/pubspec.yaml | 2 +- example/windows/CMakeLists.txt | 3 + .../windows/flutter/generated_plugins.cmake | 1 - ffigen.yaml | 8 +- hook/build.dart | 167 ++ ios/Classes/frostdart.c | 1 - ios/frostdart.podspec | 40 - lib/frostdart.dart | 125 +- lib/frostdart_bindings_generated.dart | 1834 +++++++---------- linux/CMakeLists.txt | 153 -- linux/frostdart_plugin.cc | 70 - linux/include/frostdart/frostdart_plugin.h | 26 - macos/frostdart.podspec | 25 - pubspec.yaml | 77 +- scripts/android/.cargo/config | 2 - scripts/android/build_all.sh | 60 - scripts/android/download.sh | 38 - scripts/ios/build_all.sh | 19 - scripts/ios/download.sh | 28 - scripts/linux/build_all.sh | 18 - scripts/linux/download.sh | 29 - scripts/linux/regen_bindings.sh | 5 - scripts/macos/build_all.sh | 5 - scripts/macos/build_macos.dart | 178 -- scripts/macos/download.sh | 30 - scripts/windows/build_all.bat | 21 - scripts/windows/build_all.sh | 23 - scripts/windows/download.sh | 29 - scripts/windows/regen_bindings.bat | 6 - src/CMakeLists.txt | 153 -- test/keys_test.dart | 16 +- test/support/load_framework.dart | 18 - windows/.gitignore | 17 - windows/CMakeLists.txt | 30 - 108 files changed, 1213 insertions(+), 7080 deletions(-) delete mode 100644 android/.gitignore delete mode 100644 android/build.gradle delete mode 100644 android/settings.gradle delete mode 100644 android/src/main/AndroidManifest.xml delete mode 100644 android/src/main/kotlin/com/example/frostdart/FrostdartPlugin.kt delete mode 100644 cargokit/.github/workflows/check_and_lint.yml delete mode 100644 cargokit/.github/workflows/test_example_plugin_build.yml delete mode 100644 cargokit/.gitignore delete mode 100644 cargokit/LICENSE delete mode 100644 cargokit/README delete mode 100755 cargokit/build_pod.sh delete mode 100644 cargokit/build_tool/README.md delete mode 100644 cargokit/build_tool/analysis_options.yaml delete mode 100644 cargokit/build_tool/bin/build_tool.dart delete mode 100644 cargokit/build_tool/lib/build_tool.dart delete mode 100644 cargokit/build_tool/lib/src/android_environment.dart delete mode 100644 cargokit/build_tool/lib/src/artifacts_provider.dart delete mode 100644 cargokit/build_tool/lib/src/build_cmake.dart delete mode 100644 cargokit/build_tool/lib/src/build_gradle.dart delete mode 100644 cargokit/build_tool/lib/src/build_pod.dart delete mode 100644 cargokit/build_tool/lib/src/build_tool.dart delete mode 100644 cargokit/build_tool/lib/src/builder.dart delete mode 100644 cargokit/build_tool/lib/src/cargo.dart delete mode 100644 cargokit/build_tool/lib/src/crate_hash.dart delete mode 100644 cargokit/build_tool/lib/src/environment.dart delete mode 100644 cargokit/build_tool/lib/src/logging.dart delete mode 100644 cargokit/build_tool/lib/src/options.dart delete mode 100644 cargokit/build_tool/lib/src/precompile_binaries.dart delete mode 100644 cargokit/build_tool/lib/src/rustup.dart delete mode 100644 cargokit/build_tool/lib/src/target.dart delete mode 100644 cargokit/build_tool/lib/src/util.dart delete mode 100644 cargokit/build_tool/lib/src/verify_binaries.dart delete mode 100644 cargokit/build_tool/pubspec.lock delete mode 100644 cargokit/build_tool/pubspec.yaml delete mode 100644 cargokit/build_tool/test/builder_test.dart delete mode 100644 cargokit/build_tool/test/cargo_test.dart delete mode 100644 cargokit/build_tool/test/options_test.dart delete mode 100644 cargokit/build_tool/test/rustup_test.dart delete mode 100644 cargokit/cmake/cargokit.cmake delete mode 100644 cargokit/cmake/resolve_symlinks.ps1 delete mode 100644 cargokit/docs/architecture.md delete mode 100644 cargokit/docs/precompiled_binaries.md delete mode 100644 cargokit/gradle/plugin.gradle delete mode 100644 cargokit/run_build_tool.cmd delete mode 100755 cargokit/run_build_tool.sh delete mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/build.gradle.kts delete mode 100644 example/android/build.gradle create mode 100644 example/android/build.gradle.kts delete mode 100644 example/android/settings.gradle create mode 100644 example/android/settings.gradle.kts delete mode 100644 example/macos/Podfile.lock create mode 100644 hook/build.dart delete mode 100644 ios/Classes/frostdart.c delete mode 100644 ios/frostdart.podspec delete mode 100644 linux/CMakeLists.txt delete mode 100644 linux/frostdart_plugin.cc delete mode 100644 linux/include/frostdart/frostdart_plugin.h delete mode 100644 macos/frostdart.podspec delete mode 100644 scripts/android/.cargo/config delete mode 100755 scripts/android/build_all.sh delete mode 100755 scripts/android/download.sh delete mode 100755 scripts/ios/build_all.sh delete mode 100755 scripts/ios/download.sh delete mode 100755 scripts/linux/build_all.sh delete mode 100755 scripts/linux/download.sh delete mode 100755 scripts/linux/regen_bindings.sh delete mode 100755 scripts/macos/build_all.sh delete mode 100644 scripts/macos/build_macos.dart delete mode 100755 scripts/macos/download.sh delete mode 100644 scripts/windows/build_all.bat delete mode 100644 scripts/windows/build_all.sh delete mode 100755 scripts/windows/download.sh delete mode 100644 scripts/windows/regen_bindings.bat delete mode 100644 src/CMakeLists.txt delete mode 100644 test/support/load_framework.dart delete mode 100644 windows/.gitignore delete mode 100644 windows/CMakeLists.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 156bd9c..90db335 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,211 +1,26 @@ -name: Build and Release +name: Test native assets on: push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' + pull_request: workflow_dispatch: jobs: - test: - runs-on: ubuntu-24.04 - container: - image: stackwallet/stackwallet-ci:latest - steps: - - uses: actions/checkout@v6 - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: src/serai - key: test - - - name: Run tests - run: | - cd src/serai - cargo +1.71.0 test --workspace --exclude bitcoin-serai - - build-linux: - needs: test - runs-on: ubuntu-24.04 - container: - image: stackwallet/stackwallet-ci:latest - strategy: - matrix: - include: - - target: x86_64-unknown-linux-gnu - asset: frostdart-linux-x86_64.so - - target: aarch64-unknown-linux-gnu - asset: frostdart-linux-aarch64.so - steps: - - uses: actions/checkout@v6 - - - name: Add Rust target - run: rustup +1.71.0 target add ${{ matrix.target }} - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: src/serai - key: linux-${{ matrix.target }} - - - name: Build - env: - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - run: | - cd src/serai/hrf - cargo +1.71.0 build --target ${{ matrix.target }} --release --lib - cp ../target/${{ matrix.target }}/release/libhrf_api.so \ - ../../../${{ matrix.asset }} - - - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.asset }} - path: ${{ matrix.asset }} - - build-android: - needs: test - runs-on: ubuntu-24.04 - container: - image: stackwallet/stackwallet-ci:latest strategy: matrix: - include: - - target: aarch64-linux-android - abi: arm64-v8a - - target: armv7-linux-androideabi - abi: armeabi-v7a - - target: x86_64-linux-android - abi: x86_64 + os: [ubuntu-24.04, ubuntu-24.04-arm, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 - - - name: Add Rust target - run: rustup +1.71.0 target add ${{ matrix.target }} - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: src/serai - key: android-${{ matrix.abi }} - - - name: Build - env: - ANDROID_NDK_ROOT: /opt/android-ndk-r28 - run: | - cd src/serai/hrf - sed -i 's/\[dependencies\]/[dependencies]\nopenssl = { version = "<=0.10.78", features = ["vendored"] }\nopenssl-sys = "<=0.9.114"/' Cargo.toml - cargo +1.71.0 ndk --target ${{ matrix.target }} --platform 21 build --release - cp ../target/${{ matrix.target }}/release/libhrf_api.so \ - ../../../frostdart-android-${{ matrix.abi }}.so - - - uses: actions/upload-artifact@v4 - with: - name: frostdart-android-${{ matrix.abi }}.so - path: frostdart-android-${{ matrix.abi }}.so - - build-windows: - needs: test - runs-on: ubuntu-24.04 - container: - image: stackwallet/stackwallet-ci:latest - steps: - - uses: actions/checkout@v6 - - - name: Add Rust target - run: rustup +1.71.0 target add x86_64-pc-windows-gnu - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: src/serai - - - name: Build - run: | - cd src/serai/hrf - cargo +1.71.0 build --target x86_64-pc-windows-gnu --release --lib - cp ../target/x86_64-pc-windows-gnu/release/hrf_api.dll \ - ../../../frostdart-windows-x86_64.dll - - - uses: actions/upload-artifact@v4 - with: - name: frostdart-windows-x86_64.dll - path: frostdart-windows-x86_64.dll - - build-ios: - needs: test - runs-on: macos-latest - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: '1.71.0' - targets: aarch64-apple-ios - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: src/serai - - - name: Build - run: | - cd src/serai/hrf - sed -i '' 's/name = "hrf-api"/name = "frostdart"/' Cargo.toml - export IPHONEOS_DEPLOYMENT_TARGET=15.0 - export RUSTFLAGS="-C link-arg=-mios-version-min=15.0" - cargo build --target aarch64-apple-ios --release --lib - cp ../target/aarch64-apple-ios/release/libfrostdart.a \ - ../../../frostdart-ios-aarch64.a - - - uses: actions/upload-artifact@v4 - with: - name: frostdart-ios-aarch64.a - path: frostdart-ios-aarch64.a - - build-macos: - needs: test - runs-on: macos-latest - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@master - with: - toolchain: '1.71.0' - targets: aarch64-apple-darwin - - - uses: Swatinem/rust-cache@v2 - with: - workspaces: src/serai - - - name: Build - run: | - cd src/serai/hrf - cargo build --target aarch64-apple-darwin --release --lib - xcodebuild -create-xcframework \ - -library ../target/aarch64-apple-darwin/release/libhrf_api.dylib \ - -output ../../../frostdart.xcframework - cd ../../.. - zip -r frostdart.xcframework.zip frostdart.xcframework - - - uses: actions/upload-artifact@v4 - with: - name: frostdart.xcframework.zip - path: frostdart.xcframework.zip - - release: - needs: [build-linux, build-android, build-ios, build-macos, build-windows] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/download-artifact@v4 - with: - path: artifacts - merge-multiple: true - - - name: Generate checksums - run: | - cd artifacts - sha256sum * > checksums.txt - - - uses: softprops/action-gh-release@v2 - with: - generate_release_notes: true - files: artifacts/* + - uses: dart-lang/setup-dart@v1 + with: + sdk: '3.13.0' + - uses: dtolnay/rust-toolchain@1.90.0 + - run: dart pub get --no-example + - run: dart analyze lib test hook + - run: dart test + - name: Rust tests + if: matrix.os == 'ubuntu-24.04' + working-directory: src/serai/hrf + run: cargo test --locked --workspace --exclude bitcoin-serai diff --git a/.gitignore b/.gitignore index 7474eca..37e1713 100644 --- a/.gitignore +++ b/.gitignore @@ -31,8 +31,3 @@ build/ .flutter-plugins .flutter-plugins-dependencies - -android/src/main/jniLibs/ - -/macos/frostdart.xcframework/ -/ios/libfrostdart.a diff --git a/.metadata b/.metadata index 75415e5..cdfc2c1 100644 --- a/.metadata +++ b/.metadata @@ -7,36 +7,4 @@ version: revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 channel: stable -project_type: plugin_ffi - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - - platform: android - create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - - platform: ios - create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - - platform: linux - create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - - platform: macos - create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - - platform: windows - create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' +project_type: package_ffi diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc7d8..8262515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.0 + +* Build Rust automatically with native assets (Dart 3.13+, Rust 1.90.0). +* Replace `FrostdartBindings` with generated `@Native` functions; public wrappers remain available. + ## 0.0.1 * TODO: Describe initial release. diff --git a/README.md b/README.md index 85d3fe1..a78864f 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,19 @@ # frostdart -Dart interface to the Serai FROST multisig implementation +Dart bindings for the Serai FROST multisig implementation. Rust sources live in +`src/serai`; native assets build and bundle them automatically. -## Building +Requires Dart 3.13+ (or a Flutter SDK containing it), Rust installed with rustup, +and the platform's C build tools: Xcode for Apple, the Android NDK, a Linux C +compiler, or Visual Studio's C++ build tools on Windows. Put `rustup` on PATH. +The hook locates the pinned Rust 1.90.0 tools and installs the target as needed. -``` -git clone https://www.github.com/cypherstack/frostdart -cd frostdart -cd scripts/[platform] -./build_all.sh +```sh +dart pub get --no-example +dart test ``` -or `build_all.bat` for Windows. +Consuming Flutter apps use normal `flutter run` / `flutter build` commands. +The example requires iOS 15+ or macOS 13+ for Apple builds. -Rust 1.71.0 or newer is required. `[rustup](https://rustup.rs/) toolchain install 1.71.0-x86_64-pc-windows-msvc` for Windows. +Regenerate bindings with `dart run ffigen --config ffigen.yaml` (requires libclang). diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 161bdcd..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.cxx diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index 6409a9e..0000000 --- a/android/build.gradle +++ /dev/null @@ -1,70 +0,0 @@ -// The Android Gradle Plugin builds the native code with the Android NDK. - -group 'com.example.frostdart' -version '1.0' - -buildscript { - ext.kotlin_version = "2.2.20" - repositories { - google() - mavenCentral() - } - - dependencies { - classpath("com.android.tools.build:gradle:8.7.0") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: "com.android.library" -apply plugin: "kotlin-android" - -android { - if (project.android.hasProperty("namespace")) { - namespace = "com.example.frostdart" - } - - compileSdk = 34 - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17 - } - - sourceSets { - main.java.srcDirs += "src/main/kotlin" - test.java.srcDirs += "src/test/kotlin" - } - - defaultConfig { - minSdk = 21 - } - - dependencies { - testImplementation("org.jetbrains.kotlin:kotlin-test") - testImplementation("org.mockito:mockito-core:5.0.0") - } - - testOptions { - unitTests.all { - useJUnitPlatform() - - testLogging { - events "passed", "skipped", "failed", "standardOut", "standardError" - outputs.upToDateWhen {false} - showStandardStreams = true - } - } - } -} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index e69ec59..0000000 --- a/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'frostdart' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml deleted file mode 100644 index f5b72dd..0000000 --- a/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/android/src/main/kotlin/com/example/frostdart/FrostdartPlugin.kt b/android/src/main/kotlin/com/example/frostdart/FrostdartPlugin.kt deleted file mode 100644 index cf19261..0000000 --- a/android/src/main/kotlin/com/example/frostdart/FrostdartPlugin.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.flutter_libepiccash - -import androidx.annotation.NonNull - -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result - -/** FlutterLibepiccashPlugin */ -class FrostdartPlugin: FlutterPlugin, MethodCallHandler { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel : MethodChannel - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "frostdart") - channel.setMethodCallHandler(this) - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - if (call.method == "getPlatformVersion") { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else { - result.notImplemented() - } - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } -} diff --git a/cargokit/.github/workflows/check_and_lint.yml b/cargokit/.github/workflows/check_and_lint.yml deleted file mode 100644 index d8979f0..0000000 --- a/cargokit/.github/workflows/check_and_lint.yml +++ /dev/null @@ -1,26 +0,0 @@ -on: - pull_request: - push: - branches: - - main - -name: Check and Lint - -jobs: - Flutter: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # 4.1.0 - - uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # 2.16.0 - - name: Pub Get - run: dart pub get --no-precompile - working-directory: build_tool - - name: Dart Format - run: dart format . --output=none --set-exit-if-changed - working-directory: build_tool - - name: Analyze - run: dart analyze - working-directory: build_tool - - name: Test - run: flutter test - working-directory: build_tool diff --git a/cargokit/.github/workflows/test_example_plugin_build.yml b/cargokit/.github/workflows/test_example_plugin_build.yml deleted file mode 100644 index 698ea7e..0000000 --- a/cargokit/.github/workflows/test_example_plugin_build.yml +++ /dev/null @@ -1,86 +0,0 @@ -on: - pull_request: - push: - branches: - - main - -name: Test Example Plugin - -jobs: - Build: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - macOS-latest - - windows-latest - build_mode: - - debug - - profile - - release - env: - EXAMPLE_DIR: "a b/hello_rust_ffi_plugin/example" - CARGOKIT_VERBOSE: 1 - steps: - - name: Extract branch name - shell: bash - run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT - id: extract_branch - - name: Setup Repository - shell: bash - run: | - mkdir "a b" # Space is intentional - cd "a b" - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - # "advanced" branch has extra iOS flavor and uses rust nightly for release builds - git clone -b advanced https://github.com/irondash/hello_rust_ffi_plugin - cd hello_rust_ffi_plugin - git subtree pull --prefix cargokit https://github.com/${{ github.event.pull_request.head.repo.full_name || github.repository }} ${{ steps.extract_branch.outputs.branch }} --squash - - uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # 2.16.0 - with: - channel: "stable" - - name: Install GTK - if: (matrix.os == 'ubuntu-latest') - run: sudo apt-get update && sudo apt-get install libgtk-3-dev - - name: Install ninja-build - if: (matrix.os == 'ubuntu-latest') - run: sudo apt-get update && sudo apt-get install ninja-build - - name: Build Linux (${{ matrix.build_mode }}) - if: matrix.os == 'ubuntu-latest' - shell: bash - working-directory: ${{ env.EXAMPLE_DIR }} - run: flutter build linux --${{ matrix.build_mode }} -v - - name: Build macOS (${{ matrix.build_mode }}) - if: matrix.os == 'macos-latest' - shell: bash - working-directory: ${{ env.EXAMPLE_DIR }} - run: flutter build macos --${{ matrix.build_mode }} -v - - name: Build iOS (${{ matrix.build_mode }}) - if: matrix.os == 'macos-latest' - shell: bash - working-directory: ${{ env.EXAMPLE_DIR }} - run: flutter build ios --${{ matrix.build_mode }} --no-codesign -v - - name: Build iOS (${{ matrix.build_mode }}) - flavor1 - if: matrix.os == 'macos-latest' - shell: bash - working-directory: ${{ env.EXAMPLE_DIR }} - run: flutter build ios --flavor flavor1 --${{ matrix.build_mode }} --no-codesign -v - - name: Build Windows (${{ matrix.build_mode }}) - if: matrix.os == 'windows-latest' - shell: bash - working-directory: ${{ env.EXAMPLE_DIR }} - run: flutter build windows --${{ matrix.build_mode }} -v - - name: Build Android (${{ matrix.build_mode }}) - shell: bash - working-directory: ${{ env.EXAMPLE_DIR }} - run: | - if [[ $(sysctl hw.optional.arm64) == *"hw.optional.arm64: 1"* ]]; then - export JAVA_HOME=$JAVA_HOME_17_arm64 - else - export JAVA_HOME=$JAVA_HOME_11_X64 - fi - flutter build apk --${{ matrix.build_mode }} -v - diff --git a/cargokit/.gitignore b/cargokit/.gitignore deleted file mode 100644 index cf7bb86..0000000 --- a/cargokit/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -target -.dart_tool -*.iml -!pubspec.lock diff --git a/cargokit/LICENSE b/cargokit/LICENSE deleted file mode 100644 index 54a7d58..0000000 --- a/cargokit/LICENSE +++ /dev/null @@ -1,39 +0,0 @@ -Copyright 2022 Matej Knopp - -================================================================================ - -MIT LICENSE - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -================================================================================ - -APACHE LICENSE, VERSION 2.0 - -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. - diff --git a/cargokit/README b/cargokit/README deleted file mode 100644 index 8ae4a07..0000000 --- a/cargokit/README +++ /dev/null @@ -1,8 +0,0 @@ -Experimental repository to provide glue for seamlessly integrating cargo build -with flutter plugins and packages. - -See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/ -for a tutorial on how to use Cargokit. - -Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin. - diff --git a/cargokit/build_pod.sh b/cargokit/build_pod.sh deleted file mode 100755 index ed0e0d9..0000000 --- a/cargokit/build_pod.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/sh -set -e - -BASEDIR=$(dirname "$0") - -# Workaround for https://github.com/dart-lang/pub/issues/4010 -BASEDIR=$(cd "$BASEDIR" ; pwd -P) - -# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project -NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"` - -export PATH=${NEW_PATH%?} # remove trailing : - -env - -# Platform name (macosx, iphoneos, iphonesimulator) -export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME - -# Arctive architectures (arm64, armv7, x86_64), space separated. -export CARGOKIT_DARWIN_ARCHS=$ARCHS - -# Current build configuration (Debug, Release) -export CARGOKIT_CONFIGURATION=$CONFIGURATION - -# Path to directory containing Cargo.toml. -export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1 - -# Temporary directory for build artifacts. -export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR - -# Output directory for final artifacts. -export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME - -# Directory to store built tool artifacts. -export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool - -# Directory inside root project. Not necessarily the top level directory of root project. -export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT - -FLUTTER_EXPORT_BUILD_ENVIRONMENT=( - "$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS - "$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS -) - -for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}" -do - if [[ -f "$path" ]]; then - source "$path" - fi -done - -sh "$BASEDIR/run_build_tool.sh" build-pod "$@" - -# Make a symlink from built framework to phony file, which will be used as input to -# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate -# attribute on custom build phase) -ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony" -ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out" diff --git a/cargokit/build_tool/README.md b/cargokit/build_tool/README.md deleted file mode 100644 index 3816eca..0000000 --- a/cargokit/build_tool/README.md +++ /dev/null @@ -1,2 +0,0 @@ -A sample command-line application with an entrypoint in `bin/`, library code -in `lib/`, and example unit test in `test/`. diff --git a/cargokit/build_tool/analysis_options.yaml b/cargokit/build_tool/analysis_options.yaml deleted file mode 100644 index a1aad5b..0000000 --- a/cargokit/build_tool/analysis_options.yaml +++ /dev/null @@ -1,31 +0,0 @@ -# This file configures the static analysis results for your project (errors, -# warnings, and lints). -# -# This enables the 'recommended' set of lints from `package:lints`. -# This set helps identify many issues that may lead to problems when running -# or consuming Dart code, and enforces writing Dart using a single, idiomatic -# style and format. -# -# If you want a smaller set of lints you can change this to specify -# 'package:lints/core.yaml'. These are just the most critical lints -# (the recommended set includes the core lints). -# The core lints are also what is used by pub.dev for scoring packages. - -include: package:lints/recommended.yaml - -# Uncomment the following section to specify additional rules. - -linter: - rules: - - prefer_relative_imports - - directives_ordering - -# analyzer: -# exclude: -# - path/to/excluded/files/** - -# For more information about the core and recommended set of lints, see -# https://dart.dev/go/core-lints - -# For additional information about configuring this file, see -# https://dart.dev/guides/language/analysis-options diff --git a/cargokit/build_tool/bin/build_tool.dart b/cargokit/build_tool/bin/build_tool.dart deleted file mode 100644 index f27ec75..0000000 --- a/cargokit/build_tool/bin/build_tool.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:build_tool/build_tool.dart' as build_tool; - -void main(List arguments) { - build_tool.runMain(arguments); -} diff --git a/cargokit/build_tool/lib/build_tool.dart b/cargokit/build_tool/lib/build_tool.dart deleted file mode 100644 index b329c01..0000000 --- a/cargokit/build_tool/lib/build_tool.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'src/build_tool.dart' as build_tool; - -Future runMain(List args) async { - return build_tool.runMain(args); -} diff --git a/cargokit/build_tool/lib/src/android_environment.dart b/cargokit/build_tool/lib/src/android_environment.dart deleted file mode 100644 index 9342964..0000000 --- a/cargokit/build_tool/lib/src/android_environment.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:io'; -import 'dart:isolate'; -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:path/path.dart' as path; -import 'package:version/version.dart'; - -import 'target.dart'; -import 'util.dart'; - -class AndroidEnvironment { - AndroidEnvironment({ - required this.sdkPath, - required this.ndkVersion, - required this.minSdkVersion, - required this.targetTempDir, - required this.target, - }); - - static void clangLinkerWrapper(List args) { - final clang = Platform.environment['_CARGOKIT_NDK_LINK_CLANG']; - if (clang == null) { - throw Exception( - "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_CLANG env var"); - } - final target = Platform.environment['_CARGOKIT_NDK_LINK_TARGET']; - if (target == null) { - throw Exception( - "cargo-ndk rustc linker: didn't find _CARGOKIT_NDK_LINK_TARGET env var"); - } - - runCommand(clang, [ - target, - ...args, - ]); - } - - /// Full path to Android SDK. - final String sdkPath; - - /// Full version of Android NDK. - final String ndkVersion; - - /// Minimum supported SDK version. - final int minSdkVersion; - - /// Target directory for build artifacts. - final String targetTempDir; - - /// Target being built. - final Target target; - - bool ndkIsInstalled() { - final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); - final ndkPackageXml = File(path.join(ndkPath, 'package.xml')); - return ndkPackageXml.existsSync(); - } - - void installNdk({ - required String javaHome, - }) { - final sdkManagerExtension = Platform.isWindows ? '.bat' : ''; - final sdkManager = path.join( - sdkPath, - 'cmdline-tools', - 'latest', - 'bin', - 'sdkmanager$sdkManagerExtension', - ); - - log.info('Installing NDK $ndkVersion'); - runCommand(sdkManager, [ - '--install', - 'ndk;$ndkVersion', - ], environment: { - 'JAVA_HOME': javaHome, - }); - } - - Future> buildEnvironment() async { - final hostArch = Platform.isMacOS - ? "darwin-x86_64" - : (Platform.isLinux ? "linux-x86_64" : "windows-x86_64"); - - final ndkPath = path.join(sdkPath, 'ndk', ndkVersion); - final toolchainPath = path.join( - ndkPath, - 'toolchains', - 'llvm', - 'prebuilt', - hostArch, - 'bin', - ); - - final minSdkVersion = - math.max(target.androidMinSdkVersion!, this.minSdkVersion); - - final exe = Platform.isWindows ? '.exe' : ''; - - final arKey = 'AR_${target.rust}'; - final arValue = ['${target.rust}-ar', 'llvm-ar', 'llvm-ar.exe'] - .map((e) => path.join(toolchainPath, e)) - .firstWhereOrNull((element) => File(element).existsSync()); - if (arValue == null) { - throw Exception('Failed to find ar for $target in $toolchainPath'); - } - - final targetArg = '--target=${target.rust}$minSdkVersion'; - - final ccKey = 'CC_${target.rust}'; - final ccValue = path.join(toolchainPath, 'clang$exe'); - final cfFlagsKey = 'CFLAGS_${target.rust}'; - final cFlagsValue = targetArg; - - final cxxKey = 'CXX_${target.rust}'; - final cxxValue = path.join(toolchainPath, 'clang++$exe'); - final cxxFlagsKey = 'CXXFLAGS_${target.rust}'; - final cxxFlagsValue = targetArg; - - final linkerKey = - 'cargo_target_${target.rust.replaceAll('-', '_')}_linker'.toUpperCase(); - - final ranlibKey = 'RANLIB_${target.rust}'; - final ranlibValue = path.join(toolchainPath, 'llvm-ranlib$exe'); - - final ndkVersionParsed = Version.parse(ndkVersion); - final rustFlagsKey = 'CARGO_ENCODED_RUSTFLAGS'; - final rustFlagsValue = _libGccWorkaround(targetTempDir, ndkVersionParsed); - - final runRustTool = - Platform.isWindows ? 'run_build_tool.cmd' : 'run_build_tool.sh'; - - final packagePath = (await Isolate.resolvePackageUri( - Uri.parse('package:build_tool/buildtool.dart')))! - .toFilePath(); - final selfPath = path.canonicalize(path.join( - packagePath, - '..', - '..', - '..', - runRustTool, - )); - - // Make sure that run_build_tool is working properly even initially launched directly - // through dart run. - final toolTempDir = - Platform.environment['CARGOKIT_TOOL_TEMP_DIR'] ?? targetTempDir; - - return { - arKey: arValue, - ccKey: ccValue, - cfFlagsKey: cFlagsValue, - cxxKey: cxxValue, - cxxFlagsKey: cxxFlagsValue, - ranlibKey: ranlibValue, - rustFlagsKey: rustFlagsValue, - linkerKey: selfPath, - // Recognized by main() so we know when we're acting as a wrapper - '_CARGOKIT_NDK_LINK_TARGET': targetArg, - '_CARGOKIT_NDK_LINK_CLANG': ccValue, - 'CARGOKIT_TOOL_TEMP_DIR': toolTempDir, - }; - } - - // Workaround for libgcc missing in NDK23, inspired by cargo-ndk - String _libGccWorkaround(String buildDir, Version ndkVersion) { - final workaroundDir = path.join( - buildDir, - 'cargokit', - 'libgcc_workaround', - '${ndkVersion.major}', - ); - Directory(workaroundDir).createSync(recursive: true); - if (ndkVersion.major >= 23) { - File(path.join(workaroundDir, 'libgcc.a')) - .writeAsStringSync('INPUT(-lunwind)'); - } else { - // Other way around, untested, forward libgcc.a from libunwind once Rust - // gets updated for NDK23+. - File(path.join(workaroundDir, 'libunwind.a')) - .writeAsStringSync('INPUT(-lgcc)'); - } - - var rustFlags = Platform.environment['CARGO_ENCODED_RUSTFLAGS'] ?? ''; - if (rustFlags.isNotEmpty) { - rustFlags = '$rustFlags\x1f'; - } - rustFlags = '$rustFlags-L\x1f$workaroundDir'; - return rustFlags; - } -} diff --git a/cargokit/build_tool/lib/src/artifacts_provider.dart b/cargokit/build_tool/lib/src/artifacts_provider.dart deleted file mode 100644 index ef655a9..0000000 --- a/cargokit/build_tool/lib/src/artifacts_provider.dart +++ /dev/null @@ -1,263 +0,0 @@ -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:http/http.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'builder.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'rustup.dart'; -import 'target.dart'; - -class Artifact { - /// File system location of the artifact. - final String path; - - /// Actual file name that the artifact should have in destination folder. - final String finalFileName; - - AritifactType get type { - if (finalFileName.endsWith('.dll') || - finalFileName.endsWith('.dll.lib') || - finalFileName.endsWith('.pdb') || - finalFileName.endsWith('.so') || - finalFileName.endsWith('.dylib')) { - return AritifactType.dylib; - } else if (finalFileName.endsWith('.lib') || finalFileName.endsWith('.a')) { - return AritifactType.staticlib; - } else { - throw Exception('Unknown artifact type for $finalFileName'); - } - } - - Artifact({ - required this.path, - required this.finalFileName, - }); -} - -final _log = Logger('artifacts_provider'); - -class ArtifactProvider { - ArtifactProvider({ - required this.environment, - required this.userOptions, - }); - - final BuildEnvironment environment; - final CargokitUserOptions userOptions; - - Future>> getArtifacts(List targets) async { - final result = await _getPrecompiledArtifacts(targets); - - final pendingTargets = List.of(targets); - pendingTargets.removeWhere((element) => result.containsKey(element)); - - if (pendingTargets.isEmpty) { - return result; - } - - final rustup = Rustup(); - for (final target in targets) { - final builder = RustBuilder(target: target, environment: environment); - builder.prepare(rustup); - _log.info('Building ${environment.crateInfo.packageName} for $target'); - final targetDir = await builder.build(); - // For local build accept both static and dynamic libraries. - final artifactNames = { - ...getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - aritifactType: AritifactType.dylib, - remote: false, - ), - ...getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - aritifactType: AritifactType.staticlib, - remote: false, - ) - }; - final artifacts = artifactNames - .map((artifactName) => Artifact( - path: path.join(targetDir, artifactName), - finalFileName: artifactName, - )) - .where((element) => File(element.path).existsSync()) - .toList(); - result[target] = artifacts; - } - return result; - } - - Future>> _getPrecompiledArtifacts( - List targets) async { - if (userOptions.usePrecompiledBinaries == false) { - _log.info('Precompiled binaries are disabled'); - return {}; - } - if (environment.crateOptions.precompiledBinaries == null) { - _log.fine('Precompiled binaries not enabled for this crate'); - return {}; - } - - final start = Stopwatch()..start(); - final crateHash = CrateHash.compute(environment.manifestDir, - tempStorage: environment.targetTempDir); - _log.fine( - 'Computed crate hash $crateHash in ${start.elapsedMilliseconds}ms'); - - final downloadedArtifactsDir = - path.join(environment.targetTempDir, 'precompiled', crateHash); - Directory(downloadedArtifactsDir).createSync(recursive: true); - - final res = >{}; - - for (final target in targets) { - final requiredArtifacts = getArtifactNames( - target: target, - libraryName: environment.crateInfo.packageName, - remote: true, - ); - final artifactsForTarget = []; - - for (final artifact in requiredArtifacts) { - final fileName = PrecompileBinaries.fileName(target, artifact); - final downloadedPath = path.join(downloadedArtifactsDir, fileName); - if (!File(downloadedPath).existsSync()) { - final signatureFileName = - PrecompileBinaries.signatureFileName(target, artifact); - await _tryDownloadArtifacts( - crateHash: crateHash, - fileName: fileName, - signatureFileName: signatureFileName, - finalPath: downloadedPath, - ); - } - if (File(downloadedPath).existsSync()) { - artifactsForTarget.add(Artifact( - path: downloadedPath, - finalFileName: artifact, - )); - } else { - break; - } - } - - // Only provide complete set of artifacts. - if (artifactsForTarget.length == requiredArtifacts.length) { - _log.fine('Found precompiled artifacts for $target'); - res[target] = artifactsForTarget; - } - } - - return res; - } - - static Future _get(Uri url, {Map? headers}) async { - int attempt = 0; - const maxAttempts = 10; - while (true) { - try { - return await get(url, headers: headers); - } on SocketException catch (e) { - // Try to detect reset by peer error and retry. - if (attempt++ < maxAttempts && - (e.osError?.errorCode == 54 || e.osError?.errorCode == 10054)) { - _log.severe( - 'Failed to download $url: $e, attempt $attempt of $maxAttempts, will retry...'); - await Future.delayed(Duration(seconds: 1)); - continue; - } else { - rethrow; - } - } - } - } - - Future _tryDownloadArtifacts({ - required String crateHash, - required String fileName, - required String signatureFileName, - required String finalPath, - }) async { - final precompiledBinaries = environment.crateOptions.precompiledBinaries!; - final prefix = precompiledBinaries.uriPrefix; - final url = Uri.parse('$prefix$crateHash/$fileName'); - final signatureUrl = Uri.parse('$prefix$crateHash/$signatureFileName'); - _log.fine('Downloading signature from $signatureUrl'); - final signature = await _get(signatureUrl); - if (signature.statusCode == 404) { - _log.warning( - 'Precompiled binaries not available for crate hash $crateHash ($fileName)'); - return; - } - if (signature.statusCode != 200) { - _log.severe( - 'Failed to download signature $signatureUrl: status ${signature.statusCode}'); - return; - } - _log.fine('Downloading binary from $url'); - final res = await _get(url); - if (res.statusCode != 200) { - _log.severe('Failed to download binary $url: status ${res.statusCode}'); - return; - } - if (verify( - precompiledBinaries.publicKey, res.bodyBytes, signature.bodyBytes)) { - File(finalPath).writeAsBytesSync(res.bodyBytes); - } else { - _log.shout('Signature verification failed! Ignoring binary.'); - } - } -} - -enum AritifactType { - staticlib, - dylib, -} - -AritifactType artifactTypeForTarget(Target target) { - if (target.darwinPlatform != null) { - return AritifactType.staticlib; - } else { - return AritifactType.dylib; - } -} - -List getArtifactNames({ - required Target target, - required String libraryName, - required bool remote, - AritifactType? aritifactType, -}) { - aritifactType ??= artifactTypeForTarget(target); - if (target.darwinArch != null) { - if (aritifactType == AritifactType.staticlib) { - return ['lib$libraryName.a']; - } else { - return ['lib$libraryName.dylib']; - } - } else if (target.rust.contains('-windows-')) { - if (aritifactType == AritifactType.staticlib) { - return ['$libraryName.lib']; - } else { - return [ - '$libraryName.dll', - '$libraryName.dll.lib', - if (!remote) '$libraryName.pdb' - ]; - } - } else if (target.rust.contains('-linux-')) { - if (aritifactType == AritifactType.staticlib) { - return ['lib$libraryName.a']; - } else { - return ['lib$libraryName.so']; - } - } else { - throw Exception("Unsupported target: ${target.rust}"); - } -} diff --git a/cargokit/build_tool/lib/src/build_cmake.dart b/cargokit/build_tool/lib/src/build_cmake.dart deleted file mode 100644 index 9154371..0000000 --- a/cargokit/build_tool/lib/src/build_cmake.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:io'; - -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; - -class BuildCMake { - final CargokitUserOptions userOptions; - - BuildCMake({required this.userOptions}); - - Future build() async { - final targetPlatform = Environment.targetPlatform; - final target = Target.forFlutterName(Environment.targetPlatform); - if (target == null) { - throw Exception("Unknown target platform: $targetPlatform"); - } - - final environment = BuildEnvironment.fromEnvironment(isAndroid: false); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts([target]); - - final libs = artifacts[target]!; - - for (final lib in libs) { - if (lib.type == AritifactType.dylib) { - File(lib.path) - .copySync(path.join(Environment.outputDir, lib.finalFileName)); - } - } - } -} diff --git a/cargokit/build_tool/lib/src/build_gradle.dart b/cargokit/build_tool/lib/src/build_gradle.dart deleted file mode 100644 index 469c8b2..0000000 --- a/cargokit/build_tool/lib/src/build_gradle.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; - -final log = Logger('build_gradle'); - -class BuildGradle { - BuildGradle({required this.userOptions}); - - final CargokitUserOptions userOptions; - - Future build() async { - final targets = Environment.targetPlatforms.map((arch) { - final target = Target.forFlutterName(arch); - if (target == null) { - throw Exception( - "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); - } - return target; - }).toList(); - - final environment = BuildEnvironment.fromEnvironment(isAndroid: true); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts(targets); - - for (final target in targets) { - final libs = artifacts[target]!; - final outputDir = path.join(Environment.outputDir, target.android!); - Directory(outputDir).createSync(recursive: true); - - for (final lib in libs) { - if (lib.type == AritifactType.dylib) { - File(lib.path).copySync(path.join(outputDir, lib.finalFileName)); - } - } - } - } -} diff --git a/cargokit/build_tool/lib/src/build_pod.dart b/cargokit/build_tool/lib/src/build_pod.dart deleted file mode 100644 index f01401e..0000000 --- a/cargokit/build_tool/lib/src/build_pod.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:io'; - -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'target.dart'; -import 'util.dart'; - -class BuildPod { - BuildPod({required this.userOptions}); - - final CargokitUserOptions userOptions; - - Future build() async { - final targets = Environment.darwinArchs.map((arch) { - final target = Target.forDarwin( - platformName: Environment.darwinPlatformName, darwinAarch: arch); - if (target == null) { - throw Exception( - "Unknown darwin target or platform: $arch, ${Environment.darwinPlatformName}"); - } - return target; - }).toList(); - - final environment = BuildEnvironment.fromEnvironment(isAndroid: false); - final provider = - ArtifactProvider(environment: environment, userOptions: userOptions); - final artifacts = await provider.getArtifacts(targets); - - void performLipo(String targetFile, Iterable sourceFiles) { - runCommand("lipo", [ - '-create', - ...sourceFiles, - '-output', - targetFile, - ]); - } - - final outputDir = Environment.outputDir; - - Directory(outputDir).createSync(recursive: true); - - final staticLibs = artifacts.values - .expand((element) => element) - .where((element) => element.type == AritifactType.staticlib) - .toList(); - final dynamicLibs = artifacts.values - .expand((element) => element) - .where((element) => element.type == AritifactType.dylib) - .toList(); - - final libName = environment.crateInfo.packageName; - - // If there is static lib, use it and link it with pod - if (staticLibs.isNotEmpty) { - final finalTargetFile = path.join(outputDir, "lib$libName.a"); - performLipo(finalTargetFile, staticLibs.map((e) => e.path)); - } else { - // Otherwise try to replace bundle dylib with our dylib - final bundlePaths = [ - '$libName.framework/Versions/A/$libName', - '$libName.framework/$libName', - ]; - - for (final bundlePath in bundlePaths) { - final targetFile = path.join(outputDir, bundlePath); - if (File(targetFile).existsSync()) { - performLipo(targetFile, dynamicLibs.map((e) => e.path)); - - // Replace absolute id with @rpath one so that it works properly - // when moved to Frameworks. - runCommand("install_name_tool", [ - '-id', - '@rpath/$bundlePath', - targetFile, - ]); - return; - } - } - throw Exception('Unable to find bundle for dynamic library'); - } - } -} diff --git a/cargokit/build_tool/lib/src/build_tool.dart b/cargokit/build_tool/lib/src/build_tool.dart deleted file mode 100644 index 1d9462a..0000000 --- a/cargokit/build_tool/lib/src/build_tool.dart +++ /dev/null @@ -1,268 +0,0 @@ -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:github/github.dart'; -import 'package:hex/hex.dart'; -import 'package:logging/logging.dart'; - -import 'android_environment.dart'; -import 'build_cmake.dart'; -import 'build_gradle.dart'; -import 'build_pod.dart'; -import 'logging.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'target.dart'; -import 'util.dart'; -import 'verify_binaries.dart'; - -final log = Logger('build_tool'); - -abstract class BuildCommand extends Command { - Future runBuildCommand(CargokitUserOptions options); - - @override - Future run() async { - final options = CargokitUserOptions.load(); - - if (options.verboseLogging || - Platform.environment['CARGOKIT_VERBOSE'] == '1') { - enableVerboseLogging(); - } - - await runBuildCommand(options); - } -} - -class BuildPodCommand extends BuildCommand { - @override - final name = 'build-pod'; - - @override - final description = 'Build cocoa pod library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildPod(userOptions: options); - await build.build(); - } -} - -class BuildGradleCommand extends BuildCommand { - @override - final name = 'build-gradle'; - - @override - final description = 'Build android library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildGradle(userOptions: options); - await build.build(); - } -} - -class BuildCMakeCommand extends BuildCommand { - @override - final name = 'build-cmake'; - - @override - final description = 'Build CMake library'; - - @override - Future runBuildCommand(CargokitUserOptions options) async { - final build = BuildCMake(userOptions: options); - await build.build(); - } -} - -class GenKeyCommand extends Command { - @override - final name = 'gen-key'; - - @override - final description = 'Generate key pair for signing precompiled binaries'; - - @override - void run() { - final kp = generateKey(); - final private = HEX.encode(kp.privateKey.bytes); - final public = HEX.encode(kp.publicKey.bytes); - print("Private Key: $private"); - print("Public Key: $public"); - } -} - -class PrecompileBinariesCommand extends Command { - PrecompileBinariesCommand() { - argParser - ..addOption( - 'repository', - mandatory: true, - help: 'Github repository slug in format owner/name', - ) - ..addOption( - 'manifest-dir', - mandatory: true, - help: 'Directory containing Cargo.toml', - ) - ..addMultiOption('target', - help: 'Rust target triple of artifact to build.\n' - 'Can be specified multiple times or omitted in which case\n' - 'all targets for current platform will be built.') - ..addOption( - 'android-sdk-location', - help: 'Location of Android SDK (if available)', - ) - ..addOption( - 'android-ndk-version', - help: 'Android NDK version (if available)', - ) - ..addOption( - 'android-min-sdk-version', - help: 'Android minimum rquired version (if available)', - ) - ..addOption( - 'temp-dir', - help: 'Directory to store temporary build artifacts', - ) - ..addFlag( - "verbose", - abbr: "v", - defaultsTo: false, - help: "Enable verbose logging", - ); - } - - @override - final name = 'precompile-binaries'; - - @override - final description = 'Prebuild and upload binaries\n' - 'Private key must be passed through PRIVATE_KEY environment variable. ' - 'Use gen_key through generate priave key.\n' - 'Github token must be passed as GITHUB_TOKEN environment variable.\n'; - - @override - Future run() async { - final verbose = argResults!['verbose'] as bool; - if (verbose) { - enableVerboseLogging(); - } - - final privateKeyString = Platform.environment['PRIVATE_KEY']; - if (privateKeyString == null) { - throw ArgumentError('Missing PRIVATE_KEY environment variable'); - } - final githubToken = Platform.environment['GITHUB_TOKEN']; - if (githubToken == null) { - throw ArgumentError('Missing GITHUB_TOKEN environment variable'); - } - final privateKey = HEX.decode(privateKeyString); - if (privateKey.length != 64) { - throw ArgumentError('Private key must be 64 bytes long'); - } - final manifestDir = argResults!['manifest-dir'] as String; - if (!Directory(manifestDir).existsSync()) { - throw ArgumentError('Manifest directory does not exist: $manifestDir'); - } - String? androidMinSdkVersionString = - argResults!['android-min-sdk-version'] as String?; - int? androidMinSdkVersion; - if (androidMinSdkVersionString != null) { - androidMinSdkVersion = int.tryParse(androidMinSdkVersionString); - if (androidMinSdkVersion == null) { - throw ArgumentError( - 'Invalid android-min-sdk-version: $androidMinSdkVersionString'); - } - } - final targetStrigns = argResults!['target'] as List; - final targets = targetStrigns.map((target) { - final res = Target.forRustTriple(target); - if (res == null) { - throw ArgumentError('Invalid target: $target'); - } - return res; - }).toList(growable: false); - final precompileBinaries = PrecompileBinaries( - privateKey: PrivateKey(privateKey), - githubToken: githubToken, - manifestDir: manifestDir, - repositorySlug: RepositorySlug.full(argResults!['repository'] as String), - targets: targets, - androidSdkLocation: argResults!['android-sdk-location'] as String?, - androidNdkVersion: argResults!['android-ndk-version'] as String?, - androidMinSdkVersion: androidMinSdkVersion, - tempDir: argResults!['temp-dir'] as String?, - ); - - await precompileBinaries.run(); - } -} - -class VerifyBinariesCommand extends Command { - VerifyBinariesCommand() { - argParser.addOption( - 'manifest-dir', - mandatory: true, - help: 'Directory containing Cargo.toml', - ); - } - - @override - final name = "verify-binaries"; - - @override - final description = 'Verifies published binaries\n' - 'Checks whether there is a binary published for each targets\n' - 'and checks the signature.'; - - @override - Future run() async { - final manifestDir = argResults!['manifest-dir'] as String; - final verifyBinaries = VerifyBinaries( - manifestDir: manifestDir, - ); - await verifyBinaries.run(); - } -} - -Future runMain(List args) async { - try { - // Init logging before options are loaded - initLogging(); - - if (Platform.environment['_CARGOKIT_NDK_LINK_TARGET'] != null) { - return AndroidEnvironment.clangLinkerWrapper(args); - } - - final runner = CommandRunner('build_tool', 'Cargokit built_tool') - ..addCommand(BuildPodCommand()) - ..addCommand(BuildGradleCommand()) - ..addCommand(BuildCMakeCommand()) - ..addCommand(GenKeyCommand()) - ..addCommand(PrecompileBinariesCommand()) - ..addCommand(VerifyBinariesCommand()); - - await runner.run(args); - } on ArgumentError catch (e) { - stderr.writeln(e.toString()); - exit(1); - } catch (e, s) { - log.severe(kDoubleSeparator); - log.severe('Cargokit BuildTool failed with error:'); - log.severe(kSeparator); - log.severe(e); - // This tells user to install Rust, there's no need to pollute the log with - // stack trace. - if (e is! RustupNotFoundException) { - log.severe(kSeparator); - log.severe(s); - log.severe(kSeparator); - log.severe('BuildTool arguments: $args'); - } - log.severe(kDoubleSeparator); - exit(1); - } -} diff --git a/cargokit/build_tool/lib/src/builder.dart b/cargokit/build_tool/lib/src/builder.dart deleted file mode 100644 index 570a537..0000000 --- a/cargokit/build_tool/lib/src/builder.dart +++ /dev/null @@ -1,195 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'android_environment.dart'; -import 'cargo.dart'; -import 'environment.dart'; -import 'options.dart'; -import 'rustup.dart'; -import 'target.dart'; -import 'util.dart'; - -final _log = Logger('builder'); - -enum BuildConfiguration { - debug, - release, - profile, -} - -extension on BuildConfiguration { - bool get isDebug => this == BuildConfiguration.debug; - String get rustName => switch (this) { - BuildConfiguration.debug => 'debug', - BuildConfiguration.release => 'release', - BuildConfiguration.profile => 'release', - }; -} - -class BuildException implements Exception { - final String message; - - BuildException(this.message); - - @override - String toString() { - return 'BuildException: $message'; - } -} - -class BuildEnvironment { - final BuildConfiguration configuration; - final CargokitCrateOptions crateOptions; - final String targetTempDir; - final String manifestDir; - final CrateInfo crateInfo; - - final bool isAndroid; - final String? androidSdkPath; - final String? androidNdkVersion; - final int? androidMinSdkVersion; - final String? javaHome; - - BuildEnvironment({ - required this.configuration, - required this.crateOptions, - required this.targetTempDir, - required this.manifestDir, - required this.crateInfo, - required this.isAndroid, - this.androidSdkPath, - this.androidNdkVersion, - this.androidMinSdkVersion, - this.javaHome, - }); - - static BuildConfiguration parseBuildConfiguration(String value) { - // XCode configuration adds the flavor to configuration name. - final firstSegment = value.split('-').first; - final buildConfiguration = BuildConfiguration.values.firstWhereOrNull( - (e) => e.name == firstSegment, - ); - if (buildConfiguration == null) { - _log.warning('Unknown build configuraiton $value, will assume release'); - return BuildConfiguration.release; - } - return buildConfiguration; - } - - static BuildEnvironment fromEnvironment({ - required bool isAndroid, - }) { - final buildConfiguration = - parseBuildConfiguration(Environment.configuration); - final manifestDir = Environment.manifestDir; - final crateOptions = CargokitCrateOptions.load( - manifestDir: manifestDir, - ); - final crateInfo = CrateInfo.load(manifestDir); - return BuildEnvironment( - configuration: buildConfiguration, - crateOptions: crateOptions, - targetTempDir: Environment.targetTempDir, - manifestDir: manifestDir, - crateInfo: crateInfo, - isAndroid: isAndroid, - androidSdkPath: isAndroid ? Environment.sdkPath : null, - androidNdkVersion: isAndroid ? Environment.ndkVersion : null, - androidMinSdkVersion: - isAndroid ? int.parse(Environment.minSdkVersion) : null, - javaHome: isAndroid ? Environment.javaHome : null, - ); - } -} - -class RustBuilder { - final Target target; - final BuildEnvironment environment; - - RustBuilder({ - required this.target, - required this.environment, - }); - - void prepare( - Rustup rustup, - ) { - final toolchain = _toolchain; - if (rustup.installedTargets(toolchain) == null) { - rustup.installToolchain(toolchain); - } - if (toolchain == 'nightly') { - rustup.installRustSrcForNightly(); - } - if (!rustup.installedTargets(toolchain)!.contains(target.rust)) { - rustup.installTarget(target.rust, toolchain: toolchain); - } - } - - CargoBuildOptions? get _buildOptions => - environment.crateOptions.cargo[environment.configuration]; - - String get _toolchain => _buildOptions?.toolchain.name ?? 'stable'; - - /// Returns the path of directory containing build artifacts. - Future build() async { - final extraArgs = _buildOptions?.flags ?? []; - final manifestPath = path.join(environment.manifestDir, 'Cargo.toml'); - runCommand( - 'rustup', - [ - 'run', - _toolchain, - 'cargo', - 'build', - ...extraArgs, - '--manifest-path', - manifestPath, - '-p', - environment.crateInfo.packageName, - if (!environment.configuration.isDebug) '--release', - '--target', - target.rust, - '--target-dir', - environment.targetTempDir, - ], - environment: await _buildEnvironment(), - ); - return path.join( - environment.targetTempDir, - target.rust, - environment.configuration.rustName, - ); - } - - Future> _buildEnvironment() async { - if (target.android == null) { - return {}; - } else { - final sdkPath = environment.androidSdkPath; - final ndkVersion = environment.androidNdkVersion; - final minSdkVersion = environment.androidMinSdkVersion; - if (sdkPath == null) { - throw BuildException('androidSdkPath is not set'); - } - if (ndkVersion == null) { - throw BuildException('androidNdkVersion is not set'); - } - if (minSdkVersion == null) { - throw BuildException('androidMinSdkVersion is not set'); - } - final env = AndroidEnvironment( - sdkPath: sdkPath, - ndkVersion: ndkVersion, - minSdkVersion: minSdkVersion, - targetTempDir: environment.targetTempDir, - target: target, - ); - if (!env.ndkIsInstalled() && environment.javaHome != null) { - env.installNdk(javaHome: environment.javaHome!); - } - return env.buildEnvironment(); - } - } -} diff --git a/cargokit/build_tool/lib/src/cargo.dart b/cargokit/build_tool/lib/src/cargo.dart deleted file mode 100644 index 0d4483f..0000000 --- a/cargokit/build_tool/lib/src/cargo.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:io'; - -import 'package:path/path.dart' as path; -import 'package:toml/toml.dart'; - -class ManifestException { - ManifestException(this.message, {required this.fileName}); - - final String? fileName; - final String message; - - @override - String toString() { - if (fileName != null) { - return 'Failed to parse package manifest at $fileName: $message'; - } else { - return 'Failed to parse package manifest: $message'; - } - } -} - -class CrateInfo { - CrateInfo({required this.packageName}); - - final String packageName; - - static CrateInfo parseManifest(String manifest, {final String? fileName}) { - final toml = TomlDocument.parse(manifest); - final package = toml.toMap()['package']; - if (package == null) { - throw ManifestException('Missing package section', fileName: fileName); - } - final name = package['name']; - if (name == null) { - throw ManifestException('Missing package name', fileName: fileName); - } - return CrateInfo(packageName: name); - } - - static CrateInfo load(String manifestDir) { - final manifestFile = File(path.join(manifestDir, 'Cargo.toml')); - final manifest = manifestFile.readAsStringSync(); - return parseManifest(manifest, fileName: manifestFile.path); - } -} diff --git a/cargokit/build_tool/lib/src/crate_hash.dart b/cargokit/build_tool/lib/src/crate_hash.dart deleted file mode 100644 index e58c37f..0000000 --- a/cargokit/build_tool/lib/src/crate_hash.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:convert/convert.dart'; -import 'package:crypto/crypto.dart'; -import 'package:path/path.dart' as path; - -class CrateHash { - /// Computes a hash uniquely identifying crate content. This takes into account - /// content all all .rs files inside the src directory, as well as Cargo.toml, - /// Cargo.lock, build.rs and cargokit.yaml. - /// - /// If [tempStorage] is provided, computed hash is stored in a file in that directory - /// and reused on subsequent calls if the crate content hasn't changed. - static String compute(String manifestDir, {String? tempStorage}) { - return CrateHash._( - manifestDir: manifestDir, - tempStorage: tempStorage, - )._compute(); - } - - CrateHash._({ - required this.manifestDir, - required this.tempStorage, - }); - - String _compute() { - final files = getFiles(); - final tempStorage = this.tempStorage; - if (tempStorage != null) { - final quickHash = _computeQuickHash(files); - final quickHashFolder = Directory(path.join(tempStorage, 'crate_hash')); - quickHashFolder.createSync(recursive: true); - final quickHashFile = File(path.join(quickHashFolder.path, quickHash)); - if (quickHashFile.existsSync()) { - return quickHashFile.readAsStringSync(); - } - final hash = _computeHash(files); - quickHashFile.writeAsStringSync(hash); - return hash; - } else { - return _computeHash(files); - } - } - - /// Computes a quick hash based on files stat (without reading contents). This - /// is used to cache the real hash, which is slower to compute since it involves - /// reading every single file. - String _computeQuickHash(List files) { - final output = AccumulatorSink(); - final input = sha256.startChunkedConversion(output); - - final data = ByteData(8); - for (final file in files) { - input.add(utf8.encode(file.path)); - final stat = file.statSync(); - data.setUint64(0, stat.size); - input.add(data.buffer.asUint8List()); - data.setUint64(0, stat.modified.millisecondsSinceEpoch); - input.add(data.buffer.asUint8List()); - } - - input.close(); - return base64Url.encode(output.events.single.bytes); - } - - String _computeHash(List files) { - final output = AccumulatorSink(); - final input = sha256.startChunkedConversion(output); - - void addTextFile(File file) { - // text Files are hashed by lines in case we're dealing with github checkout - // that auto-converts line endings. - final splitter = LineSplitter(); - if (file.existsSync()) { - final data = file.readAsStringSync(); - final lines = splitter.convert(data); - for (final line in lines) { - input.add(utf8.encode(line)); - } - } - } - - for (final file in files) { - addTextFile(file); - } - - input.close(); - final res = output.events.single; - - // Truncate to 128bits. - final hash = res.bytes.sublist(0, 16); - return hex.encode(hash); - } - - List getFiles() { - final src = Directory(path.join(manifestDir, 'src')); - final files = src - .listSync(recursive: true, followLinks: false) - .whereType() - .toList(); - files.sortBy((element) => element.path); - void addFile(String relative) { - final file = File(path.join(manifestDir, relative)); - if (file.existsSync()) { - files.add(file); - } - } - - addFile('Cargo.toml'); - addFile('Cargo.lock'); - addFile('build.rs'); - addFile('cargokit.yaml'); - return files; - } - - final String manifestDir; - final String? tempStorage; -} diff --git a/cargokit/build_tool/lib/src/environment.dart b/cargokit/build_tool/lib/src/environment.dart deleted file mode 100644 index 1d267ed..0000000 --- a/cargokit/build_tool/lib/src/environment.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'dart:io'; - -extension on String { - String resolveSymlink() => File(this).resolveSymbolicLinksSync(); -} - -class Environment { - /// Current build configuration (debug or release). - static String get configuration => - _getEnv("CARGOKIT_CONFIGURATION").toLowerCase(); - - static bool get isDebug => configuration == 'debug'; - static bool get isRelease => configuration == 'release'; - - /// Temporary directory where Rust build artifacts are placed. - static String get targetTempDir => _getEnv("CARGOKIT_TARGET_TEMP_DIR"); - - /// Final output directory where the build artifacts are placed. - static String get outputDir => _getEnvPath('CARGOKIT_OUTPUT_DIR'); - - /// Path to the crate manifest (containing Cargo.toml). - static String get manifestDir => _getEnvPath('CARGOKIT_MANIFEST_DIR'); - - /// Directory inside root project. Not necessarily root folder. Symlinks are - /// not resolved on purpose. - static String get rootProjectDir => _getEnv('CARGOKIT_ROOT_PROJECT_DIR'); - - // Pod - - /// Platform name (macosx, iphoneos, iphonesimulator). - static String get darwinPlatformName => - _getEnv("CARGOKIT_DARWIN_PLATFORM_NAME"); - - /// List of architectures to build for (arm64, armv7, x86_64). - static List get darwinArchs => - _getEnv("CARGOKIT_DARWIN_ARCHS").split(' '); - - // Gradle - static String get minSdkVersion => _getEnv("CARGOKIT_MIN_SDK_VERSION"); - static String get ndkVersion => _getEnv("CARGOKIT_NDK_VERSION"); - static String get sdkPath => _getEnvPath("CARGOKIT_SDK_DIR"); - static String get javaHome => _getEnvPath("CARGOKIT_JAVA_HOME"); - static List get targetPlatforms => - _getEnv("CARGOKIT_TARGET_PLATFORMS").split(','); - - // CMAKE - static String get targetPlatform => _getEnv("CARGOKIT_TARGET_PLATFORM"); - - static String _getEnv(String key) { - final res = Platform.environment[key]; - if (res == null) { - throw Exception("Missing environment variable $key"); - } - return res; - } - - static String _getEnvPath(String key) { - final res = _getEnv(key); - if (Directory(res).existsSync()) { - return res.resolveSymlink(); - } else { - return res; - } - } -} diff --git a/cargokit/build_tool/lib/src/logging.dart b/cargokit/build_tool/lib/src/logging.dart deleted file mode 100644 index 06392b9..0000000 --- a/cargokit/build_tool/lib/src/logging.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'dart:io'; - -import 'package:logging/logging.dart'; - -const String kSeparator = "--"; -const String kDoubleSeparator = "=="; - -bool _lastMessageWasSeparator = false; - -void _log(LogRecord rec) { - final prefix = '${rec.level.name}: '; - final out = rec.level == Level.SEVERE ? stderr : stdout; - if (rec.message == kSeparator) { - if (!_lastMessageWasSeparator) { - out.write(prefix); - out.writeln('-' * 80); - _lastMessageWasSeparator = true; - } - return; - } else if (rec.message == kDoubleSeparator) { - out.write(prefix); - out.writeln('=' * 80); - _lastMessageWasSeparator = true; - return; - } - out.write(prefix); - out.writeln(rec.message); - _lastMessageWasSeparator = false; -} - -void initLogging() { - Logger.root.level = Level.INFO; - Logger.root.onRecord.listen((LogRecord rec) { - final lines = rec.message.split('\n'); - for (final line in lines) { - if (line.isNotEmpty || lines.length == 1 || line != lines.last) { - _log(LogRecord( - rec.level, - line, - rec.loggerName, - )); - } - } - }); -} - -void enableVerboseLogging() { - Logger.root.level = Level.ALL; -} diff --git a/cargokit/build_tool/lib/src/options.dart b/cargokit/build_tool/lib/src/options.dart deleted file mode 100644 index 7937dca..0000000 --- a/cargokit/build_tool/lib/src/options.dart +++ /dev/null @@ -1,306 +0,0 @@ -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:hex/hex.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; -import 'package:source_span/source_span.dart'; -import 'package:yaml/yaml.dart'; - -import 'builder.dart'; -import 'environment.dart'; -import 'rustup.dart'; - -final _log = Logger('options'); - -/// A class for exceptions that have source span information attached. -class SourceSpanException implements Exception { - // This is a getter so that subclasses can override it. - /// A message describing the exception. - String get message => _message; - final String _message; - - // This is a getter so that subclasses can override it. - /// The span associated with this exception. - /// - /// This may be `null` if the source location can't be determined. - SourceSpan? get span => _span; - final SourceSpan? _span; - - SourceSpanException(this._message, this._span); - - /// Returns a string representation of `this`. - /// - /// [color] may either be a [String], a [bool], or `null`. If it's a string, - /// it indicates an ANSI terminal color escape that should be used to - /// highlight the span's text. If it's `true`, it indicates that the text - /// should be highlighted using the default color. If it's `false` or `null`, - /// it indicates that the text shouldn't be highlighted. - @override - String toString({Object? color}) { - if (span == null) return message; - return 'Error on ${span!.message(message, color: color)}'; - } -} - -enum Toolchain { - stable, - beta, - nightly, -} - -class CargoBuildOptions { - final Toolchain toolchain; - final List flags; - - CargoBuildOptions({ - required this.toolchain, - required this.flags, - }); - - static Toolchain _toolchainFromNode(YamlNode node) { - if (node case YamlScalar(value: String name)) { - final toolchain = - Toolchain.values.firstWhereOrNull((element) => element.name == name); - if (toolchain != null) { - return toolchain; - } - } - throw SourceSpanException( - 'Unknown toolchain. Must be one of ${Toolchain.values.map((e) => e.name)}.', - node.span); - } - - static CargoBuildOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargo options must be a map', node.span); - } - Toolchain toolchain = Toolchain.stable; - List flags = []; - for (final MapEntry(:key, :value) in node.nodes.entries) { - if (key case YamlScalar(value: 'toolchain')) { - toolchain = _toolchainFromNode(value); - } else if (key case YamlScalar(value: 'extra_flags')) { - if (value case YamlList(nodes: List list)) { - if (list.every((element) { - if (element case YamlScalar(value: String _)) { - return true; - } - return false; - })) { - flags = list.map((e) => e.value as String).toList(); - continue; - } - } - throw SourceSpanException( - 'Extra flags must be a list of strings', value.span); - } else { - throw SourceSpanException( - 'Unknown cargo option type. Must be "toolchain" or "extra_flags".', - key.span); - } - } - return CargoBuildOptions(toolchain: toolchain, flags: flags); - } -} - -extension on YamlMap { - /// Map that extracts keys so that we can do map case check on them. - Map get valueMap => - nodes.map((key, value) => MapEntry(key.value, value)); -} - -class PrecompiledBinaries { - final String uriPrefix; - final PublicKey publicKey; - - PrecompiledBinaries({ - required this.uriPrefix, - required this.publicKey, - }); - - static PublicKey _publicKeyFromHex(String key, SourceSpan? span) { - final bytes = HEX.decode(key); - if (bytes.length != 32) { - throw SourceSpanException( - 'Invalid public key. Must be 32 bytes long.', span); - } - return PublicKey(bytes); - } - - static PrecompiledBinaries parse(YamlNode node) { - if (node case YamlMap(valueMap: Map map)) { - if (map - case { - 'url_prefix': YamlNode urlPrefixNode, - 'public_key': YamlNode publicKeyNode, - }) { - final urlPrefix = switch (urlPrefixNode) { - YamlScalar(value: String urlPrefix) => urlPrefix, - _ => throw SourceSpanException( - 'Invalid URL prefix value.', urlPrefixNode.span), - }; - final publicKey = switch (publicKeyNode) { - YamlScalar(value: String publicKey) => - _publicKeyFromHex(publicKey, publicKeyNode.span), - _ => throw SourceSpanException( - 'Invalid public key value.', publicKeyNode.span), - }; - return PrecompiledBinaries( - uriPrefix: urlPrefix, - publicKey: publicKey, - ); - } - } - throw SourceSpanException( - 'Invalid precompiled binaries value. ' - 'Expected Map with "url_prefix" and "public_key".', - node.span); - } -} - -/// Cargokit options specified for Rust crate. -class CargokitCrateOptions { - CargokitCrateOptions({ - this.cargo = const {}, - this.precompiledBinaries, - }); - - final Map cargo; - final PrecompiledBinaries? precompiledBinaries; - - static CargokitCrateOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargokit options must be a map', node.span); - } - final options = {}; - PrecompiledBinaries? precompiledBinaries; - - for (final entry in node.nodes.entries) { - if (entry - case MapEntry( - key: YamlScalar(value: 'cargo'), - value: YamlNode node, - )) { - if (node is! YamlMap) { - throw SourceSpanException('Cargo options must be a map', node.span); - } - for (final MapEntry(:YamlNode key, :value) in node.nodes.entries) { - if (key case YamlScalar(value: String name)) { - final configuration = BuildConfiguration.values - .firstWhereOrNull((element) => element.name == name); - if (configuration != null) { - options[configuration] = CargoBuildOptions.parse(value); - continue; - } - } - throw SourceSpanException( - 'Unknown build configuration. Must be one of ${BuildConfiguration.values.map((e) => e.name)}.', - key.span); - } - } else if (entry.key case YamlScalar(value: 'precompiled_binaries')) { - precompiledBinaries = PrecompiledBinaries.parse(entry.value); - } else { - throw SourceSpanException( - 'Unknown cargokit option type. Must be "cargo" or "precompiled_binaries".', - entry.key.span); - } - } - return CargokitCrateOptions( - cargo: options, - precompiledBinaries: precompiledBinaries, - ); - } - - static CargokitCrateOptions load({ - required String manifestDir, - }) { - final uri = Uri.file(path.join(manifestDir, "cargokit.yaml")); - final file = File.fromUri(uri); - if (file.existsSync()) { - final contents = loadYamlNode(file.readAsStringSync(), sourceUrl: uri); - return parse(contents); - } else { - return CargokitCrateOptions(); - } - } -} - -class CargokitUserOptions { - // When Rustup is installed always build locally unless user opts into - // using precompiled binaries. - static bool defaultUsePrecompiledBinaries() { - return Rustup.executablePath() == null; - } - - CargokitUserOptions({ - required this.usePrecompiledBinaries, - required this.verboseLogging, - }); - - CargokitUserOptions._() - : usePrecompiledBinaries = defaultUsePrecompiledBinaries(), - verboseLogging = false; - - static CargokitUserOptions parse(YamlNode node) { - if (node is! YamlMap) { - throw SourceSpanException('Cargokit options must be a map', node.span); - } - bool usePrecompiledBinaries = defaultUsePrecompiledBinaries(); - bool verboseLogging = false; - - for (final entry in node.nodes.entries) { - if (entry.key case YamlScalar(value: 'use_precompiled_binaries')) { - if (entry.value case YamlScalar(value: bool value)) { - usePrecompiledBinaries = value; - continue; - } - throw SourceSpanException( - 'Invalid value for "use_precompiled_binaries". Must be a boolean.', - entry.value.span); - } else if (entry.key case YamlScalar(value: 'verbose_logging')) { - if (entry.value case YamlScalar(value: bool value)) { - verboseLogging = value; - continue; - } - throw SourceSpanException( - 'Invalid value for "verbose_logging". Must be a boolean.', - entry.value.span); - } else { - throw SourceSpanException( - 'Unknown cargokit option type. Must be "use_precompiled_binaries" or "verbose_logging".', - entry.key.span); - } - } - return CargokitUserOptions( - usePrecompiledBinaries: usePrecompiledBinaries, - verboseLogging: verboseLogging, - ); - } - - static CargokitUserOptions load() { - String fileName = "cargokit_options.yaml"; - var userProjectDir = Directory(Environment.rootProjectDir); - - while (userProjectDir.parent.path != userProjectDir.path) { - final configFile = File(path.join(userProjectDir.path, fileName)); - if (configFile.existsSync()) { - final contents = loadYamlNode( - configFile.readAsStringSync(), - sourceUrl: configFile.uri, - ); - final res = parse(contents); - if (res.verboseLogging) { - _log.info('Found user options file at ${configFile.path}'); - } - return res; - } - userProjectDir = userProjectDir.parent; - } - return CargokitUserOptions._(); - } - - final bool usePrecompiledBinaries; - final bool verboseLogging; -} diff --git a/cargokit/build_tool/lib/src/precompile_binaries.dart b/cargokit/build_tool/lib/src/precompile_binaries.dart deleted file mode 100644 index 39ffafc..0000000 --- a/cargokit/build_tool/lib/src/precompile_binaries.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:github/github.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'artifacts_provider.dart'; -import 'builder.dart'; -import 'cargo.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'rustup.dart'; -import 'target.dart'; - -final _log = Logger('precompile_binaries'); - -class PrecompileBinaries { - PrecompileBinaries({ - required this.privateKey, - required this.githubToken, - required this.repositorySlug, - required this.manifestDir, - required this.targets, - this.androidSdkLocation, - this.androidNdkVersion, - this.androidMinSdkVersion, - this.tempDir, - }); - - final PrivateKey privateKey; - final String githubToken; - final RepositorySlug repositorySlug; - final String manifestDir; - final List targets; - final String? androidSdkLocation; - final String? androidNdkVersion; - final int? androidMinSdkVersion; - final String? tempDir; - - static String fileName(Target target, String name) { - return '${target.rust}_$name'; - } - - static String signatureFileName(Target target, String name) { - return '${target.rust}_$name.sig'; - } - - Future run() async { - final crateInfo = CrateInfo.load(manifestDir); - - final targets = List.of(this.targets); - if (targets.isEmpty) { - targets.addAll([ - ...Target.buildableTargets(), - if (androidSdkLocation != null) ...Target.androidTargets(), - ]); - } - - _log.info('Precompiling binaries for $targets'); - - final hash = CrateHash.compute(manifestDir); - _log.info('Computed crate hash: $hash'); - - final String tagName = 'precompiled_$hash'; - - final github = GitHub(auth: Authentication.withToken(githubToken)); - final repo = github.repositories; - final release = await _getOrCreateRelease( - repo: repo, - tagName: tagName, - packageName: crateInfo.packageName, - hash: hash, - ); - - final tempDir = this.tempDir != null - ? Directory(this.tempDir!) - : Directory.systemTemp.createTempSync('precompiled_'); - - tempDir.createSync(recursive: true); - - final crateOptions = CargokitCrateOptions.load( - manifestDir: manifestDir, - ); - - final buildEnvironment = BuildEnvironment( - configuration: BuildConfiguration.release, - crateOptions: crateOptions, - targetTempDir: tempDir.path, - manifestDir: manifestDir, - crateInfo: crateInfo, - isAndroid: androidSdkLocation != null, - androidSdkPath: androidSdkLocation, - androidNdkVersion: androidNdkVersion, - androidMinSdkVersion: androidMinSdkVersion, - ); - - final rustup = Rustup(); - - for (final target in targets) { - final artifactNames = getArtifactNames( - target: target, - libraryName: crateInfo.packageName, - remote: true, - ); - - if (artifactNames.every((name) { - final fileName = PrecompileBinaries.fileName(target, name); - return (release.assets ?? []).any((e) => e.name == fileName); - })) { - _log.info("All artifacts for $target already exist - skipping"); - continue; - } - - _log.info('Building for $target'); - - final builder = - RustBuilder(target: target, environment: buildEnvironment); - builder.prepare(rustup); - final res = await builder.build(); - - final assets = []; - for (final name in artifactNames) { - final file = File(path.join(res, name)); - if (!file.existsSync()) { - throw Exception('Missing artifact: ${file.path}'); - } - - final data = file.readAsBytesSync(); - final create = CreateReleaseAsset( - name: PrecompileBinaries.fileName(target, name), - contentType: "application/octet-stream", - assetData: data, - ); - final signature = sign(privateKey, data); - final signatureCreate = CreateReleaseAsset( - name: signatureFileName(target, name), - contentType: "application/octet-stream", - assetData: signature, - ); - bool verified = verify(public(privateKey), data, signature); - if (!verified) { - throw Exception('Signature verification failed'); - } - assets.add(create); - assets.add(signatureCreate); - } - _log.info('Uploading assets: ${assets.map((e) => e.name)}'); - for (final asset in assets) { - // This seems to be failing on CI so do it one by one - int retryCount = 0; - while (true) { - try { - await repo.uploadReleaseAssets(release, [asset]); - break; - } on Exception catch (e) { - if (retryCount == 10) { - rethrow; - } - ++retryCount; - _log.shout( - 'Upload failed (attempt $retryCount, will retry): ${e.toString()}'); - await Future.delayed(Duration(seconds: 2)); - } - } - } - } - - _log.info('Cleaning up'); - tempDir.deleteSync(recursive: true); - } - - Future _getOrCreateRelease({ - required RepositoriesService repo, - required String tagName, - required String packageName, - required String hash, - }) async { - Release release; - try { - _log.info('Fetching release $tagName'); - release = await repo.getReleaseByTagName(repositorySlug, tagName); - } on ReleaseNotFound { - _log.info('Release not found - creating release $tagName'); - release = await repo.createRelease( - repositorySlug, - CreateRelease.from( - tagName: tagName, - name: 'Precompiled binaries ${hash.substring(0, 8)}', - targetCommitish: null, - isDraft: false, - isPrerelease: false, - body: 'Precompiled binaries for crate $packageName, ' - 'crate hash $hash.', - )); - } - return release; - } -} diff --git a/cargokit/build_tool/lib/src/rustup.dart b/cargokit/build_tool/lib/src/rustup.dart deleted file mode 100644 index f284179..0000000 --- a/cargokit/build_tool/lib/src/rustup.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:path/path.dart' as path; - -import 'util.dart'; - -class _Toolchain { - _Toolchain( - this.name, - this.targets, - ); - - final String name; - final List targets; -} - -class Rustup { - List? installedTargets(String toolchain) { - final targets = _installedTargets(toolchain); - return targets != null ? List.unmodifiable(targets) : null; - } - - void installToolchain(String toolchain) { - log.info("Installing Rust toolchain: $toolchain"); - runCommand("rustup", ['toolchain', 'install', toolchain]); - _installedToolchains - .add(_Toolchain(toolchain, _getInstalledTargets(toolchain))); - } - - void installTarget( - String target, { - required String toolchain, - }) { - log.info("Installing Rust target: $target"); - runCommand("rustup", [ - 'target', - 'add', - '--toolchain', - toolchain, - target, - ]); - _installedTargets(toolchain)?.add(target); - } - - final List<_Toolchain> _installedToolchains; - - Rustup() : _installedToolchains = _getInstalledToolchains(); - - List? _installedTargets(String toolchain) => _installedToolchains - .firstWhereOrNull( - (e) => e.name == toolchain || e.name.startsWith('$toolchain-')) - ?.targets; - - static List<_Toolchain> _getInstalledToolchains() { - String extractToolchainName(String line) { - // ignore (default) after toolchain name - final parts = line.split(' '); - return parts[0]; - } - - final res = runCommand("rustup", ['toolchain', 'list']); - - // To list all non-custom toolchains, we need to filter out lines that - // don't start with "stable", "beta", or "nightly". - Pattern nonCustom = RegExp(r"^(stable|beta|nightly)"); - final lines = res.stdout - .toString() - .split('\n') - .where((e) => e.isNotEmpty && e.startsWith(nonCustom)) - .map(extractToolchainName) - .toList(growable: true); - - return lines - .map( - (name) => _Toolchain( - name, - _getInstalledTargets(name), - ), - ) - .toList(growable: true); - } - - static List _getInstalledTargets(String toolchain) { - final res = runCommand("rustup", [ - 'target', - 'list', - '--toolchain', - toolchain, - '--installed', - ]); - final lines = res.stdout - .toString() - .split('\n') - .where((e) => e.isNotEmpty) - .toList(growable: true); - return lines; - } - - bool _didInstallRustSrcForNightly = false; - - void installRustSrcForNightly() { - if (_didInstallRustSrcForNightly) { - return; - } - // Useful for -Z build-std - runCommand( - "rustup", - ['component', 'add', 'rust-src', '--toolchain', 'nightly'], - ); - _didInstallRustSrcForNightly = true; - } - - static String? executablePath() { - final envPath = Platform.environment['PATH']; - final envPathSeparator = Platform.isWindows ? ';' : ':'; - final home = Platform.isWindows - ? Platform.environment['USERPROFILE'] - : Platform.environment['HOME']; - final paths = [ - if (home != null) path.join(home, '.cargo', 'bin'), - if (envPath != null) ...envPath.split(envPathSeparator), - ]; - for (final p in paths) { - final rustup = Platform.isWindows ? 'rustup.exe' : 'rustup'; - final rustupPath = path.join(p, rustup); - if (File(rustupPath).existsSync()) { - return rustupPath; - } - } - return null; - } -} diff --git a/cargokit/build_tool/lib/src/target.dart b/cargokit/build_tool/lib/src/target.dart deleted file mode 100644 index 9287b23..0000000 --- a/cargokit/build_tool/lib/src/target.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'dart:io'; - -import 'package:collection/collection.dart'; - -import 'util.dart'; - -class Target { - Target({ - required this.rust, - this.flutter, - this.android, - this.androidMinSdkVersion, - this.darwinPlatform, - this.darwinArch, - }); - - static final all = [ - Target( - rust: 'armv7-linux-androideabi', - flutter: 'android-arm', - android: 'armeabi-v7a', - androidMinSdkVersion: 16, - ), - Target( - rust: 'aarch64-linux-android', - flutter: 'android-arm64', - android: 'arm64-v8a', - androidMinSdkVersion: 21, - ), - Target( - rust: 'i686-linux-android', - flutter: 'android-x86', - android: 'x86', - androidMinSdkVersion: 16, - ), - Target( - rust: 'x86_64-linux-android', - flutter: 'android-x64', - android: 'x86_64', - androidMinSdkVersion: 21, - ), - Target( - rust: 'x86_64-pc-windows-msvc', - flutter: 'windows-x64', - ), - Target( - rust: 'x86_64-unknown-linux-gnu', - flutter: 'linux-x64', - ), - Target( - rust: 'aarch64-unknown-linux-gnu', - flutter: 'linux-arm64', - ), - Target( - rust: 'x86_64-apple-darwin', - darwinPlatform: 'macosx', - darwinArch: 'x86_64', - ), - Target( - rust: 'aarch64-apple-darwin', - darwinPlatform: 'macosx', - darwinArch: 'arm64', - ), - Target( - rust: 'aarch64-apple-ios', - darwinPlatform: 'iphoneos', - darwinArch: 'arm64', - ), - Target( - rust: 'aarch64-apple-ios-sim', - darwinPlatform: 'iphonesimulator', - darwinArch: 'arm64', - ), - Target( - rust: 'x86_64-apple-ios', - darwinPlatform: 'iphonesimulator', - darwinArch: 'x86_64', - ), - ]; - - static Target? forFlutterName(String flutterName) { - return all.firstWhereOrNull((element) => element.flutter == flutterName); - } - - static Target? forDarwin({ - required String platformName, - required String darwinAarch, - }) { - return all.firstWhereOrNull((element) => // - element.darwinPlatform == platformName && - element.darwinArch == darwinAarch); - } - - static Target? forRustTriple(String triple) { - return all.firstWhereOrNull((element) => element.rust == triple); - } - - static List androidTargets() { - return all - .where((element) => element.android != null) - .toList(growable: false); - } - - /// Returns buildable targets on current host platform ignoring Android targets. - static List buildableTargets() { - if (Platform.isLinux) { - // Right now we don't support cross-compiling on Linux. So we just return - // the host target. - final arch = runCommand('arch', []).stdout as String; - if (arch.trim() == 'aarch64') { - return [Target.forRustTriple('aarch64-unknown-linux-gnu')!]; - } else { - return [Target.forRustTriple('x86_64-unknown-linux-gnu')!]; - } - } - return all.where((target) { - if (Platform.isWindows) { - return target.rust.contains('-windows-'); - } else if (Platform.isMacOS) { - return target.darwinPlatform != null; - } - return false; - }).toList(growable: false); - } - - @override - String toString() { - return rust; - } - - final String? flutter; - final String rust; - final String? android; - final int? androidMinSdkVersion; - final String? darwinPlatform; - final String? darwinArch; -} diff --git a/cargokit/build_tool/lib/src/util.dart b/cargokit/build_tool/lib/src/util.dart deleted file mode 100644 index d8e3019..0000000 --- a/cargokit/build_tool/lib/src/util.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path; - -import 'logging.dart'; -import 'rustup.dart'; - -final log = Logger("process"); - -class CommandFailedException implements Exception { - final String executable; - final List arguments; - final ProcessResult result; - - CommandFailedException({ - required this.executable, - required this.arguments, - required this.result, - }); - - @override - String toString() { - final stdout = result.stdout.toString().trim(); - final stderr = result.stderr.toString().trim(); - return [ - "External Command: $executable ${arguments.map((e) => '"$e"').join(' ')}", - "Returned Exit Code: ${result.exitCode}", - kSeparator, - "STDOUT:", - if (stdout.isNotEmpty) stdout, - kSeparator, - "STDERR:", - if (stderr.isNotEmpty) stderr, - ].join('\n'); - } -} - -class TestRunCommandArgs { - final String executable; - final List arguments; - final String? workingDirectory; - final Map? environment; - final bool includeParentEnvironment; - final bool runInShell; - final Encoding? stdoutEncoding; - final Encoding? stderrEncoding; - - TestRunCommandArgs({ - required this.executable, - required this.arguments, - this.workingDirectory, - this.environment, - this.includeParentEnvironment = true, - this.runInShell = false, - this.stdoutEncoding, - this.stderrEncoding, - }); -} - -class TestRunCommandResult { - TestRunCommandResult({ - this.pid = 1, - this.exitCode = 0, - this.stdout = '', - this.stderr = '', - }); - - final int pid; - final int exitCode; - final String stdout; - final String stderr; -} - -TestRunCommandResult Function(TestRunCommandArgs args)? testRunCommandOverride; - -ProcessResult runCommand( - String executable, - List arguments, { - String? workingDirectory, - Map? environment, - bool includeParentEnvironment = true, - bool runInShell = false, - Encoding? stdoutEncoding = systemEncoding, - Encoding? stderrEncoding = systemEncoding, -}) { - if (testRunCommandOverride != null) { - final result = testRunCommandOverride!(TestRunCommandArgs( - executable: executable, - arguments: arguments, - workingDirectory: workingDirectory, - environment: environment, - includeParentEnvironment: includeParentEnvironment, - runInShell: runInShell, - stdoutEncoding: stdoutEncoding, - stderrEncoding: stderrEncoding, - )); - return ProcessResult( - result.pid, - result.exitCode, - result.stdout, - result.stderr, - ); - } - log.finer('Running command $executable ${arguments.join(' ')}'); - final res = Process.runSync( - _resolveExecutable(executable), - arguments, - workingDirectory: workingDirectory, - environment: environment, - includeParentEnvironment: includeParentEnvironment, - runInShell: runInShell, - stderrEncoding: stderrEncoding, - stdoutEncoding: stdoutEncoding, - ); - if (res.exitCode != 0) { - throw CommandFailedException( - executable: executable, - arguments: arguments, - result: res, - ); - } else { - return res; - } -} - -class RustupNotFoundException implements Exception { - @override - String toString() { - return [ - ' ', - 'rustup not found in PATH.', - ' ', - 'Maybe you need to install Rust? It only takes a minute:', - ' ', - if (Platform.isWindows) 'https://www.rust-lang.org/tools/install', - if (hasHomebrewRustInPath()) ...[ - '\$ brew unlink rust # Unlink homebrew Rust from PATH', - ], - if (!Platform.isWindows) - "\$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", - ' ', - ].join('\n'); - } - - static bool hasHomebrewRustInPath() { - if (!Platform.isMacOS) { - return false; - } - final envPath = Platform.environment['PATH'] ?? ''; - final paths = envPath.split(':'); - return paths.any((p) { - return p.contains('homebrew') && File(path.join(p, 'rustc')).existsSync(); - }); - } -} - -String _resolveExecutable(String executable) { - if (executable == 'rustup') { - final resolved = Rustup.executablePath(); - if (resolved != null) { - return resolved; - } - throw RustupNotFoundException(); - } else { - return executable; - } -} diff --git a/cargokit/build_tool/lib/src/verify_binaries.dart b/cargokit/build_tool/lib/src/verify_binaries.dart deleted file mode 100644 index 0094c64..0000000 --- a/cargokit/build_tool/lib/src/verify_binaries.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:io'; - -import 'package:ed25519_edwards/ed25519_edwards.dart'; -import 'package:http/http.dart'; - -import 'artifacts_provider.dart'; -import 'cargo.dart'; -import 'crate_hash.dart'; -import 'options.dart'; -import 'precompile_binaries.dart'; -import 'target.dart'; - -class VerifyBinaries { - VerifyBinaries({ - required this.manifestDir, - }); - - final String manifestDir; - - Future run() async { - final crateInfo = CrateInfo.load(manifestDir); - - final config = CargokitCrateOptions.load(manifestDir: manifestDir); - final precompiledBinaries = config.precompiledBinaries; - if (precompiledBinaries == null) { - stdout.writeln('Crate does not support precompiled binaries.'); - } else { - final crateHash = CrateHash.compute(manifestDir); - stdout.writeln('Crate hash: $crateHash'); - - for (final target in Target.all) { - final message = 'Checking ${target.rust}...'; - stdout.write(message.padRight(40)); - stdout.flush(); - - final artifacts = getArtifactNames( - target: target, - libraryName: crateInfo.packageName, - remote: true, - ); - - final prefix = precompiledBinaries.uriPrefix; - - bool ok = true; - - for (final artifact in artifacts) { - final fileName = PrecompileBinaries.fileName(target, artifact); - final signatureFileName = - PrecompileBinaries.signatureFileName(target, artifact); - - final url = Uri.parse('$prefix$crateHash/$fileName'); - final signatureUrl = - Uri.parse('$prefix$crateHash/$signatureFileName'); - - final signature = await get(signatureUrl); - if (signature.statusCode != 200) { - stdout.writeln('MISSING'); - ok = false; - break; - } - final asset = await get(url); - if (asset.statusCode != 200) { - stdout.writeln('MISSING'); - ok = false; - break; - } - - if (!verify(precompiledBinaries.publicKey, asset.bodyBytes, - signature.bodyBytes)) { - stdout.writeln('INVALID SIGNATURE'); - ok = false; - } - } - - if (ok) { - stdout.writeln('OK'); - } - } - } - } -} diff --git a/cargokit/build_tool/pubspec.lock b/cargokit/build_tool/pubspec.lock deleted file mode 100644 index 343bdd3..0000000 --- a/cargokit/build_tool/pubspec.lock +++ /dev/null @@ -1,453 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 - url: "https://pub.dev" - source: hosted - version: "64.0.0" - adaptive_number: - dependency: transitive - description: - name: adaptive_number - sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - args: - dependency: "direct main" - description: - name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - collection: - dependency: "direct main" - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - convert: - dependency: "direct main" - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - coverage: - dependency: transitive - description: - name: coverage - sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" - url: "https://pub.dev" - source: hosted - version: "1.6.3" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" - ed25519_edwards: - dependency: "direct main" - description: - name: ed25519_edwards - sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - file: - dependency: transitive - description: - name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.dev" - source: hosted - version: "6.1.4" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - github: - dependency: "direct main" - description: - name: github - sha256: "9966bc13bf612342e916b0a343e95e5f046c88f602a14476440e9b75d2295411" - url: "https://pub.dev" - source: hosted - version: "9.17.0" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - hex: - dependency: "direct main" - description: - name: hex - sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - http: - dependency: "direct main" - description: - name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.dev" - source: hosted - version: "4.8.1" - lints: - dependency: "direct dev" - description: - name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - logging: - dependency: "direct main" - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.dev" - source: hosted - version: "0.12.16" - meta: - dependency: transitive - description: - name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - mime: - dependency: transitive - description: - name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e - url: "https://pub.dev" - source: hosted - version: "1.0.4" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - path: - dependency: "direct main" - description: - name: path - sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" - url: "https://pub.dev" - source: hosted - version: "1.8.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 - url: "https://pub.dev" - source: hosted - version: "5.4.0" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - shelf: - dependency: transitive - description: - name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e - url: "https://pub.dev" - source: hosted - version: "1.1.2" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" - url: "https://pub.dev" - source: hosted - version: "0.10.12" - source_span: - dependency: "direct main" - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test: - dependency: "direct dev" - description: - name: test - sha256: "9b0dd8e36af4a5b1569029949d50a52cb2a2a2fdaa20cebb96e6603b9ae241f9" - url: "https://pub.dev" - source: hosted - version: "1.24.6" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - test_core: - dependency: transitive - description: - name: test_core - sha256: "4bef837e56375537055fdbbbf6dd458b1859881f4c7e6da936158f77d61ab265" - url: "https://pub.dev" - source: hosted - version: "0.5.6" - toml: - dependency: "direct main" - description: - name: toml - sha256: "157c5dca5160fced243f3ce984117f729c788bb5e475504f3dbcda881accee44" - url: "https://pub.dev" - source: hosted - version: "0.14.0" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - version: - dependency: "direct main" - description: - name: version - sha256: "2307e23a45b43f96469eeab946208ed63293e8afca9c28cd8b5241ff31c55f55" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" - url: "https://pub.dev" - source: hosted - version: "11.9.0" - watcher: - dependency: transitive - description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b - url: "https://pub.dev" - source: hosted - version: "2.4.0" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - yaml: - dependency: "direct main" - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=3.0.0 <4.0.0" diff --git a/cargokit/build_tool/pubspec.yaml b/cargokit/build_tool/pubspec.yaml deleted file mode 100644 index e01aa0a..0000000 --- a/cargokit/build_tool/pubspec.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: build_tool -description: Cargokit build_tool. Facilitates the build of Rust crate during Flutter application build. -publish_to: none -version: 1.0.0 - -environment: - sdk: ">=3.0.0 <4.0.0" - -# Add regular dependencies here. -dependencies: - # these are pinned on purpose because the bundle_tool_runner doesn't have - # pubspec.lock. See run_build_tool.sh - logging: 1.2.0 - path: 1.8.0 - version: 3.0.0 - collection: 1.18.0 - ed25519_edwards: 0.3.1 - hex: 0.2.0 - yaml: 3.1.2 - source_span: 1.10.0 - github: 9.17.0 - args: 2.4.2 - crypto: 3.0.3 - convert: 3.1.1 - http: 1.1.0 - toml: 0.14.0 - -dev_dependencies: - lints: ^2.1.0 - test: ^1.24.0 diff --git a/cargokit/build_tool/test/builder_test.dart b/cargokit/build_tool/test/builder_test.dart deleted file mode 100644 index e92852e..0000000 --- a/cargokit/build_tool/test/builder_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:build_tool/src/builder.dart'; -import 'package:test/test.dart'; - -void main() { - test('parseBuildConfiguration', () { - var b = BuildEnvironment.parseBuildConfiguration('debug'); - expect(b, BuildConfiguration.debug); - - b = BuildEnvironment.parseBuildConfiguration('profile'); - expect(b, BuildConfiguration.profile); - - b = BuildEnvironment.parseBuildConfiguration('release'); - expect(b, BuildConfiguration.release); - - b = BuildEnvironment.parseBuildConfiguration('debug-dev'); - expect(b, BuildConfiguration.debug); - - b = BuildEnvironment.parseBuildConfiguration('profile'); - expect(b, BuildConfiguration.profile); - - b = BuildEnvironment.parseBuildConfiguration('profile-prod'); - expect(b, BuildConfiguration.profile); - - // fallback to release - b = BuildEnvironment.parseBuildConfiguration('unknown'); - expect(b, BuildConfiguration.release); - }); -} diff --git a/cargokit/build_tool/test/cargo_test.dart b/cargokit/build_tool/test/cargo_test.dart deleted file mode 100644 index 00afe29..0000000 --- a/cargokit/build_tool/test/cargo_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:build_tool/src/cargo.dart'; -import 'package:test/test.dart'; - -final _cargoToml = """ -[workspace] - -[profile.release] -lto = true -panic = "abort" -opt-level = "z" -# strip = "symbols" - -[package] -name = "super_native_extensions" -version = "0.1.0" -edition = "2021" -resolver = "2" - -[lib] -crate-type = ["cdylib", "staticlib"] -"""; - -void main() { - test('parseCargoToml', () { - final info = CrateInfo.parseManifest(_cargoToml); - expect(info.packageName, 'super_native_extensions'); - }); -} diff --git a/cargokit/build_tool/test/options_test.dart b/cargokit/build_tool/test/options_test.dart deleted file mode 100644 index 25a85b6..0000000 --- a/cargokit/build_tool/test/options_test.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:build_tool/src/builder.dart'; -import 'package:build_tool/src/options.dart'; -import 'package:hex/hex.dart'; -import 'package:test/test.dart'; -import 'package:yaml/yaml.dart'; - -void main() { - test('parseCargoBuildOptions', () { - final yaml = """ -toolchain: nightly -extra_flags: - - -Z - # Comment here - - build-std=panic_abort,std -"""; - final node = loadYamlNode(yaml); - final options = CargoBuildOptions.parse(node); - expect(options.toolchain, Toolchain.nightly); - expect(options.flags, ['-Z', 'build-std=panic_abort,std']); - }); - - test('parsePrecompiledBinaries', () { - final yaml = """ -url_prefix: https://url-prefix -public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445 -"""; - final precompiledBinaries = PrecompiledBinaries.parse(loadYamlNode(yaml)); - final key = HEX.decode( - 'a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445'); - expect(precompiledBinaries.uriPrefix, 'https://url-prefix'); - expect(precompiledBinaries.publicKey.bytes, key); - }); - - test('parseCargokitOptions', () { - const yaml = ''' -cargo: - # For smalles binaries rebuilt the standard library with panic=abort - debug: - toolchain: nightly - extra_flags: - - -Z - # Comment here - - build-std=panic_abort,std - release: - toolchain: beta - -precompiled_binaries: - url_prefix: https://url-prefix - public_key: a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445 -'''; - final options = CargokitCrateOptions.parse(loadYamlNode(yaml)); - expect(options.precompiledBinaries?.uriPrefix, 'https://url-prefix'); - final key = HEX.decode( - 'a4c3433798eb2c36edf2b94dbb4dd899d57496ca373a8982d8a792410b7f6445'); - expect(options.precompiledBinaries?.publicKey.bytes, key); - - final debugOptions = options.cargo[BuildConfiguration.debug]!; - expect(debugOptions.toolchain, Toolchain.nightly); - expect(debugOptions.flags, ['-Z', 'build-std=panic_abort,std']); - - final releaseOptions = options.cargo[BuildConfiguration.release]!; - expect(releaseOptions.toolchain, Toolchain.beta); - expect(releaseOptions.flags, []); - }); - - test('parseCargokitUserOptions', () { - const yaml = ''' -use_precompiled_binaries: false -verbose_logging: true -'''; - final options = CargokitUserOptions.parse(loadYamlNode(yaml)); - expect(options.usePrecompiledBinaries, false); - expect(options.verboseLogging, true); - }); -} diff --git a/cargokit/build_tool/test/rustup_test.dart b/cargokit/build_tool/test/rustup_test.dart deleted file mode 100644 index af95303..0000000 --- a/cargokit/build_tool/test/rustup_test.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:build_tool/src/rustup.dart'; -import 'package:build_tool/src/util.dart'; -import 'package:test/test.dart'; - -void main() { - test('rustup with no toolchains', () { - bool didListToolchains = false; - bool didInstallStable = false; - bool didListTargets = false; - testRunCommandOverride = (args) { - expect(args.executable, 'rustup'); - switch (args.arguments) { - case ['toolchain', 'list']: - didListToolchains = true; - return TestRunCommandResult(stdout: 'no installed toolchains\n'); - case ['toolchain', 'install', 'stable']: - didInstallStable = true; - return TestRunCommandResult(); - case ['target', 'list', '--toolchain', 'stable', '--installed']: - didListTargets = true; - return TestRunCommandResult( - stdout: 'x86_64-unknown-linux-gnu\nx86_64-apple-darwin\n'); - default: - throw Exception('Unexpected call: ${args.arguments}'); - } - }; - final rustup = Rustup(); - rustup.installToolchain('stable'); - expect(didInstallStable, true); - expect(didListToolchains, true); - expect(didListTargets, true); - expect(rustup.installedTargets('stable'), [ - 'x86_64-unknown-linux-gnu', - 'x86_64-apple-darwin', - ]); - testRunCommandOverride = null; - }); - - test('rustup with esp toolchain', () { - final targetsQueried = []; - testRunCommandOverride = (args) { - expect(args.executable, 'rustup'); - switch (args.arguments) { - case ['toolchain', 'list']: - return TestRunCommandResult( - stdout: 'stable-aarch64-apple-darwin (default)\n' - 'nightly-aarch64-apple-darwin\n' - 'esp\n'); - case ['target', 'list', '--toolchain', String toolchain, '--installed']: - targetsQueried.add(toolchain); - return TestRunCommandResult(stdout: '$toolchain:target\n'); - default: - throw Exception('Unexpected call: ${args.arguments}'); - } - }; - final rustup = Rustup(); - expect(targetsQueried, [ - 'stable-aarch64-apple-darwin', - 'nightly-aarch64-apple-darwin', - ]); - expect(rustup.installedTargets('stable'), - ['stable-aarch64-apple-darwin:target']); - expect(rustup.installedTargets('nightly'), - ['nightly-aarch64-apple-darwin:target']); - }); -} diff --git a/cargokit/cmake/cargokit.cmake b/cargokit/cmake/cargokit.cmake deleted file mode 100644 index ddd05df..0000000 --- a/cargokit/cmake/cargokit.cmake +++ /dev/null @@ -1,99 +0,0 @@ -SET(cargokit_cmake_root "${CMAKE_CURRENT_LIST_DIR}/..") - -# Workaround for https://github.com/dart-lang/pub/issues/4010 -get_filename_component(cargokit_cmake_root "${cargokit_cmake_root}" REALPATH) - -if(WIN32) - # REALPATH does not properly resolve symlinks on windows :-/ - execute_process(COMMAND powershell -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_LIST_DIR}/resolve_symlinks.ps1" "${cargokit_cmake_root}" OUTPUT_VARIABLE cargokit_cmake_root OUTPUT_STRIP_TRAILING_WHITESPACE) -endif() - -# Arguments -# - target: CMAKE target to which rust library is linked -# - manifest_dir: relative path from current folder to directory containing cargo manifest -# - lib_name: cargo package name -# - any_symbol_name: name of any exported symbol from the library. -# used on windows to force linking with library. -function(apply_cargokit target manifest_dir lib_name any_symbol_name) - - set(CARGOKIT_LIB_NAME "${lib_name}") - set(CARGOKIT_LIB_FULL_NAME "${CMAKE_SHARED_MODULE_PREFIX}${CARGOKIT_LIB_NAME}${CMAKE_SHARED_MODULE_SUFFIX}") - if (CMAKE_CONFIGURATION_TYPES) - set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/$") - set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/$/${CARGOKIT_LIB_FULL_NAME}") - else() - set(CARGOKIT_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") - set(OUTPUT_LIB "${CMAKE_CURRENT_BINARY_DIR}/${CARGOKIT_LIB_FULL_NAME}") - endif() - set(CARGOKIT_TEMP_DIR "${CMAKE_CURRENT_BINARY_DIR}/cargokit_build") - - if (FLUTTER_TARGET_PLATFORM) - set(CARGOKIT_TARGET_PLATFORM "${FLUTTER_TARGET_PLATFORM}") - else() - set(CARGOKIT_TARGET_PLATFORM "windows-x64") - endif() - - set(CARGOKIT_ENV - "CARGOKIT_CMAKE=${CMAKE_COMMAND}" - "CARGOKIT_CONFIGURATION=$" - "CARGOKIT_MANIFEST_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${manifest_dir}" - "CARGOKIT_TARGET_TEMP_DIR=${CARGOKIT_TEMP_DIR}" - "CARGOKIT_OUTPUT_DIR=${CARGOKIT_OUTPUT_DIR}" - "CARGOKIT_TARGET_PLATFORM=${CARGOKIT_TARGET_PLATFORM}" - "CARGOKIT_TOOL_TEMP_DIR=${CARGOKIT_TEMP_DIR}/tool" - "CARGOKIT_ROOT_PROJECT_DIR=${CMAKE_SOURCE_DIR}" - ) - - if (WIN32) - set(SCRIPT_EXTENSION ".cmd") - set(IMPORT_LIB_EXTENSION ".lib") - else() - set(SCRIPT_EXTENSION ".sh") - set(IMPORT_LIB_EXTENSION "") - execute_process(COMMAND chmod +x "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}") - endif() - - # Using generators in custom command is only supported in CMake 3.20+ - if (CMAKE_CONFIGURATION_TYPES AND ${CMAKE_VERSION} VERSION_LESS "3.20.0") - foreach(CONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) - add_custom_command( - OUTPUT - "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG}/${CARGOKIT_LIB_FULL_NAME}" - "${CMAKE_CURRENT_BINARY_DIR}/_phony_" - COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} - "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake - VERBATIM - ) - endforeach() - else() - add_custom_command( - OUTPUT - ${OUTPUT_LIB} - "${CMAKE_CURRENT_BINARY_DIR}/_phony_" - COMMAND ${CMAKE_COMMAND} -E env ${CARGOKIT_ENV} - "${cargokit_cmake_root}/run_build_tool${SCRIPT_EXTENSION}" build-cmake - VERBATIM - ) - endif() - - - set_source_files_properties("${CMAKE_CURRENT_BINARY_DIR}/_phony_" PROPERTIES SYMBOLIC TRUE) - - if (TARGET ${target}) - # If we have actual cmake target provided create target and make existing - # target depend on it - add_custom_target("${target}_cargokit" DEPENDS ${OUTPUT_LIB}) - add_dependencies("${target}" "${target}_cargokit") - target_link_libraries("${target}" PRIVATE "${OUTPUT_LIB}${IMPORT_LIB_EXTENSION}") - if(WIN32) - target_link_options(${target} PRIVATE "/INCLUDE:${any_symbol_name}") - endif() - else() - # Otherwise (FFI) just use ALL to force building always - add_custom_target("${target}_cargokit" ALL DEPENDS ${OUTPUT_LIB}) - endif() - - # Allow adding the output library to plugin bundled libraries - set("${target}_cargokit_lib" ${OUTPUT_LIB} PARENT_SCOPE) - -endfunction() diff --git a/cargokit/cmake/resolve_symlinks.ps1 b/cargokit/cmake/resolve_symlinks.ps1 deleted file mode 100644 index 3d10d28..0000000 --- a/cargokit/cmake/resolve_symlinks.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -function Resolve-Symlinks { - [CmdletBinding()] - [OutputType([string])] - param( - [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] - [string] $Path - ) - - [string] $separator = '/' - [string[]] $parts = $Path.Split($separator) - - [string] $realPath = '' - foreach ($part in $parts) { - if ($realPath -and !$realPath.EndsWith($separator)) { - $realPath += $separator - } - $realPath += $part - $item = Get-Item $realPath - if ($item.Target) { - $realPath = $item.Target.Replace('\', '/') - } - } - $realPath -} - -$path=Resolve-Symlinks -Path $args[0] -Write-Host $path diff --git a/cargokit/docs/architecture.md b/cargokit/docs/architecture.md deleted file mode 100644 index d9bcf4e..0000000 --- a/cargokit/docs/architecture.md +++ /dev/null @@ -1,104 +0,0 @@ -# Cargokit Architecture - -Note: This is mostly relevant for plugins authors that want to see a bit under the hood rather then just following a tutorial. - -In ideal conditions the end-developer using the plugin should not even be aware of Cargokit existence. - -## Integration - -Cargokit is meant to be included in Flutter plugin (or application) that contains the Rust crate to be built during the Flutter build process. - -Cargokit can be either incuded as git submodule or git subtree (required for plugins - as pub does not support submodules for git dependencies). - -For a step by step tutorial on integrating Cargokit with a Flutter plugin see https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/. - -## build_tool - -Build tool is the core of cargokit. It is a Dart command line package that facilitates the build of Rust crate. It is invoked during the Flutter build process to build (or download) Rust artifacts, but it can be also used as a standalone tool. - -It handles the following commands: - -### build-cmake - -This is invoked from `cargokit.cmake` and it is used to build the Rust crate into a dynamic library on Linux and Windows (which use CMake as build system). - -The command takes no additional arguments, everything is controlled during environment variables set by `cargokit.cmake`. - -### build-gradle - -This is invoked from `plugin.gradle` and it is used to build the Rust crate into a dynamic library on Android. The command takes no additional arguments, everything is controlled during environment variables set by `plugin.gradle`. - -The build_tool installs NDK if needed, configures the Rust environment for cross compilation and then invokes `cargo build` with appropriate arguments and environment variables. - -The build-tool also acts a linker driver. - -### build-pod - -This is invoked from plugin's podspec `script_phase` through `build_pod.sh`. Bundle tool will build the Rust crate into a static library that gets linked into the plugin Framework. In this case must have `:execution_position` set to `:before_compile`. - -Cargokit will build binaries for all active architectures from XCode build and lipo them togherer. - -When using Cargokit to integrate Rust code with an application (not a plugin) you can also configure the `Cargo.toml` to just build a dynamic library. When Cargokit finds that the crate only built a dylib and no static lib, it will attempt to replace the Cocoapod framework binary with the dylib. In this case the script `:execution_position` must be set to `:after_compile`. This is *not* recommended for plugins and it's quite experimental. - -### gen-key, precompile-binaries, verify-binaries - -These are used as when providing precompiled binaries for Plugin. See [precompiled_binaries.md](precompiled_binaries.md) for more information. - -## Launching the build_tool during build. - -During Flutter build, the build tool can not be launched directly using `dart run`. Rather it is launched through `run_build_tool.sh` and `run_build_tool.cmd`. Because the `build_tool` is shipped as part of plugin, we generally don't want to write into the plugin directory during build, which would happen if the `build_tool` was simply invoked through `dart run` (For example the `.dart_tool/package_config.json` file would get written inside the `build_tool` directory). - -Instead the `run_build_tool` script creates a minimal Dart command line package in the build directory and references the `build_tool` as package. That way the `.dart_tool/package_config.json` file is created in the temporary build folder and not in the plugin itself. The script also precompiles the Dart code to speed up subsequent invocations. - -## Configuring Cargokit - -### Configuration for the Rust crate - -Cargokit can be configured through a `cargokit.yaml` file, which can be used to control the build of the Rust package and is placed into the Rust crate next to `Cargo.toml`. - -Here is an example `cargokit.yaml` with comments: -```yaml -cargo: - debug: # Configuration of cargo execution during debug builds - toolchain: stable # default - release: # Configuration of cargo execution for release builds - toolchain: nightly # rustup will be invoked with nightly toolchain - extra_flags: # extra arguments passed to cargo build - - -Z - - build-std=panic_abort,std - -# If crate ships with precompiled binaries, they can be configured here. -precompiled_binaries: - # Uri prefix used when downloading precompiled binaries. - url_prefix: https://github.com/superlistapp/super_native_extensions/releases/download/precompiled_ - - # Public key for verifying downloaded precompiled binaries. - public_key: 3a257ef1c7d72d84225ac4658d24812ada50a7a7a8a2138c2a91353389fdc514 -``` - -### Configuration for the application consuming the plugin - -A `cargokit_options.yaml` file can also be placed by developer using plugin to the root of the application package. In which case the file can be used to specify following options: - -```yaml -# Enables verbose logging of Cargokit during build -verbose_logging: true - -# Opts out of using precompiled binaries. If crate has configured -# and deployed precompiled binaries, these will be by default used whenever Rustup -# is not installed. With `use_precompiled_binaries` set to false, the build will -# instead be aborted prompting user to install Rustup. -use_precompiled_binaries: false -``` - -## Detecting Rustup - -When the plugin doesn't come with precompiled libraries (or user opt-out), `build_tool` will need to invoke Rustup during build to ensure that required Rust targets and toolchain are installed for current build and to build the Rust crate. - -Cargokit will attempt to detect Rustup in the default Rustup installation location (`~/.cargo/rustup`) as well as in PATH. This is done so that if user install Rustup but doesn't properly configure PATH, Cargokit will still work. - -If `build_tool` doesn't find Rustup, it will about the build with a message showing instructions to install Rustup specific to current platform. - -On macOS it will also detect a homebrew Rust installation in PATH and will prompt user to call `brew unlink rust` first to remove homebrew Rust installation from PATH, because it may interfere with Rustup. - -Homebrew Rust installation can not be used by Cargokit, because it can only build for host platform. Cargokit needs to be able to cross compile the Rust crate for iOS and Android and thus needs full Rustup installation. diff --git a/cargokit/docs/precompiled_binaries.md b/cargokit/docs/precompiled_binaries.md deleted file mode 100644 index 2026e86..0000000 --- a/cargokit/docs/precompiled_binaries.md +++ /dev/null @@ -1,95 +0,0 @@ -# Precompiled Binaries - -Because Cargokit builds the Rust crate during Flutter build, it is inherently -dependend on the Rust toolchain being installed on the developer's machine. - -To decrease the friction, it is possible for Cargokit to use precompiled binaries instead. - -This is how the process of using precompiled binaries looks from the perspective of the build on developer machine: - -1. Cargokit checks if there is `cargokit_options.yaml` file in the root folder of target application. If there is one, it will be checked for `use_precompiled_binaries` options to see if user opted out of using precompiled binaries. In which case Cargokit will insist on building from source. Cargokit will also build from source if the configuration file is absent, but user has Rustup installed. - -2. Cargokit checks if there is `cargokit.yaml` file placed in the Rust crate. If there is one, it will be checked for `precompiled_binaries` section to see if crate supports precompiled binaries. The configuration section must contain a public key and URL prefix. - -3. Cargokit computes a `crate-hash`. This is a SHA256 hash value computed from all Rust files inside crate, `Cargo.toml`, `Cargo.lock` and `cargokit.yaml`. This uniquely identifies the crate and it is used to find the correct precompiled binaries. - -4. Cargokit will attempt to download the precompiled binaries for target platform and `crate_hash` combination and a signature file for each downloaded binary. If download succeeds, the binary content will be verified against the signature and public key included in `cargokit.yaml` (which is part of Rust crate and thus part of published Flutter package). - -5. If the verification succeeds, the precompiled binaries will be used. Otherwise the binary will be discarded and Cargokit will insist on building from source. - -## Providing precompiled binaries - -Note that this assumes that precompiled binaries will be generated during github actions and deployed as github releases. - -### Use `build_tool` to generate a key-pair: - -``` -dart run build_tool gen-key -``` - -This will print the private key and public key. Store the private key securely. It needs to be provided as a secret to github action. - -The public key should be included in `cargokit.yaml` file in the Rust crate. - -### Provide a `cargokit.yaml` file in the Rust crate - -The file must be placed alongside Cargo.toml. - -```yaml -precompiled_binaries: - # Uri prefix used when downloading precompiled binaries. - url_prefix: https://github.com///releases/download/precompiled_ - - # Public key for verifying downloaded precompiled binaries. - public_key: -``` - -### Configure a github action to build and upload precompiled binaries. - -The github action should be run at every commit to main branch (and possibly other branches). - -The action needs two secrets - private key for signing binaries and GitHub token for uploading binaries as releases. Here is example action that precompiles and uploads binaries for all supported targets. - -```yaml -on: - push: - branches: [ main ] - -name: Precompile Binaries - -jobs: - Precompile: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - macOS-latest - - windows-latest - steps: - - uses: actions/checkout@v2 - - uses: dart-lang/setup-dart@v1 - - name: Install GTK - if: (matrix.os == 'ubuntu-latest') - run: sudo apt-get update && sudo apt-get install libgtk-3-dev - - name: Precompile - if: (matrix.os == 'macOS-latest') || (matrix.os == 'windows-latest') - run: dart run build_tool precompile-binaries -v --manifest-dir=../../rust --repository=superlistapp/super_native_extensions - working-directory: super_native_extensions/cargokit/build_tool - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} - PRIVATE_KEY: ${{ secrets.RELEASE_PRIVATE_KEY }} - - name: Precompile (with Android) - if: (matrix.os == 'ubuntu-latest') - run: dart run build_tool precompile-binaries -v --manifest-dir=../../rust --repository=superlistapp/super_native_extensions --android-sdk-location=/usr/local/lib/android/sdk --android-ndk-version=24.0.8215888 --android-min-sdk-version=23 - working-directory: super_native_extensions/cargokit/build_tool - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} - PRIVATE_KEY: ${{ secrets.RELEASE_PRIVATE_KEY }} -``` - -By default the `built_tool precompile-binaries` commands build and uploads the binaries for all targets buildable from current host. This can be overriden using the `--target ` argument. - -Android binaries will be built when `--android-sdk-location` and `--android-ndk-version` arguments are provided. - diff --git a/cargokit/gradle/plugin.gradle b/cargokit/gradle/plugin.gradle deleted file mode 100644 index 37dd086..0000000 --- a/cargokit/gradle/plugin.gradle +++ /dev/null @@ -1,176 +0,0 @@ -import java.nio.file.Paths -import org.apache.tools.ant.taskdefs.condition.Os - -CargoKitPlugin.file = buildscript.sourceFile - -apply plugin: CargoKitPlugin - -class CargoKitExtension { - String manifestDir; // Relative path to folder containing Cargo.toml - String libname; // Library name within Cargo.toml. Must be a cdylib -} - -abstract class CargoKitBuildTask extends DefaultTask { - - @Input - String buildMode - - @Input - String buildDir - - @Input - String outputDir - - @Input - String ndkVersion - - @Input - String sdkDirectory - - @Input - int compileSdkVersion; - - @Input - int minSdkVersion; - - @Input - String pluginFile - - @Input - List targetPlatforms - - @TaskAction - def build() { - if (project.cargokit.manifestDir == null) { - throw new GradleException("Property 'manifestDir' must be set on cargokit extension"); - } - - if (project.cargokit.libname == null) { - throw new GradleException("Property 'libname' must be set on cargokit extension"); - } - - def executableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "run_build_tool.cmd" : "run_build_tool.sh" - def path = Paths.get(new File(pluginFile).parent, "..", executableName); - - def manifestDir = Paths.get(project.buildscript.sourceFile.parent, project.cargokit.manifestDir) - - def rootProjectDir = project.rootProject.projectDir - - if (!Os.isFamily(Os.FAMILY_WINDOWS)) { - project.exec { - commandLine 'chmod', '+x', path - } - } - - project.exec { - executable path - args "build-gradle" - environment "CARGOKIT_ROOT_PROJECT_DIR", rootProjectDir - environment "CARGOKIT_TOOL_TEMP_DIR", "${buildDir}/build_tool" - environment "CARGOKIT_MANIFEST_DIR", manifestDir - environment "CARGOKIT_CONFIGURATION", buildMode - environment "CARGOKIT_TARGET_TEMP_DIR", buildDir - environment "CARGOKIT_OUTPUT_DIR", outputDir - environment "CARGOKIT_NDK_VERSION", ndkVersion - environment "CARGOKIT_SDK_DIR", sdkDirectory - environment "CARGOKIT_COMPILE_SDK_VERSION", compileSdkVersion - environment "CARGOKIT_MIN_SDK_VERSION", minSdkVersion - environment "CARGOKIT_TARGET_PLATFORMS", targetPlatforms.join(",") - environment "CARGOKIT_JAVA_HOME", System.properties['java.home'] - } - } -} - -class CargoKitPlugin implements Plugin { - - static String file; - - private Plugin findFlutterPlugin(Project rootProject) { - _findFlutterPlugin(rootProject.childProjects) - } - - private Plugin _findFlutterPlugin(Map projects) { - for (project in projects) { - for (plugin in project.value.getPlugins()) { - if (plugin.class.name == "FlutterPlugin") { - return plugin; - } - } - def plugin = _findFlutterPlugin(project.value.childProjects); - if (plugin != null) { - return plugin; - } - } - return null; - } - - @Override - void apply(Project project) { - def plugin = findFlutterPlugin(project.rootProject); - - project.extensions.create("cargokit", CargoKitExtension) - - if (plugin == null) { - print("Flutter plugin not found, CargoKit plugin will not be applied.") - return; - } - - def cargoBuildDir = "${project.buildDir}/build" - - // Determine if the project is an application or library - def isApplication = plugin.project.plugins.hasPlugin('com.android.application') - def variants = isApplication ? plugin.project.android.applicationVariants : plugin.project.android.libraryVariants - - variants.all { variant -> - - final buildType = variant.buildType.name - - def cargoOutputDir = "${project.buildDir}/jniLibs/${buildType}"; - def jniLibs = project.android.sourceSets.maybeCreate(buildType).jniLibs; - jniLibs.srcDir(new File(cargoOutputDir)) - - def platforms = plugin.getTargetPlatforms().collect() - - // Same thing addFlutterDependencies does in flutter.gradle - if (buildType == "debug") { - platforms.add("android-x86") - platforms.add("android-x64") - } - - // The task name depends on plugin properties, which are not available - // at this point - project.getGradle().afterProject { - def taskName = "cargokitCargoBuild${project.cargokit.libname.capitalize()}${buildType.capitalize()}"; - - if (project.tasks.findByName(taskName)) { - return - } - - if (plugin.project.android.ndkVersion == null) { - throw new GradleException("Please set 'android.ndkVersion' in 'app/build.gradle'.") - } - - def task = project.tasks.create(taskName, CargoKitBuildTask.class) { - buildMode = variant.buildType.name - buildDir = cargoBuildDir - outputDir = cargoOutputDir - ndkVersion = plugin.project.android.ndkVersion - sdkDirectory = plugin.project.android.sdkDirectory - minSdkVersion = plugin.project.android.defaultConfig.minSdkVersion.apiLevel as int - compileSdkVersion = plugin.project.android.compileSdkVersion.substring(8) as int - targetPlatforms = platforms - pluginFile = CargoKitPlugin.file - } - def onTask = { newTask -> - if (newTask.name == "merge${buildType.capitalize()}NativeLibs") { - newTask.dependsOn task - // Fix gradle 7.4.2 not picking up JNI library changes - newTask.outputs.upToDateWhen { false } - } - } - project.tasks.each onTask - project.tasks.whenTaskAdded onTask - } - } - } -} diff --git a/cargokit/run_build_tool.cmd b/cargokit/run_build_tool.cmd deleted file mode 100644 index c45d0aa..0000000 --- a/cargokit/run_build_tool.cmd +++ /dev/null @@ -1,91 +0,0 @@ -@echo off -setlocal - -setlocal ENABLEDELAYEDEXPANSION - -SET BASEDIR=%~dp0 - -if not exist "%CARGOKIT_TOOL_TEMP_DIR%" ( - mkdir "%CARGOKIT_TOOL_TEMP_DIR%" -) -cd /D "%CARGOKIT_TOOL_TEMP_DIR%" - -SET BUILD_TOOL_PKG_DIR=%BASEDIR%build_tool -SET DART=%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dart - -set BUILD_TOOL_PKG_DIR_POSIX=%BUILD_TOOL_PKG_DIR:\=/% - -( - echo name: build_tool_runner - echo version: 1.0.0 - echo publish_to: none - echo. - echo environment: - echo sdk: '^>=3.0.0 ^<4.0.0' - echo. - echo dependencies: - echo build_tool: - echo path: %BUILD_TOOL_PKG_DIR_POSIX% -) >pubspec.yaml - -if not exist bin ( - mkdir bin -) - -( - echo import 'package:build_tool/build_tool.dart' as build_tool; - echo void main^(List^ args^) ^{ - echo build_tool.runMain^(args^); - echo ^} -) >bin\build_tool_runner.dart - -SET PRECOMPILED=bin\build_tool_runner.dill - -REM To detect changes in package we compare output of DIR /s (recursive) -set PREV_PACKAGE_INFO=.dart_tool\package_info.prev -set CUR_PACKAGE_INFO=.dart_tool\package_info.cur - -DIR "%BUILD_TOOL_PKG_DIR%" /s > "%CUR_PACKAGE_INFO%_orig" - -REM Last line in dir output is free space on harddrive. That is bound to -REM change between invocation so we need to remove it -( - Set "Line=" - For /F "UseBackQ Delims=" %%A In ("%CUR_PACKAGE_INFO%_orig") Do ( - SetLocal EnableDelayedExpansion - If Defined Line Echo !Line! - EndLocal - Set "Line=%%A") -) >"%CUR_PACKAGE_INFO%" -DEL "%CUR_PACKAGE_INFO%_orig" - -REM Compare current directory listing with previous -FC /B "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" > nul 2>&1 - -If %ERRORLEVEL% neq 0 ( - REM Changed - copy current to previous and remove precompiled kernel - if exist "%PREV_PACKAGE_INFO%" ( - DEL "%PREV_PACKAGE_INFO%" - ) - MOVE /Y "%CUR_PACKAGE_INFO%" "%PREV_PACKAGE_INFO%" - if exist "%PRECOMPILED%" ( - DEL "%PRECOMPILED%" - ) -) - -REM There is no CUR_PACKAGE_INFO it was renamed in previous step to %PREV_PACKAGE_INFO% -REM which means we need to do pub get and precompile -if not exist "%PRECOMPILED%" ( - echo Running pub get in "%cd%" - "%DART%" pub get --no-precompile - "%DART%" compile kernel bin/build_tool_runner.dart -) - -"%DART%" "%PRECOMPILED%" %* - -REM 253 means invalid snapshot version. -If %ERRORLEVEL% equ 253 ( - "%DART%" pub get --no-precompile - "%DART%" compile kernel bin/build_tool_runner.dart - "%DART%" "%PRECOMPILED%" %* -) diff --git a/cargokit/run_build_tool.sh b/cargokit/run_build_tool.sh deleted file mode 100755 index 24b0ed8..0000000 --- a/cargokit/run_build_tool.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash - -set -e - -BASEDIR=$(dirname "$0") - -mkdir -p "$CARGOKIT_TOOL_TEMP_DIR" - -cd "$CARGOKIT_TOOL_TEMP_DIR" - -# Write a very simple bin package in temp folder that depends on build_tool package -# from Cargokit. This is done to ensure that we don't pollute Cargokit folder -# with .dart_tool contents. - -BUILD_TOOL_PKG_DIR="$BASEDIR/build_tool" - -if [[ -z $FLUTTER_ROOT ]]; then # not defined - DART=dart -else - DART="$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dart" -fi - -cat << EOF > "pubspec.yaml" -name: build_tool_runner -version: 1.0.0 -publish_to: none - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - build_tool: - path: "$BUILD_TOOL_PKG_DIR" -EOF - -mkdir -p "bin" - -cat << EOF > "bin/build_tool_runner.dart" -import 'package:build_tool/build_tool.dart' as build_tool; -void main(List args) { - build_tool.runMain(args); -} -EOF - -# Create alias for `shasum` if it does not exist and `sha1sum` exists -if ! [ -x "$(command -v shasum)" ] && [ -x "$(command -v sha1sum)" ]; then - shopt -s expand_aliases - alias shasum="sha1sum" -fi - -# Dart run will not cache any package that has a path dependency, which -# is the case for our build_tool_runner. So instead we precompile the package -# ourselves. -# To invalidate the cached kernel we use the hash of ls -LR of the build_tool -# package directory. This should be good enough, as the build_tool package -# itself is not meant to have any path dependencies. - -if [[ "$OSTYPE" == "darwin"* ]]; then - PACKAGE_HASH=$(ls -lTR "$BUILD_TOOL_PKG_DIR" | shasum) -else - PACKAGE_HASH=$(ls -lR --full-time "$BUILD_TOOL_PKG_DIR" | shasum) -fi - -PACKAGE_HASH_FILE=".package_hash" - -if [ -f "$PACKAGE_HASH_FILE" ]; then - EXISTING_HASH=$(cat "$PACKAGE_HASH_FILE") - if [ "$PACKAGE_HASH" != "$EXISTING_HASH" ]; then - rm "$PACKAGE_HASH_FILE" - fi -fi - -# Run pub get if needed. -if [ ! -f "$PACKAGE_HASH_FILE" ]; then - "$DART" pub get --no-precompile - "$DART" compile kernel bin/build_tool_runner.dart - echo "$PACKAGE_HASH" > "$PACKAGE_HASH_FILE" -fi - -# Rebuild the tool if it was deleted by Android Studio -if [ ! -f "bin/build_tool_runner.dill" ]; then - "$DART" compile kernel bin/build_tool_runner.dart -fi - -set +e - -"$DART" bin/build_tool_runner.dill "$@" - -exit_code=$? - -# 253 means invalid snapshot version. -if [ $exit_code == 253 ]; then - "$DART" pub get --no-precompile - "$DART" compile kernel bin/build_tool_runner.dart - "$DART" bin/build_tool_runner.dill "$@" - exit_code=$? -fi - -exit $exit_code diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 61b6c4d..641939d 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -7,6 +7,14 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + exclude: + - build/** + - android/** + - ios/** + - windows/** + - macos/** + - linux/** include: package:flutter_lints/flutter.yaml linter: diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle deleted file mode 100644 index 0663bbb..0000000 --- a/example/android/app/build.gradle +++ /dev/null @@ -1,72 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - namespace "com.example.frostdart_example" - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.frostdart_example" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts new file mode 100644 index 0000000..5eb5fd0 --- /dev/null +++ b/example/android/app/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.frostdart_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.frostdart_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + // Uses the version code from pubspec.yaml. When using split APKs, 1000 * ABI_VERSION + // is added automatically by Flutter. (https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions) + // You can force using the value of versionCode by specifying the `-P force-version-code-ignoring-abi=true` + // flag during build. + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index ab4c41f..399f698 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - +