diff --git a/.deny.toml b/.deny.toml index 3c079cfa..b02f2046 100644 --- a/.deny.toml +++ b/.deny.toml @@ -61,7 +61,7 @@ allow-git = [ "https://github.com/logos-blockchain/logos-blockchain-circuits.git", "https://github.com/logos-blockchain/logos-blockchain-rust-rapidsnark.git", "https://github.com/logos-blockchain/sponges", - "https://github.com/arkworks-rs/spongefish.git" + "https://github.com/arkworks-rs/spongefish.git", ] unknown-git = "deny" unknown-registry = "deny" diff --git a/.github/actions/install-logos-blockchain-circuits/action.yaml b/.github/actions/install-logos-blockchain-circuits/action.yaml deleted file mode 100644 index 361ace65..00000000 --- a/.github/actions/install-logos-blockchain-circuits/action.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: Setup Logos Blockchain Circuits - -description: Set up Logos Blockchain Circom Circuits, Rapidsnark prover and Rapidsnark verifier using the setup-logos-blockchain-circuits.sh script. - -inputs: - github-token: - description: GitHub token for downloading releases - required: true - -runs: - using: "composite" - steps: - - name: Setup logos-blockchain-circuits - shell: bash - working-directory: ${{ github.workspace }} - env: - GITHUB_TOKEN: ${{ inputs.github-token }} - run: | - curl -sSL https://raw.githubusercontent.com/logos-blockchain/logos-blockchain/6ac348bea4160ca708b70a86b3964e9f1ce82fff/scripts/setup-logos-blockchain-circuits.sh | bash \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40a0a84c..ed0de65d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ on: push: branches: - main + - dev paths-ignore: - "**.md" - "!.github/workflows/*.yml" @@ -94,7 +95,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - name: Lint workspace env: @@ -104,7 +105,7 @@ jobs: - name: Lint programs env: RISC0_SKIP_BUILD: "1" - run: cargo clippy -p "*programs" -- -D warnings + run: cargo clippy -p "*program" -- -D warnings unit-tests: runs-on: ubuntu-latest @@ -125,7 +126,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - name: Install nextest run: cargo install --locked cargo-nextest @@ -156,7 +157,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - name: Install nextest run: cargo install --locked cargo-nextest @@ -245,7 +246,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - name: Test valid proof env: @@ -268,7 +269,7 @@ jobs: uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-cache - save-if: ${{ github.ref == 'refs/heads/main' }} + save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }} - name: Install just run: cargo install --locked just diff --git a/.github/workflows/pr-review-board.yml b/.github/workflows/pr-review-board.yml new file mode 100644 index 00000000..be19e43d --- /dev/null +++ b/.github/workflows/pr-review-board.yml @@ -0,0 +1,118 @@ +name: PR review board + +# Keeps the org "LEZ PR Review Queue" board in sync for this repo. +# Adds each PR to the board and sets its Status column, enforcing the +# team rule that Approved means 2+ approvals. + +on: + pull_request: + types: [opened, reopened, ready_for_review, converted_to_draft, closed, labeled, unlabeled] + pull_request_review: + types: [submitted, dismissed] + +permissions: + contents: read + pull-requests: read + +concurrency: + group: pr-review-board-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + sync: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.PROJECT_PAT }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + PROJECT_ID: PVT_kwDODrxXXM4BbXZ_ + STATUS_FIELD: PVTSSF_lADODrxXXM4BbXZ_zhWIZnU + OPT_DRAFT: f735bdac + OPT_NEEDS: cbc7321d + OPT_CHANGES: 302bbbd6 + OPT_APPROVED: c9e0743e + PRIORITY_FIELD: PVTSSF_lADODrxXXM4BbXZ_zhWIyPA + PRIO_URGENT: b72854fd + PRIO_HIGH: 5e87ebf9 + PRIO_MEDIUM: 22367611 + PRIO_LOW: 41f60140 + steps: + - name: Sync PR status to board + run: | + set -euo pipefail + + DATA=$(gh pr view "$PR" --repo "$REPO" --json id,isDraft,mergedAt,state,latestReviews,labels) + PR_NODE=$(echo "$DATA" | jq -r '.id') + IS_DRAFT=$(echo "$DATA" | jq -r '.isDraft') + MERGED=$(echo "$DATA" | jq -r '(.mergedAt != null)') + STATE=$(echo "$DATA" | jq -r '.state') + NAP=$(echo "$DATA" | jq '[.latestReviews[] | select(.state=="APPROVED")] | length') + NCH=$(echo "$DATA" | jq '[.latestReviews[] | select(.state=="CHANGES_REQUESTED")] | length') + + if [ "$STATE" = "CLOSED" ] && [ "$MERGED" != "true" ]; then + echo "PR #$PR closed unmerged; leaving board entry untouched." + exit 0 + fi + + # Add to the project (idempotent: returns existing item if already present) + ITEM=$(gh api graphql -f query=' + mutation($p:ID!, $c:ID!) { + addProjectV2ItemById(input:{projectId:$p, contentId:$c}) { item { id } } + }' -f p="$PROJECT_ID" -f c="$PR_NODE" --jq '.data.addProjectV2ItemById.item.id') + + # Merged PRs are archived off the active board + if [ "$MERGED" = "true" ]; then + gh api graphql -f query=' + mutation($p:ID!, $i:ID!) { + archiveProjectV2Item(input:{projectId:$p, itemId:$i}) { item { id } } + }' -f p="$PROJECT_ID" -f i="$ITEM" + echo "$REPO#$PR merged -> archived" + exit 0 + fi + + # Status + if [ "$IS_DRAFT" = "true" ]; then + OPT=$OPT_DRAFT; LABEL="Draft" + elif [ "$NCH" -gt 0 ]; then + OPT=$OPT_CHANGES; LABEL="Changes requested" + elif [ "$NAP" -ge 2 ]; then + OPT=$OPT_APPROVED; LABEL="Approved" + else + OPT=$OPT_NEEDS; LABEL="Needs review" + fi + + gh api graphql -f query=' + mutation($p:ID!, $i:ID!, $f:ID!, $o:String!) { + updateProjectV2ItemFieldValue(input:{ + projectId:$p, itemId:$i, fieldId:$f, + value:{ singleSelectOptionId:$o } + }) { projectV2Item { id } } + }' -f p="$PROJECT_ID" -f i="$ITEM" -f f="$STATUS_FIELD" -f o="$OPT" + + # Set Priority from a priority:* label (clear it when none is present) + LABELS=$(echo "$DATA" | jq -r '.labels[].name') + case "$LABELS" in + *priority:urgent*) POPT=$PRIO_URGENT ;; + *priority:high*) POPT=$PRIO_HIGH ;; + *priority:medium*) POPT=$PRIO_MEDIUM ;; + *priority:low*) POPT=$PRIO_LOW ;; + *) POPT="" ;; + esac + if [ -n "$POPT" ]; then + gh api graphql -f query=' + mutation($p:ID!, $i:ID!, $f:ID!, $o:String!) { + updateProjectV2ItemFieldValue(input:{ + projectId:$p, itemId:$i, fieldId:$f, + value:{ singleSelectOptionId:$o } + }) { projectV2Item { id } } + }' -f p="$PROJECT_ID" -f i="$ITEM" -f f="$PRIORITY_FIELD" -f o="$POPT" + else + gh api graphql -f query=' + mutation($p:ID!, $i:ID!, $f:ID!) { + clearProjectV2ItemFieldValue(input:{ + projectId:$p, itemId:$i, fieldId:$f + }) { projectV2Item { id } } + }' -f p="$PROJECT_ID" -f i="$ITEM" -f f="$PRIORITY_FIELD" + fi + + echo "$REPO#$PR (approvals=$NAP changes=$NCH) -> $LABEL" diff --git a/.github/workflows/publish_images.yml b/.github/workflows/publish_images.yml index bfddda6b..28abb67c 100644 --- a/.github/workflows/publish_images.yml +++ b/.github/workflows/publish_images.yml @@ -7,7 +7,38 @@ on: - "v*" jobs: + # Shared base (toolchain + r0vm), single source of truth in + # lez/docker/risc0-base.Dockerfile. Built and pushed once so the service + # builds below can pull it as the `risc0_base` named context. The + # docker-container builder resolves named contexts from the registry (not the + # host image store), so the base must be pushed, not just loaded. + risc0_base: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to registry + uses: docker/login-action@v3 + with: + registry: ${{ secrets.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push risc0 base image + uses: docker/build-push-action@v5 + with: + context: . + file: ./lez/docker/risc0-base.Dockerfile + push: true + tags: ${{ secrets.DOCKER_REGISTRY }}/${{ github.repository }}/risc0_base:sha-${{ github.sha }} + cache-from: type=gha,scope=risc0-base + cache-to: type=gha,mode=max,scope=risc0-base + publish: + needs: risc0_base runs-on: ubuntu-latest strategy: matrix: @@ -62,5 +93,6 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: ${{ matrix.build_args }} + build-contexts: risc0_base=docker-image://${{ secrets.DOCKER_REGISTRY }}/${{ github.repository }}/risc0_base:sha-${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 4605856c..4c1a18c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,25 @@ .gitconfig + res/ target/ deps/ data/ + .idea/ .vscode/ -rocksdb + +rocksdb* sequencer/service/data/ storage.json + result + wallet-ffi/wallet_ffi.h bedrock_signing_key integration_tests/configs/debug/ venv/ + keycard_wallet/python/__pycache__/ keycard_wallet/python/keycard-py/ + +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 284e3798..b15693b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,12 +57,16 @@ Before merging a PR, consider squashing non-meaningful commits. E.g.: Could be squashed to an empty commit if they belong to the same PR. +## Default branch + +By default all PRs must be directed into the `dev` branch. This helps us to keep releases stable. + ## Branch workflow -When bringing your feature branch up to date, prefer rebasing on top of `main`. +When bringing your feature branch up to date, prefer rebasing on top of `dev`. -- Preferred: `git rebase main` -- Avoid: `git merge main` in feature branches +- Preferred: `git rebase dev` +- Avoid: `git merge dev` in feature branches This keeps commit history cleaner and makes reviews easier. diff --git a/Cargo.lock b/Cargo.lock index 2605a2cc..c22d8bda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,6 +123,7 @@ dependencies = [ "amm_core", "lee", "lee_core", + "programs", "token_core", ] @@ -205,9 +206,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arbitrary" @@ -513,6 +514,24 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156" +[[package]] +name = "associated_token_account_core" +version = "0.1.0" +dependencies = [ + "lee_core", + "risc0-zkvm", + "serde", +] + +[[package]] +name = "associated_token_account_program" +version = "0.1.0" +dependencies = [ + "associated_token_account_core", + "lee_core", + "token_core", +] + [[package]] name = "astral-tokio-tar" version = "0.6.2" @@ -665,24 +684,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "ata_core" -version = "0.1.0" -dependencies = [ - "lee_core", - "risc0-zkvm", - "serde", -] - -[[package]] -name = "ata_program" -version = "0.1.0" -dependencies = [ - "ata_core", - "lee_core", - "token_core", -] - [[package]] name = "atomic-polyfill" version = "1.0.3" @@ -758,6 +759,14 @@ dependencies = [ "serde", ] +[[package]] +name = "authenticated_transfer_program" +version = "0.1.0" +dependencies = [ + "authenticated_transfer_core", + "lee_core", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -975,9 +984,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitcoin_hashes" -version = "0.14.100" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +checksum = "4ed83caece3afc59919481b33b472e1432d1abc4641ed9100be142ef5110b406" dependencies = [ "hex-conservative", ] @@ -1145,6 +1154,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bridge_program" +version = "0.1.0" +dependencies = [ + "authenticated_transfer_core", + "bridge_core", + "lee_core", + "vault_core", +] + [[package]] name = "bs58" version = "0.5.1" @@ -1154,6 +1173,14 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "build_utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "risc0-binfmt", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -1452,6 +1479,14 @@ dependencies = [ "lee_core", ] +[[package]] +name = "clock_program" +version = "0.1.0" +dependencies = [ + "clock_core", + "lee_core", +] + [[package]] name = "cmov" version = "0.5.4" @@ -1514,9 +1549,11 @@ dependencies = [ "lee_core", "log", "logos-blockchain-common-http-client", + "programs", "serde", "serde_with", "sha2", + "system_accounts", "thiserror 2.0.18", ] @@ -1932,7 +1969,7 @@ version = "0.1.0" dependencies = [ "amm_core", "anyhow", - "ata_core", + "associated_token_account_core", "authenticated_transfer_core", "borsh", "clap", @@ -1940,9 +1977,11 @@ dependencies = [ "criterion", "lee", "lee_core", + "programs", "risc0-zkvm", "serde", "serde_json", + "test_programs", "token_core", ] @@ -2038,7 +2077,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 2.0.117", + "syn 1.0.109", ] [[package]] @@ -2640,6 +2679,16 @@ dependencies = [ "serde", ] +[[package]] +name = "faucet_program" +version = "0.1.0" +dependencies = [ + "authenticated_transfer_core", + "faucet_core", + "lee_core", + "vault_core", +] + [[package]] name = "fd-lock" version = "4.0.4" @@ -3799,6 +3848,7 @@ name = "indexer_core" version = "0.1.0" dependencies = [ "anyhow", + "arc-swap", "async-stream", "authenticated_transfer_core", "borsh", @@ -3823,16 +3873,15 @@ dependencies = [ name = "indexer_ffi" version = "0.1.0" dependencies = [ - "anyhow", "cbindgen", - "indexer_service", + "env_logger", + "futures", + "indexer_core", "indexer_service_protocol", - "indexer_service_rpc", - "jsonrpsee", "lee", "log", + "serde_json", "tokio", - "url", ] [[package]] @@ -3959,7 +4008,7 @@ name = "integration_tests" version = "0.1.0" dependencies = [ "anyhow", - "ata_core", + "associated_token_account_core", "authenticated_transfer_core", "borsh", "bridge_core", @@ -3980,12 +4029,15 @@ dependencies = [ "logos-blockchain-key-management-system-service", "logos-blockchain-zone-sdk", "num-bigint 0.4.6", + "programs", "reqwest", "sequencer_core", "sequencer_service_rpc", "serde_json", + "system_accounts", "tempfile", "test_fixtures", + "test_programs", "token_core", "tokio", "vault_core", @@ -4501,10 +4553,8 @@ dependencies = [ "anyhow", "authenticated_transfer_core", "borsh", - "bridge_core", - "clock_core", + "build_utils", "env_logger", - "faucet_core", "hex", "hex-literal 1.1.0", "k256", @@ -4512,13 +4562,12 @@ dependencies = [ "log", "rand 0.8.6", "risc0-binfmt", - "risc0-build", "risc0-zkvm", "serde", "serde_with", "sha2", "test-case", - "test_program_methods", + "test_methods", "thiserror 2.0.18", "token_core", ] @@ -7112,6 +7161,23 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pinata_program" +version = "0.1.0" +dependencies = [ + "lee_core", + "risc0-zkvm", +] + +[[package]] +name = "pinata_token_program" +version = "0.1.0" +dependencies = [ + "lee_core", + "risc0-zkvm", + "token_core", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -7265,6 +7331,14 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "privacy_preserving_circuit_program" +version = "0.1.0" +dependencies = [ + "lee_core", + "risc0-zkvm", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -7366,28 +7440,22 @@ dependencies = [ "wallet", ] -[[package]] -name = "program_methods" -version = "0.1.0" -dependencies = [ - "risc0-build", -] - [[package]] name = "programs" version = "0.1.0" dependencies = [ "amm_core", "amm_program", - "ata_core", - "ata_program", + "associated_token_account_core", + "associated_token_account_program", "authenticated_transfer_core", "bridge_core", + "build_utils", "clock_core", "faucet_core", + "lee", "lee_core", "risc0-zkvm", - "serde", "token_core", "token_program", "vault_core", @@ -7610,9 +7678,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "bytes", "getrandom 0.3.4", @@ -8842,14 +8910,17 @@ dependencies = [ "logos-blockchain-zone-sdk", "mempool", "num-bigint 0.4.6", + "programs", "rand 0.8.6", "risc0-zkvm", "serde", "serde_json", "storage", + "system_accounts", "tempfile", - "test_program_methods", + "test_programs", "testnet_initial_state", + "token_core", "tokio", "url", "vault_core", @@ -8870,6 +8941,7 @@ dependencies = [ "lee", "log", "mempool", + "programs", "sequencer_core", "sequencer_service_protocol", "sequencer_service_rpc", @@ -8882,8 +8954,10 @@ name = "sequencer_service_protocol" version = "0.1.0" dependencies = [ "common", + "hex", "lee", "lee_core", + "serde_with", ] [[package]] @@ -9388,7 +9462,9 @@ dependencies = [ "borsh", "common", "lee", + "programs", "rocksdb", + "system_accounts", "tempfile", "thiserror 2.0.18", ] @@ -9541,6 +9617,17 @@ dependencies = [ "libc", ] +[[package]] +name = "system_accounts" +version = "0.1.0" +dependencies = [ + "bridge_core", + "clock_core", + "faucet_core", + "lee_core", + "programs", +] + [[package]] name = "tachys" version = "0.2.15" @@ -9663,6 +9750,7 @@ dependencies = [ "lee", "lee_core", "log", + "programs", "sequencer_core", "sequencer_service", "sequencer_service_rpc", @@ -9676,14 +9764,23 @@ dependencies = [ ] [[package]] -name = "test_program_methods" +name = "test_methods" version = "0.1.0" dependencies = [ "risc0-build", ] [[package]] -name = "test_programs" +name = "test_methods_guests" +version = "0.1.0" +dependencies = [ + "lee_core", + "risc0-zkvm", + "serde", +] + +[[package]] +name = "test_program_guests" version = "0.1.0" dependencies = [ "authenticated_transfer_core", @@ -9691,7 +9788,14 @@ dependencies = [ "faucet_core", "lee_core", "risc0-zkvm", - "serde", +] + +[[package]] +name = "test_programs" +version = "0.1.0" +dependencies = [ + "lee", + "risc0-build", ] [[package]] @@ -9731,11 +9835,12 @@ dependencies = [ name = "testnet_initial_state" version = "0.1.0" dependencies = [ - "common", "key_protocol", "lee", "lee_core", + "programs", "serde", + "system_accounts", ] [[package]] @@ -10693,6 +10798,15 @@ dependencies = [ "serde", ] +[[package]] +name = "vault_program" +version = "0.1.0" +dependencies = [ + "authenticated_transfer_core", + "lee_core", + "vault_core", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -10721,8 +10835,8 @@ version = "0.1.0" dependencies = [ "amm_core", "anyhow", + "associated_token_account_core", "async-stream", - "ata_core", "authenticated_transfer_core", "base58", "bincode", @@ -10744,6 +10858,7 @@ dependencies = [ "lee_core", "log", "optfield", + "programs", "pyo3", "rand 0.8.6", "rpassword", @@ -10751,6 +10866,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "system_accounts", "tempfile", "testnet_initial_state", "thiserror 2.0.18", @@ -10765,16 +10881,19 @@ dependencies = [ name = "wallet-ffi" version = "0.1.0" dependencies = [ + "bip39", "cbindgen", "common", "key_protocol", "lee", "lee_core", + "programs", "risc0-zkvm", "sequencer_service_rpc", "serde_json", "tempfile", "tokio", + "vault_core", "wallet", ] diff --git a/Cargo.toml b/Cargo.toml index 841eecb7..34333b5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,25 +5,17 @@ license = "MIT or Apache-2.0" resolver = "3" members = [ "integration_tests", + "lez/storage", "lee/key_protocol", "lee/state_machine", "lee/state_machine/core", - "lez/mempool", - "lez/wallet", - "lez/wallet-ffi", - "lez/common", - "programs/amm/core", - "programs/amm", - "programs/clock/core", - "programs/token/core", - "programs/token", - "programs/associated_token_account/core", - "programs/associated_token_account", - "programs/authenticated_transfer/core", - "programs/faucet/core", - "programs/bridge/core", - "programs/vault/core", + "lee/privacy_preserving_circuit", + "lee/state_machine/test_methods", + "lee/state_machine/test_methods/guest", + + "lez", + "lez/system_accounts", "lez/sequencer/core", "lez/sequencer/service", "lez/sequencer/service/protocol", @@ -33,18 +25,36 @@ members = [ "lez/indexer/service/protocol", "lez/indexer/service/rpc", "lez/explorer_service", - "program_methods", - "program_methods/guest", - "test_program_methods", - "test_program_methods/guest", + "lez/testnet_initial_state", + "lez/indexer/ffi", + "lez/keycard_wallet", + "lez/mempool", + "lez/wallet", + "lez/wallet-ffi", + "lez/common", + "lez/programs", + "lez/programs/amm", + "lez/programs/associated_token_account", + "lez/programs/authenticated_transfer", + "lez/programs/bridge", + "lez/programs/clock", + "lez/programs/faucet", + "lez/programs/pinata", + "lez/programs/pinata_token", + "lez/programs/token", + "lez/programs/vault", + + "test_programs", + "test_programs/guest", + "examples/program_deployment", "examples/program_deployment/methods", "examples/program_deployment/methods/guest", - "lez/testnet_initial_state", - "lez/indexer/ffi", - "lez", - "lez/keycard_wallet", + + "build_utils", + "test_fixtures", + "tools/cycle_bench", "tools/crypto_primitives_bench", "tools/integration_bench", @@ -69,18 +79,23 @@ wallet = { path = "lez/wallet" } wallet-ffi = { path = "lez/wallet-ffi", default-features = false } indexer_ffi = { path = "lez/indexer/ffi" } lez = { path = "lez" } -clock_core = { path = "programs/clock/core" } -token_core = { path = "programs/token/core" } -token_program = { path = "programs/token" } -amm_core = { path = "programs/amm/core" } -amm_program = { path = "programs/amm" } -ata_core = { path = "programs/associated_token_account/core" } -ata_program = { path = "programs/associated_token_account" } -authenticated_transfer_core = { path = "programs/authenticated_transfer/core" } -faucet_core = { path = "programs/faucet/core" } -bridge_core = { path = "programs/bridge/core" } -vault_core = { path = "programs/vault/core" } -test_program_methods = { path = "test_program_methods" } +programs = { path = "lez/programs", default-features = false, features = [ + "artifacts", +] } +system_accounts = { path = "lez/system_accounts" } +clock_core = { path = "lez/programs/clock/core" } +token_core = { path = "lez/programs/token/core" } +token_program = { path = "lez/programs/token" } +amm_core = { path = "lez/programs/amm/core" } +amm_program = { path = "lez/programs/amm" } +associated_token_account_core = { path = "lez/programs/associated_token_account/core" } +ata_program = { path = "lez/programs/associated_token_account" } +authenticated_transfer_core = { path = "lez/programs/authenticated_transfer/core" } +faucet_core = { path = "lez/programs/faucet/core" } +bridge_core = { path = "lez/programs/bridge/core" } +vault_core = { path = "lez/programs/vault/core" } +build_utils = { path = "build_utils" } +test_programs = { path = "test_programs" } testnet_initial_state = { path = "lez/testnet_initial_state" } keycard_wallet = { path = "lez/keycard_wallet" } test_fixtures = { path = "test_fixtures" } @@ -117,6 +132,7 @@ hex = "0.4.3" bytemuck = "1.24.0" bytesize = { version = "2.3.1", features = ["serde"] } humantime-serde = "1.1" +arc-swap = "1.7" humantime = "2.1" aes-gcm = "0.10.3" toml = "0.9.8" diff --git a/Justfile b/Justfile index 346dc84b..afd3d9b7 100644 --- a/Justfile +++ b/Justfile @@ -4,19 +4,25 @@ default: @just --list # ---- Configuration ---- -METHODS_PATH := "program_methods" -TEST_METHODS_PATH := "test_program_methods" ARTIFACTS := "artifacts" # Build risc0 program artifacts. build-artifacts: @echo "๐Ÿ”จ Building artifacts" - @for methods_path in {{METHODS_PATH}} {{TEST_METHODS_PATH}}; do \ - echo "Building artifacts for $methods_path"; \ - CARGO_TARGET_DIR=target/$methods_path cargo risczero build --manifest-path $methods_path/guest/Cargo.toml; \ - mkdir -p {{ARTIFACTS}}/$methods_path; \ - cp target/$methods_path/riscv32im-risc0-zkvm-elf/docker/*.bin {{ARTIFACTS}}/$methods_path; \ - done + @rm -rf {{ARTIFACTS}} + @just build-artifact lee/privacy_preserving_circuit + @just build-artifact lez/programs programs + +build-artifact methods_path features="": + @echo "Building artifacts for {{methods_path}}" + @rm -rf target/{{methods_path}}/riscv32im-risc0-zkvm-elf/docker/*.bin + @if [ "{{features}}" = "" ]; then \ + CARGO_TARGET_DIR=target/{{methods_path}} cargo risczero build --manifest-path {{methods_path}}/Cargo.toml; \ + else \ + CARGO_TARGET_DIR=target/{{methods_path}} cargo risczero build --no-default-features --features {{features}} --manifest-path {{methods_path}}/Cargo.toml; \ + fi + @mkdir -p {{ARTIFACTS}}/{{methods_path}} + @cp target/{{methods_path}}/riscv32im-risc0-zkvm-elf/docker/*.bin {{ARTIFACTS}}/{{methods_path}} # Format codebase. fmt: @@ -59,10 +65,10 @@ run-indexer mock="": @echo "๐Ÿ” Running indexer" @if [ "{{mock}}" = "mock" ]; then \ echo "๐Ÿงช Using mock data"; \ - RUST_LOG=info cargo run --release --features mock-responses -p indexer_service configs/indexer_config.json; \ + RUST_LOG=info cargo run --release --features mock-responses -p indexer_service configs/debug/indexer_config.json; \ else \ echo "๐Ÿš€ Using real data"; \ - RUST_LOG=info cargo run --release -p indexer_service configs/indexer_config.json; \ + RUST_LOG=info cargo run --release -p indexer_service configs/debug/indexer_config.json; \ fi # Run Explorer. @@ -93,7 +99,7 @@ clean: @echo "๐Ÿงน Cleaning run artifacts" rm -rf lez/sequencer/service/bedrock_signing_key rm -rf lez/sequencer/service/rocksdb - rm -rf lez/indexer/service/rocksdb + rm -rf lez/indexer/service/rocksdb* rm -rf lez/wallet/configs/debug/storage.json - rm -rf rocksdb + rm -rf rocksdb* cd bedrock && docker compose down -v diff --git a/README.md b/README.md index e0de266f..401fff15 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ The sequencer and logos blockchain node can be run locally: - `docker compose up` 2. On another terminal go to the `logos-blockchain/logos-execution-zone` repo and run indexer service: - - `RUST_LOG=info cargo run -p indexer_service lez/indexer/service/configs/indexer_config.json` + - `RUST_LOG=info cargo run -p indexer_service lez/indexer/service/configs/debug/indexer_config.json` 3. On another terminal go to the `logos-blockchain/logos-execution-zone` repo and run the sequencer: - `RUST_LOG=info cargo run -p sequencer_service lez/sequencer/service/configs/debug/sequencer_config.json` diff --git a/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin new file mode 100644 index 00000000..bacba91e Binary files /dev/null and b/artifacts/lee/privacy_preserving_circuit/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/amm.bin b/artifacts/lez/programs/amm.bin similarity index 54% rename from artifacts/program_methods/amm.bin rename to artifacts/lez/programs/amm.bin index 81e483fd..11382930 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/lez/programs/amm.bin differ diff --git a/artifacts/lez/programs/associated_token_account.bin b/artifacts/lez/programs/associated_token_account.bin new file mode 100644 index 00000000..1f01ba89 Binary files /dev/null and b/artifacts/lez/programs/associated_token_account.bin differ diff --git a/artifacts/lez/programs/authenticated_transfer.bin b/artifacts/lez/programs/authenticated_transfer.bin new file mode 100644 index 00000000..2fc88f43 Binary files /dev/null and b/artifacts/lez/programs/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/bridge.bin b/artifacts/lez/programs/bridge.bin similarity index 60% rename from artifacts/program_methods/bridge.bin rename to artifacts/lez/programs/bridge.bin index fcdc6507..8ee361a7 100644 Binary files a/artifacts/program_methods/bridge.bin and b/artifacts/lez/programs/bridge.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/lez/programs/clock.bin similarity index 54% rename from artifacts/program_methods/clock.bin rename to artifacts/lez/programs/clock.bin index 953089a3..45467bd3 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/lez/programs/clock.bin differ diff --git a/artifacts/program_methods/faucet.bin b/artifacts/lez/programs/faucet.bin similarity index 63% rename from artifacts/program_methods/faucet.bin rename to artifacts/lez/programs/faucet.bin index b9c8e8ba..6204eb9c 100644 Binary files a/artifacts/program_methods/faucet.bin and b/artifacts/lez/programs/faucet.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/lez/programs/pinata.bin similarity index 55% rename from artifacts/program_methods/pinata.bin rename to artifacts/lez/programs/pinata.bin index 62ce3582..8f16d3e1 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/lez/programs/pinata.bin differ diff --git a/artifacts/lez/programs/pinata_token.bin b/artifacts/lez/programs/pinata_token.bin new file mode 100644 index 00000000..0caf5046 Binary files /dev/null and b/artifacts/lez/programs/pinata_token.bin differ diff --git a/artifacts/lez/programs/token.bin b/artifacts/lez/programs/token.bin new file mode 100644 index 00000000..1786fa01 Binary files /dev/null and b/artifacts/lez/programs/token.bin differ diff --git a/artifacts/program_methods/vault.bin b/artifacts/lez/programs/vault.bin similarity index 76% rename from artifacts/program_methods/vault.bin rename to artifacts/lez/programs/vault.bin index 4ce7c1a9..d670b6bc 100644 Binary files a/artifacts/program_methods/vault.bin and b/artifacts/lez/programs/vault.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin deleted file mode 100644 index 92509f59..00000000 Binary files a/artifacts/program_methods/associated_token_account.bin and /dev/null differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin deleted file mode 100644 index e1297e41..00000000 Binary files a/artifacts/program_methods/authenticated_transfer.bin and /dev/null differ diff --git a/artifacts/program_methods/genesis_supply_account.bin b/artifacts/program_methods/genesis_supply_account.bin deleted file mode 100644 index c377a1e6..00000000 Binary files a/artifacts/program_methods/genesis_supply_account.bin and /dev/null differ diff --git a/artifacts/program_methods/genesis_supply_private_account.bin b/artifacts/program_methods/genesis_supply_private_account.bin deleted file mode 100644 index 9d6aa313..00000000 Binary files a/artifacts/program_methods/genesis_supply_private_account.bin and /dev/null differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin deleted file mode 100644 index 73672183..00000000 Binary files a/artifacts/program_methods/pinata_token.bin and /dev/null differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin deleted file mode 100644 index 9a37f28d..00000000 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and /dev/null differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin deleted file mode 100644 index f1dcfa83..00000000 Binary files a/artifacts/program_methods/token.bin and /dev/null differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin deleted file mode 100644 index f1992b4d..00000000 Binary files a/artifacts/test_program_methods/auth_asserting_noop.bin and /dev/null differ diff --git a/artifacts/test_program_methods/auth_transfer_proxy.bin b/artifacts/test_program_methods/auth_transfer_proxy.bin deleted file mode 100644 index a8209882..00000000 Binary files a/artifacts/test_program_methods/auth_transfer_proxy.bin and /dev/null differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin deleted file mode 100644 index cd3a74e9..00000000 Binary files a/artifacts/test_program_methods/burner.bin and /dev/null differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin deleted file mode 100644 index 4b4aa25f..00000000 Binary files a/artifacts/test_program_methods/chain_caller.bin and /dev/null differ diff --git a/artifacts/test_program_methods/chain_caller_pda_drop.bin b/artifacts/test_program_methods/chain_caller_pda_drop.bin deleted file mode 100644 index 91b42aa2..00000000 Binary files a/artifacts/test_program_methods/chain_caller_pda_drop.bin and /dev/null differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin deleted file mode 100644 index cc605b0d..00000000 Binary files a/artifacts/test_program_methods/changer_claimer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin deleted file mode 100644 index bec996a3..00000000 Binary files a/artifacts/test_program_methods/claimer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin deleted file mode 100644 index 5ba85dfe..00000000 Binary files a/artifacts/test_program_methods/clock_chain_caller.bin and /dev/null differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin deleted file mode 100644 index eff945a9..00000000 Binary files a/artifacts/test_program_methods/data_changer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin deleted file mode 100644 index 8dca7ce0..00000000 Binary files a/artifacts/test_program_methods/extra_output.bin and /dev/null differ diff --git a/artifacts/test_program_methods/faucet_chain_caller.bin b/artifacts/test_program_methods/faucet_chain_caller.bin deleted file mode 100644 index d96d918f..00000000 Binary files a/artifacts/test_program_methods/faucet_chain_caller.bin and /dev/null differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin deleted file mode 100644 index 65290819..00000000 Binary files a/artifacts/test_program_methods/flash_swap_callback.bin and /dev/null differ diff --git a/artifacts/test_program_methods/flash_swap_initiator.bin b/artifacts/test_program_methods/flash_swap_initiator.bin deleted file mode 100644 index b25372d3..00000000 Binary files a/artifacts/test_program_methods/flash_swap_initiator.bin and /dev/null differ diff --git a/artifacts/test_program_methods/group_pda_spender.bin b/artifacts/test_program_methods/group_pda_spender.bin deleted file mode 100644 index 16efb8a4..00000000 Binary files a/artifacts/test_program_methods/group_pda_spender.bin and /dev/null differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin deleted file mode 100644 index 97dd62fb..00000000 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/malicious_caller_program_id.bin b/artifacts/test_program_methods/malicious_caller_program_id.bin deleted file mode 100644 index 14b642ae..00000000 Binary files a/artifacts/test_program_methods/malicious_caller_program_id.bin and /dev/null differ diff --git a/artifacts/test_program_methods/malicious_injector.bin b/artifacts/test_program_methods/malicious_injector.bin deleted file mode 100644 index 22554f08..00000000 Binary files a/artifacts/test_program_methods/malicious_injector.bin and /dev/null differ diff --git a/artifacts/test_program_methods/malicious_launderer.bin b/artifacts/test_program_methods/malicious_launderer.bin deleted file mode 100644 index 53627ccf..00000000 Binary files a/artifacts/test_program_methods/malicious_launderer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/malicious_self_program_id.bin b/artifacts/test_program_methods/malicious_self_program_id.bin deleted file mode 100644 index c6a7c78c..00000000 Binary files a/artifacts/test_program_methods/malicious_self_program_id.bin and /dev/null differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin deleted file mode 100644 index f57a0b33..00000000 Binary files a/artifacts/test_program_methods/minter.bin and /dev/null differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin deleted file mode 100644 index cb9b70ec..00000000 Binary files a/artifacts/test_program_methods/missing_output.bin and /dev/null differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin deleted file mode 100644 index 5cd8b679..00000000 Binary files a/artifacts/test_program_methods/modified_transfer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin deleted file mode 100644 index 3f95aeaa..00000000 Binary files a/artifacts/test_program_methods/nonce_changer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin deleted file mode 100644 index 5f053084..00000000 Binary files a/artifacts/test_program_methods/noop.bin and /dev/null differ diff --git a/artifacts/test_program_methods/pda_claimer.bin b/artifacts/test_program_methods/pda_claimer.bin deleted file mode 100644 index 33da1df7..00000000 Binary files a/artifacts/test_program_methods/pda_claimer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/pda_fund_spend_proxy.bin b/artifacts/test_program_methods/pda_fund_spend_proxy.bin deleted file mode 100644 index 9a04a29c..00000000 Binary files a/artifacts/test_program_methods/pda_fund_spend_proxy.bin and /dev/null differ diff --git a/artifacts/test_program_methods/pda_spend_proxy.bin b/artifacts/test_program_methods/pda_spend_proxy.bin deleted file mode 100644 index c309a8a0..00000000 Binary files a/artifacts/test_program_methods/pda_spend_proxy.bin and /dev/null differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin deleted file mode 100644 index 8e295599..00000000 Binary files a/artifacts/test_program_methods/pinata_cooldown.bin and /dev/null differ diff --git a/artifacts/test_program_methods/private_pda_claimer.bin b/artifacts/test_program_methods/private_pda_claimer.bin deleted file mode 100644 index 5a64c66d..00000000 Binary files a/artifacts/test_program_methods/private_pda_claimer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin deleted file mode 100644 index 0b4c5d3f..00000000 Binary files a/artifacts/test_program_methods/private_pda_delegator.bin and /dev/null differ diff --git a/artifacts/test_program_methods/private_pda_spender.bin b/artifacts/test_program_methods/private_pda_spender.bin deleted file mode 100644 index b9848210..00000000 Binary files a/artifacts/test_program_methods/private_pda_spender.bin and /dev/null differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin deleted file mode 100644 index 93fc918e..00000000 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin deleted file mode 100644 index c0b427a5..00000000 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/time_locked_transfer.bin b/artifacts/test_program_methods/time_locked_transfer.bin deleted file mode 100644 index 44cb8b19..00000000 Binary files a/artifacts/test_program_methods/time_locked_transfer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/two_pda_claimer.bin b/artifacts/test_program_methods/two_pda_claimer.bin deleted file mode 100644 index 3152ab2a..00000000 Binary files a/artifacts/test_program_methods/two_pda_claimer.bin and /dev/null differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin deleted file mode 100644 index cf95c2dd..00000000 Binary files a/artifacts/test_program_methods/validity_window.bin and /dev/null differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin deleted file mode 100644 index 2132e1fe..00000000 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and /dev/null differ diff --git a/bedrock/docker-compose.yml b/bedrock/docker-compose.yml index a7a0d7f6..e476a8ef 100644 --- a/bedrock/docker-compose.yml +++ b/bedrock/docker-compose.yml @@ -3,7 +3,7 @@ services: logos-blockchain-node-0: image: ghcr.io/logos-blockchain/logos-blockchain@sha256:91d6c5bf07e07fcfba5e7cf07d21ee686a6bc4b9f6210f2d28bffbcad9a3729f ports: - - "${PORT:-8080}:18080/tcp" + - "${PORT:-18080}:18080/tcp" volumes: - ./scripts:/etc/logos-blockchain/scripts - ./kzgrs_test_params:/kzgrs_test_params:z diff --git a/build_utils/Cargo.toml b/build_utils/Cargo.toml new file mode 100644 index 00000000..63ba15bf --- /dev/null +++ b/build_utils/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "build_utils" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[lints] + +[dependencies] +anyhow.workspace = true +risc0-binfmt = "3.0.2" diff --git a/build_utils/src/lib.rs b/build_utils/src/lib.rs new file mode 100644 index 00000000..1323d830 --- /dev/null +++ b/build_utils/src/lib.rs @@ -0,0 +1,69 @@ +//! Utilities for build-scripts. + +use std::{env, fmt::Write as _, fs, path::PathBuf}; + +use anyhow::{Context as _, Result, bail}; + +/// Include artifact binaries as byte arrays and their corresponding image IDs as u32 arrays in a +/// generated Rust module. +/// +/// The `artifacts_sub_dir` parameter specifies the subdirectory under `artifacts/`. +/// +/// Caller should include resulting module as follows: +/// +/// ``` +/// mod guests { +/// include!(concat!(env!("OUT_DIR"), "/artifacts_sub_dir/mod.rs")); +/// } +/// ``` +pub fn include_artifacts(artifacts_sub_dir: &str) -> Result<()> { + let manifest_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")); + let out_dir = PathBuf::from(env::var("OUT_DIR")?); + let mod_dir = out_dir.join(artifacts_sub_dir); + let mod_file = mod_dir.join("mod.rs"); + let artifacts_dir = manifest_dir.join(format!("../artifacts/{artifacts_sub_dir}/")); + + println!("cargo:rerun-if-changed={}", artifacts_dir.display()); + + let bins = fs::read_dir(&artifacts_dir) + .with_context(|| { + format!( + "Failed to read {} artifacts directory", + artifacts_dir.display() + ) + })? + .filter_map(Result::ok) + .filter(|e| e.path().extension().is_some_and(|ext| ext == "bin")) + .collect::>(); + + if bins.is_empty() { + bail!("No .bin files found in {}", artifacts_dir.display()); + } + + fs::create_dir_all(&mod_dir) + .with_context(|| format!("Failed to create directory {}", mod_dir.display()))?; + let mut src = String::new(); + for entry in bins { + let path = entry.path(); + let name = path.file_stem().unwrap().to_string_lossy(); + let bytecode = + fs::read(&path).with_context(|| format!("Failed to read {}", path.display()))?; + let image_id: [u32; 8] = risc0_binfmt::compute_image_id(&bytecode) + .with_context(|| format!("Failed to compute image ID for {}", path.display()))? + .into(); + write!( + src, + "pub const {}_ELF: &[u8] = include_bytes!(r#\"{}\"#);\n\ + #[expect(clippy::unreadable_literal, reason = \"Generated image IDs from risc0 are cryptographic hashes represented as u32 arrays\")]\n\ + pub const {}_ID: [u32; 8] = {:?};\n", + name.to_uppercase(), + path.display(), + name.to_uppercase(), + image_id + )?; + } + fs::write(&mod_file, src).with_context(|| format!("Failed to write {}", mod_file.display()))?; + println!("cargo:warning=Generated module at {}", mod_file.display()); + + Ok(()) +} diff --git a/docker-compose.override.yml b/docker-compose.override.yml index a7fddca6..4cdb486f 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -2,8 +2,6 @@ services: logos-blockchain-node-0: - ports: !override - - "18080:18080/tcp" environment: - RUST_LOG=error diff --git a/examples/program_deployment/README.md b/examples/program_deployment/README.md index 6d1bde25..240079a5 100644 --- a/examples/program_deployment/README.md +++ b/examples/program_deployment/README.md @@ -46,7 +46,7 @@ export EXAMPLE_PROGRAMS_BUILD_DIR=$(pwd)/target/riscv32im-risc0-zkvm-elf/docker > [!IMPORTANT] > **All remaining commands must be run from the `examples/program_deployment` directory.** -# 3. Hello world example +# 3. Hello world example The Hello world program reads an arbitrary sequence of bytes from its instruction and appends them to the data field of the input account. Execution succeeds only if the account is: @@ -211,7 +211,7 @@ This is the account that the program will claim and write data into. ### 3. Loading the program bytecode ```rust let bytecode: Vec = std::fs::read(program_path).unwrap(); -let program = Program::new(bytecode).unwrap(); +let program = Program::new(bytecode.into()).unwrap(); ``` The Risc0 ELF is read from disk and wrapped in a Program object, which can be used to compute the program ID. The ID is used by the node to identify which program is invoked by the transaction. @@ -266,7 +266,7 @@ The relevant part for this tutorial is the account id `7EDHyxejuynBpmbLuiEym9HMU > [!NOTE] > As with public accounts, you can use the `--label` option to assign a label: `wallet account new private --label "my-private-account"`. -You can check it's uninitialized with +You can check it's uninitialized with ```bash wallet account get --account-id Private/7EDHyxejuynBpmbLuiEym9HMUyCYxZDuF8X3B89ADeMr @@ -452,7 +452,7 @@ Because these operations may involve multiple accounts, we'll see how public and > See `methods/guest/src/bin/hello_world_with_move_function.rs`. The program just reads the instruction bytes and updates the accounts state. > All privacy handling happens on the runner side. When constructing the transaction, the runner decides which accounts are public or private and prepares the appropriate proofs. The program itself can't differentiate between privacy modes. -Let's start by deploying the program +Let's start by deploying the program ```bash wallet deploy-program $EXAMPLE_PROGRAMS_BUILD_DIR/hello_world_with_move_function.bin ``` @@ -486,7 +486,7 @@ Output: Generated new account with account_id Private/8vzkK7vsdrS2gdPhLk72La8X4FJkgJ5kJLUBRbEVkReU at path /1 ``` -Let's execute the write function +Let's execute the write function ```bash cargo run --bin run_hello_world_with_move_function \ diff --git a/examples/program_deployment/src/bin/run_hello_world.rs b/examples/program_deployment/src/bin/run_hello_world.rs index ef5545f7..3f2223a1 100644 --- a/examples/program_deployment/src/bin/run_hello_world.rs +++ b/examples/program_deployment/src/bin/run_hello_world.rs @@ -43,7 +43,7 @@ async fn main() { // Load the program let bytecode: Vec = std::fs::read(program_path).unwrap(); - let program = Program::new(bytecode).unwrap(); + let program = Program::new(bytecode.into()).unwrap(); // Define the desired greeting in ASCII let greeting: Vec = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33]; diff --git a/examples/program_deployment/src/bin/run_hello_world_private.rs b/examples/program_deployment/src/bin/run_hello_world_private.rs index 088eb19a..f6202433 100644 --- a/examples/program_deployment/src/bin/run_hello_world_private.rs +++ b/examples/program_deployment/src/bin/run_hello_world_private.rs @@ -39,7 +39,7 @@ async fn main() { // Load the program let bytecode: Vec = std::fs::read(program_path).unwrap(); - let program = Program::new(bytecode).unwrap(); + let program = Program::new(bytecode.into()).unwrap(); // Define the desired greeting in ASCII let greeting: Vec = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33]; diff --git a/examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs b/examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs index a5f449e1..6ebba70f 100644 --- a/examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs +++ b/examples/program_deployment/src/bin/run_hello_world_through_tail_call.rs @@ -43,7 +43,7 @@ async fn main() { // Load the program let bytecode: Vec = std::fs::read(program_path).unwrap(); - let program = Program::new(bytecode).unwrap(); + let program = Program::new(bytecode.into()).unwrap(); let instruction_data = (); let nonces = vec![]; diff --git a/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs b/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs index 4983afdb..d35e6521 100644 --- a/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs +++ b/examples/program_deployment/src/bin/run_hello_world_through_tail_call_private.rs @@ -44,9 +44,9 @@ async fn main() { // Load the program and its dependencies (the hellow world program) let simple_tail_call_bytecode: Vec = std::fs::read(simple_tail_call_path).unwrap(); - let simple_tail_call = Program::new(simple_tail_call_bytecode).unwrap(); + let simple_tail_call = Program::new(simple_tail_call_bytecode.into()).unwrap(); let hello_world_bytecode: Vec = std::fs::read(hello_world_path).unwrap(); - let hello_world = Program::new(hello_world_bytecode).unwrap(); + let hello_world = Program::new(hello_world_bytecode.into()).unwrap(); let dependencies: HashMap = std::iter::once((hello_world.id(), hello_world)).collect(); let program_with_dependencies = ProgramWithDependencies::new(simple_tail_call, dependencies); diff --git a/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs b/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs index 15fc028a..0d257db2 100644 --- a/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs +++ b/examples/program_deployment/src/bin/run_hello_world_with_authorization.rs @@ -45,7 +45,7 @@ async fn main() { // Load the program let bytecode: Vec = std::fs::read(program_path).unwrap(); - let program = Program::new(bytecode).unwrap(); + let program = Program::new(bytecode.into()).unwrap(); // Load signing keys to provide authorization let signing_key = wallet_core diff --git a/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs b/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs index 2b0e05ed..70688cfd 100644 --- a/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs +++ b/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs @@ -43,7 +43,7 @@ async fn main() { // Load the program let bytecode: Vec = std::fs::read(program_path).unwrap(); - let program = Program::new(bytecode).unwrap(); + let program = Program::new(bytecode.into()).unwrap(); // Compute the PDA to pass it as input account to the public execution let pda = AccountId::for_public_pda(&program.id(), &PDA_SEED); diff --git a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs index 3b175bda..a77fe2e6 100644 --- a/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs +++ b/examples/program_deployment/src/bin/run_hello_world_with_move_function.rs @@ -63,7 +63,7 @@ async fn main() { // Load the program let bytecode: Vec = std::fs::read(cli.program_path).unwrap(); - let program = Program::new(bytecode).unwrap(); + let program = Program::new(bytecode.into()).unwrap(); // Initialize wallet let wallet_core = WalletCore::from_env().unwrap(); diff --git a/flake.lock b/flake.lock index 44b0300f..912fd769 100644 --- a/flake.lock +++ b/flake.lock @@ -15,6 +15,61 @@ "type": "github" } }, + "default-container": { + "inputs": { + "logos-container": "logos-container", + "logos-nix": "logos-nix_2", + "nixpkgs": [ + "logos-liblogos", + "default-container", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781741375, + "narHash": "sha256-PP/2tFfwrbqTzbZKVl46I96xz/ZZj7hi9C3/QeLHtQE=", + "owner": "logos-co", + "repo": "logos-container-subprocess", + "rev": "f4860708a982b361a2e25c5260e13800bcee126e", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-container-subprocess", + "type": "github" + } + }, + "default-module-loader": { + "inputs": { + "logos-container": "logos-container_2", + "logos-cpp-sdk": "logos-cpp-sdk", + "logos-module": "logos-module", + "logos-module-loader": "logos-module-loader", + "logos-nix": "logos-nix_8", + "logos-protocol": "logos-protocol", + "logos-qt-sdk": "logos-qt-sdk", + "nixpkgs": [ + "logos-liblogos", + "default-module-loader", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781734000, + "narHash": "sha256-TyrUHKnb5llx+Z5GN+bMAbjkxON6w7aj4yN6gokq2eQ=", + "owner": "logos-co", + "repo": "logos-module-loader-qt", + "rev": "08213eb216ae9b33c73830d3cf2d9111d97cbbcb", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-module-loader-qt", + "type": "github" + } + }, "logos-blockchain-circuits": { "inputs": { "nixpkgs": "nixpkgs" @@ -82,8 +137,8 @@ "logos-standalone-app", "logos-cpp-sdk" ], - "logos-module": "logos-module_7", - "logos-nix": "logos-nix_16", + "logos-module": "logos-module_8", + "logos-nix": "logos-nix_26", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -112,9 +167,9 @@ }, "logos-capability-module_4": { "inputs": { - "logos-cpp-sdk": "logos-cpp-sdk_4", - "logos-module": "logos-module_8", - "logos-nix": "logos-nix_21", + "logos-cpp-sdk": "logos-cpp-sdk_5", + "logos-module": "logos-module_9", + "logos-nix": "logos-nix_31", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -174,8 +229,8 @@ "logos-standalone-app", "logos-cpp-sdk" ], - "logos-module": "logos-module_13", - "logos-nix": "logos-nix_60", + "logos-module": "logos-module_14", + "logos-nix": "logos-nix_70", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -205,9 +260,9 @@ }, "logos-capability-module_7": { "inputs": { - "logos-cpp-sdk": "logos-cpp-sdk_9", - "logos-module": "logos-module_14", - "logos-nix": "logos-nix_65", + "logos-cpp-sdk": "logos-cpp-sdk_10", + "logos-module": "logos-module_15", + "logos-nix": "logos-nix_75", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -237,24 +292,154 @@ "type": "github" } }, - "logos-cpp-sdk": { + "logos-container": { "inputs": { "logos-nix": "logos-nix", "nixpkgs": [ "logos-liblogos", - "logos-capability-module", - "logos-module-builder", + "default-container", + "logos-container", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781732360, + "narHash": "sha256-RJjquPiZz3/LkJaFUGpDxFbt1+paBiVtbbxjVhDMR3o=", + "owner": "logos-co", + "repo": "logos-container", + "rev": "15adc74f829f0cc662bce4f0bfae917cd50e7db4", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-container", + "type": "github" + } + }, + "logos-container_2": { + "inputs": { + "logos-nix": "logos-nix_3", + "nixpkgs": [ + "logos-liblogos", + "default-module-loader", + "logos-container", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781732360, + "narHash": "sha256-RJjquPiZz3/LkJaFUGpDxFbt1+paBiVtbbxjVhDMR3o=", + "owner": "logos-co", + "repo": "logos-container", + "rev": "15adc74f829f0cc662bce4f0bfae917cd50e7db4", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-container", + "type": "github" + } + }, + "logos-container_3": { + "inputs": { + "logos-nix": "logos-nix_6", + "nixpkgs": [ + "logos-liblogos", + "default-module-loader", + "logos-module-loader", + "logos-container", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781732360, + "narHash": "sha256-RJjquPiZz3/LkJaFUGpDxFbt1+paBiVtbbxjVhDMR3o=", + "owner": "logos-co", + "repo": "logos-container", + "rev": "15adc74f829f0cc662bce4f0bfae917cd50e7db4", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-container", + "type": "github" + } + }, + "logos-container_4": { + "inputs": { + "logos-nix": "logos-nix_132", + "nixpkgs": [ + "logos-liblogos", + "logos-container", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781732360, + "narHash": "sha256-RJjquPiZz3/LkJaFUGpDxFbt1+paBiVtbbxjVhDMR3o=", + "owner": "logos-co", + "repo": "logos-container", + "rev": "15adc74f829f0cc662bce4f0bfae917cd50e7db4", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-container", + "type": "github" + } + }, + "logos-container_5": { + "inputs": { + "logos-nix": "logos-nix_135", + "nixpkgs": [ + "logos-liblogos", + "logos-module-loader", + "logos-container", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781732360, + "narHash": "sha256-RJjquPiZz3/LkJaFUGpDxFbt1+paBiVtbbxjVhDMR3o=", + "owner": "logos-co", + "repo": "logos-container", + "rev": "15adc74f829f0cc662bce4f0bfae917cd50e7db4", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-container", + "type": "github" + } + }, + "logos-cpp-sdk": { + "inputs": { + "logos-lidl": "logos-lidl", + "logos-nix": "logos-nix_4", + "logos-protocol": [ + "logos-liblogos", + "default-module-loader", + "logos-protocol" + ], + "nixpkgs": [ + "logos-liblogos", + "default-module-loader", "logos-cpp-sdk", "logos-nix", "nixpkgs" ] }, "locked": { - "lastModified": 1781207077, - "narHash": "sha256-ZamVbPJxW364RFOa0WnYTJV7AiEjnUcOgfxIbfNgTO0=", + "lastModified": 1781663756, + "narHash": "sha256-q1Q2MCax3L1RLfcS40sDStVipK6dhfiPHYk31YeGyUI=", "owner": "logos-co", "repo": "logos-cpp-sdk", - "rev": "f5a127dd11589c8fc6db0a1bd4332d09ac080bb6", + "rev": "182d850e722c820b3640a09541d7c469ef79f0e6", "type": "github" }, "original": { @@ -265,7 +450,40 @@ }, "logos-cpp-sdk_10": { "inputs": { - "logos-nix": "logos-nix_66", + "logos-nix": "logos-nix_73", + "nixpkgs": [ + "logos-liblogos", + "logos-capability-module", + "logos-module-builder", + "logos-standalone-app", + "logos-liblogos", + "logos-capability-module", + "logos-module-builder", + "logos-standalone-app", + "logos-liblogos", + "logos-capability-module", + "logos-cpp-sdk", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1773956385, + "narHash": "sha256-CV0Lo1FrosBt/MSP+GWQGWXnYobxRGXGOREylNuwZ58=", + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "rev": "4b66dac015e4b977d33cfae80a4c8e1d518679f3", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "type": "github" + } + }, + "logos-cpp-sdk_11": { + "inputs": { + "logos-nix": "logos-nix_76", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -295,9 +513,9 @@ "type": "github" } }, - "logos-cpp-sdk_11": { + "logos-cpp-sdk_12": { "inputs": { - "logos-nix": "logos-nix_94", + "logos-nix": "logos-nix_104", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -323,7 +541,7 @@ "type": "github" } }, - "logos-cpp-sdk_12": { + "logos-cpp-sdk_13": { "inputs": { "logos-nix": [ "logos-liblogos", @@ -358,9 +576,14 @@ "type": "github" } }, - "logos-cpp-sdk_13": { + "logos-cpp-sdk_14": { "inputs": { - "logos-nix": "logos-nix_122", + "logos-lidl": "logos-lidl_3", + "logos-nix": "logos-nix_133", + "logos-protocol": [ + "logos-liblogos", + "logos-protocol" + ], "nixpkgs": [ "logos-liblogos", "logos-cpp-sdk", @@ -368,6 +591,32 @@ "nixpkgs" ] }, + "locked": { + "lastModified": 1781663756, + "narHash": "sha256-q1Q2MCax3L1RLfcS40sDStVipK6dhfiPHYk31YeGyUI=", + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "rev": "182d850e722c820b3640a09541d7c469ef79f0e6", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "type": "github" + } + }, + "logos-cpp-sdk_2": { + "inputs": { + "logos-nix": "logos-nix_11", + "nixpkgs": [ + "logos-liblogos", + "logos-capability-module", + "logos-module-builder", + "logos-cpp-sdk", + "logos-nix", + "nixpkgs" + ] + }, "locked": { "lastModified": 1781207077, "narHash": "sha256-ZamVbPJxW364RFOa0WnYTJV7AiEjnUcOgfxIbfNgTO0=", @@ -382,38 +631,9 @@ "type": "github" } }, - "logos-cpp-sdk_2": { - "inputs": { - "logos-nix": "logos-nix_8", - "nixpkgs": [ - "logos-liblogos", - "logos-capability-module", - "logos-module-builder", - "logos-standalone-app", - "logos-capability-module", - "logos-module-builder", - "logos-cpp-sdk", - "logos-nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1778167634, - "narHash": "sha256-gbBYvyEDxBHF8iACAE/dFcljBkIUTWr1rP3gBLj62JI=", - "owner": "logos-co", - "repo": "logos-cpp-sdk", - "rev": "25c88f4d48fa95ea4437194bcf60bd8d0cf84a74", - "type": "github" - }, - "original": { - "owner": "logos-co", - "repo": "logos-cpp-sdk", - "type": "github" - } - }, "logos-cpp-sdk_3": { "inputs": { - "logos-nix": "logos-nix_17", + "logos-nix": "logos-nix_18", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -421,7 +641,6 @@ "logos-standalone-app", "logos-capability-module", "logos-module-builder", - "logos-standalone-app", "logos-cpp-sdk", "logos-nix", "nixpkgs" @@ -443,7 +662,37 @@ }, "logos-cpp-sdk_4": { "inputs": { - "logos-nix": "logos-nix_19", + "logos-nix": "logos-nix_27", + "nixpkgs": [ + "logos-liblogos", + "logos-capability-module", + "logos-module-builder", + "logos-standalone-app", + "logos-capability-module", + "logos-module-builder", + "logos-standalone-app", + "logos-cpp-sdk", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1778167634, + "narHash": "sha256-gbBYvyEDxBHF8iACAE/dFcljBkIUTWr1rP3gBLj62JI=", + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "rev": "25c88f4d48fa95ea4437194bcf60bd8d0cf84a74", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "type": "github" + } + }, + "logos-cpp-sdk_5": { + "inputs": { + "logos-nix": "logos-nix_29", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -473,9 +722,9 @@ "type": "github" } }, - "logos-cpp-sdk_5": { + "logos-cpp-sdk_6": { "inputs": { - "logos-nix": "logos-nix_22", + "logos-nix": "logos-nix_32", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -504,9 +753,9 @@ "type": "github" } }, - "logos-cpp-sdk_6": { + "logos-cpp-sdk_7": { "inputs": { - "logos-nix": "logos-nix_50", + "logos-nix": "logos-nix_60", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -531,39 +780,9 @@ "type": "github" } }, - "logos-cpp-sdk_7": { - "inputs": { - "logos-nix": "logos-nix_52", - "nixpkgs": [ - "logos-liblogos", - "logos-capability-module", - "logos-module-builder", - "logos-standalone-app", - "logos-liblogos", - "logos-capability-module", - "logos-module-builder", - "logos-cpp-sdk", - "logos-nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1778167634, - "narHash": "sha256-gbBYvyEDxBHF8iACAE/dFcljBkIUTWr1rP3gBLj62JI=", - "owner": "logos-co", - "repo": "logos-cpp-sdk", - "rev": "25c88f4d48fa95ea4437194bcf60bd8d0cf84a74", - "type": "github" - }, - "original": { - "owner": "logos-co", - "repo": "logos-cpp-sdk", - "type": "github" - } - }, "logos-cpp-sdk_8": { "inputs": { - "logos-nix": "logos-nix_61", + "logos-nix": "logos-nix_62", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -572,7 +791,6 @@ "logos-liblogos", "logos-capability-module", "logos-module-builder", - "logos-standalone-app", "logos-cpp-sdk", "logos-nix", "nixpkgs" @@ -594,7 +812,7 @@ }, "logos-cpp-sdk_9": { "inputs": { - "logos-nix": "logos-nix_63", + "logos-nix": "logos-nix_71", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -604,19 +822,17 @@ "logos-capability-module", "logos-module-builder", "logos-standalone-app", - "logos-liblogos", - "logos-capability-module", "logos-cpp-sdk", "logos-nix", "nixpkgs" ] }, "locked": { - "lastModified": 1773956385, - "narHash": "sha256-CV0Lo1FrosBt/MSP+GWQGWXnYobxRGXGOREylNuwZ58=", + "lastModified": 1778167634, + "narHash": "sha256-gbBYvyEDxBHF8iACAE/dFcljBkIUTWr1rP3gBLj62JI=", "owner": "logos-co", "repo": "logos-cpp-sdk", - "rev": "4b66dac015e4b977d33cfae80a4c8e1d518679f3", + "rev": "25c88f4d48fa95ea4437194bcf60bd8d0cf84a74", "type": "github" }, "original": { @@ -627,7 +843,7 @@ }, "logos-design-system": { "inputs": { - "logos-nix": "logos-nix_18", + "logos-nix": "logos-nix_28", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -657,7 +873,7 @@ }, "logos-design-system_2": { "inputs": { - "logos-nix": "logos-nix_51", + "logos-nix": "logos-nix_61", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -684,7 +900,7 @@ }, "logos-design-system_3": { "inputs": { - "logos-nix": "logos-nix_62", + "logos-nix": "logos-nix_72", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -715,11 +931,17 @@ }, "logos-liblogos": { "inputs": { + "default-container": "default-container", + "default-module-loader": "default-module-loader", "logos-capability-module": "logos-capability-module", - "logos-cpp-sdk": "logos-cpp-sdk_13", - "logos-module": "logos-module_17", - "logos-nix": "logos-nix_124", + "logos-container": "logos-container_4", + "logos-cpp-sdk": "logos-cpp-sdk_14", + "logos-module": "logos-module_18", + "logos-module-loader": "logos-module-loader_2", + "logos-nix": "logos-nix_137", "logos-package-manager": "logos-package-manager_7", + "logos-protocol": "logos-protocol_2", + "logos-qt-sdk": "logos-qt-sdk_2", "nixpkgs": [ "logos-liblogos", "logos-nix", @@ -728,11 +950,11 @@ "process-stats": "process-stats_4" }, "locked": { - "lastModified": 1781211239, - "narHash": "sha256-qRWrWyxpry5cO03rG1cXYPtujQON6A/0Z0Z/R1NmUV8=", + "lastModified": 1781742244, + "narHash": "sha256-Vd9UevKBn0Mie5tvexaaYOSkiqDlC4G3qTFKD28S1gk=", "owner": "logos-co", "repo": "logos-liblogos", - "rev": "66256b54efb995adf977689ea63f6bfb6ccfdd81", + "rev": "dbcafea05c06e91be18763bc0c4b307222d8b853", "type": "github" }, "original": { @@ -744,9 +966,9 @@ "logos-liblogos_2": { "inputs": { "logos-capability-module": "logos-capability-module_4", - "logos-cpp-sdk": "logos-cpp-sdk_5", - "logos-module": "logos-module_9", - "logos-nix": "logos-nix_24", + "logos-cpp-sdk": "logos-cpp-sdk_6", + "logos-module": "logos-module_10", + "logos-nix": "logos-nix_34", "logos-package-manager": "logos-package-manager", "nixpkgs": [ "logos-liblogos", @@ -778,9 +1000,9 @@ "logos-liblogos_3": { "inputs": { "logos-capability-module": "logos-capability-module_5", - "logos-cpp-sdk": "logos-cpp-sdk_11", - "logos-module": "logos-module_16", - "logos-nix": "logos-nix_96", + "logos-cpp-sdk": "logos-cpp-sdk_12", + "logos-module": "logos-module_17", + "logos-nix": "logos-nix_106", "logos-package-manager": "logos-package-manager_5", "nixpkgs": [ "logos-liblogos", @@ -810,9 +1032,9 @@ "logos-liblogos_4": { "inputs": { "logos-capability-module": "logos-capability-module_7", - "logos-cpp-sdk": "logos-cpp-sdk_10", - "logos-module": "logos-module_15", - "logos-nix": "logos-nix_68", + "logos-cpp-sdk": "logos-cpp-sdk_11", + "logos-module": "logos-module_16", + "logos-nix": "logos-nix_78", "logos-package-manager": "logos-package-manager_3", "nixpkgs": [ "logos-liblogos", @@ -842,24 +1064,143 @@ "type": "github" } }, - "logos-module": { + "logos-lidl": { "inputs": { - "logos-nix": "logos-nix_2", + "logos-nix": [ + "logos-liblogos", + "default-module-loader", + "logos-cpp-sdk", + "logos-nix" + ], "nixpkgs": [ "logos-liblogos", - "logos-capability-module", - "logos-module-builder", + "default-module-loader", + "logos-cpp-sdk", + "logos-lidl", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781653473, + "narHash": "sha256-8l2tE2K5nY1grcFROQHC1By1jVI0NrfkS3k2v2y6R2I=", + "owner": "logos-co", + "repo": "logos-lidl", + "rev": "8c95d4f0cc6a10195c70ed71e85b7a4cddca02f8", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-lidl", + "type": "github" + } + }, + "logos-lidl_2": { + "inputs": { + "logos-nix": [ + "logos-liblogos", + "default-module-loader", + "logos-qt-sdk", + "logos-nix" + ], + "nixpkgs": [ + "logos-liblogos", + "default-module-loader", + "logos-qt-sdk", + "logos-lidl", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781653473, + "narHash": "sha256-8l2tE2K5nY1grcFROQHC1By1jVI0NrfkS3k2v2y6R2I=", + "owner": "logos-co", + "repo": "logos-lidl", + "rev": "8c95d4f0cc6a10195c70ed71e85b7a4cddca02f8", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-lidl", + "type": "github" + } + }, + "logos-lidl_3": { + "inputs": { + "logos-nix": [ + "logos-liblogos", + "logos-cpp-sdk", + "logos-nix" + ], + "nixpkgs": [ + "logos-liblogos", + "logos-cpp-sdk", + "logos-lidl", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781653473, + "narHash": "sha256-8l2tE2K5nY1grcFROQHC1By1jVI0NrfkS3k2v2y6R2I=", + "owner": "logos-co", + "repo": "logos-lidl", + "rev": "8c95d4f0cc6a10195c70ed71e85b7a4cddca02f8", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-lidl", + "type": "github" + } + }, + "logos-lidl_4": { + "inputs": { + "logos-nix": [ + "logos-liblogos", + "logos-qt-sdk", + "logos-nix" + ], + "nixpkgs": [ + "logos-liblogos", + "logos-qt-sdk", + "logos-lidl", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781653473, + "narHash": "sha256-8l2tE2K5nY1grcFROQHC1By1jVI0NrfkS3k2v2y6R2I=", + "owner": "logos-co", + "repo": "logos-lidl", + "rev": "8c95d4f0cc6a10195c70ed71e85b7a4cddca02f8", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-lidl", + "type": "github" + } + }, + "logos-module": { + "inputs": { + "logos-nix": "logos-nix_5", + "nixpkgs": [ + "logos-liblogos", + "default-module-loader", "logos-module", "logos-nix", "nixpkgs" ] }, "locked": { - "lastModified": 1780603603, - "narHash": "sha256-CV7R+lwNIP0baXZtE9uNjqu7qDLiANwyGfMkHfTjcl0=", + "lastModified": 1781311603, + "narHash": "sha256-Ym3i52f5MWuZy8Qas3+zfoxz+mxQUsyW+Qol1no6kDs=", "owner": "logos-co", "repo": "logos-module", - "rev": "780894dd40ebc8eded0fa97b1729286f31571cfb", + "rev": "a3e288a71d6f79445db598b0ffcca2ee435596b9", "type": "github" }, "original": { @@ -870,9 +1211,9 @@ }, "logos-module-builder": { "inputs": { - "logos-cpp-sdk": "logos-cpp-sdk", - "logos-module": "logos-module", - "logos-nix": "logos-nix_3", + "logos-cpp-sdk": "logos-cpp-sdk_2", + "logos-module": "logos-module_2", + "logos-nix": "logos-nix_13", "logos-plugin-core": "logos-plugin-core", "logos-plugin-qt": "logos-plugin-qt", "logos-standalone-app": "logos-standalone-app", @@ -903,9 +1244,9 @@ }, "logos-module-builder_2": { "inputs": { - "logos-cpp-sdk": "logos-cpp-sdk_2", - "logos-module": "logos-module_4", - "logos-nix": "logos-nix_10", + "logos-cpp-sdk": "logos-cpp-sdk_3", + "logos-module": "logos-module_5", + "logos-nix": "logos-nix_20", "logos-plugin-core": "logos-plugin-core_2", "logos-plugin-qt": "logos-plugin-qt_2", "logos-standalone-app": "logos-standalone-app_2", @@ -939,9 +1280,9 @@ }, "logos-module-builder_3": { "inputs": { - "logos-cpp-sdk": "logos-cpp-sdk_7", - "logos-module": "logos-module_10", - "logos-nix": "logos-nix_54", + "logos-cpp-sdk": "logos-cpp-sdk_8", + "logos-module": "logos-module_11", + "logos-nix": "logos-nix_64", "logos-plugin-core": "logos-plugin-core_3", "logos-plugin-qt": "logos-plugin-qt_3", "logos-standalone-app": "logos-standalone-app_3", @@ -974,9 +1315,91 @@ "type": "github" } }, + "logos-module-loader": { + "inputs": { + "logos-container": "logos-container_3", + "logos-nix": "logos-nix_7", + "nixpkgs": [ + "logos-liblogos", + "default-module-loader", + "logos-module-loader", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781732584, + "narHash": "sha256-v1eIGPXTnW0oCwEvEjEoBLeoidUh3ZomDGQY9m/ucNw=", + "owner": "logos-co", + "repo": "logos-module-loader", + "rev": "7ed7a5a97e1ad9142187d755c4ce56ed17d69f18", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-module-loader", + "type": "github" + } + }, + "logos-module-loader_2": { + "inputs": { + "logos-container": "logos-container_5", + "logos-nix": "logos-nix_136", + "nixpkgs": [ + "logos-liblogos", + "logos-module-loader", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781732584, + "narHash": "sha256-v1eIGPXTnW0oCwEvEjEoBLeoidUh3ZomDGQY9m/ucNw=", + "owner": "logos-co", + "repo": "logos-module-loader", + "rev": "7ed7a5a97e1ad9142187d755c4ce56ed17d69f18", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-module-loader", + "type": "github" + } + }, "logos-module_10": { "inputs": { - "logos-nix": "logos-nix_53", + "logos-nix": "logos-nix_33", + "nixpkgs": [ + "logos-liblogos", + "logos-capability-module", + "logos-module-builder", + "logos-standalone-app", + "logos-capability-module", + "logos-module-builder", + "logos-standalone-app", + "logos-liblogos", + "logos-module", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1776369033, + "narHash": "sha256-ehePoUEd/u3Ng0TvCmjocXYJWWH6P61PA7tNpgV59lo=", + "owner": "logos-co", + "repo": "logos-module", + "rev": "194778491ef4dd36967ac40cc2fec6b8a8b1d660", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-module", + "type": "github" + } + }, + "logos-module_11": { + "inputs": { + "logos-nix": "logos-nix_63", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -1004,9 +1427,9 @@ "type": "github" } }, - "logos-module_11": { + "logos-module_12": { "inputs": { - "logos-nix": "logos-nix_55", + "logos-nix": "logos-nix_65", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -1035,9 +1458,9 @@ "type": "github" } }, - "logos-module_12": { + "logos-module_13": { "inputs": { - "logos-nix": "logos-nix_57", + "logos-nix": "logos-nix_67", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -1066,41 +1489,9 @@ "type": "github" } }, - "logos-module_13": { - "inputs": { - "logos-nix": "logos-nix_59", - "nixpkgs": [ - "logos-liblogos", - "logos-capability-module", - "logos-module-builder", - "logos-standalone-app", - "logos-liblogos", - "logos-capability-module", - "logos-module-builder", - "logos-standalone-app", - "logos-capability-module", - "logos-module", - "logos-nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1773963329, - "narHash": "sha256-zdvDHoYWQDse0eJ/UCKIJcfuYJ8NMgl6QfxRcyDEovI=", - "owner": "logos-co", - "repo": "logos-module", - "rev": "ac5a4f06ea94b01dd9c5fbb9ed4f20620beab88d", - "type": "github" - }, - "original": { - "owner": "logos-co", - "repo": "logos-module", - "type": "github" - } - }, "logos-module_14": { "inputs": { - "logos-nix": "logos-nix_64", + "logos-nix": "logos-nix_69", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -1110,7 +1501,6 @@ "logos-capability-module", "logos-module-builder", "logos-standalone-app", - "logos-liblogos", "logos-capability-module", "logos-module", "logos-nix", @@ -1133,7 +1523,7 @@ }, "logos-module_15": { "inputs": { - "logos-nix": "logos-nix_67", + "logos-nix": "logos-nix_74", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -1144,17 +1534,18 @@ "logos-module-builder", "logos-standalone-app", "logos-liblogos", + "logos-capability-module", "logos-module", "logos-nix", "nixpkgs" ] }, "locked": { - "lastModified": 1776369033, - "narHash": "sha256-ehePoUEd/u3Ng0TvCmjocXYJWWH6P61PA7tNpgV59lo=", + "lastModified": 1773963329, + "narHash": "sha256-zdvDHoYWQDse0eJ/UCKIJcfuYJ8NMgl6QfxRcyDEovI=", "owner": "logos-co", "repo": "logos-module", - "rev": "194778491ef4dd36967ac40cc2fec6b8a8b1d660", + "rev": "ac5a4f06ea94b01dd9c5fbb9ed4f20620beab88d", "type": "github" }, "original": { @@ -1165,8 +1556,12 @@ }, "logos-module_16": { "inputs": { - "logos-nix": "logos-nix_95", + "logos-nix": "logos-nix_77", "nixpkgs": [ + "logos-liblogos", + "logos-capability-module", + "logos-module-builder", + "logos-standalone-app", "logos-liblogos", "logos-capability-module", "logos-module-builder", @@ -1193,8 +1588,12 @@ }, "logos-module_17": { "inputs": { - "logos-nix": "logos-nix_123", + "logos-nix": "logos-nix_105", "nixpkgs": [ + "logos-liblogos", + "logos-capability-module", + "logos-module-builder", + "logos-standalone-app", "logos-liblogos", "logos-module", "logos-nix", @@ -1215,25 +1614,48 @@ "type": "github" } }, + "logos-module_18": { + "inputs": { + "logos-nix": "logos-nix_134", + "nixpkgs": [ + "logos-liblogos", + "logos-module", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781311603, + "narHash": "sha256-Ym3i52f5MWuZy8Qas3+zfoxz+mxQUsyW+Qol1no6kDs=", + "owner": "logos-co", + "repo": "logos-module", + "rev": "a3e288a71d6f79445db598b0ffcca2ee435596b9", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-module", + "type": "github" + } + }, "logos-module_2": { "inputs": { - "logos-nix": "logos-nix_4", + "logos-nix": "logos-nix_12", "nixpkgs": [ "logos-liblogos", "logos-capability-module", "logos-module-builder", - "logos-plugin-core", "logos-module", "logos-nix", "nixpkgs" ] }, "locked": { - "lastModified": 1776369033, - "narHash": "sha256-ehePoUEd/u3Ng0TvCmjocXYJWWH6P61PA7tNpgV59lo=", + "lastModified": 1780603603, + "narHash": "sha256-CV7R+lwNIP0baXZtE9uNjqu7qDLiANwyGfMkHfTjcl0=", "owner": "logos-co", "repo": "logos-module", - "rev": "194778491ef4dd36967ac40cc2fec6b8a8b1d660", + "rev": "780894dd40ebc8eded0fa97b1729286f31571cfb", "type": "github" }, "original": { @@ -1244,12 +1666,12 @@ }, "logos-module_3": { "inputs": { - "logos-nix": "logos-nix_6", + "logos-nix": "logos-nix_14", "nixpkgs": [ "logos-liblogos", "logos-capability-module", "logos-module-builder", - "logos-plugin-qt", + "logos-plugin-core", "logos-module", "logos-nix", "nixpkgs" @@ -1271,7 +1693,34 @@ }, "logos-module_4": { "inputs": { - "logos-nix": "logos-nix_9", + "logos-nix": "logos-nix_16", + "nixpkgs": [ + "logos-liblogos", + "logos-capability-module", + "logos-module-builder", + "logos-plugin-qt", + "logos-module", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1776369033, + "narHash": "sha256-ehePoUEd/u3Ng0TvCmjocXYJWWH6P61PA7tNpgV59lo=", + "owner": "logos-co", + "repo": "logos-module", + "rev": "194778491ef4dd36967ac40cc2fec6b8a8b1d660", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-module", + "type": "github" + } + }, + "logos-module_5": { + "inputs": { + "logos-nix": "logos-nix_19", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -1298,9 +1747,9 @@ "type": "github" } }, - "logos-module_5": { + "logos-module_6": { "inputs": { - "logos-nix": "logos-nix_11", + "logos-nix": "logos-nix_21", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -1328,9 +1777,9 @@ "type": "github" } }, - "logos-module_6": { + "logos-module_7": { "inputs": { - "logos-nix": "logos-nix_13", + "logos-nix": "logos-nix_23", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -1358,40 +1807,9 @@ "type": "github" } }, - "logos-module_7": { - "inputs": { - "logos-nix": "logos-nix_15", - "nixpkgs": [ - "logos-liblogos", - "logos-capability-module", - "logos-module-builder", - "logos-standalone-app", - "logos-capability-module", - "logos-module-builder", - "logos-standalone-app", - "logos-capability-module", - "logos-module", - "logos-nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1773963329, - "narHash": "sha256-zdvDHoYWQDse0eJ/UCKIJcfuYJ8NMgl6QfxRcyDEovI=", - "owner": "logos-co", - "repo": "logos-module", - "rev": "ac5a4f06ea94b01dd9c5fbb9ed4f20620beab88d", - "type": "github" - }, - "original": { - "owner": "logos-co", - "repo": "logos-module", - "type": "github" - } - }, "logos-module_8": { "inputs": { - "logos-nix": "logos-nix_20", + "logos-nix": "logos-nix_25", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -1400,7 +1818,6 @@ "logos-capability-module", "logos-module-builder", "logos-standalone-app", - "logos-liblogos", "logos-capability-module", "logos-module", "logos-nix", @@ -1423,7 +1840,7 @@ }, "logos-module_9": { "inputs": { - "logos-nix": "logos-nix_23", + "logos-nix": "logos-nix_30", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -1433,17 +1850,18 @@ "logos-module-builder", "logos-standalone-app", "logos-liblogos", + "logos-capability-module", "logos-module", "logos-nix", "nixpkgs" ] }, "locked": { - "lastModified": 1776369033, - "narHash": "sha256-ehePoUEd/u3Ng0TvCmjocXYJWWH6P61PA7tNpgV59lo=", + "lastModified": 1773963329, + "narHash": "sha256-zdvDHoYWQDse0eJ/UCKIJcfuYJ8NMgl6QfxRcyDEovI=", "owner": "logos-co", "repo": "logos-module", - "rev": "194778491ef4dd36967ac40cc2fec6b8a8b1d660", + "rev": "ac5a4f06ea94b01dd9c5fbb9ed4f20620beab88d", "type": "github" }, "original": { @@ -1511,11 +1929,11 @@ "nixpkgs": "nixpkgs_102" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -1546,24 +1964,6 @@ "inputs": { "nixpkgs": "nixpkgs_104" }, - "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", - "owner": "logos-co", - "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", - "type": "github" - }, - "original": { - "owner": "logos-co", - "repo": "logos-nix", - "type": "github" - } - }, - "logos-nix_104": { - "inputs": { - "nixpkgs": "nixpkgs_105" - }, "locked": { "lastModified": 1773955630, "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", @@ -1578,9 +1978,9 @@ "type": "github" } }, - "logos-nix_105": { + "logos-nix_104": { "inputs": { - "nixpkgs": "nixpkgs_106" + "nixpkgs": "nixpkgs_105" }, "locked": { "lastModified": 1774455309, @@ -1596,6 +1996,24 @@ "type": "github" } }, + "logos-nix_105": { + "inputs": { + "nixpkgs": "nixpkgs_106" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, "logos-nix_106": { "inputs": { "nixpkgs": "nixpkgs_107" @@ -1619,11 +2037,11 @@ "nixpkgs": "nixpkgs_108" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -1654,24 +2072,6 @@ "inputs": { "nixpkgs": "nixpkgs_110" }, - "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", - "owner": "logos-co", - "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", - "type": "github" - }, - "original": { - "owner": "logos-co", - "repo": "logos-nix", - "type": "github" - } - }, - "logos-nix_11": { - "inputs": { - "nixpkgs": "nixpkgs_12" - }, "locked": { "lastModified": 1773955630, "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", @@ -1686,9 +2086,9 @@ "type": "github" } }, - "logos-nix_110": { + "logos-nix_11": { "inputs": { - "nixpkgs": "nixpkgs_111" + "nixpkgs": "nixpkgs_12" }, "locked": { "lastModified": 1774455309, @@ -1704,6 +2104,24 @@ "type": "github" } }, + "logos-nix_110": { + "inputs": { + "nixpkgs": "nixpkgs_111" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, "logos-nix_111": { "inputs": { "nixpkgs": "nixpkgs_112" @@ -1763,11 +2181,11 @@ "nixpkgs": "nixpkgs_115" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -1781,11 +2199,11 @@ "nixpkgs": "nixpkgs_116" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -1799,11 +2217,11 @@ "nixpkgs": "nixpkgs_117" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -1871,11 +2289,11 @@ "nixpkgs": "nixpkgs_13" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -1889,11 +2307,11 @@ "nixpkgs": "nixpkgs_121" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -1925,11 +2343,11 @@ "nixpkgs": "nixpkgs_123" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -1943,11 +2361,11 @@ "nixpkgs": "nixpkgs_124" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -1979,11 +2397,11 @@ "nixpkgs": "nixpkgs_126" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -2051,11 +2469,11 @@ "nixpkgs": "nixpkgs_130" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2069,11 +2487,11 @@ "nixpkgs": "nixpkgs_14" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2100,9 +2518,27 @@ "type": "github" } }, - "logos-nix_14": { + "logos-nix_131": { "inputs": { - "nixpkgs": "nixpkgs_15" + "nixpkgs": "nixpkgs_132" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_132": { + "inputs": { + "nixpkgs": "nixpkgs_133" }, "locked": { "lastModified": 1774455309, @@ -2118,9 +2554,27 @@ "type": "github" } }, - "logos-nix_15": { + "logos-nix_133": { "inputs": { - "nixpkgs": "nixpkgs_16" + "nixpkgs": "nixpkgs_134" + }, + "locked": { + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_134": { + "inputs": { + "nixpkgs": "nixpkgs_135" }, "locked": { "lastModified": 1773955630, @@ -2136,6 +2590,240 @@ "type": "github" } }, + "logos-nix_135": { + "inputs": { + "nixpkgs": "nixpkgs_136" + }, + "locked": { + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_136": { + "inputs": { + "nixpkgs": "nixpkgs_137" + }, + "locked": { + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_137": { + "inputs": { + "nixpkgs": "nixpkgs_138" + }, + "locked": { + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_138": { + "inputs": { + "nixpkgs": "nixpkgs_139" + }, + "locked": { + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_139": { + "inputs": { + "nixpkgs": "nixpkgs_140" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_14": { + "inputs": { + "nixpkgs": "nixpkgs_15" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_140": { + "inputs": { + "nixpkgs": "nixpkgs_141" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_141": { + "inputs": { + "nixpkgs": "nixpkgs_142" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_142": { + "inputs": { + "nixpkgs": "nixpkgs_143" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_143": { + "inputs": { + "nixpkgs": "nixpkgs_144" + }, + "locked": { + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_144": { + "inputs": { + "nixpkgs": "nixpkgs_145" + }, + "locked": { + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_145": { + "inputs": { + "nixpkgs": "nixpkgs_146" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, + "logos-nix_15": { + "inputs": { + "nixpkgs": "nixpkgs_16" + }, + "locked": { + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, "logos-nix_16": { "inputs": { "nixpkgs": "nixpkgs_17" @@ -2177,11 +2865,11 @@ "nixpkgs": "nixpkgs_19" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2213,11 +2901,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2231,11 +2919,11 @@ "nixpkgs": "nixpkgs_21" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2321,11 +3009,11 @@ "nixpkgs": "nixpkgs_26" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -2357,11 +3045,11 @@ "nixpkgs": "nixpkgs_28" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2446,24 +3134,6 @@ "inputs": { "nixpkgs": "nixpkgs_32" }, - "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", - "owner": "logos-co", - "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", - "type": "github" - }, - "original": { - "owner": "logos-co", - "repo": "logos-nix", - "type": "github" - } - }, - "logos-nix_32": { - "inputs": { - "nixpkgs": "nixpkgs_33" - }, "locked": { "lastModified": 1773955630, "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", @@ -2478,9 +3148,9 @@ "type": "github" } }, - "logos-nix_33": { + "logos-nix_32": { "inputs": { - "nixpkgs": "nixpkgs_34" + "nixpkgs": "nixpkgs_33" }, "locked": { "lastModified": 1774455309, @@ -2496,6 +3166,24 @@ "type": "github" } }, + "logos-nix_33": { + "inputs": { + "nixpkgs": "nixpkgs_34" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, "logos-nix_34": { "inputs": { "nixpkgs": "nixpkgs_35" @@ -2519,11 +3207,11 @@ "nixpkgs": "nixpkgs_36" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2555,11 +3243,11 @@ "nixpkgs": "nixpkgs_38" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -2573,11 +3261,11 @@ "nixpkgs": "nixpkgs_39" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -2609,11 +3297,11 @@ "nixpkgs": "nixpkgs_5" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2663,11 +3351,11 @@ "nixpkgs": "nixpkgs_43" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -2681,11 +3369,11 @@ "nixpkgs": "nixpkgs_44" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2699,11 +3387,11 @@ "nixpkgs": "nixpkgs_45" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2771,11 +3459,11 @@ "nixpkgs": "nixpkgs_49" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2807,11 +3495,11 @@ "nixpkgs": "nixpkgs_6" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -2825,11 +3513,11 @@ "nixpkgs": "nixpkgs_51" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -2843,11 +3531,11 @@ "nixpkgs": "nixpkgs_52" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -2897,11 +3585,11 @@ "nixpkgs": "nixpkgs_55" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -2932,24 +3620,6 @@ "inputs": { "nixpkgs": "nixpkgs_57" }, - "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", - "owner": "logos-co", - "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", - "type": "github" - }, - "original": { - "owner": "logos-co", - "repo": "logos-nix", - "type": "github" - } - }, - "logos-nix_57": { - "inputs": { - "nixpkgs": "nixpkgs_58" - }, "locked": { "lastModified": 1773955630, "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", @@ -2964,9 +3634,9 @@ "type": "github" } }, - "logos-nix_58": { + "logos-nix_57": { "inputs": { - "nixpkgs": "nixpkgs_59" + "nixpkgs": "nixpkgs_58" }, "locked": { "lastModified": 1774455309, @@ -2982,6 +3652,24 @@ "type": "github" } }, + "logos-nix_58": { + "inputs": { + "nixpkgs": "nixpkgs_59" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, "logos-nix_59": { "inputs": { "nixpkgs": "nixpkgs_60" @@ -3005,11 +3693,11 @@ "nixpkgs": "nixpkgs_7" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -3022,24 +3710,6 @@ "inputs": { "nixpkgs": "nixpkgs_61" }, - "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", - "owner": "logos-co", - "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", - "type": "github" - }, - "original": { - "owner": "logos-co", - "repo": "logos-nix", - "type": "github" - } - }, - "logos-nix_61": { - "inputs": { - "nixpkgs": "nixpkgs_62" - }, "locked": { "lastModified": 1774455309, "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", @@ -3054,9 +3724,9 @@ "type": "github" } }, - "logos-nix_62": { + "logos-nix_61": { "inputs": { - "nixpkgs": "nixpkgs_63" + "nixpkgs": "nixpkgs_62" }, "locked": { "lastModified": 1773955630, @@ -3072,6 +3742,24 @@ "type": "github" } }, + "logos-nix_62": { + "inputs": { + "nixpkgs": "nixpkgs_63" + }, + "locked": { + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, "logos-nix_63": { "inputs": { "nixpkgs": "nixpkgs_64" @@ -3095,11 +3783,11 @@ "nixpkgs": "nixpkgs_65" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -3185,11 +3873,11 @@ "nixpkgs": "nixpkgs_70" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -3239,11 +3927,11 @@ "nixpkgs": "nixpkgs_72" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -3310,24 +3998,6 @@ "inputs": { "nixpkgs": "nixpkgs_76" }, - "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", - "owner": "logos-co", - "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", - "type": "github" - }, - "original": { - "owner": "logos-co", - "repo": "logos-nix", - "type": "github" - } - }, - "logos-nix_76": { - "inputs": { - "nixpkgs": "nixpkgs_77" - }, "locked": { "lastModified": 1773955630, "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", @@ -3342,9 +4012,9 @@ "type": "github" } }, - "logos-nix_77": { + "logos-nix_76": { "inputs": { - "nixpkgs": "nixpkgs_78" + "nixpkgs": "nixpkgs_77" }, "locked": { "lastModified": 1774455309, @@ -3360,6 +4030,24 @@ "type": "github" } }, + "logos-nix_77": { + "inputs": { + "nixpkgs": "nixpkgs_78" + }, + "locked": { + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "owner": "logos-co", + "repo": "logos-nix", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-nix", + "type": "github" + } + }, "logos-nix_78": { "inputs": { "nixpkgs": "nixpkgs_79" @@ -3383,11 +4071,11 @@ "nixpkgs": "nixpkgs_80" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -3437,11 +4125,11 @@ "nixpkgs": "nixpkgs_82" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -3455,11 +4143,11 @@ "nixpkgs": "nixpkgs_83" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -3527,11 +4215,11 @@ "nixpkgs": "nixpkgs_87" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -3545,11 +4233,11 @@ "nixpkgs": "nixpkgs_88" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -3563,11 +4251,11 @@ "nixpkgs": "nixpkgs_89" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -3599,11 +4287,11 @@ "nixpkgs": "nixpkgs_10" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -3653,11 +4341,11 @@ "nixpkgs": "nixpkgs_93" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -3689,11 +4377,11 @@ "nixpkgs": "nixpkgs_95" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -3707,11 +4395,11 @@ "nixpkgs": "nixpkgs_96" }, "locked": { - "lastModified": 1773955630, - "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", + "lastModified": 1774455309, + "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", "owner": "logos-co", "repo": "logos-nix", - "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", + "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", "type": "github" }, "original": { @@ -3743,11 +4431,11 @@ "nixpkgs": "nixpkgs_98" }, "locked": { - "lastModified": 1774455309, - "narHash": "sha256-3AN7aFnArdysrbQQ2UskWzjNSFADb4hDCsnx69Fa0ng=", + "lastModified": 1773955630, + "narHash": "sha256-KqzMoWYIVp2xMgphs7v02T/BE54RKMFxpdC2duhJKG0=", "owner": "logos-co", "repo": "logos-nix", - "rev": "e637a1f5e871244d1c2df1e3c52a067f2eb406f2", + "rev": "0e9e6d66ab8eb34f59e45ed448f7dc29130feb88", "type": "github" }, "original": { @@ -3794,7 +4482,7 @@ }, "logos-package": { "inputs": { - "logos-nix": "logos-nix_26", + "logos-nix": "logos-nix_36", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -3826,7 +4514,7 @@ }, "logos-package-manager": { "inputs": { - "logos-nix": "logos-nix_25", + "logos-nix": "logos-nix_35", "logos-package": "logos-package", "nix-bundle-appimage": "nix-bundle-appimage", "nix-bundle-dir": "nix-bundle-dir_2", @@ -3860,7 +4548,7 @@ }, "logos-package-manager_2": { "inputs": { - "logos-nix": "logos-nix_42", + "logos-nix": "logos-nix_52", "logos-package": "logos-package_4", "nix-bundle-appimage": "nix-bundle-appimage_2", "nix-bundle-dir": "nix-bundle-dir_6", @@ -3893,7 +4581,7 @@ }, "logos-package-manager_3": { "inputs": { - "logos-nix": "logos-nix_69", + "logos-nix": "logos-nix_79", "logos-package": "logos-package_6", "nix-bundle-appimage": "nix-bundle-appimage_3", "nix-bundle-dir": "nix-bundle-dir_9", @@ -3928,7 +4616,7 @@ }, "logos-package-manager_4": { "inputs": { - "logos-nix": "logos-nix_86", + "logos-nix": "logos-nix_96", "logos-package": "logos-package_9", "nix-bundle-appimage": "nix-bundle-appimage_4", "nix-bundle-dir": "nix-bundle-dir_13", @@ -3962,7 +4650,7 @@ }, "logos-package-manager_5": { "inputs": { - "logos-nix": "logos-nix_97", + "logos-nix": "logos-nix_107", "logos-package": "logos-package_11", "nix-bundle-appimage": "nix-bundle-appimage_5", "nix-bundle-dir": "nix-bundle-dir_16", @@ -3993,7 +4681,7 @@ }, "logos-package-manager_6": { "inputs": { - "logos-nix": "logos-nix_114", + "logos-nix": "logos-nix_124", "logos-package": "logos-package_14", "nix-bundle-appimage": "nix-bundle-appimage_6", "nix-bundle-dir": "nix-bundle-dir_20", @@ -4023,7 +4711,7 @@ }, "logos-package-manager_7": { "inputs": { - "logos-nix": "logos-nix_125", + "logos-nix": "logos-nix_138", "logos-package": "logos-package_16", "nix-bundle-appimage": "nix-bundle-appimage_7", "nix-bundle-dir": "nix-bundle-dir_23", @@ -4035,11 +4723,11 @@ ] }, "locked": { - "lastModified": 1776374462, - "narHash": "sha256-HMkuqSLdScAWTwXEWjhqx9Yk82GiPzPIfRaHTvjG730=", + "lastModified": 1781106836, + "narHash": "sha256-vHxdyGdW3Ai3/Omspw5drtJXzXyN0TWuCYet95A5h6E=", "owner": "logos-co", "repo": "logos-package-manager", - "rev": "9101875bc103214855bc6217834e22e66802ed86", + "rev": "8a6b72f28b1e73b6d753eaabe7eba13bf425f04e", "type": "github" }, "original": { @@ -4050,7 +4738,7 @@ }, "logos-package_10": { "inputs": { - "logos-nix": "logos-nix_92", + "logos-nix": "logos-nix_102", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4082,7 +4770,7 @@ }, "logos-package_11": { "inputs": { - "logos-nix": "logos-nix_98", + "logos-nix": "logos-nix_108", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4111,7 +4799,7 @@ }, "logos-package_12": { "inputs": { - "logos-nix": "logos-nix_107", + "logos-nix": "logos-nix_117", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4139,7 +4827,7 @@ }, "logos-package_13": { "inputs": { - "logos-nix": "logos-nix_111", + "logos-nix": "logos-nix_121", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4166,7 +4854,7 @@ }, "logos-package_14": { "inputs": { - "logos-nix": "logos-nix_115", + "logos-nix": "logos-nix_125", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4194,7 +4882,7 @@ }, "logos-package_15": { "inputs": { - "logos-nix": "logos-nix_120", + "logos-nix": "logos-nix_130", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4222,7 +4910,7 @@ }, "logos-package_16": { "inputs": { - "logos-nix": "logos-nix_126", + "logos-nix": "logos-nix_139", "nixpkgs": [ "logos-liblogos", "logos-package-manager", @@ -4247,7 +4935,7 @@ }, "logos-package_2": { "inputs": { - "logos-nix": "logos-nix_35", + "logos-nix": "logos-nix_45", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4278,7 +4966,7 @@ }, "logos-package_3": { "inputs": { - "logos-nix": "logos-nix_39", + "logos-nix": "logos-nix_49", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4308,7 +4996,7 @@ }, "logos-package_4": { "inputs": { - "logos-nix": "logos-nix_43", + "logos-nix": "logos-nix_53", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4339,7 +5027,7 @@ }, "logos-package_5": { "inputs": { - "logos-nix": "logos-nix_48", + "logos-nix": "logos-nix_58", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4370,7 +5058,7 @@ }, "logos-package_6": { "inputs": { - "logos-nix": "logos-nix_70", + "logos-nix": "logos-nix_80", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4403,7 +5091,7 @@ }, "logos-package_7": { "inputs": { - "logos-nix": "logos-nix_79", + "logos-nix": "logos-nix_89", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4435,7 +5123,7 @@ }, "logos-package_8": { "inputs": { - "logos-nix": "logos-nix_83", + "logos-nix": "logos-nix_93", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4466,7 +5154,7 @@ }, "logos-package_9": { "inputs": { - "logos-nix": "logos-nix_87", + "logos-nix": "logos-nix_97", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4498,8 +5186,8 @@ }, "logos-plugin-core": { "inputs": { - "logos-module": "logos-module_2", - "logos-nix": "logos-nix_5", + "logos-module": "logos-module_3", + "logos-nix": "logos-nix_15", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4525,8 +5213,8 @@ }, "logos-plugin-core_2": { "inputs": { - "logos-module": "logos-module_5", - "logos-nix": "logos-nix_12", + "logos-module": "logos-module_6", + "logos-nix": "logos-nix_22", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4555,8 +5243,8 @@ }, "logos-plugin-core_3": { "inputs": { - "logos-module": "logos-module_11", - "logos-nix": "logos-nix_56", + "logos-module": "logos-module_12", + "logos-nix": "logos-nix_66", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4586,8 +5274,8 @@ }, "logos-plugin-qt": { "inputs": { - "logos-module": "logos-module_3", - "logos-nix": "logos-nix_7", + "logos-module": "logos-module_4", + "logos-nix": "logos-nix_17", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4613,8 +5301,8 @@ }, "logos-plugin-qt_2": { "inputs": { - "logos-module": "logos-module_6", - "logos-nix": "logos-nix_14", + "logos-module": "logos-module_7", + "logos-nix": "logos-nix_24", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4643,8 +5331,8 @@ }, "logos-plugin-qt_3": { "inputs": { - "logos-module": "logos-module_12", - "logos-nix": "logos-nix_58", + "logos-module": "logos-module_13", + "logos-nix": "logos-nix_68", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4672,9 +5360,58 @@ "type": "github" } }, + "logos-protocol": { + "inputs": { + "logos-nix": "logos-nix_9", + "nixpkgs": [ + "logos-liblogos", + "default-module-loader", + "logos-protocol", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781303997, + "narHash": "sha256-j9U4wPlHdN5oA+65h1SAo/s2YUHvNYKxij4/pCqEtAs=", + "owner": "logos-co", + "repo": "logos-protocol", + "rev": "9de4165ab68ece4071647026cb2596bed8a8d7e2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-protocol", + "type": "github" + } + }, + "logos-protocol_2": { + "inputs": { + "logos-nix": "logos-nix_143", + "nixpkgs": [ + "logos-liblogos", + "logos-protocol", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781303997, + "narHash": "sha256-j9U4wPlHdN5oA+65h1SAo/s2YUHvNYKxij4/pCqEtAs=", + "owner": "logos-co", + "repo": "logos-protocol", + "rev": "9de4165ab68ece4071647026cb2596bed8a8d7e2", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-protocol", + "type": "github" + } + }, "logos-qt-mcp": { "inputs": { - "logos-nix": "logos-nix_32", + "logos-nix": "logos-nix_42", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4703,7 +5440,7 @@ }, "logos-qt-mcp_2": { "inputs": { - "logos-nix": "logos-nix_76", + "logos-nix": "logos-nix_86", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4733,7 +5470,7 @@ }, "logos-qt-mcp_3": { "inputs": { - "logos-nix": "logos-nix_104", + "logos-nix": "logos-nix_114", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4758,13 +5495,82 @@ "type": "github" } }, + "logos-qt-sdk": { + "inputs": { + "logos-cpp-sdk": [ + "logos-liblogos", + "default-module-loader", + "logos-cpp-sdk" + ], + "logos-lidl": "logos-lidl_2", + "logos-nix": "logos-nix_10", + "logos-protocol": [ + "logos-liblogos", + "default-module-loader", + "logos-protocol" + ], + "nixpkgs": [ + "logos-liblogos", + "default-module-loader", + "logos-qt-sdk", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781655169, + "narHash": "sha256-wjbHmczSG8VUMlRkIp3pviVTLPTzReDbIRfBpqnlQr4=", + "owner": "logos-co", + "repo": "logos-qt-sdk", + "rev": "812369213719773f6486755a30c476a699469b37", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-qt-sdk", + "type": "github" + } + }, + "logos-qt-sdk_2": { + "inputs": { + "logos-cpp-sdk": [ + "logos-liblogos", + "logos-cpp-sdk" + ], + "logos-lidl": "logos-lidl_4", + "logos-nix": "logos-nix_144", + "logos-protocol": [ + "logos-liblogos", + "logos-protocol" + ], + "nixpkgs": [ + "logos-liblogos", + "logos-qt-sdk", + "logos-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1781655169, + "narHash": "sha256-wjbHmczSG8VUMlRkIp3pviVTLPTzReDbIRfBpqnlQr4=", + "owner": "logos-co", + "repo": "logos-qt-sdk", + "rev": "812369213719773f6486755a30c476a699469b37", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-qt-sdk", + "type": "github" + } + }, "logos-standalone-app": { "inputs": { "logos-capability-module": "logos-capability-module_2", - "logos-cpp-sdk": "logos-cpp-sdk_6", + "logos-cpp-sdk": "logos-cpp-sdk_7", "logos-design-system": "logos-design-system_2", "logos-liblogos": "logos-liblogos_3", - "logos-nix": "logos-nix_103", + "logos-nix": "logos-nix_113", "logos-qt-mcp": "logos-qt-mcp_3", "logos-view-module-runtime": "logos-view-module-runtime_3", "nix-bundle-lgx": "nix-bundle-lgx_7", @@ -4794,10 +5600,10 @@ "logos-standalone-app_2": { "inputs": { "logos-capability-module": "logos-capability-module_3", - "logos-cpp-sdk": "logos-cpp-sdk_3", + "logos-cpp-sdk": "logos-cpp-sdk_4", "logos-design-system": "logos-design-system", "logos-liblogos": "logos-liblogos_2", - "logos-nix": "logos-nix_31", + "logos-nix": "logos-nix_41", "logos-qt-mcp": "logos-qt-mcp", "logos-view-module-runtime": "logos-view-module-runtime", "nix-bundle-lgx": "nix-bundle-lgx", @@ -4830,10 +5636,10 @@ "logos-standalone-app_3": { "inputs": { "logos-capability-module": "logos-capability-module_6", - "logos-cpp-sdk": "logos-cpp-sdk_8", + "logos-cpp-sdk": "logos-cpp-sdk_9", "logos-design-system": "logos-design-system_3", "logos-liblogos": "logos-liblogos_4", - "logos-nix": "logos-nix_75", + "logos-nix": "logos-nix_85", "logos-qt-mcp": "logos-qt-mcp_2", "logos-view-module-runtime": "logos-view-module-runtime_2", "nix-bundle-lgx": "nix-bundle-lgx_4", @@ -4875,7 +5681,7 @@ "logos-module-builder", "logos-cpp-sdk" ], - "logos-nix": "logos-nix_37", + "logos-nix": "logos-nix_47", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4914,7 +5720,7 @@ "logos-module-builder", "logos-cpp-sdk" ], - "logos-nix": "logos-nix_81", + "logos-nix": "logos-nix_91", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4950,7 +5756,7 @@ "logos-module-builder", "logos-cpp-sdk" ], - "logos-nix": "logos-nix_109", + "logos-nix": "logos-nix_119", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -4986,7 +5792,7 @@ "logos-standalone-app", "logos-cpp-sdk" ], - "logos-nix": "logos-nix_33", + "logos-nix": "logos-nix_43", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5026,7 +5832,7 @@ "logos-standalone-app", "logos-cpp-sdk" ], - "logos-nix": "logos-nix_77", + "logos-nix": "logos-nix_87", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5056,8 +5862,8 @@ }, "logos-view-module-runtime_3": { "inputs": { - "logos-cpp-sdk": "logos-cpp-sdk_12", - "logos-nix": "logos-nix_105", + "logos-cpp-sdk": "logos-cpp-sdk_13", + "logos-nix": "logos-nix_115", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5084,7 +5890,7 @@ }, "nix-bundle-appimage": { "inputs": { - "logos-nix": "logos-nix_27", + "logos-nix": "logos-nix_37", "nix-bundle-dir": "nix-bundle-dir", "nixpkgs": [ "logos-liblogos", @@ -5117,7 +5923,7 @@ }, "nix-bundle-appimage_2": { "inputs": { - "logos-nix": "logos-nix_44", + "logos-nix": "logos-nix_54", "nix-bundle-dir": "nix-bundle-dir_5", "nixpkgs": [ "logos-liblogos", @@ -5149,7 +5955,7 @@ }, "nix-bundle-appimage_3": { "inputs": { - "logos-nix": "logos-nix_71", + "logos-nix": "logos-nix_81", "nix-bundle-dir": "nix-bundle-dir_8", "nixpkgs": [ "logos-liblogos", @@ -5183,7 +5989,7 @@ }, "nix-bundle-appimage_4": { "inputs": { - "logos-nix": "logos-nix_88", + "logos-nix": "logos-nix_98", "nix-bundle-dir": "nix-bundle-dir_12", "nixpkgs": [ "logos-liblogos", @@ -5216,7 +6022,7 @@ }, "nix-bundle-appimage_5": { "inputs": { - "logos-nix": "logos-nix_99", + "logos-nix": "logos-nix_109", "nix-bundle-dir": "nix-bundle-dir_15", "nixpkgs": [ "logos-liblogos", @@ -5246,7 +6052,7 @@ }, "nix-bundle-appimage_6": { "inputs": { - "logos-nix": "logos-nix_116", + "logos-nix": "logos-nix_126", "nix-bundle-dir": "nix-bundle-dir_19", "nixpkgs": [ "logos-liblogos", @@ -5275,7 +6081,7 @@ }, "nix-bundle-appimage_7": { "inputs": { - "logos-nix": "logos-nix_127", + "logos-nix": "logos-nix_140", "nix-bundle-dir": "nix-bundle-dir_22", "nixpkgs": [ "logos-liblogos", @@ -5301,7 +6107,7 @@ }, "nix-bundle-dir": { "inputs": { - "logos-nix": "logos-nix_28", + "logos-nix": "logos-nix_38", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5332,7 +6138,7 @@ }, "nix-bundle-dir_10": { "inputs": { - "logos-nix": "logos-nix_80", + "logos-nix": "logos-nix_90", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5364,7 +6170,7 @@ }, "nix-bundle-dir_11": { "inputs": { - "logos-nix": "logos-nix_84", + "logos-nix": "logos-nix_94", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5395,7 +6201,7 @@ }, "nix-bundle-dir_12": { "inputs": { - "logos-nix": "logos-nix_89", + "logos-nix": "logos-nix_99", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5426,7 +6232,7 @@ }, "nix-bundle-dir_13": { "inputs": { - "logos-nix": "logos-nix_90", + "logos-nix": "logos-nix_100", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5458,7 +6264,7 @@ }, "nix-bundle-dir_14": { "inputs": { - "logos-nix": "logos-nix_93", + "logos-nix": "logos-nix_103", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5490,7 +6296,7 @@ }, "nix-bundle-dir_15": { "inputs": { - "logos-nix": "logos-nix_100", + "logos-nix": "logos-nix_110", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5518,7 +6324,7 @@ }, "nix-bundle-dir_16": { "inputs": { - "logos-nix": "logos-nix_101", + "logos-nix": "logos-nix_111", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5547,7 +6353,7 @@ }, "nix-bundle-dir_17": { "inputs": { - "logos-nix": "logos-nix_108", + "logos-nix": "logos-nix_118", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5575,7 +6381,7 @@ }, "nix-bundle-dir_18": { "inputs": { - "logos-nix": "logos-nix_112", + "logos-nix": "logos-nix_122", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5602,7 +6408,7 @@ }, "nix-bundle-dir_19": { "inputs": { - "logos-nix": "logos-nix_117", + "logos-nix": "logos-nix_127", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5629,7 +6435,7 @@ }, "nix-bundle-dir_2": { "inputs": { - "logos-nix": "logos-nix_29", + "logos-nix": "logos-nix_39", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5661,7 +6467,7 @@ }, "nix-bundle-dir_20": { "inputs": { - "logos-nix": "logos-nix_118", + "logos-nix": "logos-nix_128", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5689,7 +6495,7 @@ }, "nix-bundle-dir_21": { "inputs": { - "logos-nix": "logos-nix_121", + "logos-nix": "logos-nix_131", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5717,7 +6523,7 @@ }, "nix-bundle-dir_22": { "inputs": { - "logos-nix": "logos-nix_128", + "logos-nix": "logos-nix_141", "nixpkgs": [ "logos-liblogos", "logos-package-manager", @@ -5741,7 +6547,7 @@ }, "nix-bundle-dir_23": { "inputs": { - "logos-nix": "logos-nix_129", + "logos-nix": "logos-nix_142", "nixpkgs": [ "logos-liblogos", "logos-package-manager", @@ -5766,7 +6572,7 @@ }, "nix-bundle-dir_3": { "inputs": { - "logos-nix": "logos-nix_36", + "logos-nix": "logos-nix_46", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5797,7 +6603,7 @@ }, "nix-bundle-dir_4": { "inputs": { - "logos-nix": "logos-nix_40", + "logos-nix": "logos-nix_50", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5827,7 +6633,7 @@ }, "nix-bundle-dir_5": { "inputs": { - "logos-nix": "logos-nix_45", + "logos-nix": "logos-nix_55", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5857,7 +6663,7 @@ }, "nix-bundle-dir_6": { "inputs": { - "logos-nix": "logos-nix_46", + "logos-nix": "logos-nix_56", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5888,7 +6694,7 @@ }, "nix-bundle-dir_7": { "inputs": { - "logos-nix": "logos-nix_49", + "logos-nix": "logos-nix_59", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5919,7 +6725,7 @@ }, "nix-bundle-dir_8": { "inputs": { - "logos-nix": "logos-nix_72", + "logos-nix": "logos-nix_82", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5951,7 +6757,7 @@ }, "nix-bundle-dir_9": { "inputs": { - "logos-nix": "logos-nix_73", + "logos-nix": "logos-nix_83", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -5984,7 +6790,7 @@ }, "nix-bundle-lgx": { "inputs": { - "logos-nix": "logos-nix_34", + "logos-nix": "logos-nix_44", "logos-package": "logos-package_2", "nix-bundle-dir": "nix-bundle-dir_3", "nixpkgs": [ @@ -6015,7 +6821,7 @@ }, "nix-bundle-lgx_2": { "inputs": { - "logos-nix": "logos-nix_38", + "logos-nix": "logos-nix_48", "logos-package": "logos-package_3", "nix-bundle-dir": "nix-bundle-dir_4", "nixpkgs": [ @@ -6046,7 +6852,7 @@ }, "nix-bundle-lgx_3": { "inputs": { - "logos-nix": "logos-nix_47", + "logos-nix": "logos-nix_57", "logos-package": "logos-package_5", "nix-bundle-dir": "nix-bundle-dir_7", "nixpkgs": [ @@ -6078,7 +6884,7 @@ }, "nix-bundle-lgx_4": { "inputs": { - "logos-nix": "logos-nix_78", + "logos-nix": "logos-nix_88", "logos-package": "logos-package_7", "nix-bundle-dir": "nix-bundle-dir_10", "nixpkgs": [ @@ -6110,7 +6916,7 @@ }, "nix-bundle-lgx_5": { "inputs": { - "logos-nix": "logos-nix_82", + "logos-nix": "logos-nix_92", "logos-package": "logos-package_8", "nix-bundle-dir": "nix-bundle-dir_11", "nixpkgs": [ @@ -6142,7 +6948,7 @@ }, "nix-bundle-lgx_6": { "inputs": { - "logos-nix": "logos-nix_91", + "logos-nix": "logos-nix_101", "logos-package": "logos-package_10", "nix-bundle-dir": "nix-bundle-dir_14", "nixpkgs": [ @@ -6175,7 +6981,7 @@ }, "nix-bundle-lgx_7": { "inputs": { - "logos-nix": "logos-nix_106", + "logos-nix": "logos-nix_116", "logos-package": "logos-package_12", "nix-bundle-dir": "nix-bundle-dir_17", "nixpkgs": [ @@ -6204,7 +7010,7 @@ }, "nix-bundle-lgx_8": { "inputs": { - "logos-nix": "logos-nix_110", + "logos-nix": "logos-nix_120", "logos-package": "logos-package_13", "nix-bundle-dir": "nix-bundle-dir_18", "nixpkgs": [ @@ -6232,7 +7038,7 @@ }, "nix-bundle-lgx_9": { "inputs": { - "logos-nix": "logos-nix_119", + "logos-nix": "logos-nix_129", "logos-package": "logos-package_15", "nix-bundle-dir": "nix-bundle-dir_21", "nixpkgs": [ @@ -6261,7 +7067,7 @@ }, "nix-bundle-logos-module-install": { "inputs": { - "logos-nix": "logos-nix_41", + "logos-nix": "logos-nix_51", "logos-package-manager": "logos-package-manager_2", "nix-bundle-lgx": "nix-bundle-lgx_3", "nixpkgs": [ @@ -6292,7 +7098,7 @@ }, "nix-bundle-logos-module-install_2": { "inputs": { - "logos-nix": "logos-nix_85", + "logos-nix": "logos-nix_95", "logos-package-manager": "logos-package-manager_4", "nix-bundle-lgx": "nix-bundle-lgx_6", "nixpkgs": [ @@ -6324,7 +7130,7 @@ }, "nix-bundle-logos-module-install_3": { "inputs": { - "logos-nix": "logos-nix_113", + "logos-nix": "logos-nix_123", "logos-package-manager": "logos-package-manager_6", "nix-bundle-lgx": "nix-bundle-lgx_9", "nixpkgs": [ @@ -6944,16 +7750,128 @@ }, "nixpkgs_132": { "locked": { - "lastModified": 1767313136, - "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-25.05", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_133": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_134": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_135": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_136": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_137": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_138": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_139": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } @@ -6974,6 +7892,134 @@ "type": "github" } }, + "nixpkgs_140": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_141": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_142": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_143": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_144": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_145": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_146": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_147": { + "locked": { + "lastModified": 1767313136, + "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs_15": { "locked": { "lastModified": 1759036355, @@ -8464,7 +9510,7 @@ }, "process-stats": { "inputs": { - "logos-nix": "logos-nix_30", + "logos-nix": "logos-nix_40", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -8495,7 +9541,7 @@ }, "process-stats_2": { "inputs": { - "logos-nix": "logos-nix_74", + "logos-nix": "logos-nix_84", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -8527,7 +9573,7 @@ }, "process-stats_3": { "inputs": { - "logos-nix": "logos-nix_102", + "logos-nix": "logos-nix_112", "nixpkgs": [ "logos-liblogos", "logos-capability-module", @@ -8555,7 +9601,7 @@ }, "process-stats_4": { "inputs": { - "logos-nix": "logos-nix_130", + "logos-nix": "logos-nix_145", "nixpkgs": [ "logos-liblogos", "process-stats", @@ -8597,11 +9643,11 @@ ] }, "locked": { - "lastModified": 1781234414, - "narHash": "sha256-HdA+P4fKRGOomkewnI/Tww5Wz4xK1O7+hDO90YAsPB4=", + "lastModified": 1781752752, + "narHash": "sha256-kVG5tV9hddPviGAgqf9sGSuStvv+HAB9onfKqGptV0k=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "1d18bfe3de6244c641ca4e8011186d0981b81d76", + "rev": "c06d86dabe5b92982b9d67acccb9990d58da3a0e", "type": "github" }, "original": { @@ -8612,7 +9658,7 @@ }, "rust-rapidsnark": { "inputs": { - "nixpkgs": "nixpkgs_132" + "nixpkgs": "nixpkgs_147" }, "locked": { "lastModified": 1781090841, diff --git a/flake.nix b/flake.nix index 3babf94c..0aaad40c 100644 --- a/flake.nix +++ b/flake.nix @@ -99,6 +99,28 @@ sha256 = recursionZkrHash; }; + # risc0 compiles its Metal (GPU) prover kernels by invoking + # `xcrun metal` / `xcrun metallib`. Under nix, the darwin stdenv sets + # DEVELOPER_DIR/SDKROOT to its own SDK, which makes `xcrun` look for + # the `metal` tool in the wrong place and fail with + # error: cannot execute tool 'metal' due to missing Metal Toolchain + # even when a working Metal Toolchain is installed. This wrapper, put + # first in PATH, clears those two vars for metal/metallib invocations + # only โ€” so they resolve the real system Xcode Metal Toolchain โ€” while + # every other xcrun call passes through with the nix environment + # intact. (On recent macOS the Metal Toolchain is a per-user component; + # `xcodebuild -downloadComponent MetalToolchain` must have been run.) + metalStub = pkgs.writeShellScriptBin "xcrun" '' + tool= + for a in "$@"; do + case "$a" in metal|metallib) tool=1 ;; esac + done + if [ -n "$tool" ]; then + unset DEVELOPER_DIR SDKROOT + fi + exec /usr/bin/xcrun "$@" + ''; + commonArgs = { inherit src; buildInputs = [ pkgs.openssl ]; @@ -118,13 +140,14 @@ RECURSION_SRC_PATH = "${recursionZkr}"; # Provide a writable HOME so risc0-build-kernel can use its cache directory # (needed on macOS for Metal kernel compilation cache). - # On macOS, append /usr/bin to PATH so xcrun (Metal compiler) can be found, - # while keeping Nix tools (like gnutar) first in PATH. + # On macOS, put the metalStub xcrun wrapper first so `xcrun metal` / + # `metallib` resolve the system Metal Toolchain (see metalStub above), + # and append /usr/bin for the real xcrun it execs. # This requires running with --option sandbox false for Metal GPU support. preBuild = '' export HOME=$(mktemp -d) '' + pkgs.lib.optionalString pkgs.stdenv.isDarwin '' - export PATH="$PATH:/usr/bin" + export PATH="${metalStub}/bin:$PATH:/usr/bin" ''; }; diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 3b1731d8..bba06ec0 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -9,7 +9,6 @@ workspace = true [dependencies] test_fixtures.workspace = true - lee_core = { workspace = true, features = ["host"] } lee.workspace = true authenticated_transfer_core.workspace = true @@ -19,7 +18,7 @@ common.workspace = true key_protocol.workspace = true serde_json.workspace = true token_core.workspace = true -ata_core.workspace = true +associated_token_account_core.workspace = true vault_core.workspace = true faucet_core.workspace = true bridge_core.workspace = true @@ -28,6 +27,9 @@ sequencer_service_rpc = { workspace = true, features = ["client"] } wallet-ffi.workspace = true indexer_ffi.workspace = true indexer_service_protocol.workspace = true +system_accounts.workspace = true +programs.workspace = true +test_programs.workspace = true logos-blockchain-http-api-common.workspace = true logos-blockchain-core.workspace = true diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 48f69559..bf395101 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -6,12 +6,169 @@ use std::time::Duration; use anyhow::{Context as _, Result}; +use key_protocol::key_management::key_tree::chain_index::ChainIndex; +use lee::AccountId; use log::info; +use sequencer_service_rpc::RpcClient as _; pub use test_fixtures::*; +use wallet::{ + cli::{ + CliAccountMention, Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + programs::{ + native_token_transfer::AuthTransferSubcommand, token::TokenProgramAgnosticSubcommand, + }, + }, + storage::key_chain::FoundPrivateAccount, +}; /// Maximum time to wait for the indexer to catch up to the sequencer. pub const L2_TO_L1_TIMEOUT: Duration = Duration::from_mins(6); +/// Create a private or public account at the given chain index and return its ID. +/// Pass `cci: None` to use the wallet's next available chain index. +pub async fn new_account( + ctx: &mut TestContext, + private: bool, + cci: Option, +) -> Result { + let subcommand = if private { + NewSubcommand::Private { cci, label: None } + } else { + NewSubcommand::Public { cci, label: None } + }; + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(subcommand)), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { account_id } = result else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + Ok(account_id) +} + +/// Send `amount` from `from` to `to` via an authenticated transfer (identifier 0). +pub async fn send( + ctx: &mut TestContext, + from: CliAccountMention, + to: CliAccountMention, + amount: u128, +) -> Result<()> { + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from, + to: Some(to), + to_npk: None, + to_vpk: None, + to_keys: None, + to_identifier: Some(0), + amount, + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + Ok(()) +} + +/// Create a token (New) and wait for the block to be included. +pub async fn create_token( + ctx: &mut TestContext, + definition_account_id: CliAccountMention, + supply_account_id: CliAccountMention, + name: impl Into, + total_supply: u128, +) -> Result<()> { + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_account_id, + supply_account_id, + name: name.into(), + total_supply, + }; + wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + Ok(()) +} + +/// Send tokens and wait for the block to be included. +pub async fn token_send( + ctx: &mut TestContext, + from: CliAccountMention, + to: CliAccountMention, + amount: u128, +) -> Result<()> { + let subcommand = TokenProgramAgnosticSubcommand::Send { + from, + to: Some(to), + to_npk: None, + to_vpk: None, + to_keys: None, + to_identifier: Some(0), + amount, + }; + wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; + info!("Waiting for next block creation"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + Ok(()) +} + +/// Retrieve the native token balance for `account_id`. +pub async fn account_balance(ctx: &TestContext, account_id: AccountId) -> Result { + Ok(ctx + .sequencer_client() + .get_account_balance(account_id) + .await?) +} + +/// Fetch the full account state for `account_id` from the sequencer. +pub async fn get_account(ctx: &TestContext, account_id: AccountId) -> Result { + Ok(ctx.sequencer_client().get_account(account_id).await?) +} + +/// Fetch the current commitment for `account_id` and assert it is present in the sequencer state. +pub async fn assert_private_commitment_in_state( + ctx: &TestContext, + account_id: AccountId, + label: &str, +) -> Result<()> { + let commitment = ctx + .wallet() + .get_private_account_commitment(account_id) + .with_context(|| format!("Failed to get commitment for {label}"))?; + assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await); + Ok(()) +} + +/// Sync the wallet's private accounts. +pub async fn sync_private(ctx: &mut TestContext) -> Result<()> { + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::SyncPrivate {}), + ) + .await?; + Ok(()) +} + +/// Look up a restored private account for `account_id`, panicking with `label` if absent. +pub fn restored_private_account<'ctx>( + ctx: &'ctx TestContext, + account_id: AccountId, + label: &str, +) -> FoundPrivateAccount<'ctx> { + ctx.wallet() + .storage() + .key_chain() + .private_account(account_id) + .unwrap_or_else(|| panic!("{label} should be restored")) +} + +/// Assert that a restored public account's signing key exists, panicking with `label` if absent. +pub fn assert_public_account_restored(ctx: &TestContext, account_id: AccountId, label: &str) { + ctx.wallet() + .storage() + .key_chain() + .pub_account_signing_key(account_id) + .unwrap_or_else(|| panic!("{label} should be restored")); +} + /// Poll the indexer until its last finalized block id reaches the sequencer's /// current last block id or until [`L2_TO_L1_TIMEOUT`] elapses. /// Returns the last indexer block id observed. diff --git a/integration_tests/tests/account.rs b/integration_tests/tests/account.rs index 3490b42b..2b69f7e0 100644 --- a/integration_tests/tests/account.rs +++ b/integration_tests/tests/account.rs @@ -4,12 +4,11 @@ )] use anyhow::{Context as _, Result}; -use integration_tests::{TestContext, private_mention}; +use integration_tests::{TestContext, get_account, new_account, private_mention}; use key_protocol::key_management::KeyChain; -use lee::{Data, program::Program}; +use lee::Data; use lee_core::account::Nonce; use log::info; -use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::{ account::{AccountIdWithPrivacy, HumanReadableAccount, Label}, @@ -24,14 +23,11 @@ use wallet::{ async fn get_existing_account() -> Result<()> { let ctx = TestContext::new().await?; - let account = ctx - .sequencer_client() - .get_account(ctx.existing_public_accounts()[0]) - .await?; + let account = get_account(&ctx, ctx.existing_public_accounts()[0]).await?; assert_eq!( account.program_owner, - Program::authenticated_transfer_program().id() + programs::authenticated_transfer().id() ); assert_eq!(account.balance, 10000); assert!(account.data.is_empty()); @@ -95,18 +91,7 @@ async fn add_label_to_existing_account() -> Result<()> { async fn new_public_account_without_label() -> Result<()> { let mut ctx = TestContext::new().await?; - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })); - - let result = execute_subcommand(ctx.wallet_mut(), command).await?; - - // Extract the account_id from the result - - let wallet::cli::SubcommandReturnValue::RegisterAccount { account_id } = result else { - panic!("Expected RegisterAccount return value") - }; + let account_id = new_account(&mut ctx, false, None).await?; // Verify no label was stored for the account id assert!( @@ -162,7 +147,7 @@ async fn import_private_account() -> Result<()> { 0, )); let account = lee::Account { - program_owner: Program::authenticated_transfer_program().id(), + program_owner: programs::authenticated_transfer().id(), balance: 777, data: Data::default(), nonce: Nonce::default(), @@ -226,7 +211,7 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<( serde_json::to_string(&key_chain).context("Failed to serialize key chain")?; let initial_account = lee::Account { - program_owner: Program::authenticated_transfer_program().id(), + program_owner: programs::authenticated_transfer().id(), balance: 100, data: Data::default(), nonce: Nonce::default(), @@ -245,7 +230,7 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<( .await?; let updated_account = lee::Account { - program_owner: Program::authenticated_transfer_program().id(), + program_owner: programs::authenticated_transfer().id(), balance: 999, data: Data::default(), nonce: Nonce::default(), diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index 9f953001..6e270413 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -7,16 +7,18 @@ use std::time::Duration; use anyhow::Result; -use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention}; +use integration_tests::{ + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, create_token, get_account, new_account, + public_mention, token_send, +}; use log::info; -use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::{ account::Label, cli::{ Command, SubcommandReturnValue, account::{AccountSubcommand, NewSubcommand}, - programs::{amm::AmmProgramAgnosticSubcommand, token::TokenProgramAgnosticSubcommand}, + programs::amm::AmmProgramAgnosticSubcommand, }, }; @@ -25,148 +27,60 @@ async fn amm_public() -> Result<()> { let mut ctx = TestContext::new().await?; // Create new account for the token definition - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id_1, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id_1 = new_account(&mut ctx, false, None).await?; // Create new account for the token supply holder - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id_1, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id_1 = new_account(&mut ctx, false, None).await?; // Create new account for receiving a token transaction - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id_1, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id_1 = new_account(&mut ctx, false, None).await?; // Create new account for the token definition - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id_2, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id_2 = new_account(&mut ctx, false, None).await?; // Create new account for the token supply holder - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id_2, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id_2 = new_account(&mut ctx, false, None).await?; // Create new account for receiving a token transaction - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id_2, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id_2 = new_account(&mut ctx, false, None).await?; // Create new token - let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_account_id_1), - supply_account_id: public_mention(supply_account_id_1), - name: "A NAM1".to_owned(), - - total_supply: 37, - }; - wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + create_token( + &mut ctx, + public_mention(definition_account_id_1), + public_mention(supply_account_id_1), + "A NAM1".to_owned(), + 37, + ) + .await?; // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1` - let subcommand = TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_account_id_1), - to: Some(public_mention(recipient_account_id_1)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 7, - }; - - wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + token_send( + &mut ctx, + public_mention(supply_account_id_1), + public_mention(recipient_account_id_1), + 7, + ) + .await?; // Create new token - let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_account_id_2), - supply_account_id: public_mention(supply_account_id_2), - name: "A NAM2".to_owned(), - - total_supply: 37, - }; - wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + create_token( + &mut ctx, + public_mention(definition_account_id_2), + public_mention(supply_account_id_2), + "A NAM2".to_owned(), + 37, + ) + .await?; // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_2` - let subcommand = TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_account_id_2), - to: Some(public_mention(recipient_account_id_2)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 7, - }; - - wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + token_send( + &mut ctx, + public_mention(supply_account_id_2), + public_mention(recipient_account_id_2), + 7, + ) + .await?; info!("=================== SETUP FINISHED ==============="); @@ -174,19 +88,7 @@ async fn amm_public() -> Result<()> { // Setup accounts // Create new account for the user holding lp - let SubcommandReturnValue::RegisterAccount { - account_id: user_holding_lp, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let user_holding_lp = new_account(&mut ctx, false, None).await?; // Send creation tx let subcommand = AmmProgramAgnosticSubcommand::New { @@ -201,17 +103,11 @@ async fn amm_public() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let user_holding_a_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_1) - .await?; + let user_holding_a_acc = get_account(&ctx, recipient_account_id_1).await?; - let user_holding_b_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_2) - .await?; + let user_holding_b_acc = get_account(&ctx, recipient_account_id_2).await?; - let user_holding_lp_acc = ctx.sequencer_client().get_account(user_holding_lp).await?; + let user_holding_lp_acc = get_account(&ctx, user_holding_lp).await?; assert_eq!( u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()), @@ -244,17 +140,11 @@ async fn amm_public() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let user_holding_a_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_1) - .await?; + let user_holding_a_acc = get_account(&ctx, recipient_account_id_1).await?; - let user_holding_b_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_2) - .await?; + let user_holding_b_acc = get_account(&ctx, recipient_account_id_2).await?; - let user_holding_lp_acc = ctx.sequencer_client().get_account(user_holding_lp).await?; + let user_holding_lp_acc = get_account(&ctx, user_holding_lp).await?; assert_eq!( u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()), @@ -287,17 +177,11 @@ async fn amm_public() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let user_holding_a_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_1) - .await?; + let user_holding_a_acc = get_account(&ctx, recipient_account_id_1).await?; - let user_holding_b_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_2) - .await?; + let user_holding_b_acc = get_account(&ctx, recipient_account_id_2).await?; - let user_holding_lp_acc = ctx.sequencer_client().get_account(user_holding_lp).await?; + let user_holding_lp_acc = get_account(&ctx, user_holding_lp).await?; assert_eq!( u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()), @@ -331,17 +215,11 @@ async fn amm_public() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let user_holding_a_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_1) - .await?; + let user_holding_a_acc = get_account(&ctx, recipient_account_id_1).await?; - let user_holding_b_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_2) - .await?; + let user_holding_b_acc = get_account(&ctx, recipient_account_id_2).await?; - let user_holding_lp_acc = ctx.sequencer_client().get_account(user_holding_lp).await?; + let user_holding_lp_acc = get_account(&ctx, user_holding_lp).await?; assert_eq!( u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()), @@ -375,17 +253,11 @@ async fn amm_public() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let user_holding_a_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_1) - .await?; + let user_holding_a_acc = get_account(&ctx, recipient_account_id_1).await?; - let user_holding_b_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_2) - .await?; + let user_holding_b_acc = get_account(&ctx, recipient_account_id_2).await?; - let user_holding_lp_acc = ctx.sequencer_client().get_account(user_holding_lp).await?; + let user_holding_lp_acc = get_account(&ctx, user_holding_lp).await?; assert_eq!( u128::from_le_bytes(user_holding_a_acc.data[33..].try_into().unwrap()), @@ -412,33 +284,9 @@ async fn amm_new_pool_using_labels() -> Result<()> { let mut ctx = TestContext::new().await?; // Create token 1 accounts - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id_1, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id_1 = new_account(&mut ctx, false, None).await?; - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id_1, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id_1 = new_account(&mut ctx, false, None).await?; // Create holding_a with a label let holding_a_label = Label::new("amm-holding-a-label"); @@ -457,33 +305,9 @@ async fn amm_new_pool_using_labels() -> Result<()> { }; // Create token 2 accounts - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id_2, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id_2 = new_account(&mut ctx, false, None).await?; - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id_2, - } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id_2 = new_account(&mut ctx, false, None).await?; // Create holding_b with a label let holding_b_label = Label::new("amm-holding-b-label"); @@ -518,48 +342,40 @@ async fn amm_new_pool_using_labels() -> Result<()> { }; // Create token 1 and distribute to holding_a - let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_account_id_1), - supply_account_id: public_mention(supply_account_id_1), - name: "TOKEN1".to_owned(), - total_supply: 10, - }; - wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + create_token( + &mut ctx, + public_mention(definition_account_id_1), + public_mention(supply_account_id_1), + "TOKEN1".to_owned(), + 10, + ) + .await?; - let subcommand = TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_account_id_1), - to: Some(public_mention(holding_a_id)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 5, - }; - wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + token_send( + &mut ctx, + public_mention(supply_account_id_1), + public_mention(holding_a_id), + 5, + ) + .await?; // Create token 2 and distribute to holding_b - let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_account_id_2), - supply_account_id: public_mention(supply_account_id_2), - name: "TOKEN2".to_owned(), - total_supply: 10, - }; - wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + create_token( + &mut ctx, + public_mention(definition_account_id_2), + public_mention(supply_account_id_2), + "TOKEN2".to_owned(), + 10, + ) + .await?; - let subcommand = TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_account_id_2), - to: Some(public_mention(holding_b_id)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 5, - }; - wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + token_send( + &mut ctx, + public_mention(supply_account_id_2), + public_mention(holding_b_id), + 5, + ) + .await?; // Create AMM pool using account labels instead of IDs let subcommand = AmmProgramAgnosticSubcommand::New { @@ -572,7 +388,7 @@ async fn amm_new_pool_using_labels() -> Result<()> { wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?; tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let holding_lp_acc = ctx.sequencer_client().get_account(holding_lp_id).await?; + let holding_lp_acc = get_account(&ctx, holding_lp_id).await?; // LP balance should be 3 (geometric mean of 3, 3) assert_eq!( diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index 9e37061b..21905fb9 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -7,78 +7,36 @@ use std::time::Duration; use anyhow::{Context as _, Result}; -use ata_core::{compute_ata_seed, get_associated_token_account_id}; +use associated_token_account_core::{compute_ata_seed, get_associated_token_account_id}; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention, - verify_commitment_is_in_state, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, create_token, get_account, new_account, + private_mention, public_mention, token_send, verify_commitment_is_in_state, }; -use lee::program::Program; use log::info; use sequencer_service_rpc::RpcClient as _; use token_core::{TokenDefinition, TokenHolding}; use tokio::test; -use wallet::cli::{ - Command, SubcommandReturnValue, - account::{AccountSubcommand, NewSubcommand}, - programs::{ata::AtaSubcommand, token::TokenProgramAgnosticSubcommand}, -}; - -/// Create a public account and return its ID. -async fn new_public_account(ctx: &mut TestContext) -> Result { - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { account_id } = result else { - anyhow::bail!("Expected RegisterAccount return value"); - }; - Ok(account_id) -} - -/// Create a private account and return its ID. -async fn new_private_account(ctx: &mut TestContext) -> Result { - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { account_id } = result else { - anyhow::bail!("Expected RegisterAccount return value"); - }; - Ok(account_id) -} +use wallet::cli::{Command, programs::ata::AtaSubcommand}; #[test] async fn create_ata_initializes_holding_account() -> Result<()> { let mut ctx = TestContext::new().await?; - let definition_account_id = new_public_account(&mut ctx).await?; - let supply_account_id = new_public_account(&mut ctx).await?; - let owner_account_id = new_public_account(&mut ctx).await?; + let definition_account_id = new_account(&mut ctx, false, None).await?; + let supply_account_id = new_account(&mut ctx, false, None).await?; + let owner_account_id = new_account(&mut ctx, false, None).await?; // Create a fungible token let total_supply = 100_u128; - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_account_id), - supply_account_id: public_mention(supply_account_id), - name: "TEST".to_owned(), - total_supply, - }), + create_token( + &mut ctx, + public_mention(definition_account_id), + public_mention(supply_account_id), + "TEST".to_owned(), + total_supply, ) .await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - // Create the ATA for owner + definition wallet::cli::execute_subcommand( ctx.wallet_mut(), @@ -93,7 +51,7 @@ async fn create_ata_initializes_holding_account() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Derive expected ATA address and check on-chain state - let ata_program_id = Program::ata().id(); + let ata_program_id = programs::ata().id(); let ata_id = get_associated_token_account_id( &ata_program_id, &compute_ata_seed(owner_account_id, definition_account_id), @@ -105,7 +63,7 @@ async fn create_ata_initializes_holding_account() -> Result<()> { .await .context("ATA account not found")?; - assert_eq!(ata_acc.program_owner, Program::token().id()); + assert_eq!(ata_acc.program_owner, programs::token().id()); let holding = TokenHolding::try_from(&ata_acc.data)?; assert_eq!( holding, @@ -122,25 +80,20 @@ async fn create_ata_initializes_holding_account() -> Result<()> { async fn create_ata_is_idempotent() -> Result<()> { let mut ctx = TestContext::new().await?; - let definition_account_id = new_public_account(&mut ctx).await?; - let supply_account_id = new_public_account(&mut ctx).await?; - let owner_account_id = new_public_account(&mut ctx).await?; + let definition_account_id = new_account(&mut ctx, false, None).await?; + let supply_account_id = new_account(&mut ctx, false, None).await?; + let owner_account_id = new_account(&mut ctx, false, None).await?; // Create a fungible token - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_account_id), - supply_account_id: public_mention(supply_account_id), - name: "TEST".to_owned(), - total_supply: 100, - }), + create_token( + &mut ctx, + public_mention(definition_account_id), + public_mention(supply_account_id), + "TEST".to_owned(), + 100, ) .await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - // Create the ATA once wallet::cli::execute_subcommand( ctx.wallet_mut(), @@ -168,7 +121,7 @@ async fn create_ata_is_idempotent() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // State must be unchanged - let ata_program_id = Program::ata().id(); + let ata_program_id = programs::ata().id(); let ata_id = get_associated_token_account_id( &ata_program_id, &compute_ata_seed(owner_account_id, definition_account_id), @@ -180,7 +133,7 @@ async fn create_ata_is_idempotent() -> Result<()> { .await .context("ATA account not found")?; - assert_eq!(ata_acc.program_owner, Program::token().id()); + assert_eq!(ata_acc.program_owner, programs::token().id()); let holding = TokenHolding::try_from(&ata_acc.data)?; assert_eq!( holding, @@ -197,30 +150,25 @@ async fn create_ata_is_idempotent() -> Result<()> { async fn transfer_and_burn_via_ata() -> Result<()> { let mut ctx = TestContext::new().await?; - let definition_account_id = new_public_account(&mut ctx).await?; - let supply_account_id = new_public_account(&mut ctx).await?; - let sender_account_id = new_public_account(&mut ctx).await?; - let recipient_account_id = new_public_account(&mut ctx).await?; + let definition_account_id = new_account(&mut ctx, false, None).await?; + let supply_account_id = new_account(&mut ctx, false, None).await?; + let sender_account_id = new_account(&mut ctx, false, None).await?; + let recipient_account_id = new_account(&mut ctx, false, None).await?; let total_supply = 1000_u128; // Create a fungible token, supply goes to supply_account_id - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_account_id), - supply_account_id: public_mention(supply_account_id), - name: "TEST".to_owned(), - total_supply, - }), + create_token( + &mut ctx, + public_mention(definition_account_id), + public_mention(supply_account_id), + "TEST".to_owned(), + total_supply, ) .await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - // Derive ATA addresses - let ata_program_id = Program::ata().id(); + let ata_program_id = programs::ata().id(); let sender_ata_id = get_associated_token_account_id( &ata_program_id, &compute_ata_seed(sender_account_id, definition_account_id), @@ -253,23 +201,14 @@ async fn transfer_and_burn_via_ata() -> Result<()> { // Fund sender's ATA from the supply account (direct token transfer) let fund_amount = 200_u128; - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_account_id), - to: Some(public_mention(sender_ata_id)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: fund_amount, - }), + token_send( + &mut ctx, + public_mention(supply_account_id), + public_mention(sender_ata_id), + fund_amount, ) .await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - // Transfer from sender's ATA to recipient's ATA via the ATA program let transfer_amount = 50_u128; wallet::cli::execute_subcommand( @@ -287,7 +226,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Verify sender ATA balance decreased - let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?; + let sender_ata_acc = get_account(&ctx, sender_ata_id).await?; let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?; assert_eq!( sender_holding, @@ -298,7 +237,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> { ); // Verify recipient ATA balance increased - let recipient_ata_acc = ctx.sequencer_client().get_account(recipient_ata_id).await?; + let recipient_ata_acc = get_account(&ctx, recipient_ata_id).await?; let recipient_holding = TokenHolding::try_from(&recipient_ata_acc.data)?; assert_eq!( recipient_holding, @@ -324,7 +263,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Verify sender ATA balance after burn - let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?; + let sender_ata_acc = get_account(&ctx, sender_ata_id).await?; let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?; assert_eq!( sender_holding, @@ -335,10 +274,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> { ); // Verify the token definition total_supply decreased by burn_amount - let definition_acc = ctx - .sequencer_client() - .get_account(definition_account_id) - .await?; + let definition_acc = get_account(&ctx, definition_account_id).await?; let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!( token_definition, @@ -356,25 +292,20 @@ async fn transfer_and_burn_via_ata() -> Result<()> { async fn create_ata_with_private_owner() -> Result<()> { let mut ctx = TestContext::new().await?; - let definition_account_id = new_public_account(&mut ctx).await?; - let supply_account_id = new_public_account(&mut ctx).await?; - let owner_account_id = new_private_account(&mut ctx).await?; + let definition_account_id = new_account(&mut ctx, false, None).await?; + let supply_account_id = new_account(&mut ctx, false, None).await?; + let owner_account_id = new_account(&mut ctx, true, None).await?; // Create a fungible token - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_account_id), - supply_account_id: public_mention(supply_account_id), - name: "TEST".to_owned(), - total_supply: 100, - }), + create_token( + &mut ctx, + public_mention(definition_account_id), + public_mention(supply_account_id), + "TEST".to_owned(), + 100, ) .await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - // Create the ATA for the private owner + definition wallet::cli::execute_subcommand( ctx.wallet_mut(), @@ -389,7 +320,7 @@ async fn create_ata_with_private_owner() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Derive expected ATA address and check on-chain state - let ata_program_id = Program::ata().id(); + let ata_program_id = programs::ata().id(); let ata_id = get_associated_token_account_id( &ata_program_id, &compute_ata_seed(owner_account_id, definition_account_id), @@ -401,7 +332,7 @@ async fn create_ata_with_private_owner() -> Result<()> { .await .context("ATA account not found")?; - assert_eq!(ata_acc.program_owner, Program::token().id()); + assert_eq!(ata_acc.program_owner, programs::token().id()); let holding = TokenHolding::try_from(&ata_acc.data)?; assert_eq!( holding, @@ -425,30 +356,25 @@ async fn create_ata_with_private_owner() -> Result<()> { async fn transfer_via_ata_private_owner() -> Result<()> { let mut ctx = TestContext::new().await?; - let definition_account_id = new_public_account(&mut ctx).await?; - let supply_account_id = new_public_account(&mut ctx).await?; - let sender_account_id = new_private_account(&mut ctx).await?; - let recipient_account_id = new_public_account(&mut ctx).await?; + let definition_account_id = new_account(&mut ctx, false, None).await?; + let supply_account_id = new_account(&mut ctx, false, None).await?; + let sender_account_id = new_account(&mut ctx, true, None).await?; + let recipient_account_id = new_account(&mut ctx, false, None).await?; let total_supply = 1000_u128; // Create a fungible token - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_account_id), - supply_account_id: public_mention(supply_account_id), - name: "TEST".to_owned(), - total_supply, - }), + create_token( + &mut ctx, + public_mention(definition_account_id), + public_mention(supply_account_id), + "TEST".to_owned(), + total_supply, ) .await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - // Derive ATA addresses - let ata_program_id = Program::ata().id(); + let ata_program_id = programs::ata().id(); let sender_ata_id = get_associated_token_account_id( &ata_program_id, &compute_ata_seed(sender_account_id, definition_account_id), @@ -481,23 +407,14 @@ async fn transfer_via_ata_private_owner() -> Result<()> { // Fund sender's ATA from the supply account (direct token transfer) let fund_amount = 200_u128; - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_account_id), - to: Some(public_mention(sender_ata_id)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: fund_amount, - }), + token_send( + &mut ctx, + public_mention(supply_account_id), + public_mention(sender_ata_id), + fund_amount, ) .await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - // Transfer from sender's ATA (private owner) to recipient's ATA let transfer_amount = 50_u128; wallet::cli::execute_subcommand( @@ -515,7 +432,7 @@ async fn transfer_via_ata_private_owner() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Verify sender ATA balance decreased - let sender_ata_acc = ctx.sequencer_client().get_account(sender_ata_id).await?; + let sender_ata_acc = get_account(&ctx, sender_ata_id).await?; let sender_holding = TokenHolding::try_from(&sender_ata_acc.data)?; assert_eq!( sender_holding, @@ -526,7 +443,7 @@ async fn transfer_via_ata_private_owner() -> Result<()> { ); // Verify recipient ATA balance increased - let recipient_ata_acc = ctx.sequencer_client().get_account(recipient_ata_id).await?; + let recipient_ata_acc = get_account(&ctx, recipient_ata_id).await?; let recipient_holding = TokenHolding::try_from(&recipient_ata_acc.data)?; assert_eq!( recipient_holding, @@ -550,29 +467,24 @@ async fn transfer_via_ata_private_owner() -> Result<()> { async fn burn_via_ata_private_owner() -> Result<()> { let mut ctx = TestContext::new().await?; - let definition_account_id = new_public_account(&mut ctx).await?; - let supply_account_id = new_public_account(&mut ctx).await?; - let holder_account_id = new_private_account(&mut ctx).await?; + let definition_account_id = new_account(&mut ctx, false, None).await?; + let supply_account_id = new_account(&mut ctx, false, None).await?; + let holder_account_id = new_account(&mut ctx, true, None).await?; let total_supply = 500_u128; // Create a fungible token - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: public_mention(definition_account_id), - supply_account_id: public_mention(supply_account_id), - name: "TEST".to_owned(), - total_supply, - }), + create_token( + &mut ctx, + public_mention(definition_account_id), + public_mention(supply_account_id), + "TEST".to_owned(), + total_supply, ) .await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - // Derive holder's ATA address - let ata_program_id = Program::ata().id(); + let ata_program_id = programs::ata().id(); let holder_ata_id = get_associated_token_account_id( &ata_program_id, &compute_ata_seed(holder_account_id, definition_account_id), @@ -593,23 +505,14 @@ async fn burn_via_ata_private_owner() -> Result<()> { // Fund holder's ATA from the supply account let fund_amount = 300_u128; - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Token(TokenProgramAgnosticSubcommand::Send { - from: public_mention(supply_account_id), - to: Some(public_mention(holder_ata_id)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: fund_amount, - }), + token_send( + &mut ctx, + public_mention(supply_account_id), + public_mention(holder_ata_id), + fund_amount, ) .await?; - info!("Waiting for next block creation"); - tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - // Burn from holder's ATA (private owner) let burn_amount = 100_u128; wallet::cli::execute_subcommand( @@ -626,7 +529,7 @@ async fn burn_via_ata_private_owner() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Verify holder ATA balance after burn - let holder_ata_acc = ctx.sequencer_client().get_account(holder_ata_id).await?; + let holder_ata_acc = get_account(&ctx, holder_ata_id).await?; let holder_holding = TokenHolding::try_from(&holder_ata_acc.data)?; assert_eq!( holder_holding, @@ -637,10 +540,7 @@ async fn burn_via_ata_private_owner() -> Result<()> { ); // Verify the token definition total_supply decreased by burn_amount - let definition_acc = ctx - .sequencer_client() - .get_account(definition_account_id) - .await?; + let definition_acc = get_account(&ctx, definition_account_id).await?; let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!( token_definition, diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index a862e334..205cbfed 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -3,15 +3,18 @@ use std::time::Duration; use anyhow::{Context as _, Result}; use common::transaction::LeeTransaction; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, private_mention, - public_mention, verify_commitment_is_in_state, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, + assert_private_commitment_in_state, fetch_privacy_preserving_tx, get_account, new_account, + private_mention, public_mention, send, sync_private, verify_commitment_is_in_state, }; use lee::{ AccountId, execute_and_prove, privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, }; use lee_core::{ - InputAccountIdentity, NullifierPublicKey, account::AccountWithMetadata, + DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, NullifierPublicKey, + account::{Account, AccountWithMetadata}, + compute_digest_for_path, encryption::ViewingPublicKey, }; use log::info; @@ -33,32 +36,13 @@ async fn private_transfer_to_owned_account() -> Result<()> { let from: AccountId = ctx.existing_private_accounts()[0]; let to: AccountId = ctx.existing_private_accounts()[1]; - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: private_mention(from), - to: Some(private_mention(to)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + send(&mut ctx, private_mention(from), private_mention(to), 100).await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let new_commitment1 = ctx - .wallet() - .get_private_account_commitment(from) - .context("Failed to get private account commitment for sender")?; - assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await); - - let new_commitment2 = ctx - .wallet() - .get_private_account_commitment(to) - .context("Failed to get private account commitment for receiver")?; - assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await); + assert_private_commitment_in_state(&ctx, from, "sender").await?; + assert_private_commitment_in_state(&ctx, to, "receiver").await?; info!("Successfully transferred privately to owned account"); @@ -124,17 +108,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> { .context("Failed to get sender's private account")?; assert_eq!(from_acc.balance, 10000); - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: private_mention(from), - to: Some(public_mention(to)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + send(&mut ctx, private_mention(from), public_mention(to), 100).await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -143,13 +117,9 @@ async fn deshielded_transfer_to_public_account() -> Result<()> { .wallet() .get_account_private(from) .context("Failed to get sender's private account")?; - let new_commitment = ctx - .wallet() - .get_private_account_commitment(from) - .context("Failed to get private account commitment")?; - assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await); + assert_private_commitment_in_state(&ctx, from, "sender").await?; - let acc_2_balance = ctx.sequencer_client().get_account_balance(to).await?; + let acc_2_balance = account_balance(&ctx, to).await?; assert_eq!(from_acc.balance, 9900); assert_eq!(acc_2_balance, 20100); @@ -166,18 +136,7 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> { let from: AccountId = ctx.existing_private_accounts()[0]; // Create a new private account - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })); - - let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - let SubcommandReturnValue::RegisterAccount { - account_id: to_account_id, - } = sub_ret - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let to_account_id = new_account(&mut ctx, true, None).await?; // Get the keys for the newly created account let to = ctx @@ -206,14 +165,13 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> { let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await; // Sync the wallet to claim the new account - let command = Command::Account(AccountSubcommand::SyncPrivate {}); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + sync_private(&mut ctx).await?; - let new_commitment1 = ctx + let sender_commitment = ctx .wallet() .get_private_account_commitment(from) .context("Failed to get private account commitment for sender")?; - assert_eq!(tx.message.new_commitments[0], new_commitment1); + assert_eq!(tx.message.new_commitments[0], sender_commitment); assert_eq!(tx.message.new_commitments.len(), 2); for commitment in tx.message.new_commitments { @@ -238,17 +196,7 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> { let from: AccountId = ctx.existing_public_accounts()[0]; let to: AccountId = ctx.existing_private_accounts()[1]; - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: public_mention(from), - to: Some(private_mention(to)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + send(&mut ctx, public_mention(from), private_mention(to), 100).await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -257,13 +205,9 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> { .wallet() .get_account_private(to) .context("Failed to get receiver's private account")?; - let new_commitment = ctx - .wallet() - .get_private_account_commitment(to) - .context("Failed to get receiver's commitment")?; - assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await); + assert_private_commitment_in_state(&ctx, to, "receiver").await?; - let acc_from_balance = ctx.sequencer_client().get_account_balance(from).await?; + let acc_from_balance = account_balance(&ctx, from).await?; assert_eq!(acc_from_balance, 9900); assert_eq!(acc_to.balance, 20100); @@ -302,7 +246,7 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> { let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await; - let acc_1_balance = ctx.sequencer_client().get_account_balance(from).await?; + let acc_1_balance = account_balance(&ctx, from).await?; assert!( verify_commitment_is_in_state( @@ -331,18 +275,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> { let from: AccountId = ctx.existing_private_accounts()[0]; // Create a new private account - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })); - let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - - let SubcommandReturnValue::RegisterAccount { - account_id: to_account_id, - } = sub_ret - else { - anyhow::bail!("Failed to register account"); - }; + let to_account_id = new_account(&mut ctx, true, None).await?; // Get the newly created account's keys let to = ctx @@ -395,14 +328,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> { async fn initialize_private_account() -> Result<()> { let mut ctx = TestContext::new().await?; - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })); - let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - let SubcommandReturnValue::RegisterAccount { account_id } = result else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let account_id = new_account(&mut ctx, true, None).await?; let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: private_mention(account_id), @@ -412,14 +338,9 @@ async fn initialize_private_account() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Syncing private accounts"); - let command = Command::Account(AccountSubcommand::SyncPrivate {}); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + sync_private(&mut ctx).await?; - let new_commitment = ctx - .wallet() - .get_private_account_commitment(account_id) - .context("Failed to get private account commitment")?; - assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await); + assert_private_commitment_in_state(&ctx, account_id, "account").await?; let account = ctx .wallet() @@ -428,7 +349,7 @@ async fn initialize_private_account() -> Result<()> { assert_eq!( account.program_owner, - Program::authenticated_transfer_program().id() + programs::authenticated_transfer().id() ); assert_eq!(account.balance, 0); assert!(account.data.is_empty()); @@ -454,32 +375,19 @@ async fn private_transfer_using_from_label() -> Result<()> { wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; // Send using the label instead of account ID - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: CliAccountMention::Label(label), - to: Some(private_mention(to)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + send( + &mut ctx, + CliAccountMention::Label(label), + private_mention(to), + 100, + ) + .await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let new_commitment1 = ctx - .wallet() - .get_private_account_commitment(from) - .context("Failed to get private account commitment for sender")?; - assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await); - - let new_commitment2 = ctx - .wallet() - .get_private_account_commitment(to) - .context("Failed to get private account commitment for receiver")?; - assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await); + assert_private_commitment_in_state(&ctx, from, "sender").await?; + assert_private_commitment_in_state(&ctx, to, "receiver").await?; info!("Successfully transferred privately using from_label"); @@ -509,14 +417,9 @@ async fn initialize_private_account_using_label() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let command = Command::Account(AccountSubcommand::SyncPrivate {}); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + sync_private(&mut ctx).await?; - let new_commitment = ctx - .wallet() - .get_private_account_commitment(account_id) - .context("Failed to get private account commitment")?; - assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await); + assert_private_commitment_in_state(&ctx, account_id, "account").await?; let account = ctx .wallet() @@ -525,7 +428,7 @@ async fn initialize_private_account_using_label() -> Result<()> { assert_eq!( account.program_owner, - Program::authenticated_transfer_program().id() + programs::authenticated_transfer().id() ); info!("Successfully initialized private account using label"); @@ -592,11 +495,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::SyncPrivate {}), - ) - .await?; + sync_private(&mut ctx).await?; // Both accounts must be discovered with the correct balances. let account_id_1 = AccountId::for_regular_private_account(&npk, &vpk, identifier_1); @@ -645,23 +544,20 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { async fn ppt_cant_chain_call_faucet() -> Result<()> { let ctx = TestContext::new().await?; - let binary = std::fs::read( - std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../artifacts/test_program_methods/faucet_chain_caller.bin"), - )?; + let faucet_chain_caller = test_programs::faucet_chain_caller(); let deploy_tx = LeeTransaction::ProgramDeployment(lee::ProgramDeploymentTransaction::new( - lee::program_deployment_transaction::Message::new(binary.clone()), + lee::program_deployment_transaction::Message::new(faucet_chain_caller.elf().to_owned()), )); ctx.sequencer_client().send_transaction(deploy_tx).await?; info!("Waiting for deploy block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let faucet_account_id = lee::system_faucet_account_id(); + let faucet_account_id = system_accounts::faucet_account_id(); let attacker_id = ctx.existing_public_accounts()[0]; - let faucet_program_id = Program::faucet().id(); - let vault_program_id = Program::vault().id(); - let auth_transfer_program_id = Program::authenticated_transfer_program().id(); + let faucet_program_id = programs::faucet().id(); + let vault_program_id = programs::vault().id(); + let auth_transfer_program_id = programs::authenticated_transfer().id(); let nsk: lee_core::NullifierSecretKey = [3; 32]; let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); @@ -672,30 +568,22 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { let amount: u128 = 1; let faucet_pre = AccountWithMetadata::new( - ctx.sequencer_client() - .get_account(faucet_account_id) - .await?, + get_account(&ctx, faucet_account_id).await?, false, faucet_account_id, ); let vault_pda_pre = AccountWithMetadata::new( - ctx.sequencer_client() - .get_account(attacker_vault_id) - .await?, + get_account(&ctx, attacker_vault_id).await?, false, attacker_vault_id, ); - let faucet_chain_caller = Program::new(binary)?; let program_with_deps = ProgramWithDependencies::new( faucet_chain_caller, [ - (faucet_program_id, Program::faucet()), - (vault_program_id, Program::vault()), - ( - auth_transfer_program_id, - Program::authenticated_transfer_program(), - ), + (faucet_program_id, programs::faucet()), + (vault_program_id, programs::vault()), + (auth_transfer_program_id, programs::authenticated_transfer()), ] .into(), ); @@ -713,6 +601,7 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { random_seed: [0; 32], npk, identifier: 1337, + commitment_root: DUMMY_COMMITMENT_HASH, seed: None, }, ], @@ -723,3 +612,99 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { Ok(()) } + +async fn prove_init_with_commitment_root( + ctx: &TestContext, + commitment_root: lee_core::CommitmentSetDigest, +) -> Result { + let program = programs::authenticated_transfer(); + let sender_id = ctx.existing_public_accounts()[0]; + let sender_pre = AccountWithMetadata::new( + ctx.sequencer_client().get_account(sender_id).await?, + true, + sender_id, + ); + + let nsk: lee_core::NullifierSecretKey = [7; 32]; + let npk = NullifierPublicKey::from(&nsk); + let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); + let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0); + let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); + + let (output, _) = execute_and_prove( + vec![sender_pre, recipient], + Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { + amount: 1, + })?, + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + vpk, + random_seed: [0; 32], + npk, + identifier: 0, + commitment_root, + }, + ], + &program.into(), + )?; + + Ok(output) +} + +#[test] +async fn init_with_dummy_commitment_root_produces_valid_root() -> Result<()> { + let ctx = TestContext::new().await?; + + let dummy_proof = ctx + .sequencer_client() + .get_proof_for_commitment(DUMMY_COMMITMENT) + .await? + .expect("DUMMY_COMMITMENT must be in genesis commitment set"); + let expected_digest = compute_digest_for_path(&DUMMY_COMMITMENT, &dummy_proof); + + let nsk: lee_core::NullifierSecretKey = [7; 32]; + let npk = NullifierPublicKey::from(&nsk); + let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); + let recipient_account_id = AccountId::for_regular_private_account(&npk, &vpk, 0); + + let output = prove_init_with_commitment_root(&ctx, expected_digest).await?; + + assert_eq!(output.new_nullifiers.len(), 1); + let (nullifier, digest) = &output.new_nullifiers[0]; + assert_eq!( + *nullifier, + Nullifier::for_account_initialization(&recipient_account_id) + ); + assert_eq!(*digest, expected_digest); + assert_ne!(*digest, DUMMY_COMMITMENT_HASH); + + Ok(()) +} + +#[test] +async fn init_nullifier_digest_is_bound_to_commitment_root() -> Result<()> { + let ctx = TestContext::new().await?; + + let dummy_proof = ctx + .sequencer_client() + .get_proof_for_commitment(DUMMY_COMMITMENT) + .await? + .expect("DUMMY_COMMITMENT must be in genesis commitment set"); + let expected_digest = compute_digest_for_path(&DUMMY_COMMITMENT, &dummy_proof); + + let output_with_root = prove_init_with_commitment_root(&ctx, expected_digest).await?; + let output_without_root = prove_init_with_commitment_root(&ctx, DUMMY_COMMITMENT_HASH).await?; + + assert_eq!(output_with_root.new_nullifiers[0].1, expected_digest); + assert_eq!( + output_without_root.new_nullifiers[0].1, + DUMMY_COMMITMENT_HASH + ); + assert_ne!( + output_with_root.new_nullifiers[0].1, + output_without_root.new_nullifiers[0].1, + ); + + Ok(()) +} diff --git a/integration_tests/tests/auth_transfer/public.rs b/integration_tests/tests/auth_transfer/public.rs index d00b7964..c42bc20c 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -1,17 +1,19 @@ -use std::{path::PathBuf, time::Duration}; +use std::time::Duration; use anyhow::Result; use common::transaction::LeeTransaction; -use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention}; -use lee::{program::Program, public_transaction, system_faucet_account_id}; +use integration_tests::{ + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account, new_account, + public_mention, send, +}; +use lee::public_transaction; use log::info; use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::{ account::Label, cli::{ - CliAccountMention, Command, SubcommandReturnValue, - account::{AccountSubcommand, NewSubcommand}, + CliAccountMention, Command, account::AccountSubcommand, programs::native_token_transfer::AuthTransferSubcommand, }, }; @@ -20,30 +22,22 @@ use wallet::{ async fn successful_transfer_to_existing_account() -> Result<()> { let mut ctx = TestContext::new().await?; - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: public_mention(ctx.existing_public_accounts()[0]), - to: Some(public_mention(ctx.existing_public_accounts()[1])), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let sender = ctx.existing_public_accounts()[0]; + let receiver = ctx.existing_public_accounts()[1]; + send( + &mut ctx, + public_mention(sender), + public_mention(receiver), + 100, + ) + .await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Checking correct balance move"); - let acc_1_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[0]) - .await?; - let acc_2_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[1]) - .await?; + let acc_1_balance = account_balance(&ctx, sender).await?; + let acc_2_balance = account_balance(&ctx, receiver).await?; info!("Balance of sender: {acc_1_balance:#?}"); info!("Balance of receiver: {acc_2_balance:#?}"); @@ -58,51 +52,23 @@ async fn successful_transfer_to_existing_account() -> Result<()> { pub async fn successful_transfer_to_new_account() -> Result<()> { let mut ctx = TestContext::new().await?; - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })); + let new_persistent_account_id = new_account(&mut ctx, false, None).await?; - wallet::cli::execute_subcommand(ctx.wallet_mut(), command) - .await - .unwrap(); - - let new_persistent_account_id = ctx - .wallet() - .storage() - .key_chain() - .public_account_ids() - .map(|(account_id, _)| account_id) - .find(|acc_id| { - *acc_id != ctx.existing_public_accounts()[0] - && *acc_id != ctx.existing_public_accounts()[1] - }) - .expect("Failed to find newly created account in the wallet storage"); - - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: public_mention(ctx.existing_public_accounts()[0]), - to: Some(public_mention(new_persistent_account_id)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let sender = ctx.existing_public_accounts()[0]; + send( + &mut ctx, + public_mention(sender), + public_mention(new_persistent_account_id), + 100, + ) + .await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Checking correct balance move"); - let acc_1_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[0]) - .await?; - let acc_2_balance = ctx - .sequencer_client() - .get_account_balance(new_persistent_account_id) - .await?; + let acc_1_balance = account_balance(&ctx, sender).await?; + let acc_2_balance = account_balance(&ctx, new_persistent_account_id).await?; info!("Balance of sender: {acc_1_balance:#?}"); info!("Balance of receiver: {acc_2_balance:#?}"); @@ -134,14 +100,8 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Checking balances unchanged"); - let acc_1_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[0]) - .await?; - let acc_2_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[1]) - .await?; + let acc_1_balance = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?; + let acc_2_balance = account_balance(&ctx, ctx.existing_public_accounts()[1]).await?; info!("Balance of sender: {acc_1_balance:#?}"); info!("Balance of receiver: {acc_2_balance:#?}"); @@ -156,31 +116,24 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> { async fn two_consecutive_successful_transfers() -> Result<()> { let mut ctx = TestContext::new().await?; - // First transfer - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: public_mention(ctx.existing_public_accounts()[0]), - to: Some(public_mention(ctx.existing_public_accounts()[1])), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); + let sender = ctx.existing_public_accounts()[0]; + let receiver = ctx.existing_public_accounts()[1]; - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + // First transfer + send( + &mut ctx, + public_mention(sender), + public_mention(receiver), + 100, + ) + .await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Checking correct balance move after first transfer"); - let acc_1_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[0]) - .await?; - let acc_2_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[1]) - .await?; + let acc_1_balance = account_balance(&ctx, sender).await?; + let acc_2_balance = account_balance(&ctx, receiver).await?; info!("Balance of sender: {acc_1_balance:#?}"); info!("Balance of receiver: {acc_2_balance:#?}"); @@ -191,30 +144,20 @@ async fn two_consecutive_successful_transfers() -> Result<()> { info!("First TX Success!"); // Second transfer - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: public_mention(ctx.existing_public_accounts()[0]), - to: Some(public_mention(ctx.existing_public_accounts()[1])), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + send( + &mut ctx, + public_mention(sender), + public_mention(receiver), + 100, + ) + .await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Checking correct balance move after second transfer"); - let acc_1_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[0]) - .await?; - let acc_2_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[1]) - .await?; + let acc_1_balance = account_balance(&ctx, sender).await?; + let acc_2_balance = account_balance(&ctx, receiver).await?; info!("Balance of sender: {acc_1_balance:#?}"); info!("Balance of receiver: {acc_2_balance:#?}"); @@ -231,14 +174,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> { async fn initialize_public_account() -> Result<()> { let mut ctx = TestContext::new().await?; - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })); - let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - let SubcommandReturnValue::RegisterAccount { account_id } = result else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let account_id = new_account(&mut ctx, false, None).await?; let command = Command::AuthTransfer(AuthTransferSubcommand::Init { account_id: public_mention(account_id), @@ -246,11 +182,11 @@ async fn initialize_public_account() -> Result<()> { wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; info!("Checking correct execution"); - let account = ctx.sequencer_client().get_account(account_id).await?; + let account = get_account(&ctx, account_id).await?; assert_eq!( account.program_owner, - Program::authenticated_transfer_program().id() + programs::authenticated_transfer().id() ); assert_eq!(account.balance, 0); assert_eq!(account.nonce.0, 1); @@ -274,30 +210,22 @@ async fn successful_transfer_using_from_label() -> Result<()> { wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; // Send using the label instead of account ID - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: CliAccountMention::Label(label), - to: Some(public_mention(ctx.existing_public_accounts()[1])), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let sender = ctx.existing_public_accounts()[0]; + let receiver = ctx.existing_public_accounts()[1]; + send( + &mut ctx, + CliAccountMention::Label(label), + public_mention(receiver), + 100, + ) + .await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Checking correct balance move"); - let acc_1_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[0]) - .await?; - let acc_2_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[1]) - .await?; + let acc_1_balance = account_balance(&ctx, sender).await?; + let acc_2_balance = account_balance(&ctx, receiver).await?; assert_eq!(acc_1_balance, 9900); assert_eq!(acc_2_balance, 20100); @@ -320,30 +248,22 @@ async fn successful_transfer_using_to_label() -> Result<()> { wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; // Send using the label for the recipient - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: public_mention(ctx.existing_public_accounts()[0]), - to: Some(CliAccountMention::Label(label)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let sender = ctx.existing_public_accounts()[0]; + let receiver = ctx.existing_public_accounts()[1]; + send( + &mut ctx, + public_mention(sender), + CliAccountMention::Label(label), + 100, + ) + .await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Checking correct balance move"); - let acc_1_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[0]) - .await?; - let acc_2_balance = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[1]) - .await?; + let acc_1_balance = account_balance(&ctx, sender).await?; + let acc_2_balance = account_balance(&ctx, receiver).await?; assert_eq!(acc_1_balance, 9900); assert_eq!(acc_2_balance, 20100); @@ -356,21 +276,15 @@ async fn successful_transfer_using_to_label() -> Result<()> { #[test] async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> { let ctx = TestContext::new().await?; - let faucet_account_id = system_faucet_account_id(); + let faucet_account_id = system_accounts::faucet_account_id(); let recipient = ctx.existing_public_accounts()[0]; - let recipient_balance_before = ctx - .sequencer_client() - .get_account_balance(recipient) - .await?; - let faucet_balance_before = ctx - .sequencer_client() - .get_account_balance(faucet_account_id) - .await?; + let recipient_balance_before = account_balance(&ctx, recipient).await?; + let faucet_balance_before = account_balance(&ctx, faucet_account_id).await?; let amount = 1_u128; let message = public_transaction::Message::try_new( - Program::authenticated_transfer_program().id(), + programs::authenticated_transfer().id(), vec![faucet_account_id, recipient], vec![], authenticated_transfer_core::Instruction::Transfer { amount }, @@ -387,14 +301,8 @@ async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let recipient_balance_after = ctx - .sequencer_client() - .get_account_balance(recipient) - .await?; - let faucet_balance_after = ctx - .sequencer_client() - .get_account_balance(faucet_account_id) - .await?; + let recipient_balance_after = account_balance(&ctx, recipient).await?; + let faucet_balance_after = account_balance(&ctx, faucet_account_id).await?; let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?; assert_eq!(recipient_balance_after, recipient_balance_before); @@ -407,24 +315,18 @@ async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> { #[test] async fn cannot_execute_faucet_program() -> Result<()> { let ctx = TestContext::new().await?; - let faucet_account_id = system_faucet_account_id(); + let faucet_account_id = system_accounts::faucet_account_id(); let recipient = ctx.existing_public_accounts()[0]; - let vault_program_id = Program::vault().id(); + let vault_program_id = programs::vault().id(); let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient); - let recipient_balance_before = ctx - .sequencer_client() - .get_account_balance(recipient) - .await?; - let faucet_balance_before = ctx - .sequencer_client() - .get_account_balance(faucet_account_id) - .await?; + let recipient_balance_before = account_balance(&ctx, recipient).await?; + let faucet_balance_before = account_balance(&ctx, faucet_account_id).await?; let amount = 1_u128; let message = public_transaction::Message::try_new( - Program::faucet().id(), + programs::faucet().id(), vec![faucet_account_id, recipient_vault_id], vec![], faucet_core::Instruction::GenesisTransferVault { @@ -445,14 +347,8 @@ async fn cannot_execute_faucet_program() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let recipient_balance_after = ctx - .sequencer_client() - .get_account_balance(recipient) - .await?; - let faucet_balance_after = ctx - .sequencer_client() - .get_account_balance(faucet_account_id) - .await?; + let recipient_balance_after = account_balance(&ctx, recipient).await?; + let faucet_balance_after = account_balance(&ctx, faucet_account_id).await?; let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?; assert_eq!(recipient_balance_after, recipient_balance_before); @@ -466,28 +362,24 @@ async fn cannot_execute_faucet_program() -> Result<()> { async fn user_tx_that_chain_calls_faucet_is_dropped() -> Result<()> { let ctx = TestContext::new().await?; - let binary = std::fs::read( - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../artifacts/test_program_methods/faucet_chain_caller.bin"), - )?; - let faucet_chain_caller_id = Program::new(binary.clone())?.id(); + let faucet_chain_caller = test_programs::faucet_chain_caller(); let deploy_tx = LeeTransaction::ProgramDeployment(lee::ProgramDeploymentTransaction::new( - lee::program_deployment_transaction::Message::new(binary), + lee::program_deployment_transaction::Message::new(faucet_chain_caller.elf().to_owned()), )); ctx.sequencer_client().send_transaction(deploy_tx).await?; info!("Waiting for deploy block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let faucet_account_id = system_faucet_account_id(); + let faucet_account_id = system_accounts::faucet_account_id(); let attacker = ctx.existing_public_accounts()[0]; - let faucet_program_id = Program::faucet().id(); - let vault_program_id = Program::vault().id(); + let faucet_program_id = programs::faucet().id(); + let vault_program_id = programs::vault().id(); let attacker_vault_id = vault_core::compute_vault_account_id(vault_program_id, attacker); let amount: u128 = 1; let message = public_transaction::Message::try_new( - faucet_chain_caller_id, + faucet_chain_caller.id(), vec![faucet_account_id, attacker_vault_id], vec![], (faucet_program_id, vault_program_id, attacker, amount), @@ -497,28 +389,16 @@ async fn user_tx_that_chain_calls_faucet_is_dropped() -> Result<()> { lee::public_transaction::WitnessSet::from_raw_parts(vec![]), )); - let faucet_balance_before = ctx - .sequencer_client() - .get_account_balance(faucet_account_id) - .await?; - let vault_balance_before = ctx - .sequencer_client() - .get_account_balance(attacker_vault_id) - .await?; + let faucet_balance_before = account_balance(&ctx, faucet_account_id).await?; + let vault_balance_before = account_balance(&ctx, attacker_vault_id).await?; let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let faucet_balance_after = ctx - .sequencer_client() - .get_account_balance(faucet_account_id) - .await?; - let vault_balance_after = ctx - .sequencer_client() - .get_account_balance(attacker_vault_id) - .await?; + let faucet_balance_after = account_balance(&ctx, faucet_account_id).await?; + let vault_balance_after = account_balance(&ctx, attacker_vault_id).await?; let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?; assert_eq!(faucet_balance_after, faucet_balance_before); diff --git a/integration_tests/tests/block_size_limit.rs b/integration_tests/tests/block_size_limit.rs index d95150f5..d97b695d 100644 --- a/integration_tests/tests/block_size_limit.rs +++ b/integration_tests/tests/block_size_limit.rs @@ -94,16 +94,12 @@ async fn accept_transaction_within_limit() -> Result<()> { #[test] async fn transaction_deferred_to_next_block_when_current_full() -> Result<()> { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let artifacts_dir = - std::path::PathBuf::from(manifest_dir).join("../artifacts/test_program_methods"); - - let burner_bytecode = std::fs::read(artifacts_dir.join("burner.bin"))?; - let chain_caller_bytecode = std::fs::read(artifacts_dir.join("chain_caller.bin"))?; + let claimer = test_programs::claimer(); + let chain_caller = test_programs::chain_caller(); // Calculate block size to fit only one of the two transactions, leaving some room for headers // (e.g., 10 KiB) - let max_program_size = burner_bytecode.len().max(chain_caller_bytecode.len()); + let max_program_size = claimer.elf().len().max(chain_caller.elf().len()); let block_size = ByteSize::b((max_program_size + 10 * 1024) as u64); let ctx = TestContext::builder() @@ -116,16 +112,13 @@ async fn transaction_deferred_to_next_block_when_current_full() -> Result<()> { .build() .await?; - let burner_id = Program::new(burner_bytecode.clone())?.id(); - let chain_caller_id = Program::new(chain_caller_bytecode.clone())?.id(); - let initial_block_height = ctx.sequencer_client().get_last_block_id().await?; // Submit both program deployments ctx.sequencer_client() .send_transaction(LeeTransaction::ProgramDeployment( lee::ProgramDeploymentTransaction::new( - lee::program_deployment_transaction::Message::new(burner_bytecode), + lee::program_deployment_transaction::Message::new(claimer.elf().to_owned()), ), )) .await?; @@ -133,7 +126,7 @@ async fn transaction_deferred_to_next_block_when_current_full() -> Result<()> { ctx.sequencer_client() .send_transaction(LeeTransaction::ProgramDeployment( lee::ProgramDeploymentTransaction::new( - lee::program_deployment_transaction::Message::new(chain_caller_bytecode), + lee::program_deployment_transaction::Message::new(chain_caller.elf().to_owned()), ), )) .await?; @@ -156,7 +149,7 @@ async fn transaction_deferred_to_next_block_when_current_full() -> Result<()> { .filter_map(|tx| { if let LeeTransaction::ProgramDeployment(deployment) = tx { let bytecode = deployment.message.clone().into_bytecode(); - Program::new(bytecode).ok().map(|p| p.id()) + Program::new(bytecode.into()).ok().map(|p| p.id()) } else { None } @@ -173,8 +166,9 @@ async fn transaction_deferred_to_next_block_when_current_full() -> Result<()> { "Expected exactly one program deployment in block 1" ); assert_eq!( - block1_program_ids[0], burner_id, - "Expected burner program to be deployed in block 1" + block1_program_ids[0], + claimer.id(), + "Expected claimer program to be deployed in block 1" ); // Wait for second block @@ -194,7 +188,8 @@ async fn transaction_deferred_to_next_block_when_current_full() -> Result<()> { "Expected exactly one program deployment in block 2" ); assert_eq!( - block2_program_ids[0], chain_caller_id, + block2_program_ids[0], + chain_caller.id(), "Expected chain_caller program to be deployed in block 2" ); diff --git a/integration_tests/tests/bridge.rs b/integration_tests/tests/bridge.rs index 41781a68..9c241ebb 100644 --- a/integration_tests/tests/bridge.rs +++ b/integration_tests/tests/bridge.rs @@ -11,7 +11,8 @@ use borsh::BorshSerialize; use common::transaction::LeeTransaction; use futures::StreamExt as _; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, wait_for_indexer_to_catch_up, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account, + wait_for_indexer_to_catch_up, }; use lee::{ AccountId, execute_and_prove, privacy_preserving_transaction, program::Program, @@ -43,12 +44,12 @@ async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { let ctx = TestContext::new().await?; let recipient_id = ctx.existing_public_accounts()[0]; - let bridge_account_id = lee::system_bridge_account_id(); - let vault_program_id = Program::vault().id(); + let bridge_account_id = system_accounts::bridge_account_id(); + let vault_program_id = programs::vault().id(); let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id); let message = public_transaction::Message::try_new( - Program::bridge().id(), + programs::bridge().id(), vec![bridge_account_id, recipient_vault_id], vec![], bridge_core::Instruction::Deposit { @@ -65,6 +66,54 @@ async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { lee::public_transaction::WitnessSet::from_raw_parts(vec![]), )); + let bridge_balance_before = account_balance(&ctx, bridge_account_id).await?; + let vault_balance_before = account_balance(&ctx, recipient_vault_id).await?; + + let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?; + + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + let bridge_balance_after = account_balance(&ctx, bridge_account_id).await?; + let vault_balance_after = account_balance(&ctx, recipient_vault_id).await?; + let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?; + + assert_eq!(bridge_balance_after, bridge_balance_before); + assert_eq!(vault_balance_after, vault_balance_before); + assert!( + tx_on_chain.is_none(), + "Direct public bridge::Deposit invocation should be rejected" + ); + + Ok(()) +} + +#[test] +async fn public_bridge_deposit_with_zero_amount_is_rejected() -> anyhow::Result<()> { + let ctx = TestContext::new().await?; + + let recipient_id = ctx.existing_public_accounts()[0]; + let bridge_account_id = system_accounts::bridge_account_id(); + let vault_program_id = programs::vault().id(); + let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id); + + let message = public_transaction::Message::try_new( + programs::bridge().id(), + vec![bridge_account_id, recipient_vault_id], + vec![], + bridge_core::Instruction::Deposit { + l1_deposit_op_id: [0_u8; 32], + vault_program_id, + recipient_id, + amount: 0, + }, + ) + .context("Failed to build zero-amount public bridge deposit transaction")?; + + let attack_tx = LeeTransaction::Public(lee::PublicTransaction::new( + message, + lee::public_transaction::WitnessSet::from_raw_parts(vec![]), + )); + let bridge_balance_before = ctx .sequencer_client() .get_account_balance(bridge_account_id) @@ -92,7 +141,7 @@ async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { assert_eq!(vault_balance_after, vault_balance_before); assert!( tx_on_chain.is_none(), - "Direct public bridge::Deposit invocation should be rejected" + "Public bridge::Deposit with zero amount should be rejected" ); Ok(()) @@ -103,22 +152,18 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { let ctx = TestContext::new().await?; let recipient_id = ctx.existing_public_accounts()[0]; - let bridge_account_id = lee::system_bridge_account_id(); - let vault_program_id = Program::vault().id(); + let bridge_account_id = system_accounts::bridge_account_id(); + let vault_program_id = programs::vault().id(); let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id); // Get pre-state of bridge and vault accounts let bridge_pre = AccountWithMetadata::new( - ctx.sequencer_client() - .get_account(bridge_account_id) - .await?, + get_account(&ctx, bridge_account_id).await?, false, bridge_account_id, ); let vault_pre = AccountWithMetadata::new( - ctx.sequencer_client() - .get_account(recipient_vault_id) - .await?, + get_account(&ctx, recipient_vault_id).await?, false, recipient_vault_id, ); @@ -126,12 +171,12 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { // Create program with dependencies let program_with_deps = lee::privacy_preserving_transaction::circuit::ProgramWithDependencies::new( - Program::bridge(), + programs::bridge(), [ - (vault_program_id, Program::vault()), + (vault_program_id, programs::vault()), ( - Program::authenticated_transfer_program().id(), - Program::authenticated_transfer_program(), + programs::authenticated_transfer().id(), + programs::authenticated_transfer(), ), ] .into(), @@ -169,27 +214,15 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { witness_set, )); - let bridge_balance_before = ctx - .sequencer_client() - .get_account_balance(bridge_account_id) - .await?; - let vault_balance_before = ctx - .sequencer_client() - .get_account_balance(recipient_vault_id) - .await?; + let bridge_balance_before = account_balance(&ctx, bridge_account_id).await?; + let vault_balance_before = account_balance(&ctx, recipient_vault_id).await?; let tx_hash = ctx.sequencer_client().send_transaction(attack_tx).await?; tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let bridge_balance_after = ctx - .sequencer_client() - .get_account_balance(bridge_account_id) - .await?; - let vault_balance_after = ctx - .sequencer_client() - .get_account_balance(recipient_vault_id) - .await?; + let bridge_balance_after = account_balance(&ctx, bridge_account_id).await?; + let vault_balance_after = account_balance(&ctx, recipient_vault_id).await?; let tx_on_chain = ctx.sequencer_client().get_transaction(tx_hash).await?; assert_eq!(bridge_balance_after, bridge_balance_before); @@ -361,7 +394,7 @@ async fn wait_for_vault_balance( + Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS); tokio::time::timeout(timeout, async { loop { - let balance = ctx.sequencer_client().get_account_balance(vault_id).await?; + let balance = account_balance(ctx, vault_id).await?; if balance == expected_balance { return Ok(()); } @@ -386,17 +419,11 @@ async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Res let bedrock_account_pk = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26"; let recipient_id = ctx.existing_public_accounts()[0]; let amount = 1_u64; - let vault_program_id = Program::vault().id(); + let vault_program_id = programs::vault().id(); let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient_id); - let vault_balance_before = ctx - .sequencer_client() - .get_account_balance(recipient_vault_id) - .await?; - let recipient_balance_before = ctx - .sequencer_client() - .get_account_balance(recipient_id) - .await?; + let vault_balance_before = account_balance(&ctx, recipient_vault_id).await?; + let recipient_balance_before = account_balance(&ctx, recipient_id).await?; // Submit deposit to Bedrock submit_bedrock_deposit(ctx.bedrock_addr(), bedrock_account_pk, recipient_id, amount) @@ -447,14 +474,8 @@ async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Res tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; let claim_on_chain = ctx.sequencer_client().get_transaction(claim_hash).await?; - let vault_balance_after_claim = ctx - .sequencer_client() - .get_account_balance(recipient_vault_id) - .await?; - let recipient_balance_after_claim = ctx - .sequencer_client() - .get_account_balance(recipient_id) - .await?; + let vault_balance_after_claim = account_balance(&ctx, recipient_vault_id).await?; + let recipient_balance_after_claim = account_balance(&ctx, recipient_id).await?; assert!( claim_on_chain.is_some(), @@ -474,7 +495,7 @@ async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Res // state as the sequencer โ€” including the bridge system account the deposit // modifies, which is the case the hot fix unblocks. wait_for_indexer_to_catch_up(&ctx).await?; - let bridge_account_id = lee::system_bridge_account_id(); + let bridge_account_id = system_accounts::bridge_account_id(); for account_id in [recipient_id, recipient_vault_id, bridge_account_id] { let indexer_account = indexer_service_rpc::RpcClient::get_account( // `deref` is needed for correct trait resolution @@ -483,7 +504,7 @@ async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Res account_id.into(), ) .await?; - let sequencer_account = ctx.sequencer_client().get_account(account_id).await?; + let sequencer_account = get_account(&ctx, account_id).await?; assert_eq!( indexer_account, sequencer_account.into(), diff --git a/integration_tests/tests/indexer_ffi_block_batching.rs b/integration_tests/tests/indexer_ffi_block_batching.rs index eeef276d..c244fbb0 100644 --- a/integration_tests/tests/indexer_ffi_block_batching.rs +++ b/integration_tests/tests/indexer_ffi_block_batching.rs @@ -5,8 +5,7 @@ )] use anyhow::Result; -use indexer_ffi::{Runtime, api::types::FfiOption}; -use integration_tests::L2_TO_L1_TIMEOUT; +use indexer_ffi::api::types::FfiOption; use log::info; #[path = "indexer_ffi_helpers/mod.rs"] @@ -14,21 +13,15 @@ mod indexer_ffi_helpers; #[test] fn indexer_ffi_block_batching() -> Result<()> { - let (ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?; + // `_ctx` keeps the bedrock/sequencer harness (and its runtime) alive for the + // duration of the test; the indexer was started on that runtime. + let (_ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?; - // WAIT + // WAIT: poll until the indexer has finalized at least two blocks (so the + // chain-consistency check below verifies at least one block link), returning + // early instead of sleeping for the full timeout. info!("Waiting for indexer to parse blocks"); - std::thread::sleep(L2_TO_L1_TIMEOUT); - - // Safety: ctx runtime is valid for the lifetime of the returned Runtime - let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; - let last_block_indexer_ffi_res = unsafe { - indexer_ffi_helpers::query_last_block(&raw const runtime, &raw const indexer_ffi) - }; - - assert!(last_block_indexer_ffi_res.error.is_ok()); - - let last_block_indexer = unsafe { *last_block_indexer_ffi_res.value }; + let last_block_indexer = indexer_ffi_helpers::wait_for_indexer_ffi_block(&indexer_ffi, 2)?; info!("Last block on indexer FFI now is {last_block_indexer}"); @@ -37,14 +30,8 @@ fn indexer_ffi_block_batching() -> Result<()> { let before_ffi = FfiOption::::from_none(); let limit = 100; - let block_batch_ffi_res = unsafe { - indexer_ffi_helpers::query_block_vec( - &raw const runtime, - &raw const indexer_ffi, - before_ffi, - limit, - ) - }; + let block_batch_ffi_res = + unsafe { indexer_ffi_helpers::query_block_vec(&raw const indexer_ffi, before_ffi, limit) }; assert!(block_batch_ffi_res.error.is_ok()); diff --git a/integration_tests/tests/indexer_ffi_helpers/mod.rs b/integration_tests/tests/indexer_ffi_helpers/mod.rs index c3c3caff..170102fd 100644 --- a/integration_tests/tests/indexer_ffi_helpers/mod.rs +++ b/integration_tests/tests/indexer_ffi_helpers/mod.rs @@ -13,6 +13,7 @@ use indexer_ffi::{ api::{ PointerResult, lifecycle::InitializedIndexerServiceFFIResult, + query::LastBlockIdResult, types::{FfiAccountId, FfiOption, FfiVec, account::FfiAccount, block::FfiBlock}, }, }; @@ -20,20 +21,15 @@ use integration_tests::{BlockingTestContext, TestContext}; use tempfile::TempDir; unsafe extern "C" { - pub unsafe fn query_last_block( - runtime: *const Runtime, - indexer: *const IndexerServiceFFI, - ) -> PointerResult; + pub unsafe fn query_last_block(indexer: *const IndexerServiceFFI) -> LastBlockIdResult; pub unsafe fn query_block_vec( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, before: FfiOption, limit: u64, ) -> PointerResult, OperationStatus>; pub unsafe fn query_account( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, account_id: FfiAccountId, ) -> PointerResult; @@ -41,14 +37,11 @@ unsafe extern "C" { pub unsafe fn start_indexer( runtime: *const Runtime, config_path: *const c_char, - port: u16, + storage_dir: *const c_char, ) -> InitializedIndexerServiceFFIResult; } -pub fn setup_indexer_ffi( - runtime: &Runtime, - bedrock_addr: SocketAddr, -) -> Result<(IndexerServiceFFI, TempDir)> { +pub fn setup_indexer_ffi(bedrock_addr: SocketAddr) -> Result<(IndexerServiceFFI, TempDir)> { let temp_indexer_dir = tempfile::tempdir().context("Failed to create temp dir for indexer home")?; @@ -57,9 +50,8 @@ pub fn setup_indexer_ffi( temp_indexer_dir.path().display() ); - let indexer_config = - integration_tests::config::indexer_config(bedrock_addr, temp_indexer_dir.path().to_owned()) - .context("Failed to create Indexer config")?; + let indexer_config = integration_tests::config::indexer_config(bedrock_addr) + .context("Failed to create Indexer config")?; let config_json = serde_json::to_vec(&indexer_config)?; let config_path = temp_indexer_dir.path().join("indexer_config.json"); @@ -67,9 +59,13 @@ pub fn setup_indexer_ffi( file.write_all(&config_json)?; file.flush()?; + let config_path_c = CString::new(config_path.to_str().unwrap())?; + let storage_dir_c = CString::new(temp_indexer_dir.path().to_str().unwrap())?; let res = - // SAFETY: lib function ensures validity of value. - unsafe { start_indexer(std::ptr::from_ref(runtime), CString::new(config_path.to_str().unwrap())?.as_ptr(), 0) }; + // SAFETY: null runtime โ†’ the FFI creates and owns its own tokio runtime, + // so there is no external runtime whose address we must keep stable. The + // temp dir is the indexer's storage location. + unsafe { start_indexer(std::ptr::null(), config_path_c.as_ptr(), storage_dir_c.as_ptr()) }; if res.error.is_error() { anyhow::bail!("Indexer FFI error {:?}", res.error); @@ -84,8 +80,35 @@ pub fn setup_indexer_ffi( pub fn setup() -> Result<(BlockingTestContext, IndexerServiceFFI, TempDir)> { let ctx = TestContext::builder().disable_indexer().build_blocking()?; - // Safety: ctx runtime is valid for the lifetime of the returned Runtime - let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; - let (indexer_ffi, indexer_dir) = setup_indexer_ffi(&runtime, ctx.ctx().bedrock_addr())?; + // Don't borrow `ctx.runtime()`: `ctx` (and its by-value tokio runtime) is + // moved into the returned tuple, which would leave any pointer into it + // dangling. Pass a null runtime so the FFI owns its own โ€” the same path the + // production module uses. + let (indexer_ffi, indexer_dir) = setup_indexer_ffi(ctx.ctx().bedrock_addr())?; Ok((ctx, indexer_ffi, indexer_dir)) } + +/// Poll the indexer FFI until its last finalized block id reaches `min_block_id` +/// or until [`integration_tests::L2_TO_L1_TIMEOUT`] elapses. +/// +/// This avoids blindly sleeping for the full timeout: the indexer typically +/// catches up in a fraction of that time, so we return as soon as it does and +/// only use the timeout as a ceiling. Returns the last observed block id. +pub fn wait_for_indexer_ffi_block(indexer: &IndexerServiceFFI, min_block_id: u64) -> Result { + let start = std::time::Instant::now(); + loop { + // SAFETY: `indexer` is a valid reference for the duration of the call. + let res = unsafe { query_last_block(std::ptr::from_ref(indexer)) }; + if res.error.is_ok() && res.is_some && res.block_id >= min_block_id { + return Ok(res.block_id); + } + if start.elapsed() >= integration_tests::L2_TO_L1_TIMEOUT { + anyhow::bail!( + "Indexer FFI did not reach block {min_block_id} within {:?}. Last observed block id: {}", + integration_tests::L2_TO_L1_TIMEOUT, + res.block_id + ); + } + std::thread::sleep(std::time::Duration::from_secs(2)); + } +} diff --git a/integration_tests/tests/indexer_ffi_state_consistency.rs b/integration_tests/tests/indexer_ffi_state_consistency.rs index f84a3790..0a41c68c 100644 --- a/integration_tests/tests/indexer_ffi_state_consistency.rs +++ b/integration_tests/tests/indexer_ffi_state_consistency.rs @@ -8,7 +8,6 @@ use std::time::Duration; use anyhow::{Context as _, Result}; -use indexer_ffi::Runtime; use indexer_service_protocol::Account; use integration_tests::{ L2_TO_L1_TIMEOUT, TIME_TO_WAIT_FOR_BLOCK_SECONDS, private_mention, public_mention, @@ -102,11 +101,8 @@ fn indexer_ffi_state_consistency() -> Result<()> { info!("Waiting for indexer to parse blocks"); std::thread::sleep(L2_TO_L1_TIMEOUT); - // Safety: ctx runtime is valid for the lifetime of the returned Runtime - let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; let acc1_ind_state_ffi = unsafe { indexer_ffi_helpers::query_account( - &raw const runtime, &raw const indexer_ffi, (&ctx.ctx().existing_public_accounts()[0]).into(), ) @@ -119,7 +115,6 @@ fn indexer_ffi_state_consistency() -> Result<()> { let acc2_ind_state_ffi = unsafe { indexer_ffi_helpers::query_account( - &raw const runtime, &raw const indexer_ffi, (&ctx.ctx().existing_public_accounts()[1]).into(), ) diff --git a/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs b/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs index 34d5a4d7..fbc0b422 100644 --- a/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs +++ b/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs @@ -8,7 +8,6 @@ use std::time::Duration; use anyhow::Result; -use indexer_ffi::Runtime; use indexer_service_protocol::Account; use integration_tests::{L2_TO_L1_TIMEOUT, TIME_TO_WAIT_FOR_BLOCK_SECONDS, public_mention}; use log::info; @@ -75,11 +74,8 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> { info!("Waiting for indexer to parse blocks"); std::thread::sleep(L2_TO_L1_TIMEOUT); - // Safety: ctx runtime is valid for the lifetime of the returned Runtime - let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; let acc1_ind_state_ffi = unsafe { indexer_ffi_helpers::query_account( - &raw const runtime, &raw const indexer_ffi, (&ctx.ctx().existing_public_accounts()[0]).into(), ) diff --git a/integration_tests/tests/indexer_state_consistency.rs b/integration_tests/tests/indexer_state_consistency.rs index e87927bc..4ed2fd26 100644 --- a/integration_tests/tests/indexer_state_consistency.rs +++ b/integration_tests/tests/indexer_state_consistency.rs @@ -1,51 +1,36 @@ #![expect( - clippy::shadow_unrelated, clippy::tests_outside_test_module, reason = "We don't care about these in tests" )] use std::time::Duration; -use anyhow::{Context as _, Result}; +use anyhow::Result; use indexer_service_rpc::RpcClient as _; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention, - verify_commitment_is_in_state, wait_for_indexer_to_catch_up, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, + assert_private_commitment_in_state, get_account, private_mention, public_mention, send, + wait_for_indexer_to_catch_up, }; use lee::AccountId; use log::info; -use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand}; #[tokio::test] async fn indexer_state_consistency() -> Result<()> { let mut ctx = TestContext::new().await?; - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: public_mention(ctx.existing_public_accounts()[0]), - to: Some(public_mention(ctx.existing_public_accounts()[1])), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let (acc0, acc1) = ( + ctx.existing_public_accounts()[0], + ctx.existing_public_accounts()[1], + ); + send(&mut ctx, public_mention(acc0), public_mention(acc1), 100).await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Checking correct balance move"); - let acc_1_balance = sequencer_service_rpc::RpcClient::get_account_balance( - ctx.sequencer_client(), - ctx.existing_public_accounts()[0], - ) - .await?; - let acc_2_balance = sequencer_service_rpc::RpcClient::get_account_balance( - ctx.sequencer_client(), - ctx.existing_public_accounts()[1], - ) - .await?; + let acc_1_balance = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?; + let acc_2_balance = account_balance(&ctx, ctx.existing_public_accounts()[1]).await?; info!("Balance of sender: {acc_1_balance:#?}"); info!("Balance of receiver: {acc_2_balance:#?}"); @@ -56,32 +41,13 @@ async fn indexer_state_consistency() -> Result<()> { let from: AccountId = ctx.existing_private_accounts()[0]; let to: AccountId = ctx.existing_private_accounts()[1]; - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: private_mention(from), - to: Some(private_mention(to)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + send(&mut ctx, private_mention(from), private_mention(to), 100).await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let new_commitment1 = ctx - .wallet() - .get_private_account_commitment(from) - .context("Failed to get private account commitment for sender")?; - assert!(verify_commitment_is_in_state(new_commitment1, ctx.sequencer_client()).await); - - let new_commitment2 = ctx - .wallet() - .get_private_account_commitment(to) - .context("Failed to get private account commitment for receiver")?; - assert!(verify_commitment_is_in_state(new_commitment2, ctx.sequencer_client()).await); + assert_private_commitment_in_state(&ctx, from, "sender").await?; + assert_private_commitment_in_state(&ctx, to, "receiver").await?; info!("Successfully transferred privately to owned account"); @@ -100,16 +66,8 @@ async fn indexer_state_consistency() -> Result<()> { .unwrap(); info!("Checking correct state transition"); - let acc1_seq_state = sequencer_service_rpc::RpcClient::get_account( - ctx.sequencer_client(), - ctx.existing_public_accounts()[0], - ) - .await?; - let acc2_seq_state = sequencer_service_rpc::RpcClient::get_account( - ctx.sequencer_client(), - ctx.existing_public_accounts()[1], - ) - .await?; + let acc1_seq_state = get_account(&ctx, ctx.existing_public_accounts()[0]).await?; + let acc2_seq_state = get_account(&ctx, ctx.existing_public_accounts()[1]).await?; assert_eq!(acc1_ind_state, acc1_seq_state.into()); assert_eq!(acc2_ind_state, acc2_seq_state.into()); diff --git a/integration_tests/tests/indexer_state_consistency_with_labels.rs b/integration_tests/tests/indexer_state_consistency_with_labels.rs index 5f561d6f..219c3ebf 100644 --- a/integration_tests/tests/indexer_state_consistency_with_labels.rs +++ b/integration_tests/tests/indexer_state_consistency_with_labels.rs @@ -9,12 +9,13 @@ use std::time::Duration; use anyhow::Result; use indexer_service_rpc::RpcClient as _; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention, wait_for_indexer_to_catch_up, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account, public_mention, + send, wait_for_indexer_to_catch_up, }; use log::info; use wallet::{ account::Label, - cli::{CliAccountMention, Command, programs::native_token_transfer::AuthTransferSubcommand}, + cli::{CliAccountMention, Command}, }; #[tokio::test] @@ -38,31 +39,19 @@ async fn indexer_state_consistency_with_labels() -> Result<()> { wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd).await?; // Send using labels instead of account IDs - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: CliAccountMention::Label(from_label), - to: Some(CliAccountMention::Label(to_label)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + send( + &mut ctx, + CliAccountMention::Label(from_label), + CliAccountMention::Label(to_label), + 100, + ) + .await?; info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let acc_1_balance = sequencer_service_rpc::RpcClient::get_account_balance( - ctx.sequencer_client(), - ctx.existing_public_accounts()[0], - ) - .await?; - let acc_2_balance = sequencer_service_rpc::RpcClient::get_account_balance( - ctx.sequencer_client(), - ctx.existing_public_accounts()[1], - ) - .await?; + let acc_1_balance = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?; + let acc_2_balance = account_balance(&ctx, ctx.existing_public_accounts()[1]).await?; assert_eq!(acc_1_balance, 9900); assert_eq!(acc_2_balance, 20100); @@ -75,11 +64,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> { .get_account(ctx.existing_public_accounts()[0].into()) .await .unwrap(); - let acc1_seq_state = sequencer_service_rpc::RpcClient::get_account( - ctx.sequencer_client(), - ctx.existing_public_accounts()[0], - ) - .await?; + let acc1_seq_state = get_account(&ctx, ctx.existing_public_accounts()[0]).await?; assert_eq!(acc1_ind_state, acc1_seq_state.into()); diff --git a/integration_tests/tests/indexer_test_run_ffi.rs b/integration_tests/tests/indexer_test_run_ffi.rs index 2c7c2103..e37b619e 100644 --- a/integration_tests/tests/indexer_test_run_ffi.rs +++ b/integration_tests/tests/indexer_test_run_ffi.rs @@ -1,12 +1,9 @@ #![expect( clippy::tests_outside_test_module, - clippy::undocumented_unsafe_blocks, reason = "We don't care about these in tests" )] use anyhow::Result; -use indexer_ffi::Runtime; -use integration_tests::L2_TO_L1_TIMEOUT; use log::info; #[path = "indexer_ffi_helpers/mod.rs"] @@ -14,20 +11,13 @@ mod indexer_ffi_helpers; #[test] fn indexer_test_run_ffi() -> Result<()> { - let (ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?; + // `_ctx` keeps the bedrock/sequencer harness (and its runtime) alive for the + // duration of the test; the indexer was started on that runtime. + let (_ctx, indexer_ffi, _indexer_dir) = indexer_ffi_helpers::setup()?; - // RUN OBSERVATION - std::thread::sleep(L2_TO_L1_TIMEOUT); - - // Safety: ctx runtime is valid for the lifetime of the returned Runtime - let runtime = unsafe { Runtime::from_borrowed(ctx.runtime()) }; - let last_block_indexer_ffi_res = unsafe { - indexer_ffi_helpers::query_last_block(&raw const runtime, &raw const indexer_ffi) - }; - - assert!(last_block_indexer_ffi_res.error.is_ok()); - - let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value }; + // RUN OBSERVATION: poll until the indexer has finalized at least one block, + // returning early instead of sleeping for the full timeout. + let last_block_indexer_ffi = indexer_ffi_helpers::wait_for_indexer_ffi_block(&indexer_ffi, 1)?; info!("Last block on indexer FFI now is {last_block_indexer_ffi}"); diff --git a/integration_tests/tests/keys.rs b/integration_tests/tests/keys.rs index 01af23cd..7fc73c62 100644 --- a/integration_tests/tests/keys.rs +++ b/integration_tests/tests/keys.rs @@ -8,17 +8,17 @@ use std::{str::FromStr as _, time::Duration}; use anyhow::{Context as _, Result}; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, fetch_privacy_preserving_tx, private_mention, - public_mention, verify_commitment_is_in_state, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, assert_public_account_restored, + fetch_privacy_preserving_tx, new_account, private_mention, public_mention, + restored_private_account, send, verify_commitment_is_in_state, }; use key_protocol::key_management::key_tree::chain_index::ChainIndex; -use lee::{AccountId, program::Program}; +use lee::AccountId; use log::info; use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::cli::{ - Command, SubcommandReturnValue, - account::{AccountSubcommand, NewSubcommand}, + Command, SubcommandReturnValue, account::AccountSubcommand, programs::native_token_transfer::AuthTransferSubcommand, }; @@ -28,35 +28,12 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> { let from: AccountId = ctx.existing_private_accounts()[0]; - // Create a new private account - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })); - + // Key Tree shift โ€” create 3 accounts to advance the key index for _ in 0..3 { - // Key Tree shift - // This way we have account with child index > 0. - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { account_id: _ } = result else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + new_account(&mut ctx, true, None).await?; } - let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - let SubcommandReturnValue::RegisterAccount { - account_id: to_account_id, - } = sub_ret - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let to_account_id = new_account(&mut ctx, true, None).await?; // Get the keys for the newly created account let to_account = ctx @@ -118,107 +95,47 @@ async fn restore_keys_from_seed() -> Result<()> { let from: AccountId = ctx.existing_private_accounts()[0]; - // Create first private account at root - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: Some(ChainIndex::root()), - label: None, - })); - let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - let SubcommandReturnValue::RegisterAccount { - account_id: to_account_id1, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + // Create private accounts at root and /0 + let to_account_id1 = new_account(&mut ctx, true, Some(ChainIndex::root())).await?; + let to_account_id2 = new_account(&mut ctx, true, Some(ChainIndex::from_str("/0")?)).await?; - // Create second private account at /0 - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: Some(ChainIndex::from_str("/0")?), - label: None, - })); - let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - let SubcommandReturnValue::RegisterAccount { - account_id: to_account_id2, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; - - // Send to first private account - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: private_mention(from), - to: Some(private_mention(to_account_id1)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 100, - }); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - - // Send to second private account - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: private_mention(from), - to: Some(private_mention(to_account_id2)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 101, - }); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + // Send to both private accounts + send( + &mut ctx, + private_mention(from), + private_mention(to_account_id1), + 100, + ) + .await?; + send( + &mut ctx, + private_mention(from), + private_mention(to_account_id2), + 101, + ) + .await?; let from: AccountId = ctx.existing_public_accounts()[0]; - // Create first public account at root - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: Some(ChainIndex::root()), - label: None, - })); - let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - let SubcommandReturnValue::RegisterAccount { - account_id: to_account_id3, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + // Create public accounts at root and /0 + let to_account_id3 = new_account(&mut ctx, false, Some(ChainIndex::root())).await?; + let to_account_id4 = new_account(&mut ctx, false, Some(ChainIndex::from_str("/0")?)).await?; - // Create second public account at /0 - let command = Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: Some(ChainIndex::from_str("/0")?), - label: None, - })); - let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - let SubcommandReturnValue::RegisterAccount { - account_id: to_account_id4, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; - - // Send to first public account - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: public_mention(from), - to: Some(public_mention(to_account_id3)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 102, - }); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - - // Send to second public account - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: public_mention(from), - to: Some(public_mention(to_account_id4)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 103, - }); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + // Send to both public accounts + send( + &mut ctx, + public_mention(from), + public_mention(to_account_id3), + 102, + ) + .await?; + send( + &mut ctx, + public_mention(from), + public_mention(to_account_id4), + 103, + ) + .await?; info!("Preparation complete, performing keys restoration"); @@ -226,42 +143,20 @@ async fn restore_keys_from_seed() -> Result<()> { wallet::cli::execute_keys_restoration(ctx.wallet_mut(), 10).await?; // Verify restored private accounts - let acc1 = ctx - .wallet() - .storage() - .key_chain() - .private_account(to_account_id1) - .expect("Acc 1 should be restored"); - - let acc2 = ctx - .wallet() - .storage() - .key_chain() - .private_account(to_account_id2) - .expect("Acc 2 should be restored"); + let acc1 = restored_private_account(&ctx, to_account_id1, "Acc 1"); + let acc2 = restored_private_account(&ctx, to_account_id2, "Acc 2"); // Verify restored public accounts - let _acc3 = ctx - .wallet() - .storage() - .key_chain() - .pub_account_signing_key(to_account_id3) - .expect("Acc 3 should be restored"); - - let _acc4 = ctx - .wallet() - .storage() - .key_chain() - .pub_account_signing_key(to_account_id4) - .expect("Acc 4 should be restored"); + assert_public_account_restored(&ctx, to_account_id3, "Acc 3"); + assert_public_account_restored(&ctx, to_account_id4, "Acc 4"); assert_eq!( acc1.account.program_owner, - Program::authenticated_transfer_program().id() + programs::authenticated_transfer().id() ); assert_eq!( acc2.account.program_owner, - Program::authenticated_transfer_program().id() + programs::authenticated_transfer().id() ); assert_eq!(acc1.account.balance, 100); @@ -270,27 +165,20 @@ async fn restore_keys_from_seed() -> Result<()> { info!("Tree checks passed, testing restored accounts can transact"); // Test that restored accounts can send transactions - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: private_mention(to_account_id1), - to: Some(private_mention(to_account_id2)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 10, - }); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; - - let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: public_mention(to_account_id3), - to: Some(public_mention(to_account_id4)), - to_npk: None, - to_vpk: None, - to_keys: None, - to_identifier: Some(0), - amount: 11, - }); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + send( + &mut ctx, + private_mention(to_account_id1), + private_mention(to_account_id2), + 10, + ) + .await?; + send( + &mut ctx, + public_mention(to_account_id3), + public_mention(to_account_id4), + 11, + ) + .await?; tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; diff --git a/integration_tests/tests/pinata.rs b/integration_tests/tests/pinata.rs index 9beb5b1f..cea7a707 100644 --- a/integration_tests/tests/pinata.rs +++ b/integration_tests/tests/pinata.rs @@ -7,17 +7,14 @@ use std::time::Duration; use anyhow::{Context as _, Result}; -use common::PINATA_BASE58; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention, - verify_commitment_is_in_state, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, new_account, private_mention, + public_mention, sync_private, verify_commitment_is_in_state, }; use log::info; -use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::cli::{ Command, SubcommandReturnValue, - account::{AccountSubcommand, NewSubcommand}, programs::{ native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, }, @@ -27,25 +24,9 @@ use wallet::cli::{ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> { let mut ctx = TestContext::new().await?; - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: winner_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let winner_account_id = new_account(&mut ctx, false, None).await?; - let pinata_balance_pre = ctx - .sequencer_client() - .get_account_balance(PINATA_BASE58.parse().unwrap()) - .await?; + let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?; let claim_result = wallet::cli::execute_subcommand( ctx.wallet_mut(), @@ -65,10 +46,7 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> "Expected init guidance, got: {err}", ); - let pinata_balance_post = ctx - .sequencer_client() - .get_account_balance(PINATA_BASE58.parse().unwrap()) - .await?; + let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?; assert_eq!(pinata_balance_post, pinata_balance_pre); @@ -79,25 +57,9 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<()> { let mut ctx = TestContext::new().await?; - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: winner_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let winner_account_id = new_account(&mut ctx, true, None).await?; - let pinata_balance_pre = ctx - .sequencer_client() - .get_account_balance(PINATA_BASE58.parse().unwrap()) - .await?; + let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?; let claim_result = wallet::cli::execute_subcommand( ctx.wallet_mut(), @@ -117,10 +79,7 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<() "Expected init guidance, got: {err}", ); - let pinata_balance_post = ctx - .sequencer_client() - .get_account_balance(PINATA_BASE58.parse().unwrap()) - .await?; + let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?; assert_eq!(pinata_balance_post, pinata_balance_pre); @@ -136,10 +95,7 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> { to: public_mention(ctx.existing_public_accounts()[0]), }); - let pinata_balance_pre = ctx - .sequencer_client() - .get_account_balance(PINATA_BASE58.parse().unwrap()) - .await?; + let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?; wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -147,15 +103,9 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Checking correct balance move"); - let pinata_balance_post = ctx - .sequencer_client() - .get_account_balance(PINATA_BASE58.parse().unwrap()) - .await?; + let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?; - let winner_balance_post = ctx - .sequencer_client() - .get_account_balance(ctx.existing_public_accounts()[0]) - .await?; + let winner_balance_post = account_balance(&ctx, ctx.existing_public_accounts()[0]).await?; assert_eq!(pinata_balance_post, pinata_balance_pre - pinata_prize); assert_eq!(winner_balance_post, 10000 + pinata_prize); @@ -174,10 +124,7 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> { to: private_mention(ctx.existing_private_accounts()[0]), }); - let pinata_balance_pre = ctx - .sequencer_client() - .get_account_balance(PINATA_BASE58.parse().unwrap()) - .await?; + let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?; let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash: _ } = result else { @@ -188,8 +135,7 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; info!("Syncing private accounts"); - let command = Command::Account(AccountSubcommand::SyncPrivate {}); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + sync_private(&mut ctx).await?; let new_commitment = ctx .wallet() @@ -197,10 +143,7 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> { .context("Failed to get private account commitment")?; assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await); - let pinata_balance_post = ctx - .sequencer_client() - .get_account_balance(PINATA_BASE58.parse().unwrap()) - .await?; + let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?; assert_eq!(pinata_balance_post, pinata_balance_pre - pinata_prize); @@ -216,20 +159,7 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { let pinata_prize = 150; // Create new private account - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: winner_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let winner_account_id = new_account(&mut ctx, true, None).await?; // Initialize account under auth transfer program let command = Command::AuthTransfer(AuthTransferSubcommand::Init { @@ -251,10 +181,7 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { to: private_mention(winner_account_id), }); - let pinata_balance_pre = ctx - .sequencer_client() - .get_account_balance(PINATA_BASE58.parse().unwrap()) - .await?; + let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?; wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -267,10 +194,7 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { .context("Failed to get private account commitment")?; assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await); - let pinata_balance_post = ctx - .sequencer_client() - .get_account_balance(PINATA_BASE58.parse().unwrap()) - .await?; + let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?; assert_eq!(pinata_balance_post, pinata_balance_pre - pinata_prize); diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index e605ceee..0c67049f 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -3,14 +3,13 @@ reason = "We don't care about these in tests" )] -use std::{path::PathBuf, time::Duration}; +use std::time::Duration; use anyhow::{Context as _, Result}; use authenticated_transfer_core::Instruction as AuthTransferInstruction; use common::transaction::LeeTransaction; use integration_tests::{ - LEE_PROGRAM_FOR_TEST_PDA_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, - verify_commitment_is_in_state, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, sync_private, verify_commitment_is_in_state, }; use lee::{ AccountId, PrivacyPreservingTransaction, ProgramId, @@ -22,7 +21,7 @@ use lee::{ program::Program, }; use lee_core::{ - InputAccountIdentity, NullifierPublicKey, + DUMMY_COMMITMENT_HASH, InputAccountIdentity, NullifierPublicKey, account::{Account, AccountWithMetadata}, encryption::ViewingPublicKey, program::PdaSeed, @@ -30,10 +29,7 @@ use lee_core::{ use log::info; use sequencer_service_rpc::RpcClient as _; use tokio::test; -use wallet::{ - AccountIdentity, WalletCore, - cli::{Command, account::AccountSubcommand}, -}; +use wallet::{AccountIdentity, WalletCore}; /// Funds a private PDA by calling `auth_transfer` directly. #[expect( @@ -74,6 +70,7 @@ async fn fund_private_pda( random_seed: [0; 32], npk, identifier, + commitment_root: DUMMY_COMMITMENT_HASH, seed: Some((seed, authority_program_id)), }, ]; @@ -160,14 +157,8 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { (kc.nullifier_public_key, kc.viewing_public_key.clone()) }; - let proxy = { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../artifacts/test_program_methods") - .join(LEE_PROGRAM_FOR_TEST_PDA_SPEND_PROXY); - Program::new(std::fs::read(&path).with_context(|| format!("reading {path:?}"))?) - .context("invalid pda_spend_proxy binary")? - }; - let auth_transfer = Program::authenticated_transfer_program(); + let proxy = test_programs::pda_spend_proxy(); + let auth_transfer = programs::authenticated_transfer(); let proxy_id = proxy.id(); let auth_transfer_id = auth_transfer.id(); let seed = PdaSeed::new([42; 32]); @@ -219,11 +210,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Sync so alice's wallet discovers and stores both PDAs. - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::SyncPrivate {}), - ) - .await?; + sync_private(&mut ctx).await?; // Both PDAs must be discoverable and have the correct balance. let pda_0_account = ctx @@ -302,11 +289,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { info!("Waiting for block"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::SyncPrivate {}), - ) - .await?; + sync_private(&mut ctx).await?; // After spending, PDAs should have the remaining balance. let pda_0_spent = ctx diff --git a/integration_tests/tests/program_deployment.rs b/integration_tests/tests/program_deployment.rs index 77ac1fb9..2b242549 100644 --- a/integration_tests/tests/program_deployment.rs +++ b/integration_tests/tests/program_deployment.rs @@ -3,30 +3,25 @@ reason = "We don't care about these in tests" )] -use std::{path::PathBuf, time::Duration}; +use std::{io::Write as _, time::Duration}; use anyhow::Result; use common::transaction::LeeTransaction; -use integration_tests::{ - LEE_PROGRAM_FOR_TEST_DATA_CHANGER, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, -}; -use lee::program::Program; +use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, get_account, new_account}; use log::info; use sequencer_service_rpc::RpcClient as _; use tokio::test; -use wallet::cli::{ - Command, SubcommandReturnValue, - account::{AccountSubcommand, NewSubcommand}, -}; +use wallet::cli::Command; #[test] async fn deploy_and_execute_program() -> Result<()> { let mut ctx = TestContext::new().await?; - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let binary_filepath: PathBuf = PathBuf::from(manifest_dir) - .join("../artifacts/test_program_methods") - .join(LEE_PROGRAM_FOR_TEST_DATA_CHANGER); + let claimer = test_programs::claimer(); + let mut tempfile = tempfile::NamedTempFile::new()?; + tempfile.write_all(claimer.elf())?; + + let binary_filepath = tempfile.path().to_owned(); let command = Command::DeployProgram { binary_filepath: binary_filepath.clone(), @@ -37,36 +32,15 @@ async fn deploy_and_execute_program() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - // The program is the data changer and takes one account as input. - // We pass an uninitialized account and we expect after execution to be owned by the data - // changer program (LEE account claiming mechanism) with data equal to [0] (due to program - // logic) - let bytecode = std::fs::read(binary_filepath)?; - let data_changer = Program::new(bytecode)?; - - let SubcommandReturnValue::RegisterAccount { account_id } = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await? - else { - panic!("Expected RegisterAccount return value"); - }; + let account_id = new_account(&mut ctx, false, None).await?; let nonces = ctx.wallet().get_accounts_nonces(vec![account_id]).await?; let private_key = ctx .wallet() .get_account_public_signing_key(account_id) .unwrap(); - let message = lee::public_transaction::Message::try_new( - data_changer.id(), - vec![account_id], - nonces, - vec![0], - )?; + let message = + lee::public_transaction::Message::try_new(claimer.id(), vec![account_id], nonces, ())?; let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[private_key]); let transaction = lee::PublicTransaction::new(message, witness_set); let _response = ctx @@ -79,11 +53,12 @@ async fn deploy_and_execute_program() -> Result<()> { // block tokio::time::sleep(Duration::from_secs(2 * TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let post_state_account = ctx.sequencer_client().get_account(account_id).await?; + let post_state_account = get_account(&ctx, account_id).await?; - assert_eq!(post_state_account.program_owner, data_changer.id()); + let expected_data: &[u8] = &[]; + assert_eq!(post_state_account.program_owner, claimer.id()); assert_eq!(post_state_account.balance, 0); - assert_eq!(post_state_account.data.as_ref(), &[0]); + assert_eq!(post_state_account.data.as_ref(), expected_data); assert_eq!(post_state_account.nonce.0, 1); info!("Successfully deployed and executed program"); diff --git a/integration_tests/tests/shared_accounts.rs b/integration_tests/tests/shared_accounts.rs index 39bdd36c..cc6e6e1d 100644 --- a/integration_tests/tests/shared_accounts.rs +++ b/integration_tests/tests/shared_accounts.rs @@ -19,7 +19,7 @@ use std::time::Duration; use anyhow::{Context as _, Result}; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention, sync_private, }; use log::info; use tokio::test; @@ -197,8 +197,7 @@ async fn fund_shared_account_from_public() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Sync private accounts - let command = Command::Account(AccountSubcommand::SyncPrivate); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + sync_private(&mut ctx).await?; // Fund from a public account let from_public = ctx.existing_public_accounts()[0]; @@ -216,8 +215,7 @@ async fn fund_shared_account_from_public() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Sync private accounts - let command = Command::Account(AccountSubcommand::SyncPrivate); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + sync_private(&mut ctx).await?; // Verify the shared account was updated let entry = ctx diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index b0c569e8..d7578a22 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -8,13 +8,11 @@ use std::time::Duration; use anyhow::{Context as _, Result}; use integration_tests::{ - TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, private_mention, public_mention, - verify_commitment_is_in_state, + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, get_account, new_account, private_mention, + public_mention, sync_private, verify_commitment_is_in_state, }; use key_protocol::key_management::key_tree::chain_index::ChainIndex; -use lee::program::Program; use log::info; -use sequencer_service_rpc::RpcClient as _; use token_core::{TokenDefinition, TokenHolding}; use tokio::test; use wallet::{ @@ -31,52 +29,13 @@ async fn create_and_transfer_public_token() -> Result<()> { let mut ctx = TestContext::new().await?; // Create new account for the token definition - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id = new_account(&mut ctx, false, None).await?; // Create new account for the token supply holder - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id = new_account(&mut ctx, false, None).await?; // Create new account for receiving a token transaction - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id = new_account(&mut ctx, false, None).await?; // Create new token let name = "A NAME".to_owned(); @@ -93,13 +52,10 @@ async fn create_and_transfer_public_token() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Check the status of the token definition account - let definition_acc = ctx - .sequencer_client() - .get_account(definition_account_id) - .await?; + let definition_acc = get_account(&ctx, definition_account_id).await?; let token_definition = TokenDefinition::try_from(&definition_acc.data)?; - assert_eq!(definition_acc.program_owner, Program::token().id()); + assert_eq!(definition_acc.program_owner, programs::token().id()); assert_eq!( token_definition, TokenDefinition::Fungible { @@ -110,13 +66,10 @@ async fn create_and_transfer_public_token() -> Result<()> { ); // Check the status of the token holding account with the total supply - let supply_acc = ctx - .sequencer_client() - .get_account(supply_account_id) - .await?; + let supply_acc = get_account(&ctx, supply_account_id).await?; // The account must be owned by the token program - assert_eq!(supply_acc.program_owner, Program::token().id()); + assert_eq!(supply_acc.program_owner, programs::token().id()); let token_holding = TokenHolding::try_from(&supply_acc.data)?; assert_eq!( token_holding, @@ -144,11 +97,8 @@ async fn create_and_transfer_public_token() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Check the status of the supply account after transfer - let supply_acc = ctx - .sequencer_client() - .get_account(supply_account_id) - .await?; - assert_eq!(supply_acc.program_owner, Program::token().id()); + let supply_acc = get_account(&ctx, supply_account_id).await?; + assert_eq!(supply_acc.program_owner, programs::token().id()); let token_holding = TokenHolding::try_from(&supply_acc.data)?; assert_eq!( token_holding, @@ -159,11 +109,8 @@ async fn create_and_transfer_public_token() -> Result<()> { ); // Check the status of the recipient account after transfer - let recipient_acc = ctx - .sequencer_client() - .get_account(recipient_account_id) - .await?; - assert_eq!(recipient_acc.program_owner, Program::token().id()); + let recipient_acc = get_account(&ctx, recipient_account_id).await?; + assert_eq!(recipient_acc.program_owner, programs::token().id()); let token_holding = TokenHolding::try_from(&recipient_acc.data)?; assert_eq!( token_holding, @@ -187,10 +134,7 @@ async fn create_and_transfer_public_token() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Check the status of the token definition account after burn - let definition_acc = ctx - .sequencer_client() - .get_account(definition_account_id) - .await?; + let definition_acc = get_account(&ctx, definition_account_id).await?; let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!( @@ -203,10 +147,7 @@ async fn create_and_transfer_public_token() -> Result<()> { ); // Check the status of the recipient account after burn - let recipient_acc = ctx - .sequencer_client() - .get_account(recipient_account_id) - .await?; + let recipient_acc = get_account(&ctx, recipient_account_id).await?; let token_holding = TokenHolding::try_from(&recipient_acc.data)?; assert_eq!( @@ -235,10 +176,7 @@ async fn create_and_transfer_public_token() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Check the status of the token definition account after mint - let definition_acc = ctx - .sequencer_client() - .get_account(definition_account_id) - .await?; + let definition_acc = get_account(&ctx, definition_account_id).await?; let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!( @@ -251,10 +189,7 @@ async fn create_and_transfer_public_token() -> Result<()> { ); // Check the status of the recipient account after mint - let recipient_acc = ctx - .sequencer_client() - .get_account(recipient_account_id) - .await?; + let recipient_acc = get_account(&ctx, recipient_account_id).await?; let token_holding = TokenHolding::try_from(&recipient_acc.data)?; assert_eq!( @@ -275,52 +210,13 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { let mut ctx = TestContext::new().await?; // Create new account for the token definition (public) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id = new_account(&mut ctx, false, None).await?; // Create new account for the token supply holder (private) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id = new_account(&mut ctx, true, None).await?; // Create new account for receiving a token transaction (private) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id = new_account(&mut ctx, true, None).await?; // Create new token let name = "A NAME".to_owned(); @@ -338,13 +234,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Check the status of the token definition account - let definition_acc = ctx - .sequencer_client() - .get_account(definition_account_id) - .await?; + let definition_acc = get_account(&ctx, definition_account_id).await?; let token_definition = TokenDefinition::try_from(&definition_acc.data)?; - assert_eq!(definition_acc.program_owner, Program::token().id()); + assert_eq!(definition_acc.program_owner, programs::token().id()); assert_eq!( token_definition, TokenDefinition::Fungible { @@ -403,10 +296,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Check the token definition account after burn - let definition_acc = ctx - .sequencer_client() - .get_account(definition_account_id) - .await?; + let definition_acc = get_account(&ctx, definition_account_id).await?; let token_definition = TokenDefinition::try_from(&definition_acc.data)?; assert_eq!( @@ -449,36 +339,10 @@ async fn create_token_with_private_definition() -> Result<()> { let mut ctx = TestContext::new().await?; // Create token definition account (private) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: Some(ChainIndex::root()), - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id = new_account(&mut ctx, true, Some(ChainIndex::root())).await?; // Create supply account (public) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: Some(ChainIndex::root()), - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id = new_account(&mut ctx, false, Some(ChainIndex::root())).await?; // Create token with private definition let name = "A NAME".to_owned(); @@ -503,12 +367,9 @@ async fn create_token_with_private_definition() -> Result<()> { assert!(verify_commitment_is_in_state(new_commitment, ctx.sequencer_client()).await); // Verify supply account - let supply_acc = ctx - .sequencer_client() - .get_account(supply_account_id) - .await?; + let supply_acc = get_account(&ctx, supply_account_id).await?; - assert_eq!(supply_acc.program_owner, Program::token().id()); + assert_eq!(supply_acc.program_owner, programs::token().id()); let token_holding = TokenHolding::try_from(&supply_acc.data)?; assert_eq!( token_holding, @@ -519,36 +380,10 @@ async fn create_token_with_private_definition() -> Result<()> { ); // Create private recipient account - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id_private, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id_private = new_account(&mut ctx, true, None).await?; // Create public recipient account - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id_public, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id_public = new_account(&mut ctx, false, None).await?; // Mint to public account let mint_amount_public = 10; @@ -584,10 +419,7 @@ async fn create_token_with_private_definition() -> Result<()> { ); // Verify public recipient received tokens - let recipient_acc = ctx - .sequencer_client() - .get_account(recipient_account_id_public) - .await?; + let recipient_acc = get_account(&ctx, recipient_account_id_public).await?; let token_holding = TokenHolding::try_from(&recipient_acc.data)?; assert_eq!( @@ -647,36 +479,10 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { let mut ctx = TestContext::new().await?; // Create token definition account (private) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id = new_account(&mut ctx, true, None).await?; // Create supply account (private) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id = new_account(&mut ctx, true, None).await?; // Create token with both private definition and supply let name = "A NAME".to_owned(); @@ -723,20 +529,7 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { ); // Create recipient account - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id = new_account(&mut ctx, true, None).await?; // Transfer tokens let transfer_amount = 7; @@ -805,52 +598,13 @@ async fn shielded_token_transfer() -> Result<()> { let mut ctx = TestContext::new().await?; // Create token definition account (public) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id = new_account(&mut ctx, false, None).await?; // Create supply account (public) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id = new_account(&mut ctx, false, None).await?; // Create recipient account (private) for shielded transfer - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id = new_account(&mut ctx, true, None).await?; // Create token let name = "A NAME".to_owned(); @@ -885,10 +639,7 @@ async fn shielded_token_transfer() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Verify supply account balance - let supply_acc = ctx - .sequencer_client() - .get_account(supply_account_id) - .await?; + let supply_acc = get_account(&ctx, supply_account_id).await?; let token_holding = TokenHolding::try_from(&supply_acc.data)?; assert_eq!( token_holding, @@ -929,52 +680,13 @@ async fn deshielded_token_transfer() -> Result<()> { let mut ctx = TestContext::new().await?; // Create token definition account (public) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id = new_account(&mut ctx, false, None).await?; // Create supply account (private) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id = new_account(&mut ctx, true, None).await?; // Create recipient account (public) for deshielded transfer - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id = new_account(&mut ctx, false, None).await?; // Create token with private supply let name = "A NAME".to_owned(); @@ -1030,10 +742,7 @@ async fn deshielded_token_transfer() -> Result<()> { ); // Verify recipient balance - let recipient_acc = ctx - .sequencer_client() - .get_account(recipient_account_id) - .await?; + let recipient_acc = get_account(&ctx, recipient_account_id).await?; let token_holding = TokenHolding::try_from(&recipient_acc.data)?; assert_eq!( token_holding, @@ -1053,36 +762,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { let mut ctx = TestContext::new().await?; // Create token definition account (private) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id = new_account(&mut ctx, true, None).await?; // Create supply account (private) - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: supply_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let supply_account_id = new_account(&mut ctx, true, None).await?; // Create token let name = "A NAME".to_owned(); @@ -1100,20 +783,7 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Create new private account for claiming path - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Private { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id = new_account(&mut ctx, true, None).await?; // Get keys for foreign mint (claiming path) let holder = ctx @@ -1144,8 +814,7 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; // Sync to claim the account - let command = Command::Account(AccountSubcommand::SyncPrivate {}); - wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + sync_private(&mut ctx).await?; // Verify commitment exists let recipient_commitment = ctx @@ -1225,13 +894,10 @@ async fn create_token_using_labels() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let definition_acc = ctx - .sequencer_client() - .get_account(definition_account_id) - .await?; + let definition_acc = get_account(&ctx, definition_account_id).await?; let token_definition = TokenDefinition::try_from(&definition_acc.data)?; - assert_eq!(definition_acc.program_owner, Program::token().id()); + assert_eq!(definition_acc.program_owner, programs::token().id()); assert_eq!( token_definition, TokenDefinition::Fungible { @@ -1241,10 +907,7 @@ async fn create_token_using_labels() -> Result<()> { } ); - let supply_acc = ctx - .sequencer_client() - .get_account(supply_account_id) - .await?; + let supply_acc = get_account(&ctx, supply_account_id).await?; let token_holding = TokenHolding::try_from(&supply_acc.data)?; assert_eq!( token_holding, @@ -1264,20 +927,7 @@ async fn transfer_token_using_from_label() -> Result<()> { let mut ctx = TestContext::new().await?; // Create definition account - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: definition_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let definition_account_id = new_account(&mut ctx, false, None).await?; // Create supply account with a label let supply_label = Label::new("token-supply-sender"); @@ -1297,20 +947,7 @@ async fn transfer_token_using_from_label() -> Result<()> { }; // Create recipient account - let result = wallet::cli::execute_subcommand( - ctx.wallet_mut(), - Command::Account(AccountSubcommand::New(NewSubcommand::Public { - cci: None, - label: None, - })), - ) - .await?; - let SubcommandReturnValue::RegisterAccount { - account_id: recipient_account_id, - } = result - else { - anyhow::bail!("Expected RegisterAccount return value"); - }; + let recipient_account_id = new_account(&mut ctx, false, None).await?; // Create token let total_supply = 50; @@ -1341,10 +978,7 @@ async fn transfer_token_using_from_label() -> Result<()> { info!("Waiting for next block creation"); tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; - let recipient_acc = ctx - .sequencer_client() - .get_account(recipient_account_id) - .await?; + let recipient_acc = get_account(&ctx, recipient_account_id).await?; let token_holding = TokenHolding::try_from(&recipient_acc.data)?; assert_eq!( token_holding, diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 78116c49..71fb132b 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -22,7 +22,7 @@ use lee::{ public_transaction as putx, }; use lee_core::{ - InputAccountIdentity, MembershipProof, NullifierPublicKey, + DUMMY_COMMITMENT_HASH, InputAccountIdentity, MembershipProof, NullifierPublicKey, account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; @@ -75,7 +75,7 @@ impl TpsTestManager { &self, sequencer_client: &sequencer_service_rpc::SequencerClient, ) -> Result<()> { - let vault_program_id = Program::vault().id(); + let vault_program_id = programs::vault().id(); let mut tx_hashes = Vec::with_capacity(self.public_keypairs.len()); for (private_key, account_id) in &self.public_keypairs { @@ -125,7 +125,7 @@ impl TpsTestManager { /// Must be called after `claim_vault_funds`, which sets each account's nonce to 1. pub fn build_public_txs(&self) -> Vec { // Create valid public transactions - let program = Program::authenticated_transfer_program(); + let program = programs::authenticated_transfer(); let public_txs: Vec = self .public_keypairs .windows(2) @@ -253,7 +253,7 @@ pub async fn tps_test() -> Result<()> { /// multiple times with the purpose of testing the node's processing performance. #[expect(dead_code, reason = "No idea if we need this, should we remove it?")] fn build_privacy_transaction() -> PrivacyPreservingTransaction { - let program = Program::authenticated_transfer_program(); + let program = programs::authenticated_transfer(); let sender_nsk = [1; 32]; let sender_vpk = ViewingPublicKey::from_seed(&[99_u8; 32], &[100_u8; 32]); let sender_npk = NullifierPublicKey::from(&sender_nsk); @@ -303,6 +303,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { random_seed: [0; 32], npk: recipient_npk, identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, }, ], &program.into(), diff --git a/integration_tests/tests/vault.rs b/integration_tests/tests/vault.rs index 33d2e9a9..e9ea2075 100644 --- a/integration_tests/tests/vault.rs +++ b/integration_tests/tests/vault.rs @@ -5,7 +5,6 @@ use anyhow::{Context as _, Result}; use integration_tests::{TestContext, private_mention, public_mention}; -use lee::program::Program; use sequencer_service_rpc::RpcClient as _; use tokio::test; use wallet::cli::{Command, SubcommandReturnValue, programs::vault::VaultSubcommand}; @@ -18,7 +17,7 @@ async fn public_transfer_and_public_claim() -> Result<()> { let sender = ctx.existing_public_accounts()[0]; let recipient = ctx.existing_public_accounts()[1]; - let vault_program_id = Program::vault().id(); + let vault_program_id = programs::vault().id(); let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient); let sender_balance_before = ctx.sequencer_client().get_account_balance(sender).await?; @@ -109,7 +108,7 @@ async fn private_transfer_and_private_claim() -> Result<()> { let sender = ctx.existing_private_accounts()[0]; let owner = ctx.existing_private_accounts()[1]; - let vault_program_id = Program::vault().id(); + let vault_program_id = programs::vault().id(); let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner); let sender_balance_before = ctx diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index a68a3db6..321c874d 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -16,6 +16,7 @@ use std::{ ffi::{CStr, CString, c_char}, io::Write as _, path::Path, + str::FromStr as _, time::Duration, }; @@ -28,11 +29,14 @@ use lee::{ use lee_core::program::DEFAULT_PROGRAM_ID; use log::info; use tempfile::tempdir; -use wallet::account::HumanReadableAccount; +use wallet::{account::HumanReadableAccount, program_facades::vault::Vault}; use wallet_ffi::{ - FfiAccount, FfiAccountIdentity, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, - FfiPublicAccountKey, FfiTransferResult, FfiU128, WalletHandle, error, + FfiAccount, FfiAccountIdWithPrivacy, FfiAccountIdentity, FfiAccountList, FfiBytes32, + FfiPrivateAccountKeys, FfiProgramId, FfiPublicAccountKey, FfiTransferResult, FfiU128, + WalletHandle, error, generic_transaction::{FfiProgramWithDependencies, FfiTransactionResult}, + label::{AccountIdResolvedFromLabel, LabelAvailability, LabelList}, + wallet::FfiCreateWalletOutput, }; unsafe extern "C" { @@ -40,7 +44,7 @@ unsafe extern "C" { config_path: *const c_char, storage_path: *const c_char, password: *const c_char, - ) -> *mut WalletHandle; + ) -> FfiCreateWalletOutput; fn wallet_ffi_open( config_path: *const c_char, @@ -165,6 +169,34 @@ unsafe extern "C" { fn wallet_ffi_free_transfer_result(result: *mut FfiTransferResult); + fn wallet_ffi_bridge_withdraw( + handle: *mut WalletHandle, + from: *const FfiBytes32, + amount: u64, + bedrock_account_pk: *const FfiBytes32, + out_result: *mut FfiTransferResult, + ) -> error::WalletFfiError; + + fn wallet_ffi_get_vault_balance( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + out_balance: *mut [u8; 16], + ) -> error::WalletFfiError; + + fn wallet_ffi_vault_claim( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, + ) -> error::WalletFfiError; + + fn wallet_ffi_vault_claim_private( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, + ) -> error::WalletFfiError; + fn wallet_ffi_register_public_account( handle: *mut WalletHandle, account_id: *const FfiBytes32, @@ -186,6 +218,13 @@ unsafe extern "C" { out_block_height: *mut u64, ) -> error::WalletFfiError; + fn wallet_ffi_restore_data( + handle: *mut WalletHandle, + mnemonic: *const c_char, + password: *const c_char, + depth: u32, + ) -> error::WalletFfiError; + fn wallet_ffi_resolve_public_account( account_id: FfiBytes32, needs_sign: bool, @@ -198,7 +237,7 @@ unsafe extern "C" { account_identities_size: usize, instruction_words: *const u32, instruction_words_size: usize, - program_with_dependencies: *const FfiProgramWithDependencies, + program_id: FfiProgramId, out_result: *mut FfiTransactionResult, ) -> error::WalletFfiError; @@ -221,12 +260,35 @@ unsafe extern "C" { fn wallet_ffi_free_transaction_result(result: *mut FfiTransactionResult); fn wallet_ffi_free_account_identity(account_identity: *mut FfiAccountIdentity); + + fn wallet_ffi_check_label_available( + handle: *mut WalletHandle, + label: *const c_char, + ) -> LabelAvailability; + + fn wallet_ffi_add_label( + handle: *mut WalletHandle, + label: *const c_char, + account_id_with_privacy: FfiAccountIdWithPrivacy, + ) -> error::WalletFfiError; + + fn wallet_ffi_resolve_label( + handle: *mut WalletHandle, + label: *const c_char, + ) -> AccountIdResolvedFromLabel; + + fn wallet_ffi_get_all_labels_for_account( + handle: *mut WalletHandle, + account_id_with_privacy: FfiAccountIdWithPrivacy, + ) -> LabelList; + + fn wallet_ffi_free_label_list(label_list: *mut LabelList) -> error::WalletFfiError; } fn new_wallet_ffi_with_test_context_config( ctx: &BlockingTestContext, home: &Path, -) -> Result<*mut WalletHandle> { +) -> Result { let config_path = home.join("wallet_config.json"); let storage_path = home.join("storage.json"); let mut config = ctx.ctx().wallet().config().to_owned(); @@ -247,7 +309,7 @@ fn new_wallet_ffi_with_test_context_config( let storage_path = CString::new(storage_path.to_str().unwrap())?; let password = CString::new(ctx.ctx().wallet_password())?; - let wallet_ffi_handle = unsafe { + let create_wallet_result = unsafe { wallet_ffi_create_new( config_path.as_ptr(), storage_path.as_ptr(), @@ -265,8 +327,10 @@ fn new_wallet_ffi_with_test_context_config( .unwrap() .to_string(); let private_key_hex = CString::new(private_key_hex)?; - unsafe { wallet_ffi_import_public_account(wallet_ffi_handle, private_key_hex.as_ptr()) } - .unwrap(); + unsafe { + wallet_ffi_import_public_account(create_wallet_result.wallet, private_key_hex.as_ptr()) + } + .unwrap(); } for (account_id, _chain_index) in source_key_chain.private_account_ids() { @@ -289,7 +353,7 @@ fn new_wallet_ffi_with_test_context_config( unsafe { wallet_ffi_import_private_account( - wallet_ffi_handle, + create_wallet_result.wallet, key_chain_json.as_ptr(), chain_index_ptr, &raw const identifier, @@ -299,10 +363,10 @@ fn new_wallet_ffi_with_test_context_config( .unwrap(); } - Ok(wallet_ffi_handle) + Ok(create_wallet_result) } -fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandle> { +fn new_wallet_ffi_with_default_config(password: &str) -> Result { let tempdir = tempdir()?; let config_path = tempdir.path().join("wallet_config.json"); let storage_path = tempdir.path().join("storage.json"); @@ -310,13 +374,15 @@ fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandl let storage_path_c = CString::new(storage_path.to_str().unwrap())?; let password = CString::new(password)?; - Ok(unsafe { + let create_wallet_result = unsafe { wallet_ffi_create_new( config_path_c.as_ptr(), storage_path_c.as_ptr(), password.as_ptr(), ) - }) + }; + + Ok(create_wallet_result) } fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> { @@ -337,7 +403,10 @@ fn wallet_ffi_create_public_accounts() -> Result<()> { let new_public_account_ids_ffi = unsafe { let mut account_ids = Vec::new(); - let wallet_ffi_handle = new_wallet_ffi_with_default_config(password)?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_default_config(password)?; for _ in 0..n_accounts { let mut out_account_id = FfiBytes32::from_bytes([0; 32]); wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut out_account_id).unwrap(); @@ -373,7 +442,10 @@ fn wallet_ffi_create_private_accounts() -> Result<()> { let new_npks_ffi = unsafe { let mut npks = Vec::new(); - let wallet_ffi_handle = new_wallet_ffi_with_default_config(password)?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_default_config(password)?; for _ in 0..n_accounts { let mut out_keys = FfiPrivateAccountKeys::default(); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap(); @@ -402,7 +474,10 @@ fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> { let home = tempfile::tempdir()?; // Create a receiving key and save let first_npk = unsafe { - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let mut out_keys = FfiPrivateAccountKeys::default(); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap(); let npk = out_keys.nullifier_public_key.data; @@ -439,7 +514,10 @@ fn test_wallet_ffi_list_accounts() -> Result<()> { // Create the wallet FFI and track which account IDs were created as public/private let (wallet_ffi_handle, created_public_ids) = unsafe { - let handle = new_wallet_ffi_with_default_config(password)?; + let FfiCreateWalletOutput { + wallet: handle, + mnemonic: _, + } = new_wallet_ffi_with_default_config(password)?; let mut public_ids: Vec<[u8; 32]> = Vec::new(); // Create 5 public accounts and 5 receiving keys @@ -504,7 +582,10 @@ fn test_wallet_ffi_get_balance_public() -> Result<()> { let ctx = BlockingTestContext::new()?; let account_id: AccountId = ctx.ctx().existing_public_accounts()[0]; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let balance = unsafe { let mut out_balance: [u8; 16] = [0; 16]; @@ -534,7 +615,10 @@ fn test_wallet_ffi_get_account_public() -> Result<()> { let ctx = BlockingTestContext::new()?; let account_id: AccountId = ctx.ctx().existing_public_accounts()[0]; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let mut out_account = FfiAccount::default(); let account: Account = unsafe { @@ -550,7 +634,7 @@ fn test_wallet_ffi_get_account_public() -> Result<()> { assert_eq!( account.program_owner, - Program::authenticated_transfer_program().id() + programs::authenticated_transfer().id() ); assert_eq!(account.balance, 10000); assert!(account.data.is_empty()); @@ -571,7 +655,10 @@ fn test_wallet_ffi_get_account_private() -> Result<()> { let ctx = BlockingTestContext::new()?; let account_id: AccountId = ctx.ctx().existing_private_accounts()[0]; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let mut out_account = FfiAccount::default(); let account: Account = unsafe { @@ -587,7 +674,7 @@ fn test_wallet_ffi_get_account_private() -> Result<()> { assert_eq!( account.program_owner, - Program::authenticated_transfer_program().id() + programs::authenticated_transfer().id() ); assert_eq!(account.balance, 10000); assert!(account.data.is_empty()); @@ -607,7 +694,10 @@ fn test_wallet_ffi_get_public_account_keys() -> Result<()> { let ctx = BlockingTestContext::new()?; let account_id: AccountId = ctx.ctx().existing_public_accounts()[0]; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let mut out_key = FfiPublicAccountKey::default(); let key: PublicKey = unsafe { @@ -646,7 +736,10 @@ fn test_wallet_ffi_get_private_account_keys() -> Result<()> { let ctx = BlockingTestContext::new()?; let account_id: AccountId = ctx.ctx().existing_private_accounts()[0]; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let mut keys = FfiPrivateAccountKeys::default(); unsafe { @@ -728,7 +821,10 @@ fn wallet_ffi_base58_to_account_id() -> Result<()> { fn wallet_ffi_init_public_account_auth_transfer() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; // Create a new uninitialized public account let mut out_account_id = FfiBytes32::from_bytes([0; 32]); @@ -776,7 +872,7 @@ fn wallet_ffi_init_public_account_auth_transfer() -> Result<()> { }; assert_eq!( account.program_owner, - Program::authenticated_transfer_program().id() + programs::authenticated_transfer().id() ); unsafe { @@ -791,7 +887,10 @@ fn wallet_ffi_init_public_account_auth_transfer() -> Result<()> { fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; // Create a new private account let mut out_account_id = FfiBytes32::default(); @@ -833,7 +932,7 @@ fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> { }; assert_eq!( account.program_owner, - Program::authenticated_transfer_program().id() + programs::authenticated_transfer().id() ); unsafe { @@ -848,7 +947,10 @@ fn wallet_ffi_init_private_account_auth_transfer() -> Result<()> { fn test_wallet_ffi_transfer_public() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into(); let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[1].into(); let amount: [u8; 16] = 100_u128.to_le_bytes(); @@ -902,7 +1004,10 @@ fn test_wallet_ffi_transfer_public() -> Result<()> { fn test_wallet_ffi_transfer_shielded() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into(); let (to, to_keys) = unsafe { let mut out_keys = FfiPrivateAccountKeys::default(); @@ -982,7 +1087,10 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> { fn test_wallet_ffi_transfer_deshielded() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into(); let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into(); let amount: [u8; 16] = 100_u128.to_le_bytes(); @@ -1042,7 +1150,10 @@ fn test_wallet_ffi_transfer_deshielded() -> Result<()> { fn test_wallet_ffi_transfer_private() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into(); let (to, to_keys) = unsafe { @@ -1118,11 +1229,374 @@ fn test_wallet_ffi_transfer_private() -> Result<()> { Ok(()) } +#[test] +fn restore_keys_from_seed_ffi() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let home = tempfile::tempdir()?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + + let mnemonic = unsafe { CString::from_raw(mnemonic) }; + + // Create 2 new private accounts + let (private_account_id_1, private_account_1_keys) = unsafe { + let mut out_keys = FfiPrivateAccountKeys::default(); + wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap(); + let account_id = lee::AccountId::for_regular_private_account( + &out_keys.npk(), + &out_keys.vpk().unwrap(), + 0_u128, + ); + let to: FfiBytes32 = account_id.into(); + (to, out_keys) + }; + + let (private_account_id_2, private_account_2_keys) = unsafe { + let mut out_keys = FfiPrivateAccountKeys::default(); + wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys).unwrap(); + let account_id = lee::AccountId::for_regular_private_account( + &out_keys.npk(), + &out_keys.vpk().unwrap(), + 0_u128, + ); + let to: FfiBytes32 = account_id.into(); + (to, out_keys) + }; + + // Create 2 new public accounts + let mut public_account_id_1 = FfiBytes32::default(); + unsafe { + wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut public_account_id_1).unwrap(); + } + + let mut public_account_id_2 = FfiBytes32::default(); + unsafe { + wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut public_account_id_2).unwrap(); + } + + info!("Accounts created"); + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + // Sync private account local storage with onchain encrypted state + unsafe { + let mut current_height = 0; + wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap(); + wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap(); + }; + + // Send funds to accounts + let from_private: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into(); + let from_public: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into(); + + let amount_1: [u8; 16] = 100_u128.to_le_bytes(); + + let mut transfer_result_1 = FfiTransferResult::default(); + unsafe { + let to_identifier = FfiU128 { + data: 0_u128.to_le_bytes(), + }; + wallet_ffi_transfer_private( + wallet_ffi_handle, + &raw const from_private, + &raw const private_account_1_keys, + &raw const to_identifier, + &raw const amount_1, + &raw mut transfer_result_1, + ) + .unwrap(); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + // Sync private account local storage with onchain encrypted state + unsafe { + let mut current_height = 0; + wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap(); + wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap(); + }; + + let amount_2: [u8; 16] = 101_u128.to_le_bytes(); + + let mut transfer_result_2 = FfiTransferResult::default(); + unsafe { + let to_identifier = FfiU128 { + data: 0_u128.to_le_bytes(), + }; + wallet_ffi_transfer_private( + wallet_ffi_handle, + &raw const from_private, + &raw const private_account_2_keys, + &raw const to_identifier, + &raw const amount_2, + &raw mut transfer_result_2, + ) + .unwrap(); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + // Sync private account local storage with onchain encrypted state + unsafe { + let mut current_height = 0; + wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap(); + wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap(); + }; + + let amount_3: [u8; 16] = 102_u128.to_le_bytes(); + + let mut transfer_result_3 = FfiTransferResult::default(); + unsafe { + wallet_ffi_transfer_public( + wallet_ffi_handle, + &raw const from_public, + &raw const public_account_id_1, + &raw const amount_3, + &raw mut transfer_result_3, + ) + .unwrap(); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + // Sync private account local storage with onchain encrypted state + unsafe { + let mut current_height = 0; + wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap(); + wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap(); + }; + + let amount_4: [u8; 16] = 103_u128.to_le_bytes(); + + let mut transfer_result_4 = FfiTransferResult::default(); + unsafe { + wallet_ffi_transfer_public( + wallet_ffi_handle, + &raw const from_public, + &raw const public_account_id_2, + &raw const amount_4, + &raw mut transfer_result_4, + ) + .unwrap(); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + // Sync private account local storage with onchain encrypted state + unsafe { + let mut current_height = 0; + wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap(); + wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap(); + }; + + unsafe { + wallet_ffi_free_transfer_result(&raw mut transfer_result_1); + wallet_ffi_free_transfer_result(&raw mut transfer_result_2); + wallet_ffi_free_transfer_result(&raw mut transfer_result_3); + wallet_ffi_free_transfer_result(&raw mut transfer_result_4); + } + + info!("Preparation complete, performing keys restoration"); + + let password = CString::new(ctx.ctx().wallet_password())?; + + info!("Checking balance correctness before restoration"); + + let private_account_id_1_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const private_account_id_1, + false, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + + let private_account_id_2_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const private_account_id_2, + false, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + + let public_account_id_1_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const public_account_id_1, + true, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + + let public_account_id_2_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const public_account_id_2, + true, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + + assert_eq!(private_account_id_1_balance, 100); + assert_eq!(private_account_id_2_balance, 101); + assert_eq!(public_account_id_1_balance, 102); + assert_eq!(public_account_id_2_balance, 103); + + unsafe { + wallet_ffi_restore_data(wallet_ffi_handle, mnemonic.as_ptr(), password.as_ptr(), 5) + .unwrap(); + } + + // Sync private account local storage with onchain encrypted state + unsafe { + let mut current_height = 0; + wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap(); + wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap(); + }; + + info!("Checking balance correctness after restoration"); + + let private_account_id_1_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const private_account_id_1, + false, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + + let private_account_id_2_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const private_account_id_2, + false, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + + let public_account_id_1_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const public_account_id_1, + true, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + + let public_account_id_2_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const public_account_id_2, + true, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + + assert_eq!(private_account_id_1_balance, 100); + assert_eq!(private_account_id_2_balance, 101); + assert_eq!(public_account_id_1_balance, 102); + assert_eq!(public_account_id_2_balance, 103); + + info!("Accounts restored"); + + Ok(()) +} + +#[test] +fn test_wallet_ffi_bridge_withdraw() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let home = tempfile::tempdir()?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into(); + let bridge_account: FfiBytes32 = system_accounts::bridge_account_id().into(); + let bedrock_account_pk = FfiBytes32::from_bytes([0x42; 32]); + let amount = 100_u64; + + let mut transfer_result = FfiTransferResult::default(); + unsafe { + wallet_ffi_bridge_withdraw( + wallet_ffi_handle, + &raw const from, + amount, + &raw const bedrock_account_pk, + &raw mut transfer_result, + ) + .unwrap(); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + let from_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const from, + true, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + + let bridge_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const bridge_account, + true, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + + assert_eq!(from_balance, 9900); + assert_eq!(bridge_balance, 1_000_100); + + unsafe { + wallet_ffi_free_transfer_result(&raw mut transfer_result); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + #[test] fn test_wallet_ffi_transfer_generic_public() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let from: FfiBytes32 = ctx.ctx().existing_public_accounts()[0].into(); let to: FfiBytes32 = ctx.ctx().existing_public_accounts()[1].into(); let amount = 100_u128; @@ -1153,8 +1627,7 @@ fn test_wallet_ffi_transfer_generic_public() -> Result<()> { let instruction_words_size = instruction_data.len(); let instruction_words = Box::into_raw(instruction_data.into_boxed_slice()) as *const u32; - let program: ProgramWithDependencies = Program::authenticated_transfer_program().into(); - let program_with_dependencies: FfiProgramWithDependencies = program.into(); + let program_id = programs::authenticated_transfer().id(); unsafe { wallet_ffi_send_generic_public_transaction( @@ -1163,7 +1636,7 @@ fn test_wallet_ffi_transfer_generic_public() -> Result<()> { account_identities_size, instruction_words, instruction_words_size, - &raw const program_with_dependencies, + program_id.into(), &raw mut transaction_result, ) .unwrap(); @@ -1214,7 +1687,10 @@ fn test_wallet_ffi_transfer_generic_public() -> Result<()> { fn test_wallet_ffi_transfer_generic_private() -> Result<()> { let ctx = BlockingTestContext::new()?; let home = tempfile::tempdir()?; - let wallet_ffi_handle = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; let from: FfiBytes32 = ctx.ctx().existing_private_accounts()[0].into(); let to: FfiBytes32 = ctx.ctx().existing_private_accounts()[1].into(); let amount = 100_u128; @@ -1247,7 +1723,7 @@ fn test_wallet_ffi_transfer_generic_private() -> Result<()> { let instruction_words_size = instruction_data.len(); let instruction_words = Box::into_raw(instruction_data.into_boxed_slice()) as *const u32; - let program: ProgramWithDependencies = Program::authenticated_transfer_program().into(); + let program: ProgramWithDependencies = programs::authenticated_transfer().into(); let program_with_dependencies: FfiProgramWithDependencies = program.into(); unsafe { @@ -1315,3 +1791,302 @@ fn test_wallet_ffi_transfer_generic_private() -> Result<()> { Ok(()) } + +#[test] +fn test_wallet_ffi_vault_balance_and_claim_public() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let home = tempfile::tempdir()?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + + let sender = ctx.ctx().existing_public_accounts()[0]; + let owner = ctx.ctx().existing_public_accounts()[1]; + let owner_ffi: FfiBytes32 = owner.into(); + let amount: u128 = 100; + + // Fund the owner's vault, simulating an L1 bridge deposit. + ctx.block_on(|ctx| async move { + Vault(ctx.wallet()) + .send_transfer(sender, owner, amount) + .await + }) + .unwrap(); + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + let vault_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_vault_balance( + wallet_ffi_handle, + &raw const owner_ffi, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + assert_eq!(vault_balance, amount); + + let mut transfer_result = FfiTransferResult::default(); + let claim_amount: [u8; 16] = amount.to_le_bytes(); + unsafe { + wallet_ffi_vault_claim( + wallet_ffi_handle, + &raw const owner_ffi, + &raw const claim_amount, + &raw mut transfer_result, + ) + .unwrap(); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + let vault_balance_after_claim = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_vault_balance( + wallet_ffi_handle, + &raw const owner_ffi, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + assert_eq!(vault_balance_after_claim, 0); + + let owner_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const owner_ffi, + true, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + assert_eq!(owner_balance, 20_000 + amount); + + unsafe { + wallet_ffi_free_transfer_result(&raw mut transfer_result); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + +#[test] +fn test_wallet_ffi_vault_balance_and_claim_private() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let home = tempfile::tempdir()?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + + let sender = ctx.ctx().existing_public_accounts()[0]; + let owner = ctx.ctx().existing_private_accounts()[0]; + let owner_ffi: FfiBytes32 = owner.into(); + let amount: u128 = 100; + + // Fund the owner's vault. Real deposits always land via a public transfer (the bridge + // program crediting the vault PDA), regardless of whether the owner is private. + ctx.block_on(|ctx| async move { + Vault(ctx.wallet()) + .send_transfer(sender, owner, amount) + .await + }) + .unwrap(); + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + let vault_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_vault_balance( + wallet_ffi_handle, + &raw const owner_ffi, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + assert_eq!(vault_balance, amount); + + let mut transfer_result = FfiTransferResult::default(); + let claim_amount: [u8; 16] = amount.to_le_bytes(); + unsafe { + wallet_ffi_vault_claim_private( + wallet_ffi_handle, + &raw const owner_ffi, + &raw const claim_amount, + &raw mut transfer_result, + ) + .unwrap(); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + // Sync private account local storage with onchain encrypted state + unsafe { + let mut current_height = 0; + wallet_ffi_get_current_block_height(wallet_ffi_handle, &raw mut current_height).unwrap(); + wallet_ffi_sync_to_block(wallet_ffi_handle, current_height).unwrap(); + }; + + let vault_balance_after_claim = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + wallet_ffi_get_vault_balance( + wallet_ffi_handle, + &raw const owner_ffi, + &raw mut out_balance, + ) + .unwrap(); + u128::from_le_bytes(out_balance) + }; + assert_eq!(vault_balance_after_claim, 0); + + let owner_balance = unsafe { + let mut out_balance: [u8; 16] = [0; 16]; + let _result = wallet_ffi_get_balance( + wallet_ffi_handle, + &raw const owner_ffi, + false, + &raw mut out_balance, + ); + u128::from_le_bytes(out_balance) + }; + assert_eq!(owner_balance, 10_000 + amount); + + unsafe { + wallet_ffi_free_transfer_result(&raw mut transfer_result); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + +#[test] +fn test_wallet_ffi_single_label() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let home = tempfile::tempdir()?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + + let mut out_account_id_1 = FfiBytes32::from_bytes([0; 32]); + unsafe { + wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut out_account_id_1).unwrap(); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + let lab_1 = CString::from_str("LABEL1").unwrap().into_raw(); + + let lab_1_availability = unsafe { wallet_ffi_check_label_available(wallet_ffi_handle, lab_1) }; + + assert_eq!(lab_1_availability.error, error::WalletFfiError::Success); + assert!(lab_1_availability.is_available); + + let acc_1_id_with_privacy = FfiAccountIdWithPrivacy { + account_id: out_account_id_1, + is_private: false, + }; + + let err = unsafe { wallet_ffi_add_label(wallet_ffi_handle, lab_1, acc_1_id_with_privacy) }; + + assert_eq!(err, error::WalletFfiError::Success); + + let lab_1_availability = unsafe { wallet_ffi_check_label_available(wallet_ffi_handle, lab_1) }; + + assert!(!lab_1_availability.is_available); + + let acc_resolved = unsafe { wallet_ffi_resolve_label(wallet_ffi_handle, lab_1) }; + + assert_eq!(acc_resolved.account_id, acc_1_id_with_privacy); + + unsafe { + wallet_ffi_free_string(lab_1); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} + +#[test] +fn test_wallet_ffi_more_labels() -> Result<()> { + let ctx = BlockingTestContext::new()?; + let home = tempfile::tempdir()?; + let FfiCreateWalletOutput { + wallet: wallet_ffi_handle, + mnemonic: _, + } = new_wallet_ffi_with_test_context_config(&ctx, home.path())?; + + let mut out_account_id_1 = FfiBytes32::from_bytes([0; 32]); + unsafe { + wallet_ffi_create_account_public(wallet_ffi_handle, &raw mut out_account_id_1).unwrap(); + } + + info!("Waiting for next block creation"); + std::thread::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)); + + let lab_1 = CString::from_str("LABEL1").unwrap().into_raw(); + let lab_2 = CString::from_str("LABEL2").unwrap().into_raw(); + let lab_3 = CString::from_str("LABEL3").unwrap().into_raw(); + + let acc_1_id_with_privacy = FfiAccountIdWithPrivacy { + account_id: out_account_id_1, + is_private: false, + }; + + let err = unsafe { wallet_ffi_add_label(wallet_ffi_handle, lab_1, acc_1_id_with_privacy) }; + + assert_eq!(err, error::WalletFfiError::Success); + + let err = unsafe { wallet_ffi_add_label(wallet_ffi_handle, lab_2, acc_1_id_with_privacy) }; + + assert_eq!(err, error::WalletFfiError::Success); + + let err = unsafe { wallet_ffi_add_label(wallet_ffi_handle, lab_3, acc_1_id_with_privacy) }; + + assert_eq!(err, error::WalletFfiError::Success); + + let mut label_list_for_out_acc = + unsafe { wallet_ffi_get_all_labels_for_account(wallet_ffi_handle, acc_1_id_with_privacy) }; + + assert_eq!(label_list_for_out_acc.error, error::WalletFfiError::Success); + assert_eq!(label_list_for_out_acc.labels_size, 3); + + let lab_ref_1 = unsafe { &*label_list_for_out_acc.labels_data.add(0) }; + let lab_ref_c_str_1 = unsafe { CStr::from_ptr(*lab_ref_1) }; + + assert_eq!(lab_ref_c_str_1.to_str().unwrap(), "LABEL1"); + + let lab_ref_2 = unsafe { &*label_list_for_out_acc.labels_data.add(1) }; + let lab_ref_c_str_2 = unsafe { CStr::from_ptr(*lab_ref_2) }; + + assert_eq!(lab_ref_c_str_2.to_str().unwrap(), "LABEL2"); + + let lab_ref_3 = unsafe { &*label_list_for_out_acc.labels_data.add(2) }; + let lab_ref_c_str_3 = unsafe { CStr::from_ptr(*lab_ref_3) }; + + assert_eq!(lab_ref_c_str_3.to_str().unwrap(), "LABEL3"); + + let err = unsafe { wallet_ffi_free_label_list(&raw mut label_list_for_out_acc) }; + + assert_eq!(err, error::WalletFfiError::Success); + + unsafe { + wallet_ffi_free_string(lab_1); + wallet_ffi_free_string(lab_2); + wallet_ffi_free_string(lab_3); + wallet_ffi_destroy(wallet_ffi_handle); + } + + Ok(()) +} diff --git a/lee/key_protocol/src/key_management/ephemeral_key_holder.rs b/lee/key_protocol/src/key_management/ephemeral_key_holder.rs index a53ae47c..9bc81391 100644 --- a/lee/key_protocol/src/key_management/ephemeral_key_holder.rs +++ b/lee/key_protocol/src/key_management/ephemeral_key_holder.rs @@ -48,14 +48,3 @@ impl EphemeralKeyHolder { self.shared_secret } } - -/// Encapsulates a fresh shared secret toward `vpk` and returns `(shared_secret, ciphertext)`. -/// -/// Used when the local side is acting as an "ephemeral receiver" โ€” i.e. generating a -/// one-sided encryption that only the holder of the VSK can decrypt. -#[must_use] -pub fn produce_one_sided_shared_secret_receiver( - vpk: &ViewingPublicKey, -) -> (SharedSecretKey, EphemeralPublicKey) { - SharedSecretKey::encapsulate(vpk) -} diff --git a/lee/key_protocol/src/key_management/group_key_holder.rs b/lee/key_protocol/src/key_management/group_key_holder.rs index 7bb94792..0821e4cd 100644 --- a/lee/key_protocol/src/key_management/group_key_holder.rs +++ b/lee/key_protocol/src/key_management/group_key_holder.rs @@ -1,7 +1,7 @@ use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _}; use lee_core::{ SharedSecretKey, - encryption::{EphemeralPublicKey, ViewingPublicKey}, + encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN, ViewingPublicKey}, program::{PdaSeed, ProgramId}, }; use rand::{RngCore as _, rngs::OsRng}; @@ -150,7 +150,7 @@ impl GroupKeyHolder { /// /// Uses ML-KEM-768 encapsulation to derive a shared secret, then AES-256-GCM to encrypt /// the payload. The returned bytes are - /// `kem_ciphertext (1088) || nonce (12) || ciphertext+tag (48)` = 1148 bytes. + /// `kem_ciphertext (ML_KEM_768_CIPHERTEXT_LEN) || nonce (12) || ciphertext+tag (48)`. /// /// Each call generates a fresh KEM encapsulation, so two seals of the same holder produce /// different ciphertexts. @@ -170,7 +170,7 @@ impl GroupKeyHolder { .encrypt(&nonce, self.gms.as_ref()) .expect("AES-GCM encryption should not fail with valid key/nonce"); - let capacity = 1088_usize + let capacity = ML_KEM_768_CIPHERTEXT_LEN .checked_add(12) .and_then(|n| n.checked_add(ciphertext.len())) .expect("seal capacity overflow"); @@ -186,21 +186,21 @@ impl GroupKeyHolder { /// Returns `Err` if the ciphertext is too short or the AES-GCM authentication tag /// doesn't verify (wrong key or tampered data). pub fn unseal(sealed: &[u8], own_key: &SealingSecretKey) -> Result { - // kem_ciphertext (1088) + nonce (12) = header, then AES-GCM tag (16) minimum. - const KEM_CT_LEN: usize = 1088; - const HEADER_LEN: usize = KEM_CT_LEN + 12; + // kem_ciphertext (ML_KEM_768_CIPHERTEXT_LEN) + nonce (12) = header, then AES-GCM tag (16) + // minimum. + const HEADER_LEN: usize = ML_KEM_768_CIPHERTEXT_LEN + 12; const MIN_LEN: usize = HEADER_LEN + 16; if sealed.len() < MIN_LEN { return Err(SealError::TooShort); } - let kem_ct = EphemeralPublicKey(sealed[..KEM_CT_LEN].to_vec()); - let nonce = aes_gcm::Nonce::from_slice(&sealed[KEM_CT_LEN..HEADER_LEN]); + let kem_ct = EphemeralPublicKey(sealed[..ML_KEM_768_CIPHERTEXT_LEN].to_vec()); + let nonce = aes_gcm::Nonce::from_slice(&sealed[ML_KEM_768_CIPHERTEXT_LEN..HEADER_LEN]); let ciphertext = &sealed[HEADER_LEN..]; let shared = SharedSecretKey::decapsulate(&kem_ct, &own_key.d, &own_key.z) - .expect("key_protocol::group_key_holder::GroupKeyHolder::unseal: KEM_CT_LEN guarantees exactly 1088 bytes"); + .expect("key_protocol::group_key_holder::GroupKeyHolder::unseal: ML_KEM_768_CIPHERTEXT_LEN guarantees exactly 1088 bytes"); let aes_key = Self::seal_kdf(&shared); let cipher = Aes256Gcm::new(&aes_key.into()); diff --git a/lee/key_protocol/src/key_management/key_tree/chain_index.rs b/lee/key_protocol/src/key_management/key_tree/chain_index.rs index b22dc779..6ea2e8a1 100644 --- a/lee/key_protocol/src/key_management/key_tree/chain_index.rs +++ b/lee/key_protocol/src/key_management/key_tree/chain_index.rs @@ -139,7 +139,7 @@ impl ChainIndex { .map(|item| Self(item.into_iter().copied().collect())) } - pub fn chain_ids_at_depth(depth: usize) -> impl Iterator { + fn collect_chain_ids_at_depth(depth: usize) -> Vec { let mut stack = vec![Self(vec![0; depth])]; let mut cumulative_stack = vec![Self(vec![0; depth])]; @@ -152,23 +152,18 @@ impl ChainIndex { } } - cumulative_stack.into_iter().unique() + cumulative_stack + } + + pub fn chain_ids_at_depth(depth: usize) -> impl Iterator { + Self::collect_chain_ids_at_depth(depth).into_iter().unique() } pub fn chain_ids_at_depth_rev(depth: usize) -> impl Iterator { - let mut stack = vec![Self(vec![0; depth])]; - let mut cumulative_stack = vec![Self(vec![0; depth])]; - - while let Some(top_id) = stack.pop() { - if let Some(collapsed_id) = top_id.collapse_back() { - for id in collapsed_id.shuffle_iter() { - stack.push(id.clone()); - cumulative_stack.push(id); - } - } - } - - cumulative_stack.into_iter().rev().unique() + Self::collect_chain_ids_at_depth(depth) + .into_iter() + .rev() + .unique() } } diff --git a/lee/key_protocol/src/key_management/key_tree/keys_private.rs b/lee/key_protocol/src/key_management/key_tree/keys_private.rs index b33733fb..8165e808 100644 --- a/lee/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/lee/key_protocol/src/key_management/key_tree/keys_private.rs @@ -6,7 +6,7 @@ use sha2::Digest as _; use crate::key_management::{ KeyChain, - key_tree::traits::KeyTreeNode, + key_tree::{split_hash, traits::KeyTreeNode}, secret_holders::{PrivateKeyHolder, SecretSpendingKey}, }; @@ -23,38 +23,11 @@ impl ChildKeysPrivate { #[must_use] pub fn root(seed: [u8; 64]) -> Self { let hash_value = hmac_sha512::HMAC::mac(seed, b"LEE_master_priv"); + let (first, ccc) = split_hash(&hash_value); - let ssk = SecretSpendingKey( - *hash_value - .first_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get first 32"), - ); - let ccc = *hash_value - .last_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get last 32"); + let ssk = SecretSpendingKey(first); - let nsk = ssk.generate_nullifier_secret_key(None); - let vsk = ssk.generate_viewing_secret_seed_key(None); - - let npk = NullifierPublicKey::from(&nsk); - let vpk = ViewingPublicKey::from(&vsk); - - Self { - value: ( - KeyChain { - secret_spending_key: ssk, - nullifier_public_key: npk, - viewing_public_key: vpk, - private_key_holder: PrivateKeyHolder { - nullifier_secret_key: nsk, - viewing_secret_key: vsk, - }, - }, - BTreeMap::from_iter([(PrivateAccountKind::Regular(0), lee::Account::default())]), - ), - ccc, - cci: None, - } + Self::from_ssk_and_ccc(ssk, ccc, None) } #[must_use] @@ -77,18 +50,16 @@ impl ChildKeysPrivate { input.extend_from_slice(&cci.to_be_bytes()); let hash_value = hmac_sha512::HMAC::mac(input, self.ccc); + let (first, ccc) = split_hash(&hash_value); - let ssk = SecretSpendingKey( - *hash_value - .first_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get first 32"), - ); - let ccc = *hash_value - .last_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get last 32"); + let ssk = SecretSpendingKey(first); - let nsk = ssk.generate_nullifier_secret_key(Some(cci)); - let vsk = ssk.generate_viewing_secret_seed_key(Some(cci)); + Self::from_ssk_and_ccc(ssk, ccc, Some(cci)) + } + + fn from_ssk_and_ccc(ssk: SecretSpendingKey, ccc: [u8; 32], cci: Option) -> Self { + let nsk = ssk.generate_nullifier_secret_key(cci); + let vsk = ssk.generate_viewing_secret_seed_key(cci); let npk = NullifierPublicKey::from(&nsk); let vpk = ViewingPublicKey::from(&vsk); @@ -107,7 +78,7 @@ impl ChildKeysPrivate { BTreeMap::from_iter([(PrivateAccountKind::Regular(0), lee::Account::default())]), ), ccc, - cci: Some(cci), + cci, } } } @@ -138,16 +109,16 @@ mod tests { use super::*; use crate::key_management::{self, secret_holders::ViewingSecretKey}; + const SEED: [u8; 64] = [ + 252, 56, 204, 83, 232, 123, 209, 188, 187, 167, 39, 213, 71, 39, 58, 65, 125, 134, 255, 49, + 43, 108, 92, 53, 173, 164, 94, 142, 150, 74, 21, 163, 43, 144, 226, 87, 199, 18, 129, 223, + 176, 198, 5, 150, 157, 70, 210, 254, 14, 105, 89, 191, 246, 27, 52, 170, 56, 114, 39, 38, + 118, 197, 205, 225, + ]; + #[test] fn master_key_generation() { - let seed: [u8; 64] = [ - 252, 56, 204, 83, 232, 123, 209, 188, 187, 167, 39, 213, 71, 39, 58, 65, 125, 134, 255, - 49, 43, 108, 92, 53, 173, 164, 94, 142, 150, 74, 21, 163, 43, 144, 226, 87, 199, 18, - 129, 223, 176, 198, 5, 150, 157, 70, 210, 254, 14, 105, 89, 191, 246, 27, 52, 170, 56, - 114, 39, 38, 118, 197, 205, 225, - ]; - - let keys = ChildKeysPrivate::root(seed); + let keys = ChildKeysPrivate::root(SEED); let expected_ssk = key_management::secret_holders::SecretSpendingKey([ 246, 79, 26, 124, 135, 95, 52, 51, 201, 27, 48, 194, 2, 144, 51, 219, 245, 128, 139, @@ -180,6 +151,7 @@ mod tests { ], ); + // Length matches MlKem768EncapsulationKey::LEN. let expected_vpk: [u8; 1184] = [ 127, 229, 162, 212, 104, 117, 4, 150, 192, 103, 122, 195, 14, 35, 12, 60, 52, 23, 220, 150, 100, 203, 34, 34, 127, 232, 156, 43, 218, 109, 6, 160, 67, 35, 210, 194, 25, 181, @@ -254,14 +226,7 @@ mod tests { #[test] fn child_keys_generation() { - let seed: [u8; 64] = [ - 252, 56, 204, 83, 232, 123, 209, 188, 187, 167, 39, 213, 71, 39, 58, 65, 125, 134, 255, - 49, 43, 108, 92, 53, 173, 164, 94, 142, 150, 74, 21, 163, 43, 144, 226, 87, 199, 18, - 129, 223, 176, 198, 5, 150, 157, 70, 210, 254, 14, 105, 89, 191, 246, 27, 52, 170, 56, - 114, 39, 38, 118, 197, 205, 225, - ]; - - let root_node = ChildKeysPrivate::root(seed); + let root_node = ChildKeysPrivate::root(SEED); let child_node = ChildKeysPrivate::nth_child(&root_node, 42_u32); let expected_ssk = key_management::secret_holders::SecretSpendingKey([ @@ -294,6 +259,7 @@ mod tests { ], ); + // Length matches MlKem768EncapsulationKey::LEN. let expected_vpk: [u8; 1184] = [ 215, 229, 207, 120, 148, 177, 148, 197, 72, 222, 134, 3, 231, 146, 123, 226, 36, 84, 232, 179, 205, 16, 241, 142, 9, 81, 58, 54, 12, 115, 148, 182, 19, 245, 22, 203, 57, diff --git a/lee/key_protocol/src/key_management/key_tree/keys_public.rs b/lee/key_protocol/src/key_management/key_tree/keys_public.rs index 947fb83c..4caad0e7 100644 --- a/lee/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/lee/key_protocol/src/key_management/key_tree/keys_public.rs @@ -1,7 +1,7 @@ use k256::elliptic_curve::PrimeField as _; use serde::{Deserialize, Serialize}; -use crate::key_management::key_tree::traits::KeyTreeNode; +use crate::key_management::key_tree::{split_hash, traits::KeyTreeNode}; #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))] @@ -21,52 +21,32 @@ impl ChildKeysPublic { #[must_use] pub fn root(seed: [u8; 64]) -> Self { let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub"); + let (first, cc) = split_hash(&hash_value); - let sk = lee::PrivateKey::try_new( - *hash_value - .first_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get first 32"), - ) - .expect("Expect a valid Private Key"); - let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::root()`: Invalid private key produced from `tweak`"); + let sk = lee::PrivateKey::try_new(first).expect("Expect a valid Private Key"); - let cc = *hash_value - .last_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get last 32"); - let pk = lee::PublicKey::new_from_private_key(&ssk); - - Self { - sk, - ssk, - pk, - cc, - cci: None, - } + Self::from_sk_and_cc(sk, cc, None) } #[must_use] pub fn nth_child(&self, cci: u32) -> Self { let hash_value = self.compute_hash_value(cci); + let (first, cc) = split_hash(&hash_value); - let lhs = k256::Scalar::from_repr( - (*hash_value - .first_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get first 32")) - .into(), - ) - .expect("Expect a valid k256 scalar"); + let lhs = k256::Scalar::from_repr(first.into()).expect("Expect a valid k256 scalar"); let rhs = k256::Scalar::from_repr((*self.sk.value()).into()).expect("Expect a valid k256 scalar"); let sk = lee::PrivateKey::try_new(lhs.add(&rhs).to_bytes().into()) .expect("Expect a valid private key"); - let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::nth_child()`: Invalid private key produced from `tweak`"); - - let cc = *hash_value - .last_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get last 32"); + Self::from_sk_and_cc(sk, cc, Some(cci)) + } + fn from_sk_and_cc(sk: lee::PrivateKey, cc: [u8; 32], cci: Option) -> Self { + let ssk = lee::PrivateKey::tweak(sk.value()).expect( + "`key_protocol::key_management::keys_public::ChildKeysPublic`: Invalid private key produced from `tweak`", + ); let pk = lee::PublicKey::new_from_private_key(&ssk); Self { @@ -74,7 +54,7 @@ impl ChildKeysPublic { ssk, pk, cc, - cci: Some(cci), + cci, } } @@ -128,15 +108,16 @@ mod tests { use super::*; + const SEED: [u8; 64] = [ + 88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173, + 134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87, 22, + 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6, 187, + 148, 92, 44, 253, 210, 37, + ]; + #[test] fn master_keys_generation() { - let seed = [ - 88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173, - 134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87, - 22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6, - 187, 148, 92, 44, 253, 210, 37, - ]; - let keys = ChildKeysPublic::root(seed); + let keys = ChildKeysPublic::root(SEED); let expected_cc = [ 238, 94, 84, 154, 56, 224, 80, 218, 133, 249, 179, 222, 9, 24, 17, 252, 120, 127, 222, @@ -169,13 +150,7 @@ mod tests { #[test] fn child_keys_generation() { - let seed = [ - 88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173, - 134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87, - 22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6, - 187, 148, 92, 44, 253, 210, 37, - ]; - let root_keys = ChildKeysPublic::root(seed); + let root_keys = ChildKeysPublic::root(SEED); let cci = (2_u32).pow(31) + 13; let child_keys = ChildKeysPublic::nth_child(&root_keys, cci); diff --git a/lee/key_protocol/src/key_management/key_tree/mod.rs b/lee/key_protocol/src/key_management/key_tree/mod.rs index e7d490d2..463c757a 100644 --- a/lee/key_protocol/src/key_management/key_tree/mod.rs +++ b/lee/key_protocol/src/key_management/key_tree/mod.rs @@ -39,16 +39,7 @@ impl KeyTree { .try_into() .expect("SeedHolder seed is 64 bytes long"); - let root_keys = N::from_seed(seed_fit); - let account_id_map = root_keys - .account_ids() - .map(|id| (id, ChainIndex::root())) - .collect(); - - Self { - key_map: BTreeMap::from_iter([(ChainIndex::root(), root_keys)]), - account_id_map, - } + Self::new_from_root(N::from_seed(seed_fit)) } pub fn new_from_root(root: N) -> Self { @@ -63,6 +54,15 @@ impl KeyTree { } } + fn insert_child(&mut self, child_keys: N, chain_index: ChainIndex) -> ChainIndex { + for account_id in child_keys.account_ids() { + self.account_id_map.insert(account_id, chain_index.clone()); + } + self.key_map.insert(chain_index.clone(), child_keys); + + chain_index + } + pub fn generate_new_node(&mut self, parent_cci: &ChainIndex) -> Option { let parent_keys = self.key_map.get(parent_cci)?; let next_child_id = self @@ -71,14 +71,8 @@ impl KeyTree { let next_cci = parent_cci.nth_child(next_child_id); let child_keys = parent_keys.derive_child(next_child_id); - let account_ids = child_keys.account_ids(); - for account_id in account_ids { - self.account_id_map.insert(account_id, next_cci.clone()); - } - self.key_map.insert(next_cci.clone(), child_keys); - - Some(next_cci) + Some(self.insert_child(child_keys, next_cci)) } pub fn fill_node(&mut self, chain_index: &ChainIndex) -> Option { @@ -86,14 +80,8 @@ impl KeyTree { let child_id = *chain_index.chain().last()?; let child_keys = parent_keys.derive_child(child_id); - let account_ids = child_keys.account_ids(); - for account_id in account_ids { - self.account_id_map.insert(account_id, chain_index.clone()); - } - self.key_map.insert(chain_index.clone(), child_keys); - - Some(chain_index.clone()) + Some(self.insert_child(child_keys, chain_index.clone())) } #[must_use] @@ -200,24 +188,27 @@ impl KeyTree { } impl KeyTree { + /// Pairs `cci` with the account ID of the node stored at it. + fn account_id_for_cci(&self, cci: ChainIndex) -> Option<(lee::AccountId, ChainIndex)> { + let node = self.key_map.get(&cci)?; + let account_id = node.account_ids().next()?; + Some((account_id, cci)) + } + /// Generate a new public key node, returning the account ID and chain index. pub fn generate_new_public_node( &mut self, parent_cci: &ChainIndex, ) -> Option<(lee::AccountId, ChainIndex)> { let cci = self.generate_new_node(parent_cci)?; - let node = self.key_map.get(&cci)?; - let account_id = node.account_ids().next()?; - Some((account_id, cci)) + self.account_id_for_cci(cci) } /// Generate a new public key node using layered placement, returning the account ID and chain /// index. pub fn generate_new_public_node_layered(&mut self) -> Option<(lee::AccountId, ChainIndex)> { let cci = self.generate_new_node_layered()?; - let node = self.key_map.get(&cci)?; - let account_id = node.account_ids().next()?; - Some((account_id, cci)) + self.account_id_for_cci(cci) } /// Cleanup of non-initialized accounts in a public tree. @@ -323,6 +314,16 @@ impl KeyTree { } } +const fn split_hash(hash_value: &[u8; 64]) -> ([u8; 32], [u8; 32]) { + let first = *hash_value + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"); + let last = *hash_value + .last_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get last 32"); + (first, last) +} + #[cfg(test)] mod tests { #![expect(clippy::shadow_unrelated, reason = "We don't care about this in tests")] @@ -340,6 +341,18 @@ mod tests { } } + #[test] + fn split_hash_splits_into_first_and_last_32_bytes() { + let mut hash_value = [0_u8; 64]; + hash_value[..32].fill(0xAA); + hash_value[32..].fill(0xBB); + + let (first, last) = split_hash(&hash_value); + + assert_eq!(first, [0xAA; 32]); + assert_eq!(last, [0xBB; 32]); + } + #[test] fn simple_key_tree() { let seed_holder = seed_holder_for_tests(); diff --git a/lee/key_protocol/src/key_management/mod.rs b/lee/key_protocol/src/key_management/mod.rs index 459badf0..3a066fd9 100644 --- a/lee/key_protocol/src/key_management/mod.rs +++ b/lee/key_protocol/src/key_management/mod.rs @@ -25,8 +25,22 @@ impl KeyChain { #[must_use] pub fn new_os_random() -> Self { // Currently dropping SeedHolder at the end of initialization. - // Now entirely sure if we need it in the future. + // Not entirely sure if we need it in the future. let seed_holder = SeedHolder::new_os_random(); + + Self::from_seed_holder(&seed_holder) + } + + #[must_use] + pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) { + // Currently dropping SeedHolder at the end of initialization. + // Not entirely sure if we need it in the future. + let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase); + + (Self::from_seed_holder(&seed_holder), mnemonic) + } + + fn from_seed_holder(seed_holder: &SeedHolder) -> Self { let secret_spending_key = seed_holder.produce_top_secret_key_holder(); let private_key_holder = secret_spending_key.produce_private_key_holder(None); @@ -42,29 +56,6 @@ impl KeyChain { } } - #[must_use] - pub fn new_mnemonic(passphrase: &str) -> (Self, bip39::Mnemonic) { - // Currently dropping SeedHolder at the end of initialization. - // Not entirely sure if we need it in the future. - let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase); - let secret_spending_key = seed_holder.produce_top_secret_key_holder(); - - let private_key_holder = secret_spending_key.produce_private_key_holder(None); - - let nullifier_public_key = private_key_holder.generate_nullifier_public_key(); - let viewing_public_key = private_key_holder.generate_viewing_public_key(); - - ( - Self { - secret_spending_key, - private_key_holder, - nullifier_public_key, - viewing_public_key, - }, - mnemonic, - ) - } - #[must_use] pub fn calculate_shared_secret_receiver( &self, diff --git a/lee/key_protocol/src/key_management/secret_holders.rs b/lee/key_protocol/src/key_management/secret_holders.rs index 7bda4ffb..b8225a4b 100644 --- a/lee/key_protocol/src/key_management/secret_holders.rs +++ b/lee/key_protocol/src/key_management/secret_holders.rs @@ -43,16 +43,7 @@ pub struct PrivateKeyHolder { impl SeedHolder { #[must_use] pub fn new_os_random() -> Self { - let mut enthopy_bytes: [u8; 32] = [0; 32]; - OsRng.fill_bytes(&mut enthopy_bytes); - - let mnemonic = Mnemonic::from_entropy(&enthopy_bytes) - .expect("Enthropy must be a multiple of 32 bytes"); - let seed_wide = mnemonic.to_seed("mnemonic"); - - Self { - seed: seed_wide.to_vec(), - } + Self::new_mnemonic("mnemonic").0 } #[must_use] @@ -62,14 +53,8 @@ impl SeedHolder { let mnemonic = Mnemonic::from_entropy(&entropy_bytes).expect("Entropy must be a multiple of 32 bytes"); - let seed_wide = mnemonic.to_seed(passphrase); - ( - Self { - seed: seed_wide.to_vec(), - }, - mnemonic, - ) + (Self::from_mnemonic(&mnemonic, passphrase), mnemonic) } #[must_use] @@ -107,10 +92,7 @@ impl SecretSpendingKey { const SUFFIX_1: &[u8; 1] = &[1]; const SUFFIX_2: &[u8; 19] = &[0; 19]; - let index = match index { - None => 0_u32, - _ => index.expect("Expect a valid u32"), - }; + let index = index.unwrap_or(0); let mut hasher = sha2::Sha256::new(); hasher.update(PREFIX); @@ -129,10 +111,7 @@ impl SecretSpendingKey { const SUFFIX_1: &[u8; 1] = &[2]; const SUFFIX_2: &[u8; 19] = &[0; 19]; - let index = match index { - None => 0_u32, - _ => index.expect("Expect a valid u32"), - }; + let index = index.unwrap_or(0); let mut bytes: Vec = Vec::with_capacity(64); bytes.extend_from_slice(PREFIX); @@ -146,14 +125,7 @@ impl SecretSpendingKey { let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed"); - ViewingSecretKey::new( - *full_seed - .first_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get first 32"), - *full_seed - .last_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get last 32"), - ) + Self::generate_viewing_secret_key(full_seed) } #[must_use] @@ -181,7 +153,7 @@ impl From<&ViewingSecretKey> for ViewingPublicKey { seed_bytes[32..].copy_from_slice(&sk.z); let dk = ::DecapsulationKey::from_seed(Seed::from(seed_bytes)); Self::from_bytes(dk.encapsulation_key().to_bytes().to_vec()) - .expect("key_protocol::secret_holders::From<&ViewingSecretKey>: ML-KEM-768 encapsulation key is always 1184 bytes") + .expect("key_protocol::secret_holders::From<&ViewingSecretKey>: ML-KEM-768 encapsulation key is always ViewingPublicKey::LEN bytes") } } @@ -201,7 +173,6 @@ impl PrivateKeyHolder { mod tests { use super::*; - // TODO? are these necessary? #[test] fn seed_generation_test() { let seed_holder = SeedHolder::new_os_random(); diff --git a/lee/privacy_preserving_circuit/Cargo.toml b/lee/privacy_preserving_circuit/Cargo.toml new file mode 100644 index 00000000..26dfa132 --- /dev/null +++ b/lee/privacy_preserving_circuit/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "privacy_preserving_circuit_program" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +# Renaming binary to privacy_preserving_circuit for better looking. +[[bin]] +name = "privacy_preserving_circuit" +path = "src/main.rs" + +[lints] +workspace = true + +[dependencies] +lee_core.workspace = true +risc0-zkvm.workspace = true diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs b/lee/privacy_preserving_circuit/src/execution_state.rs similarity index 100% rename from program_methods/guest/src/bin/privacy_preserving_circuit/execution_state.rs rename to lee/privacy_preserving_circuit/src/execution_state.rs diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit/main.rs b/lee/privacy_preserving_circuit/src/main.rs similarity index 100% rename from program_methods/guest/src/bin/privacy_preserving_circuit/main.rs rename to lee/privacy_preserving_circuit/src/main.rs diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs b/lee/privacy_preserving_circuit/src/output.rs similarity index 96% rename from program_methods/guest/src/bin/privacy_preserving_circuit/output.rs rename to lee/privacy_preserving_circuit/src/output.rs index c799a1db..c9543e05 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs +++ b/lee/privacy_preserving_circuit/src/output.rs @@ -1,7 +1,7 @@ use lee_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme, - EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, - NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, + Commitment, CommitmentSetDigest, EncryptedAccountData, EncryptionScheme, EphemeralSecretKey, + InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, + PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, account::{Account, AccountId, Nonce}, compute_digest_for_path, encryption::ViewingPublicKey, @@ -45,6 +45,7 @@ pub fn compute_circuit_output( random_seed, nsk, identifier, + commitment_root, } => { let npk = NullifierPublicKey::from(nsk); let account_id = AccountId::for_regular_private_account(&npk, vpk, *identifier); @@ -62,7 +63,7 @@ pub fn compute_circuit_output( let new_nullifier = ( Nullifier::for_account_initialization(&account_id), - DUMMY_COMMITMENT_HASH, + *commitment_root, ); let new_nonce = Nonce::private_account_nonce_init(&account_id); @@ -121,6 +122,7 @@ pub fn compute_circuit_output( random_seed, npk, identifier, + commitment_root, } => { let account_id = AccountId::for_regular_private_account(npk, vpk, *identifier); @@ -137,7 +139,7 @@ pub fn compute_circuit_output( let new_nullifier = ( Nullifier::for_account_initialization(&account_id), - DUMMY_COMMITMENT_HASH, + *commitment_root, ); let new_nonce = Nonce::private_account_nonce_init(&account_id); @@ -159,6 +161,7 @@ pub fn compute_circuit_output( random_seed, npk, identifier, + commitment_root, seed: _, } => { // The npk-to-account_id binding is established upstream in @@ -179,7 +182,7 @@ pub fn compute_circuit_output( let new_nullifier = ( Nullifier::for_account_initialization(&pre_state.account_id), - DUMMY_COMMITMENT_HASH, + *commitment_root, ); let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id); diff --git a/lee/state_machine/Cargo.toml b/lee/state_machine/Cargo.toml index 8777db09..2f3dbc2c 100644 --- a/lee/state_machine/Cargo.toml +++ b/lee/state_machine/Cargo.toml @@ -9,9 +9,6 @@ workspace = true [dependencies] lee_core = { workspace = true, features = ["host"] } -clock_core.workspace = true -faucet_core.workspace = true -bridge_core.workspace = true anyhow.workspace = true thiserror.workspace = true @@ -27,14 +24,13 @@ risc0-binfmt = "3.0.2" log.workspace = true [build-dependencies] -risc0-build = "3.0.3" -risc0-binfmt = "3.0.2" +build_utils.workspace = true [dev-dependencies] lee_core = { workspace = true, features = ["test_utils"] } token_core.workspace = true authenticated_transfer_core.workspace = true -test_program_methods.workspace = true +test_methods = { path = "test_methods" } env_logger.workspace = true hex-literal = "1.0.0" diff --git a/lee/state_machine/build.rs b/lee/state_machine/build.rs index 5e8ac989..de7c6095 100644 --- a/lee/state_machine/build.rs +++ b/lee/state_machine/build.rs @@ -1,43 +1,5 @@ -use std::{env, fmt::Write as _, fs, path::PathBuf}; - fn main() -> Result<(), Box> { - let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); - let out_dir = PathBuf::from(env::var("OUT_DIR")?); - let mod_dir = out_dir.join("program_methods"); - let mod_file = mod_dir.join("mod.rs"); - let program_methods_dir = manifest_dir.join("../../artifacts/program_methods/"); - - println!("cargo:rerun-if-changed={}", program_methods_dir.display()); - - let bins = fs::read_dir(&program_methods_dir)? - .filter_map(Result::ok) - .filter(|e| e.path().extension().is_some_and(|ext| ext == "bin")) - .collect::>(); - - if bins.is_empty() { - return Err(format!("No .bin files found in {}", program_methods_dir.display()).into()); - } - - fs::create_dir_all(&mod_dir)?; - let mut src = String::new(); - for entry in bins { - let path = entry.path(); - let name = path.file_stem().unwrap().to_string_lossy(); - let bytecode = fs::read(&path)?; - let image_id: [u32; 8] = risc0_binfmt::compute_image_id(&bytecode)?.into(); - write!( - src, - "pub const {}_ELF: &[u8] = include_bytes!(r#\"{}\"#);\n\ - #[expect(clippy::unreadable_literal, reason = \"Generated image IDs from risc0 are cryptographic hashes represented as u32 arrays\")]\n\ - pub const {}_ID: [u32; 8] = {:?};\n", - name.to_uppercase(), - path.display(), - name.to_uppercase(), - image_id - )?; - } - fs::write(&mod_file, src)?; - println!("cargo:warning=Generated module at {}", mod_file.display()); + build_utils::include_artifacts("lee/privacy_preserving_circuit")?; Ok(()) } diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index 9bead310..d441ff19 100644 --- a/lee/state_machine/core/src/circuit_io.rs +++ b/lee/state_machine/core/src/circuit_io.rs @@ -35,6 +35,7 @@ pub enum InputAccountIdentity { random_seed: [u8; 32], nsk: NullifierSecretKey, identifier: Identifier, + commitment_root: CommitmentSetDigest, }, /// Update of an authorized standalone private account: existing on-chain commitment, with /// membership proof. @@ -52,6 +53,7 @@ pub enum InputAccountIdentity { random_seed: [u8; 32], npk: NullifierPublicKey, identifier: Identifier, + commitment_root: CommitmentSetDigest, }, /// Init of a private PDA, unauthorized. The npk-to-account_id binding is proven upstream /// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. The identifier diversifies the @@ -62,6 +64,7 @@ pub enum InputAccountIdentity { random_seed: [u8; 32], npk: NullifierPublicKey, identifier: Identifier, + commitment_root: CommitmentSetDigest, /// When `Some((seed, authority_program_id))`, the circuit binds this position via the /// external derivation check /// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) == diff --git a/lee/state_machine/core/src/encoding.rs b/lee/state_machine/core/src/encoding.rs index 59df4b06..0fe3e08f 100644 --- a/lee/state_machine/core/src/encoding.rs +++ b/lee/state_machine/core/src/encoding.rs @@ -7,7 +7,7 @@ use std::io::Read as _; #[cfg(feature = "host")] use crate::Nullifier; #[cfg(feature = "host")] -use crate::encryption::EphemeralPublicKey; +use crate::encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN}; #[cfg(feature = "host")] use crate::error::LeeCoreError; use crate::{ @@ -168,7 +168,7 @@ impl EphemeralPublicKey { /// Deserializes an ML-KEM-768 ciphertext from a cursor. /// Reads exactly 1088 bytes โ€” the fixed ciphertext size for ML-KEM-768. pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let mut value = vec![0_u8; 1088]; + let mut value = vec![0_u8; ML_KEM_768_CIPHERTEXT_LEN]; cursor.read_exact(&mut value)?; Ok(Self(value)) } diff --git a/lee/state_machine/core/src/encryption/mod.rs b/lee/state_machine/core/src/encryption/mod.rs index be0dbd19..f503279d 100644 --- a/lee/state_machine/core/src/encryption/mod.rs +++ b/lee/state_machine/core/src/encryption/mod.rs @@ -10,6 +10,9 @@ pub use shared_key_derivation::{MlKem768EncapsulationKey, ViewingPublicKey}; use crate::{Commitment, account::Account, program::PrivateAccountKind}; pub mod shared_key_derivation; +/// Length in bytes of an ML-KEM-768 ciphertext (the `EphemeralPublicKey` payload). +pub const ML_KEM_768_CIPHERTEXT_LEN: usize = 1088; + pub type Scalar = [u8; 32]; #[derive(Serialize, Deserialize, Clone, Copy)] @@ -41,7 +44,7 @@ impl EphemeralSecretKey { pub struct SharedSecretKey(pub [u8; 32]); /// The ML-KEM-768 ciphertext produced during encapsulation; transmitted on-wire in place of the -/// former ECDH ephemeral public key. Always 1088 bytes for ML-KEM-768. +/// former ECDH ephemeral public key. Always `ML_KEM_768_CIPHERTEXT_LEN` (1088) bytes. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct EphemeralPublicKey(pub Vec); diff --git a/lee/state_machine/core/src/encryption/shared_key_derivation.rs b/lee/state_machine/core/src/encryption/shared_key_derivation.rs index a339a282..3255080b 100644 --- a/lee/state_machine/core/src/encryption/shared_key_derivation.rs +++ b/lee/state_machine/core/src/encryption/shared_key_derivation.rs @@ -68,7 +68,7 @@ impl SharedSecretKey { let ek_bytes: ml_kem::kem::Key = ek.0.as_slice() .try_into() - .expect("MlKem768EncapsulationKey must be 1184 bytes"); + .expect("MlKem768EncapsulationKey must be MlKem768EncapsulationKey::LEN bytes"); let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect( "MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key", ); @@ -96,7 +96,7 @@ impl SharedSecretKey { let ek_bytes: ml_kem::kem::Key = ek.0.as_slice() .try_into() - .expect("MlKem768EncapsulationKey must be 1184 bytes"); + .expect("MlKem768EncapsulationKey must be MlKem768EncapsulationKey::LEN bytes"); let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect( "MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key", ); @@ -110,8 +110,9 @@ impl SharedSecretKey { /// Receiver: decapsulate the shared secret from a KEM ciphertext. /// - /// Returns `None` if the `EphemeralPublicKey` is not exactly 1088 bytes โ€” callers on - /// the wallet scan path should skip the output rather than panic on malformed chain data. + /// Returns `None` if the `EphemeralPublicKey` is not exactly [`ML_KEM_768_CIPHERTEXT_LEN`] + /// bytes โ€” callers on the wallet scan path should skip the output rather than panic on + /// malformed chain data. /// /// `d` and `z` are the two 32-byte halves of the FIPS 203 `ViewingSecretKey` seed. #[must_use] @@ -138,6 +139,7 @@ mod tests { use ml_kem::KeyExport as _; use super::*; + use crate::ML_KEM_768_CIPHERTEXT_LEN; #[test] fn encapsulate_decapsulate_round_trip() { @@ -156,11 +158,15 @@ mod tests { let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &z).unwrap(); assert_eq!(sender_ss.0, receiver_ss.0, "shared secrets must match"); - assert_eq!(epk.0.len(), 1088, "ML-KEM-768 ciphertext is 1088 bytes"); + assert_eq!( + epk.0.len(), + ML_KEM_768_CIPHERTEXT_LEN, + "ML-KEM-768 ciphertext length" + ); assert_eq!( ek.0.len(), - 1184, - "ML-KEM-768 encapsulation key is 1184 bytes" + MlKem768EncapsulationKey::LEN, + "ML-KEM-768 encapsulation key length" ); } @@ -169,15 +175,15 @@ mod tests { let d = [1_u8; 32]; let z = [2_u8; 32]; - // Too short โ€” 100 bytes instead of 1088. + // Too short โ€” 100 bytes instead of ML_KEM_768_CIPHERTEXT_LEN. let short_epk = EphemeralPublicKey(vec![42_u8; 100]); assert!( SharedSecretKey::decapsulate(&short_epk, &d, &z).is_none(), "short EphemeralPublicKey must return None" ); - // Too long โ€” 1089 bytes instead of 1088. - let long_epk = EphemeralPublicKey(vec![42_u8; 1089]); + // Too long โ€” ML_KEM_768_CIPHERTEXT_LEN + 1. + let long_epk = EphemeralPublicKey(vec![42_u8; ML_KEM_768_CIPHERTEXT_LEN + 1]); assert!( SharedSecretKey::decapsulate(&long_epk, &d, &z).is_none(), "long EphemeralPublicKey must return None" diff --git a/lee/state_machine/core/src/lib.rs b/lee/state_machine/core/src/lib.rs index c5d42ac9..4bcb23a4 100644 --- a/lee/state_machine/core/src/lib.rs +++ b/lee/state_machine/core/src/lib.rs @@ -12,7 +12,7 @@ pub use commitment::{ }; pub use encryption::{ EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey, - SharedSecretKey, ViewTag, + ML_KEM_768_CIPHERTEXT_LEN, SharedSecretKey, ViewTag, }; pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey}; pub use program::PrivateAccountKind; diff --git a/lee/state_machine/core/src/program.rs b/lee/state_machine/core/src/program/mod.rs similarity index 66% rename from lee/state_machine/core/src/program.rs rename to lee/state_machine/core/src/program/mod.rs index 1c99cb36..0d96642e 100644 --- a/lee/state_machine/core/src/program.rs +++ b/lee/state_machine/core/src/program/mod.rs @@ -237,7 +237,7 @@ impl ChainedCall { /// Represents the final state of an `Account` after a program execution. /// /// A post state may optionally request that the executing program -/// becomes the owner of the account (a โ€œclaimโ€). This is used to signal +/// becomes the owner of the account (a "claim"). This is used to signal /// that the program intends to take ownership of the account. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(any(feature = "host", test), derive(PartialEq, Eq))] @@ -773,346 +773,4 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn validity_window_unbounded_accepts_any_value() { - let w: ValidityWindow = ValidityWindow::new_unbounded(); - assert!(w.is_valid_for(0)); - assert!(w.is_valid_for(u64::MAX)); - } - - #[test] - fn validity_window_bounded_range_includes_from_excludes_to() { - let w: ValidityWindow = (Some(5), Some(10)).try_into().unwrap(); - assert!(!w.is_valid_for(4)); - assert!(w.is_valid_for(5)); - assert!(w.is_valid_for(9)); - assert!(!w.is_valid_for(10)); - } - - #[test] - fn validity_window_only_from_bound() { - let w: ValidityWindow = (Some(5), None).try_into().unwrap(); - assert!(!w.is_valid_for(4)); - assert!(w.is_valid_for(5)); - assert!(w.is_valid_for(u64::MAX)); - } - - #[test] - fn validity_window_only_to_bound() { - let w: ValidityWindow = (None, Some(5)).try_into().unwrap(); - assert!(w.is_valid_for(0)); - assert!(w.is_valid_for(4)); - assert!(!w.is_valid_for(5)); - } - - #[test] - fn validity_window_adjacent_bounds_are_invalid() { - // [5, 5) is an empty range โ€” from == to - assert!(ValidityWindow::::try_from((Some(5), Some(5))).is_err()); - } - - #[test] - fn validity_window_inverted_bounds_are_invalid() { - assert!(ValidityWindow::::try_from((Some(10), Some(5))).is_err()); - } - - #[test] - fn validity_window_getters_match_construction() { - let w: ValidityWindow = (Some(3), Some(7)).try_into().unwrap(); - assert_eq!(w.start(), Some(3)); - assert_eq!(w.end(), Some(7)); - } - - #[test] - fn validity_window_getters_for_unbounded() { - let w: ValidityWindow = ValidityWindow::new_unbounded(); - assert_eq!(w.start(), None); - assert_eq!(w.end(), None); - } - - #[test] - fn validity_window_from_range() { - let w: ValidityWindow = ValidityWindow::try_from(5_u64..10).unwrap(); - assert_eq!(w.start(), Some(5)); - assert_eq!(w.end(), Some(10)); - } - - #[test] - fn validity_window_from_range_empty_is_invalid() { - assert!(ValidityWindow::::try_from(5_u64..5).is_err()); - } - - #[test] - fn validity_window_from_range_inverted_is_invalid() { - let from = 10_u64; - let to = 5_u64; - assert!(ValidityWindow::::try_from(from..to).is_err()); - } - - #[test] - fn validity_window_from_range_from() { - let w: ValidityWindow = (5_u64..).into(); - assert_eq!(w.start(), Some(5)); - assert_eq!(w.end(), None); - } - - #[test] - fn validity_window_from_range_to() { - let w: ValidityWindow = (..10_u64).into(); - assert_eq!(w.start(), None); - assert_eq!(w.end(), Some(10)); - } - - #[test] - fn validity_window_from_range_full() { - let w: ValidityWindow = (..).into(); - assert_eq!(w.start(), None); - assert_eq!(w.end(), None); - } - - #[test] - fn program_output_try_with_block_validity_window_range() { - let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![]) - .try_with_block_validity_window(10_u64..100) - .unwrap(); - assert_eq!(output.block_validity_window.start(), Some(10)); - assert_eq!(output.block_validity_window.end(), Some(100)); - } - - #[test] - fn program_output_with_block_validity_window_range_from() { - let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![]) - .with_block_validity_window(10_u64..); - assert_eq!(output.block_validity_window.start(), Some(10)); - assert_eq!(output.block_validity_window.end(), None); - } - - #[test] - fn program_output_with_block_validity_window_range_to() { - let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![]) - .with_block_validity_window(..100_u64); - assert_eq!(output.block_validity_window.start(), None); - assert_eq!(output.block_validity_window.end(), Some(100)); - } - - #[test] - fn program_output_try_with_block_validity_window_empty_range_fails() { - let result = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![]) - .try_with_block_validity_window(5_u64..5); - assert!(result.is_err()); - } - - #[test] - fn post_state_new_with_claim_constructor() { - let account = Account { - program_owner: [1, 2, 3, 4, 5, 6, 7, 8], - balance: 1337, - data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), - nonce: 10_u128.into(), - }; - - let account_post_state = AccountPostState::new_claimed(account.clone(), Claim::Authorized); - - assert_eq!(account, account_post_state.account); - assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized)); - } - - #[test] - fn post_state_new_without_claim_constructor() { - let account = Account { - program_owner: [1, 2, 3, 4, 5, 6, 7, 8], - balance: 1337, - data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), - nonce: 10_u128.into(), - }; - - let account_post_state = AccountPostState::new(account.clone()); - - assert_eq!(account, account_post_state.account); - assert!(account_post_state.required_claim().is_none()); - } - - #[test] - fn post_state_account_getter() { - let mut account = Account { - program_owner: [1, 2, 3, 4, 5, 6, 7, 8], - balance: 1337, - data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), - nonce: 10_u128.into(), - }; - - let mut account_post_state = AccountPostState::new(account.clone()); - - assert_eq!(account_post_state.account(), &account); - assert_eq!(account_post_state.account_mut(), &mut account); - } - - // ---- AccountId::for_private_pda tests ---- - - /// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific - /// `(program_id, seed, npk, identifier)` tuple. Any change to `PRIVATE_PDA_PREFIX`, byte - /// ordering, or the underlying hash breaks this test. - #[test] - fn for_private_pda_matches_pinned_value() { - let program_id: ProgramId = [1; 8]; - let seed = PdaSeed::new([2; 32]); - let npk = NullifierPublicKey([3; 32]); - let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); - let identifier: Identifier = u128::MAX; - let expected = AccountId::new([ - 5, 87, 128, 244, 206, 244, 65, 130, 178, 88, 225, 183, 0, 159, 201, 201, 212, 206, 6, - 156, 13, 55, 32, 139, 91, 222, 209, 83, 172, 148, 123, 179, - ]); - assert_eq!( - AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier), - expected - ); - } - - /// Two groups with different viewing keys at the same (program, seed) get different addresses. - #[test] - fn for_private_pda_differs_for_different_npk() { - let program_id: ProgramId = [1; 8]; - let seed = PdaSeed::new([2; 32]); - let npk_a = NullifierPublicKey([3; 32]); - let npk_b = NullifierPublicKey([4; 32]); - let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); - assert_ne!( - AccountId::for_private_pda(&program_id, &seed, &npk_a, &vpk, u128::MAX), - AccountId::for_private_pda(&program_id, &seed, &npk_b, &vpk, u128::MAX), - ); - } - - /// Different seeds produce different addresses, even with the same program and npk. - #[test] - fn for_private_pda_differs_for_different_seed() { - let program_id: ProgramId = [1; 8]; - let seed_a = PdaSeed::new([2; 32]); - let seed_b = PdaSeed::new([5; 32]); - let npk = NullifierPublicKey([3; 32]); - let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); - assert_ne!( - AccountId::for_private_pda(&program_id, &seed_a, &npk, &vpk, u128::MAX), - AccountId::for_private_pda(&program_id, &seed_b, &npk, &vpk, u128::MAX), - ); - } - - /// Different programs produce different addresses, even with the same seed and npk. - #[test] - fn for_private_pda_differs_for_different_program_id() { - let program_id_a: ProgramId = [1; 8]; - let program_id_b: ProgramId = [9; 8]; - let seed = PdaSeed::new([2; 32]); - let npk = NullifierPublicKey([3; 32]); - let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); - assert_ne!( - AccountId::for_private_pda(&program_id_a, &seed, &npk, &vpk, u128::MAX), - AccountId::for_private_pda(&program_id_b, &seed, &npk, &vpk, u128::MAX), - ); - } - - /// Different identifiers produce different addresses for the same `(program_id, seed, npk)`, - /// confirming that each `(program_id, seed, npk)` tuple controls a family of 2^128 addresses. - #[test] - fn for_private_pda_differs_for_different_identifier() { - let program_id: ProgramId = [1; 8]; - let seed = PdaSeed::new([2; 32]); - let npk = NullifierPublicKey([3; 32]); - let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); - assert_ne!( - AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 0), - AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 1), - ); - assert_ne!( - AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 0), - AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX), - ); - } - - /// A private PDA at the same (program, seed) has a different address than a public PDA, - /// because the private formula uses a different prefix and includes npk. - #[test] - fn for_private_pda_differs_from_public_pda() { - let program_id: ProgramId = [1; 8]; - let seed = PdaSeed::new([2; 32]); - let npk = NullifierPublicKey([3; 32]); - let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); - let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX); - let public_id = AccountId::for_public_pda(&program_id, &seed); - assert_ne!(private_id, public_id); - } - - #[cfg(feature = "host")] - #[test] - fn private_account_kind_header_round_trips() { - let regular = PrivateAccountKind::Regular(42); - let pda = PrivateAccountKind::Pda { - program_id: [1_u32; 8], - seed: PdaSeed::new([2_u8; 32]), - identifier: u128::MAX, - }; - assert_eq!( - PrivateAccountKind::from_header_bytes(®ular.to_header_bytes()), - Some(regular) - ); - assert_eq!( - PrivateAccountKind::from_header_bytes(&pda.to_header_bytes()), - Some(pda) - ); - } - - #[cfg(feature = "host")] - #[test] - fn private_account_kind_unknown_discriminant_returns_none() { - let mut bytes = [0_u8; PrivateAccountKind::HEADER_LEN]; - bytes[0] = 0xFF; - assert_eq!(PrivateAccountKind::from_header_bytes(&bytes), None); - } - - #[test] - fn for_private_account_dispatches_correctly() { - let program_id: ProgramId = [1; 8]; - let seed = PdaSeed::new([2; 32]); - let npk = NullifierPublicKey([3; 32]); - let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); - let identifier: Identifier = 77; - - assert_eq!( - AccountId::for_private_account(&npk, &vpk, &PrivateAccountKind::Regular(identifier)), - AccountId::for_regular_private_account(&npk, &vpk, identifier), - ); - assert_eq!( - AccountId::for_private_account( - &npk, - &vpk, - &PrivateAccountKind::Pda { - program_id, - seed, - identifier - } - ), - AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier), - ); - } - - #[test] - fn compute_public_authorized_pdas_with_seeds() { - let caller: ProgramId = [1; 8]; - let seed = PdaSeed::new([2; 32]); - let result = compute_public_authorized_pdas(Some(caller), &[seed]); - let expected = AccountId::for_public_pda(&caller, &seed); - assert!(result.contains(&expected)); - assert_eq!(result.len(), 1); - } - - /// With no caller (top-level call), the result is always empty. - #[test] - fn compute_public_authorized_pdas_no_caller_returns_empty() { - let seed = PdaSeed::new([2; 32]); - let result = compute_public_authorized_pdas(None, &[seed]); - assert!(result.is_empty()); - } -} +mod tests; diff --git a/lee/state_machine/core/src/program/tests.rs b/lee/state_machine/core/src/program/tests.rs new file mode 100644 index 00000000..19a259a8 --- /dev/null +++ b/lee/state_machine/core/src/program/tests.rs @@ -0,0 +1,341 @@ +use super::*; + +#[test] +fn validity_window_unbounded_accepts_any_value() { + let w: ValidityWindow = ValidityWindow::new_unbounded(); + assert!(w.is_valid_for(0)); + assert!(w.is_valid_for(u64::MAX)); +} + +#[test] +fn validity_window_bounded_range_includes_from_excludes_to() { + let w: ValidityWindow = (Some(5), Some(10)).try_into().unwrap(); + assert!(!w.is_valid_for(4)); + assert!(w.is_valid_for(5)); + assert!(w.is_valid_for(9)); + assert!(!w.is_valid_for(10)); +} + +#[test] +fn validity_window_only_from_bound() { + let w: ValidityWindow = (Some(5), None).try_into().unwrap(); + assert!(!w.is_valid_for(4)); + assert!(w.is_valid_for(5)); + assert!(w.is_valid_for(u64::MAX)); +} + +#[test] +fn validity_window_only_to_bound() { + let w: ValidityWindow = (None, Some(5)).try_into().unwrap(); + assert!(w.is_valid_for(0)); + assert!(w.is_valid_for(4)); + assert!(!w.is_valid_for(5)); +} + +#[test] +fn validity_window_adjacent_bounds_are_invalid() { + // [5, 5) is an empty range โ€” from == to + assert!(ValidityWindow::::try_from((Some(5), Some(5))).is_err()); +} + +#[test] +fn validity_window_inverted_bounds_are_invalid() { + assert!(ValidityWindow::::try_from((Some(10), Some(5))).is_err()); +} + +#[test] +fn validity_window_getters_match_construction() { + let w: ValidityWindow = (Some(3), Some(7)).try_into().unwrap(); + assert_eq!(w.start(), Some(3)); + assert_eq!(w.end(), Some(7)); +} + +#[test] +fn validity_window_getters_for_unbounded() { + let w: ValidityWindow = ValidityWindow::new_unbounded(); + assert_eq!(w.start(), None); + assert_eq!(w.end(), None); +} + +#[test] +fn validity_window_from_range() { + let w: ValidityWindow = ValidityWindow::try_from(5_u64..10).unwrap(); + assert_eq!(w.start(), Some(5)); + assert_eq!(w.end(), Some(10)); +} + +#[test] +fn validity_window_from_range_empty_is_invalid() { + assert!(ValidityWindow::::try_from(5_u64..5).is_err()); +} + +#[test] +fn validity_window_from_range_inverted_is_invalid() { + let from = 10_u64; + let to = 5_u64; + assert!(ValidityWindow::::try_from(from..to).is_err()); +} + +#[test] +fn validity_window_from_range_from() { + let w: ValidityWindow = (5_u64..).into(); + assert_eq!(w.start(), Some(5)); + assert_eq!(w.end(), None); +} + +#[test] +fn validity_window_from_range_to() { + let w: ValidityWindow = (..10_u64).into(); + assert_eq!(w.start(), None); + assert_eq!(w.end(), Some(10)); +} + +#[test] +fn validity_window_from_range_full() { + let w: ValidityWindow = (..).into(); + assert_eq!(w.start(), None); + assert_eq!(w.end(), None); +} + +#[test] +fn program_output_try_with_block_validity_window_range() { + let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![]) + .try_with_block_validity_window(10_u64..100) + .unwrap(); + assert_eq!(output.block_validity_window.start(), Some(10)); + assert_eq!(output.block_validity_window.end(), Some(100)); +} + +#[test] +fn program_output_with_block_validity_window_range_from() { + let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![]) + .with_block_validity_window(10_u64..); + assert_eq!(output.block_validity_window.start(), Some(10)); + assert_eq!(output.block_validity_window.end(), None); +} + +#[test] +fn program_output_with_block_validity_window_range_to() { + let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![]) + .with_block_validity_window(..100_u64); + assert_eq!(output.block_validity_window.start(), None); + assert_eq!(output.block_validity_window.end(), Some(100)); +} + +#[test] +fn program_output_try_with_block_validity_window_empty_range_fails() { + let result = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![]) + .try_with_block_validity_window(5_u64..5); + assert!(result.is_err()); +} + +#[test] +fn post_state_new_with_claim_constructor() { + let account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1337, + data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), + nonce: 10_u128.into(), + }; + + let account_post_state = AccountPostState::new_claimed(account.clone(), Claim::Authorized); + + assert_eq!(account, account_post_state.account); + assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized)); +} + +#[test] +fn post_state_new_without_claim_constructor() { + let account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1337, + data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), + nonce: 10_u128.into(), + }; + + let account_post_state = AccountPostState::new(account.clone()); + + assert_eq!(account, account_post_state.account); + assert!(account_post_state.required_claim().is_none()); +} + +#[test] +fn post_state_account_getter() { + let mut account = Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + balance: 1337, + data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(), + nonce: 10_u128.into(), + }; + + let mut account_post_state = AccountPostState::new(account.clone()); + + assert_eq!(account_post_state.account(), &account); + assert_eq!(account_post_state.account_mut(), &mut account); +} + +// ---- AccountId::for_private_pda tests ---- + +/// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific +/// `(program_id, seed, npk, identifier)` tuple. Any change to `PRIVATE_PDA_PREFIX`, byte +/// ordering, or the underlying hash breaks this test. +#[test] +fn for_private_pda_matches_pinned_value() { + let program_id: ProgramId = [1; 8]; + let seed = PdaSeed::new([2; 32]); + let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); + let identifier: Identifier = u128::MAX; + let expected = AccountId::new([ + 5, 87, 128, 244, 206, 244, 65, 130, 178, 88, 225, 183, 0, 159, 201, 201, 212, 206, 6, 156, + 13, 55, 32, 139, 91, 222, 209, 83, 172, 148, 123, 179, + ]); + assert_eq!( + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier), + expected + ); +} + +/// Two groups with different viewing keys at the same (program, seed) get different addresses. +#[test] +fn for_private_pda_differs_for_different_npk() { + let program_id: ProgramId = [1; 8]; + let seed = PdaSeed::new([2; 32]); + let npk_a = NullifierPublicKey([3; 32]); + let npk_b = NullifierPublicKey([4; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); + assert_ne!( + AccountId::for_private_pda(&program_id, &seed, &npk_a, &vpk, u128::MAX), + AccountId::for_private_pda(&program_id, &seed, &npk_b, &vpk, u128::MAX), + ); +} + +/// Different seeds produce different addresses, even with the same program and npk. +#[test] +fn for_private_pda_differs_for_different_seed() { + let program_id: ProgramId = [1; 8]; + let seed_a = PdaSeed::new([2; 32]); + let seed_b = PdaSeed::new([5; 32]); + let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); + assert_ne!( + AccountId::for_private_pda(&program_id, &seed_a, &npk, &vpk, u128::MAX), + AccountId::for_private_pda(&program_id, &seed_b, &npk, &vpk, u128::MAX), + ); +} + +/// Different programs produce different addresses, even with the same seed and npk. +#[test] +fn for_private_pda_differs_for_different_program_id() { + let program_id_a: ProgramId = [1; 8]; + let program_id_b: ProgramId = [9; 8]; + let seed = PdaSeed::new([2; 32]); + let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); + assert_ne!( + AccountId::for_private_pda(&program_id_a, &seed, &npk, &vpk, u128::MAX), + AccountId::for_private_pda(&program_id_b, &seed, &npk, &vpk, u128::MAX), + ); +} + +/// Different identifiers produce different addresses for the same `(program_id, seed, npk)`, +/// confirming that each `(program_id, seed, npk)` tuple controls a family of 2^128 addresses. +#[test] +fn for_private_pda_differs_for_different_identifier() { + let program_id: ProgramId = [1; 8]; + let seed = PdaSeed::new([2; 32]); + let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); + assert_ne!( + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 0), + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 1), + ); + assert_ne!( + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, 0), + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX), + ); +} + +/// A private PDA at the same (program, seed) has a different address than a public PDA, +/// because the private formula uses a different prefix and includes npk. +#[test] +fn for_private_pda_differs_from_public_pda() { + let program_id: ProgramId = [1; 8]; + let seed = PdaSeed::new([2; 32]); + let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); + let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX); + let public_id = AccountId::for_public_pda(&program_id, &seed); + assert_ne!(private_id, public_id); +} + +#[cfg(feature = "host")] +#[test] +fn private_account_kind_header_round_trips() { + let regular = PrivateAccountKind::Regular(42); + let pda = PrivateAccountKind::Pda { + program_id: [1_u32; 8], + seed: PdaSeed::new([2_u8; 32]), + identifier: u128::MAX, + }; + assert_eq!( + PrivateAccountKind::from_header_bytes(®ular.to_header_bytes()), + Some(regular) + ); + assert_eq!( + PrivateAccountKind::from_header_bytes(&pda.to_header_bytes()), + Some(pda) + ); +} + +#[cfg(feature = "host")] +#[test] +fn private_account_kind_unknown_discriminant_returns_none() { + let mut bytes = [0_u8; PrivateAccountKind::HEADER_LEN]; + bytes[0] = 0xFF; + assert_eq!(PrivateAccountKind::from_header_bytes(&bytes), None); +} + +#[test] +fn for_private_account_dispatches_correctly() { + let program_id: ProgramId = [1; 8]; + let seed = PdaSeed::new([2; 32]); + let npk = NullifierPublicKey([3; 32]); + let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]); + let identifier: Identifier = 77; + + assert_eq!( + AccountId::for_private_account(&npk, &vpk, &PrivateAccountKind::Regular(identifier)), + AccountId::for_regular_private_account(&npk, &vpk, identifier), + ); + assert_eq!( + AccountId::for_private_account( + &npk, + &vpk, + &PrivateAccountKind::Pda { + program_id, + seed, + identifier + } + ), + AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, identifier), + ); +} + +#[test] +fn compute_public_authorized_pdas_with_seeds() { + let caller: ProgramId = [1; 8]; + let seed = PdaSeed::new([2; 32]); + let result = compute_public_authorized_pdas(Some(caller), &[seed]); + let expected = AccountId::for_public_pda(&caller, &seed); + assert!(result.contains(&expected)); + assert_eq!(result.len(), 1); +} + +/// With no caller (top-level call), the result is always empty. +#[test] +fn compute_public_authorized_pdas_no_caller_returns_empty() { + let seed = PdaSeed::new([2; 32]); + let result = compute_public_authorized_pdas(None, &[seed]); + assert!(result.is_empty()); +} diff --git a/lee/state_machine/src/lib.rs b/lee/state_machine/src/lib.rs index 129821b5..f8cc034a 100644 --- a/lee/state_machine/src/lib.rs +++ b/lee/state_machine/src/lib.rs @@ -9,17 +9,16 @@ pub use lee_core::{ encryption::EphemeralPublicKey, program::ProgramId, }; +pub use privacy_preserving_circuit::{ + PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID, +}; pub use privacy_preserving_transaction::{ PrivacyPreservingTransaction, circuit::execute_and_prove, }; pub use program_deployment_transaction::ProgramDeploymentTransaction; -pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID; pub use public_transaction::PublicTransaction; pub use signature::{PrivateKey, PublicKey, Signature}; -pub use state::{ - CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, - CLOCK_PROGRAM_ACCOUNT_IDS, V03State, system_bridge_account_id, system_faucet_account_id, -}; +pub use state::V03State; pub use validated_state_diff::ValidatedStateDiff; pub mod encoding; @@ -33,6 +32,238 @@ mod signature; mod state; mod validated_state_diff; -pub mod program_methods { - include!(concat!(env!("OUT_DIR"), "/program_methods/mod.rs")); +mod privacy_preserving_circuit { + include!(concat!( + env!("OUT_DIR"), + "/lee/privacy_preserving_circuit/mod.rs" + )); +} + +#[cfg(test)] +mod test_methods { + use std::borrow::Cow; + + use crate::program::Program; + + #[must_use] + pub const fn simple_balance_transfer() -> Program { + Program::new_unchecked( + test_methods::SIMPLE_BALANCE_TRANSFER_ID, + Cow::Borrowed(test_methods::SIMPLE_BALANCE_TRANSFER_ELF), + ) + } + + #[must_use] + pub const fn nonce_changer() -> Program { + Program::new_unchecked( + test_methods::NONCE_CHANGER_ID, + Cow::Borrowed(test_methods::NONCE_CHANGER_ELF), + ) + } + + #[must_use] + pub const fn extra_output() -> Program { + Program::new_unchecked( + test_methods::EXTRA_OUTPUT_ID, + Cow::Borrowed(test_methods::EXTRA_OUTPUT_ELF), + ) + } + + #[must_use] + pub const fn missing_output() -> Program { + Program::new_unchecked( + test_methods::MISSING_OUTPUT_ID, + Cow::Borrowed(test_methods::MISSING_OUTPUT_ELF), + ) + } + + #[must_use] + pub const fn program_owner_changer() -> Program { + Program::new_unchecked( + test_methods::PROGRAM_OWNER_CHANGER_ID, + Cow::Borrowed(test_methods::PROGRAM_OWNER_CHANGER_ELF), + ) + } + + #[must_use] + pub const fn data_changer() -> Program { + Program::new_unchecked( + test_methods::DATA_CHANGER_ID, + Cow::Borrowed(test_methods::DATA_CHANGER_ELF), + ) + } + + #[must_use] + pub const fn minter() -> Program { + Program::new_unchecked( + test_methods::MINTER_ID, + Cow::Borrowed(test_methods::MINTER_ELF), + ) + } + + #[must_use] + pub const fn burner() -> Program { + Program::new_unchecked( + test_methods::BURNER_ID, + Cow::Borrowed(test_methods::BURNER_ELF), + ) + } + + #[must_use] + pub const fn auth_asserting_noop() -> Program { + Program::new_unchecked( + test_methods::AUTH_ASSERTING_NOOP_ID, + Cow::Borrowed(test_methods::AUTH_ASSERTING_NOOP_ELF), + ) + } + + #[must_use] + pub const fn private_pda_delegator() -> Program { + Program::new_unchecked( + test_methods::PRIVATE_PDA_DELEGATOR_ID, + Cow::Borrowed(test_methods::PRIVATE_PDA_DELEGATOR_ELF), + ) + } + + #[must_use] + pub const fn pda_claimer() -> Program { + Program::new_unchecked( + test_methods::PDA_CLAIMER_ID, + Cow::Borrowed(test_methods::PDA_CLAIMER_ELF), + ) + } + + #[must_use] + pub const fn two_pda_claimer() -> Program { + Program::new_unchecked( + test_methods::TWO_PDA_CLAIMER_ID, + Cow::Borrowed(test_methods::TWO_PDA_CLAIMER_ELF), + ) + } + + #[must_use] + pub const fn noop() -> Program { + Program::new_unchecked(test_methods::NOOP_ID, Cow::Borrowed(test_methods::NOOP_ELF)) + } + + #[must_use] + pub const fn chain_caller() -> Program { + Program::new_unchecked( + test_methods::CHAIN_CALLER_ID, + Cow::Borrowed(test_methods::CHAIN_CALLER_ELF), + ) + } + + #[must_use] + pub const fn modified_transfer_program() -> Program { + Program::new_unchecked( + test_methods::MODIFIED_TRANSFER_ID, + Cow::Borrowed(test_methods::MODIFIED_TRANSFER_ELF), + ) + } + + #[must_use] + pub const fn malicious_authorization_changer() -> Program { + Program::new_unchecked( + test_methods::MALICIOUS_AUTHORIZATION_CHANGER_ID, + Cow::Borrowed(test_methods::MALICIOUS_AUTHORIZATION_CHANGER_ELF), + ) + } + + #[must_use] + pub const fn validity_window() -> Program { + Program::new_unchecked( + test_methods::VALIDITY_WINDOW_ID, + Cow::Borrowed(test_methods::VALIDITY_WINDOW_ELF), + ) + } + + #[must_use] + pub const fn flash_swap_initiator() -> Program { + Program::new_unchecked( + test_methods::FLASH_SWAP_INITIATOR_ID, + Cow::Borrowed(test_methods::FLASH_SWAP_INITIATOR_ELF), + ) + } + + #[must_use] + pub const fn flash_swap_callback() -> Program { + Program::new_unchecked( + test_methods::FLASH_SWAP_CALLBACK_ID, + Cow::Borrowed(test_methods::FLASH_SWAP_CALLBACK_ELF), + ) + } + + #[must_use] + pub const fn malicious_self_program_id() -> Program { + Program::new_unchecked( + test_methods::MALICIOUS_SELF_PROGRAM_ID_ID, + Cow::Borrowed(test_methods::MALICIOUS_SELF_PROGRAM_ID_ELF), + ) + } + + #[must_use] + pub const fn malicious_caller_program_id() -> Program { + Program::new_unchecked( + test_methods::MALICIOUS_CALLER_PROGRAM_ID_ID, + Cow::Borrowed(test_methods::MALICIOUS_CALLER_PROGRAM_ID_ELF), + ) + } + + #[must_use] + pub const fn pda_spend_proxy() -> Program { + Program::new_unchecked( + test_methods::PDA_SPEND_PROXY_ID, + Cow::Borrowed(test_methods::PDA_SPEND_PROXY_ELF), + ) + } + + #[must_use] + pub const fn claimer() -> Program { + Program::new_unchecked( + test_methods::CLAIMER_ID, + Cow::Borrowed(test_methods::CLAIMER_ELF), + ) + } + + #[must_use] + pub const fn changer_claimer() -> Program { + Program::new_unchecked( + test_methods::CHANGER_CLAIMER_ID, + Cow::Borrowed(test_methods::CHANGER_CLAIMER_ELF), + ) + } + + #[must_use] + pub const fn validity_window_chain_caller() -> Program { + Program::new_unchecked( + test_methods::VALIDITY_WINDOW_CHAIN_CALLER_ID, + Cow::Borrowed(test_methods::VALIDITY_WINDOW_CHAIN_CALLER_ELF), + ) + } + + #[must_use] + #[inline] + pub const fn simple_transfer_proxy() -> Program { + Program::new_unchecked( + test_methods::SIMPLE_TRANSFER_PROXY_ID, + Cow::Borrowed(test_methods::SIMPLE_TRANSFER_PROXY_ELF), + ) + } + + #[must_use] + pub const fn malicious_injector() -> Program { + Program::new_unchecked( + test_methods::MALICIOUS_INJECTOR_ID, + Cow::Borrowed(test_methods::MALICIOUS_INJECTOR_ELF), + ) + } + + #[must_use] + pub const fn malicious_launderer() -> Program { + Program::new_unchecked( + test_methods::MALICIOUS_LAUNDERER_ID, + Cow::Borrowed(test_methods::MALICIOUS_LAUNDERER_ELF), + ) + } } diff --git a/lee/state_machine/src/merkle_tree/mod.rs b/lee/state_machine/src/merkle_tree/mod.rs index e439d092..ee8106c5 100644 --- a/lee/state_machine/src/merkle_tree/mod.rs +++ b/lee/state_machine/src/merkle_tree/mod.rs @@ -164,398 +164,4 @@ const fn prev_power_of_two(x: usize) -> usize { } #[cfg(test)] -mod tests { - use hex_literal::hex; - - use super::*; - - impl MerkleTree { - pub fn new(values: &[Value]) -> Self { - let mut this = Self::with_capacity(values.len()); - for value in values.iter().copied() { - this.insert(value); - } - this - } - } - - #[test] - fn empty_merkle_tree() { - let tree = MerkleTree::with_capacity(4); - let expected_root = - hex!("0000000000000000000000000000000000000000000000000000000000000000"); - assert_eq!(tree.root(), expected_root); - assert_eq!(tree.capacity, 4); - assert_eq!(tree.length, 0); - } - - #[test] - fn merkle_tree_0() { - let values = [[0; 32]]; - let tree = MerkleTree::new(&values); - assert_eq!(tree.root(), hash_value(&[0; 32])); - assert_eq!(tree.capacity, 1); - assert_eq!(tree.length, 1); - } - - #[test] - fn merkle_tree_1() { - let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; - let tree = MerkleTree::new(&values); - let expected_root = - hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"); - assert_eq!(tree.root(), expected_root); - assert_eq!(tree.capacity, 4); - assert_eq!(tree.length, 4); - } - - #[test] - fn merkle_tree_2() { - let values = [[1; 32], [2; 32], [3; 32], [0; 32]]; - let tree = MerkleTree::new(&values); - let expected_root = - hex!("c9bbb83096df85157a146e7d770455a98412dee0633187ee86fee6c8a45b831a"); - assert_eq!(tree.root(), expected_root); - assert_eq!(tree.capacity, 4); - assert_eq!(tree.length, 4); - } - - #[test] - fn merkle_tree_3() { - let values = [[1; 32], [2; 32], [3; 32]]; - let tree = MerkleTree::new(&values); - let expected_root = - hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); - assert_eq!(tree.root(), expected_root); - assert_eq!(tree.capacity, 4); - assert_eq!(tree.length, 3); - } - - #[test] - fn merkle_tree_4() { - let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]]; - let tree = MerkleTree::new(&values); - let expected_root = - hex!("ef418aed5aa20702d4d94c92da79a4012f2e36f1008bfdb3cd1e38749dca2499"); - - assert_eq!(tree.root(), expected_root); - assert_eq!(tree.capacity, 8); - assert_eq!(tree.length, 5); - } - - #[test] - fn merkle_tree_5() { - let values = [ - [11; 32], [12; 32], [12; 32], [13; 32], [14; 32], [15; 32], [15; 32], [13; 32], - [13; 32], [15; 32], [11; 32], - ]; - let tree = MerkleTree::new(&values); - let expected_root = - hex!("3f72d2ff55921a86c48e5988ec3e19ee9d0d5aa3e23197842970a903508ed767"); - assert_eq!(tree.root(), expected_root); - assert_eq!(tree.capacity, 16); - assert_eq!(tree.length, 11); - } - - #[test] - fn merkle_tree_6() { - let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; - let tree = MerkleTree::new(&values); - let expected_root = - hex!("069cb8259a06fe6edb3fa7ff7933a6dd7dca6fca299314379794a688926c3792"); - assert_eq!(tree.root(), expected_root); - } - - #[test] - fn with_capacity_4() { - let tree = MerkleTree::with_capacity(4); - - assert_eq!(tree.length, 0); - assert_eq!(tree.nodes.len(), 7); - for i in 3..7 { - assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0], "{i}"); - } - for i in 1..3 { - assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1], "{i}"); - } - assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[2]); - } - - #[test] - fn with_capacity_5() { - let tree = MerkleTree::with_capacity(5); - - assert_eq!(tree.length, 0); - assert_eq!(tree.nodes.len(), 15); - for i in 7..15 { - assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0]); - } - for i in 3..7 { - assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1]); - } - for i in 1..3 { - assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[2]); - } - assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[3]); - } - - #[test] - fn with_capacity_6() { - let mut tree = MerkleTree::with_capacity(100); - - let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; - - let expected_root = - hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"); - - assert_eq!(0, tree.insert(values[0])); - assert_eq!(1, tree.insert(values[1])); - assert_eq!(2, tree.insert(values[2])); - assert_eq!(3, tree.insert(values[3])); - - assert_eq!(tree.root(), expected_root); - } - - #[test] - fn with_capacity_7() { - let mut tree = MerkleTree::with_capacity(599); - - let values = [[1; 32], [2; 32], [3; 32]]; - - let expected_root = - hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); - - assert_eq!(0, tree.insert(values[0])); - assert_eq!(1, tree.insert(values[1])); - assert_eq!(2, tree.insert(values[2])); - - assert_eq!(tree.root(), expected_root); - } - - #[test] - fn with_capacity_8() { - let mut tree = MerkleTree::with_capacity(1); - - let values = [[1; 32], [2; 32], [3; 32]]; - - let expected_root = - hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); - - assert_eq!(0, tree.insert(values[0])); - assert_eq!(1, tree.insert(values[1])); - assert_eq!(2, tree.insert(values[2])); - - assert_eq!(tree.root(), expected_root); - } - - #[test] - fn insert_value_1() { - let mut tree = MerkleTree::with_capacity(1); - - let values = [[1; 32], [2; 32], [3; 32]]; - let expected_tree = MerkleTree::new(&values); - - assert_eq!(0, tree.insert(values[0])); - assert_eq!(1, tree.insert(values[1])); - assert_eq!(2, tree.insert(values[2])); - - assert_eq!(expected_tree, tree); - } - - #[test] - fn insert_value_2() { - let mut tree = MerkleTree::with_capacity(1); - - let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; - let expected_tree = MerkleTree::new(&values); - - assert_eq!(0, tree.insert(values[0])); - assert_eq!(1, tree.insert(values[1])); - assert_eq!(2, tree.insert(values[2])); - assert_eq!(3, tree.insert(values[3])); - - assert_eq!(expected_tree, tree); - } - - #[test] - fn insert_value_3() { - let mut tree = MerkleTree::with_capacity(1); - - let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]]; - let expected_tree = MerkleTree::new(&values); - - tree.insert(values[0]); - tree.insert(values[1]); - tree.insert(values[2]); - tree.insert(values[3]); - tree.insert(values[4]); - - assert_eq!(expected_tree, tree); - } - - // Reference implementation - fn verify_authentication_path(value: &Value, index: usize, path: &[Node], root: &Node) -> bool { - let mut result = hash_value(value); - let mut level_index = index; - for node in path { - let is_left_child = level_index & 1 == 0; - if is_left_child { - result = hash_two(&result, node); - } else { - result = hash_two(node, &result); - } - level_index >>= 1; - } - &result == root - } - - #[test] - fn authentication_path_1() { - let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; - let tree = MerkleTree::new(&values); - let expected_authentication_path = vec![ - hex!("9f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed"), - hex!("50a27d4746f357cb700cbe9d4883b77fb64f0128828a3489dc6a6f21ddbf2414"), - ]; - - let authentication_path = tree.get_authentication_path_for(2).unwrap(); - assert_eq!(authentication_path, expected_authentication_path); - } - - #[test] - fn authentication_path_2() { - let values = [[1; 32], [2; 32], [3; 32]]; - let tree = MerkleTree::new(&values); - let expected_authentication_path = vec![ - hex!("75877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a"), - hex!("a41b855d2db4de9052cd7be5ec67d6586629cb9f6e3246a4afa5ba313f07a9c5"), - ]; - - let authentication_path = tree.get_authentication_path_for(0).unwrap(); - assert_eq!(authentication_path, expected_authentication_path); - } - - #[test] - fn authentication_path_3() { - let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; - let tree = MerkleTree::new(&values); - let expected_authentication_path = vec![ - hex!("0000000000000000000000000000000000000000000000000000000000000000"), - hex!("f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"), - hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"), - ]; - - let authentication_path = tree.get_authentication_path_for(4).unwrap(); - assert_eq!(authentication_path, expected_authentication_path); - } - - #[test] - fn authentication_path_4() { - let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; - let tree = MerkleTree::new(&values); - assert!(tree.get_authentication_path_for(5).is_none()); - } - - #[test] - fn authentication_path_5() { - let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; - let tree = MerkleTree::new(&values); - let index = 4; - let value = values[index]; - let path = tree.get_authentication_path_for(index).unwrap(); - assert!(verify_authentication_path( - &value, - index, - &path, - &tree.root() - )); - } - - #[test] - fn tree_with_63_insertions() { - let values = [ - hex!("cd00acab0f45736e6c6311f1953becc0b69a062e7c2a7310875d28bdf9ef9c5b"), - hex!("0df5a6afbcc7bf126caf7084acfc593593ab512e6ca433c61c1a922be40a04ea"), - hex!("23c1258620266c7bedb6d1ee32f6da9413e4010ace975239dccb34e727e07c40"), - hex!("f33ccc3a11476b0ef62326ca5ec292056759b05e6a28023d2d1ce66165611353"), - hex!("77f914ab016b8049f6bea7704000e413a393865918a3824f9285c3db0aacff23"), - hex!("910a1c23188e54d57fd167ddb0f8bf68c6b70ed9ec76ef56c4b7f2632f82ca7f"), - hex!("047ee85526197d1e7403a559cf6d2f22c1926c8ad59481a2e2f1b697af45e40b"), - hex!("9d355cf89fb382ae34bf80566b28489278d10f2cebb5b0ea42fab1bac5adae0c"), - hex!("604018b95232596b2685a9bc737b6cccb53b10e483d2d9a2f4a755410b02a188"), - hex!("a16708ef7b6bf1796063addaf57d6a566b6f87b0bbe42af43a4590d05f1684cb"), - hex!("820f2dfa271cd2fd41e1452406d5dad552c85c1223c45d45dbd7446759fdc6b8"), - hex!("680b6912d7e219f8805d4d28adb4428dd78fea0dc1b8cdb2412645c4b1962c88"), - hex!("14d5471ce6c45506753982b17cac5790ac7bc29e6f388f31052d7dfd62b294e5"), - hex!("8b364200172b777d4aa16d2098b5eb98ac3dd4a1b9597e5c2bf6f6930031f230"), - hex!("9bb45b910711874339dda8a21a9aad73822286f5e52d7d3de0ed78dfbba329a5"), - hex!("d6806d5df5cb25ce5d531042f09b3cb34fb9e47c61182b63cccd9d44392f6027"), - hex!("b8cfa90ebc8fd09c04682d93a08fddd3e8e57715174dcc92451edd191264a58b"), - hex!("3463c7f81d00f809b3dfa83195447c927fb4045b3913dac6f45bee6c4010d7ed"), - hex!("1d6ad7f7d677905feb506c58f4b404a79370ebc567296abea3a368b61d5a8239"), - hex!("a58085ecf00963cb22da23c901b9b3ddc56462bb96ff03c923d67708e10dd29c"), - hex!("c3319f4a65fb5bbb8447137b0972c03cbd84ebf7d9da194e0fcbd68c2d4d5bdb"), - hex!("4aa31e90e0090faf3648d05e5d5499df2c78ebed4d6e6c23d8147de5d67dae73"), - hex!("9f33b1d2c8bc7bd265336de1033ede6344bc41260313bdcb43f1108b83b9be92"), - hex!("6500d4ad93d41c16ec81eaa5e70f173194aabe5c1072ac263b5727296f5b7cac"), - hex!("3584f5d260003669fad98786e13171376b0f19410cb232ce65606cbff79e6768"), - hex!("c8410946ebf56f13141c894a34ced85a5230088af70dcea581e44f52847830ac"), - hex!("71dd90281cdebb70422f2d04ae446d5d2d5ea64b803c16128d37e3fcd5d1a4cc"), - hex!("c05acf8d77ab4d659a538bd35af590864a7ad9c055ff5d6cda9d5aecfccecba3"), - hex!("f1df98822ea084cce9021aa9cc81b1746cd1e84a75690da63e10fd877633ed77"), - hex!("2ca822bc8f67bceb0a71a0d06fea7349036ef3e5ec21795a851e4182bd35ce01"), - hex!("7fd2179abc3bcf89b4d8092988ba8c23952b3bbd3d7caea6b5ea0c13cf19f68b"), - hex!("91b6ad516e017f6aa5a2e95776538bd3a3e933c1b1d32bb5e0f00a9db63c9c24"), - hex!("cd31a8b5eef5ca0be5ef1cb261d0bf0a74d774a3152bb99739cfd296a1d0b85e"), - hex!("3fb16f48b2bf93f3815979e6638f975d7f935088ec37db0be0f07965fbc78339"), - hex!("c60c61b99bf486af5f4bf780a69860dafcd35c1474306a8575666fb5449bcec0"), - hex!("8048d0d7e14091251f3f6c6b10bf6b5880a014b513f9f8c2395501dbffa6192a"), - hex!("778b5af10b9dbe80b60a8e4f0bb91caf4476bcb812801099760754ae623fbd84"), - hex!("d3ac25467920a4e08998b7a3226b8b54bfe66ac58cfedc71f15b2402fee0054a"), - hex!("029aa94598fae2961a0d43937b8a9a3138bcfeae99a7cb15f77fac7c506f8432"), - hex!("2eee5ef52fe669cb6882a68c893abdc1262dcf4424e4ba7a479da7cf1c10171d"), - hex!("de3fb3d070e3a90f0eed8b5e65088a8dc0e4e3c342b9c0bf33bab714eae5dfec"), - hex!("14d40177e833ab45bbfdc5f2b11fba7efaebb3f69facc554f24b549a2efe8538"), - hex!("5734355069702448774fb2df95f1d562e1b9fe1514aeb6b922554ee9d2d01068"), - hex!("8a273d49ac110343cec2cf3359d16eb2906b446bd9ec9833e2a640cebc8d5155"), - hex!("e3fa984dd3cbeb9a7e827ed32d3d4e6a6ba643a55d82be97d9ddb06ee809fa3e"), - hex!("90b1d5a364e17c8b7965396b06ec6e13749b5fc16500731518ad8fc30ae33e77"), - hex!("7517376541b2e8ec83cbab04522b54a26610908a9872feb663451385aea58eb1"), - hex!("5cba2e4cf7448e526d161133c4b2ea7c919ac4813a7308612595f46f11dea6cd"), - hex!("c721911b300bec0691c8a2dfaabfef1d66b7b6258918914d3c3ad690729f05b7"), - hex!("d0d0a70d8ae0d27806fa0b711c507290c260a89cbca0436d339d1dccdd087d62"), - hex!("2a625c28ea763c5e82dd0a93ecfca7ec371ccbb363cd42be359c2c875f58009d"), - hex!("174ef0119932ed890397d9f3837dd85f9100558b6fc9085d4af947ae8cf74bbc"), - hex!("b497bc267151e8efa3c6daa461e6804b01a3f05f44f1f4d5b41d5f0d3f5219b1"), - hex!("e987e91f5734630ddd7e6b58733b4fcdbc316ee9e8cac0e94c36c91cf58e59cc"), - hex!("55019ad8bbe656c51eb042190c1c8da53f42baf43fd2350ebea38fc7cca2fae3"), - hex!("c45a638edd18a6d9f5ad20b870c81b8626459bcb22dae7d58add7a6b6c6a84a8"), - hex!("d42d3a5fb2ad50b2027fe5a36d59dd71e49a63e4b1b299073c96bbf7ba5d68a1"), - hex!("9599e561054bcd3f647eb018ab0b069d3176497d42be9c4466551cbb959be47c"), - hex!("42f33b23775327ff71aea6569548255f3cc9929da73373cc9bb1743d417f7cda"), - hex!("ab24294f44fc6fdbeb96e0f6e93c4f6d97d035b73b9a337c353e18c6d0603bdd"), - hex!("33954ec63520334f99b640a2982ac966b68c363fed383d621a1ab573934f1d33"), - hex!("5e2a1f7df963d1fd8f50a285387cfbb5df581426619b325563e20bf7886c62b7"), - hex!("13ffde471d4e27c473254e766fd1328ad80c42cab4d4955cffeae43d866f86e5"), - ]; - - let expected_root = - hex!("1cf9b214217d7823f9de51b8f6cb34d0a99436a3a1bb762f90b815672a6afcc0"); - - let mut tree_less_capacity = MerkleTree::with_capacity(1); - let mut tree_exact_capacity = MerkleTree::with_capacity(64); - let mut tree_more_capacity = MerkleTree::with_capacity(128); - - for value in &values { - tree_less_capacity.insert(*value); - tree_exact_capacity.insert(*value); - tree_more_capacity.insert(*value); - } - - assert_eq!(tree_more_capacity.root(), expected_root); - assert_eq!(tree_less_capacity.root(), expected_root); - assert_eq!(tree_exact_capacity.root(), expected_root); - } -} - -// +mod tests; diff --git a/lee/state_machine/src/merkle_tree/tests.rs b/lee/state_machine/src/merkle_tree/tests.rs new file mode 100644 index 00000000..756fd45f --- /dev/null +++ b/lee/state_machine/src/merkle_tree/tests.rs @@ -0,0 +1,380 @@ +use hex_literal::hex; + +use super::*; + +impl MerkleTree { + pub fn new(values: &[Value]) -> Self { + let mut this = Self::with_capacity(values.len()); + for value in values.iter().copied() { + this.insert(value); + } + this + } +} + +#[test] +fn empty_merkle_tree() { + let tree = MerkleTree::with_capacity(4); + let expected_root = hex!("0000000000000000000000000000000000000000000000000000000000000000"); + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 4); + assert_eq!(tree.length, 0); +} + +#[test] +fn merkle_tree_0() { + let values = [[0; 32]]; + let tree = MerkleTree::new(&values); + assert_eq!(tree.root(), hash_value(&[0; 32])); + assert_eq!(tree.capacity, 1); + assert_eq!(tree.length, 1); +} + +#[test] +fn merkle_tree_1() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; + let tree = MerkleTree::new(&values); + let expected_root = hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"); + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 4); + assert_eq!(tree.length, 4); +} + +#[test] +fn merkle_tree_2() { + let values = [[1; 32], [2; 32], [3; 32], [0; 32]]; + let tree = MerkleTree::new(&values); + let expected_root = hex!("c9bbb83096df85157a146e7d770455a98412dee0633187ee86fee6c8a45b831a"); + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 4); + assert_eq!(tree.length, 4); +} + +#[test] +fn merkle_tree_3() { + let values = [[1; 32], [2; 32], [3; 32]]; + let tree = MerkleTree::new(&values); + let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 4); + assert_eq!(tree.length, 3); +} + +#[test] +fn merkle_tree_4() { + let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]]; + let tree = MerkleTree::new(&values); + let expected_root = hex!("ef418aed5aa20702d4d94c92da79a4012f2e36f1008bfdb3cd1e38749dca2499"); + + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 8); + assert_eq!(tree.length, 5); +} + +#[test] +fn merkle_tree_5() { + let values = [ + [11; 32], [12; 32], [12; 32], [13; 32], [14; 32], [15; 32], [15; 32], [13; 32], [13; 32], + [15; 32], [11; 32], + ]; + let tree = MerkleTree::new(&values); + let expected_root = hex!("3f72d2ff55921a86c48e5988ec3e19ee9d0d5aa3e23197842970a903508ed767"); + assert_eq!(tree.root(), expected_root); + assert_eq!(tree.capacity, 16); + assert_eq!(tree.length, 11); +} + +#[test] +fn merkle_tree_6() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; + let tree = MerkleTree::new(&values); + let expected_root = hex!("069cb8259a06fe6edb3fa7ff7933a6dd7dca6fca299314379794a688926c3792"); + assert_eq!(tree.root(), expected_root); +} + +#[test] +fn with_capacity_4() { + let tree = MerkleTree::with_capacity(4); + + assert_eq!(tree.length, 0); + assert_eq!(tree.nodes.len(), 7); + for i in 3..7 { + assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0], "{i}"); + } + for i in 1..3 { + assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1], "{i}"); + } + assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[2]); +} + +#[test] +fn with_capacity_5() { + let tree = MerkleTree::with_capacity(5); + + assert_eq!(tree.length, 0); + assert_eq!(tree.nodes.len(), 15); + for i in 7..15 { + assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0]); + } + for i in 3..7 { + assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1]); + } + for i in 1..3 { + assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[2]); + } + assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[3]); +} + +#[test] +fn with_capacity_6() { + let mut tree = MerkleTree::with_capacity(100); + + let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; + + let expected_root = hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"); + + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + assert_eq!(3, tree.insert(values[3])); + + assert_eq!(tree.root(), expected_root); +} + +#[test] +fn with_capacity_7() { + let mut tree = MerkleTree::with_capacity(599); + + let values = [[1; 32], [2; 32], [3; 32]]; + + let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); + + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + + assert_eq!(tree.root(), expected_root); +} + +#[test] +fn with_capacity_8() { + let mut tree = MerkleTree::with_capacity(1); + + let values = [[1; 32], [2; 32], [3; 32]]; + + let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568"); + + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + + assert_eq!(tree.root(), expected_root); +} + +#[test] +fn insert_value_1() { + let mut tree = MerkleTree::with_capacity(1); + + let values = [[1; 32], [2; 32], [3; 32]]; + let expected_tree = MerkleTree::new(&values); + + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + + assert_eq!(expected_tree, tree); +} + +#[test] +fn insert_value_2() { + let mut tree = MerkleTree::with_capacity(1); + + let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; + let expected_tree = MerkleTree::new(&values); + + assert_eq!(0, tree.insert(values[0])); + assert_eq!(1, tree.insert(values[1])); + assert_eq!(2, tree.insert(values[2])); + assert_eq!(3, tree.insert(values[3])); + + assert_eq!(expected_tree, tree); +} + +#[test] +fn insert_value_3() { + let mut tree = MerkleTree::with_capacity(1); + + let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]]; + let expected_tree = MerkleTree::new(&values); + + tree.insert(values[0]); + tree.insert(values[1]); + tree.insert(values[2]); + tree.insert(values[3]); + tree.insert(values[4]); + + assert_eq!(expected_tree, tree); +} + +// Reference implementation +fn verify_authentication_path(value: &Value, index: usize, path: &[Node], root: &Node) -> bool { + let mut result = hash_value(value); + let mut level_index = index; + for node in path { + let is_left_child = level_index & 1 == 0; + if is_left_child { + result = hash_two(&result, node); + } else { + result = hash_two(node, &result); + } + level_index >>= 1; + } + &result == root +} + +#[test] +fn authentication_path_1() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32]]; + let tree = MerkleTree::new(&values); + let expected_authentication_path = vec![ + hex!("9f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed"), + hex!("50a27d4746f357cb700cbe9d4883b77fb64f0128828a3489dc6a6f21ddbf2414"), + ]; + + let authentication_path = tree.get_authentication_path_for(2).unwrap(); + assert_eq!(authentication_path, expected_authentication_path); +} + +#[test] +fn authentication_path_2() { + let values = [[1; 32], [2; 32], [3; 32]]; + let tree = MerkleTree::new(&values); + let expected_authentication_path = vec![ + hex!("75877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a"), + hex!("a41b855d2db4de9052cd7be5ec67d6586629cb9f6e3246a4afa5ba313f07a9c5"), + ]; + + let authentication_path = tree.get_authentication_path_for(0).unwrap(); + assert_eq!(authentication_path, expected_authentication_path); +} + +#[test] +fn authentication_path_3() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; + let tree = MerkleTree::new(&values); + let expected_authentication_path = vec![ + hex!("0000000000000000000000000000000000000000000000000000000000000000"), + hex!("f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"), + hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"), + ]; + + let authentication_path = tree.get_authentication_path_for(4).unwrap(); + assert_eq!(authentication_path, expected_authentication_path); +} + +#[test] +fn authentication_path_4() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; + let tree = MerkleTree::new(&values); + assert!(tree.get_authentication_path_for(5).is_none()); +} + +#[test] +fn authentication_path_5() { + let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]]; + let tree = MerkleTree::new(&values); + let index = 4; + let value = values[index]; + let path = tree.get_authentication_path_for(index).unwrap(); + assert!(verify_authentication_path( + &value, + index, + &path, + &tree.root() + )); +} + +#[test] +fn tree_with_63_insertions() { + let values = [ + hex!("cd00acab0f45736e6c6311f1953becc0b69a062e7c2a7310875d28bdf9ef9c5b"), + hex!("0df5a6afbcc7bf126caf7084acfc593593ab512e6ca433c61c1a922be40a04ea"), + hex!("23c1258620266c7bedb6d1ee32f6da9413e4010ace975239dccb34e727e07c40"), + hex!("f33ccc3a11476b0ef62326ca5ec292056759b05e6a28023d2d1ce66165611353"), + hex!("77f914ab016b8049f6bea7704000e413a393865918a3824f9285c3db0aacff23"), + hex!("910a1c23188e54d57fd167ddb0f8bf68c6b70ed9ec76ef56c4b7f2632f82ca7f"), + hex!("047ee85526197d1e7403a559cf6d2f22c1926c8ad59481a2e2f1b697af45e40b"), + hex!("9d355cf89fb382ae34bf80566b28489278d10f2cebb5b0ea42fab1bac5adae0c"), + hex!("604018b95232596b2685a9bc737b6cccb53b10e483d2d9a2f4a755410b02a188"), + hex!("a16708ef7b6bf1796063addaf57d6a566b6f87b0bbe42af43a4590d05f1684cb"), + hex!("820f2dfa271cd2fd41e1452406d5dad552c85c1223c45d45dbd7446759fdc6b8"), + hex!("680b6912d7e219f8805d4d28adb4428dd78fea0dc1b8cdb2412645c4b1962c88"), + hex!("14d5471ce6c45506753982b17cac5790ac7bc29e6f388f31052d7dfd62b294e5"), + hex!("8b364200172b777d4aa16d2098b5eb98ac3dd4a1b9597e5c2bf6f6930031f230"), + hex!("9bb45b910711874339dda8a21a9aad73822286f5e52d7d3de0ed78dfbba329a5"), + hex!("d6806d5df5cb25ce5d531042f09b3cb34fb9e47c61182b63cccd9d44392f6027"), + hex!("b8cfa90ebc8fd09c04682d93a08fddd3e8e57715174dcc92451edd191264a58b"), + hex!("3463c7f81d00f809b3dfa83195447c927fb4045b3913dac6f45bee6c4010d7ed"), + hex!("1d6ad7f7d677905feb506c58f4b404a79370ebc567296abea3a368b61d5a8239"), + hex!("a58085ecf00963cb22da23c901b9b3ddc56462bb96ff03c923d67708e10dd29c"), + hex!("c3319f4a65fb5bbb8447137b0972c03cbd84ebf7d9da194e0fcbd68c2d4d5bdb"), + hex!("4aa31e90e0090faf3648d05e5d5499df2c78ebed4d6e6c23d8147de5d67dae73"), + hex!("9f33b1d2c8bc7bd265336de1033ede6344bc41260313bdcb43f1108b83b9be92"), + hex!("6500d4ad93d41c16ec81eaa5e70f173194aabe5c1072ac263b5727296f5b7cac"), + hex!("3584f5d260003669fad98786e13171376b0f19410cb232ce65606cbff79e6768"), + hex!("c8410946ebf56f13141c894a34ced85a5230088af70dcea581e44f52847830ac"), + hex!("71dd90281cdebb70422f2d04ae446d5d2d5ea64b803c16128d37e3fcd5d1a4cc"), + hex!("c05acf8d77ab4d659a538bd35af590864a7ad9c055ff5d6cda9d5aecfccecba3"), + hex!("f1df98822ea084cce9021aa9cc81b1746cd1e84a75690da63e10fd877633ed77"), + hex!("2ca822bc8f67bceb0a71a0d06fea7349036ef3e5ec21795a851e4182bd35ce01"), + hex!("7fd2179abc3bcf89b4d8092988ba8c23952b3bbd3d7caea6b5ea0c13cf19f68b"), + hex!("91b6ad516e017f6aa5a2e95776538bd3a3e933c1b1d32bb5e0f00a9db63c9c24"), + hex!("cd31a8b5eef5ca0be5ef1cb261d0bf0a74d774a3152bb99739cfd296a1d0b85e"), + hex!("3fb16f48b2bf93f3815979e6638f975d7f935088ec37db0be0f07965fbc78339"), + hex!("c60c61b99bf486af5f4bf780a69860dafcd35c1474306a8575666fb5449bcec0"), + hex!("8048d0d7e14091251f3f6c6b10bf6b5880a014b513f9f8c2395501dbffa6192a"), + hex!("778b5af10b9dbe80b60a8e4f0bb91caf4476bcb812801099760754ae623fbd84"), + hex!("d3ac25467920a4e08998b7a3226b8b54bfe66ac58cfedc71f15b2402fee0054a"), + hex!("029aa94598fae2961a0d43937b8a9a3138bcfeae99a7cb15f77fac7c506f8432"), + hex!("2eee5ef52fe669cb6882a68c893abdc1262dcf4424e4ba7a479da7cf1c10171d"), + hex!("de3fb3d070e3a90f0eed8b5e65088a8dc0e4e3c342b9c0bf33bab714eae5dfec"), + hex!("14d40177e833ab45bbfdc5f2b11fba7efaebb3f69facc554f24b549a2efe8538"), + hex!("5734355069702448774fb2df95f1d562e1b9fe1514aeb6b922554ee9d2d01068"), + hex!("8a273d49ac110343cec2cf3359d16eb2906b446bd9ec9833e2a640cebc8d5155"), + hex!("e3fa984dd3cbeb9a7e827ed32d3d4e6a6ba643a55d82be97d9ddb06ee809fa3e"), + hex!("90b1d5a364e17c8b7965396b06ec6e13749b5fc16500731518ad8fc30ae33e77"), + hex!("7517376541b2e8ec83cbab04522b54a26610908a9872feb663451385aea58eb1"), + hex!("5cba2e4cf7448e526d161133c4b2ea7c919ac4813a7308612595f46f11dea6cd"), + hex!("c721911b300bec0691c8a2dfaabfef1d66b7b6258918914d3c3ad690729f05b7"), + hex!("d0d0a70d8ae0d27806fa0b711c507290c260a89cbca0436d339d1dccdd087d62"), + hex!("2a625c28ea763c5e82dd0a93ecfca7ec371ccbb363cd42be359c2c875f58009d"), + hex!("174ef0119932ed890397d9f3837dd85f9100558b6fc9085d4af947ae8cf74bbc"), + hex!("b497bc267151e8efa3c6daa461e6804b01a3f05f44f1f4d5b41d5f0d3f5219b1"), + hex!("e987e91f5734630ddd7e6b58733b4fcdbc316ee9e8cac0e94c36c91cf58e59cc"), + hex!("55019ad8bbe656c51eb042190c1c8da53f42baf43fd2350ebea38fc7cca2fae3"), + hex!("c45a638edd18a6d9f5ad20b870c81b8626459bcb22dae7d58add7a6b6c6a84a8"), + hex!("d42d3a5fb2ad50b2027fe5a36d59dd71e49a63e4b1b299073c96bbf7ba5d68a1"), + hex!("9599e561054bcd3f647eb018ab0b069d3176497d42be9c4466551cbb959be47c"), + hex!("42f33b23775327ff71aea6569548255f3cc9929da73373cc9bb1743d417f7cda"), + hex!("ab24294f44fc6fdbeb96e0f6e93c4f6d97d035b73b9a337c353e18c6d0603bdd"), + hex!("33954ec63520334f99b640a2982ac966b68c363fed383d621a1ab573934f1d33"), + hex!("5e2a1f7df963d1fd8f50a285387cfbb5df581426619b325563e20bf7886c62b7"), + hex!("13ffde471d4e27c473254e766fd1328ad80c42cab4d4955cffeae43d866f86e5"), + ]; + + let expected_root = hex!("1cf9b214217d7823f9de51b8f6cb34d0a99436a3a1bb762f90b815672a6afcc0"); + + let mut tree_less_capacity = MerkleTree::with_capacity(1); + let mut tree_exact_capacity = MerkleTree::with_capacity(64); + let mut tree_more_capacity = MerkleTree::with_capacity(128); + + for value in &values { + tree_less_capacity.insert(*value); + tree_exact_capacity.insert(*value); + tree_more_capacity.insert(*value); + } + + assert_eq!(tree_more_capacity.root(), expected_root); + assert_eq!(tree_less_capacity.root(), expected_root); + assert_eq!(tree_exact_capacity.root(), expected_root); +} diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs deleted file mode 100644 index 4f2597c4..00000000 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs +++ /dev/null @@ -1,996 +0,0 @@ -use std::collections::{HashMap, VecDeque}; - -use borsh::{BorshDeserialize, BorshSerialize}; -use lee_core::{ - InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, - account::AccountWithMetadata, - program::{ChainedCall, InstructionData, ProgramId, ProgramOutput}, -}; -use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover}; - -use crate::{ - error::{InvalidProgramBehaviorError, LeeError}, - program::Program, - program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}, - state::MAX_NUMBER_CHAINED_CALLS, -}; - -/// Proof of the privacy preserving execution circuit. -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -pub struct Proof(pub(crate) Vec); - -impl Proof { - #[must_use] - pub fn into_inner(self) -> Vec { - self.0 - } - - #[must_use] - pub const fn from_inner(inner: Vec) -> Self { - Self(inner) - } - - pub(crate) fn is_valid_for(&self, circuit_output: &PrivacyPreservingCircuitOutput) -> bool { - let Ok(inner) = borsh::from_slice::(&self.0) else { - return false; - }; - let receipt = Receipt::new(inner, circuit_output.to_bytes()); - receipt.verify(PRIVACY_PRESERVING_CIRCUIT_ID).is_ok() - } -} - -#[derive(Clone)] -pub struct ProgramWithDependencies { - pub program: Program, - // TODO: avoid having a copy of the bytecode of each dependency. - pub dependencies: HashMap, -} - -impl ProgramWithDependencies { - #[must_use] - pub const fn new(program: Program, dependencies: HashMap) -> Self { - Self { - program, - dependencies, - } - } -} - -impl From for ProgramWithDependencies { - fn from(program: Program) -> Self { - Self::new(program, HashMap::new()) - } -} - -/// Generates a proof of the execution of a LEE program inside the privacy preserving execution -/// circuit. -pub fn execute_and_prove( - pre_states: Vec, - instruction_data: InstructionData, - account_identities: Vec, - program_with_dependencies: &ProgramWithDependencies, -) -> Result<(PrivacyPreservingCircuitOutput, Proof), LeeError> { - let ProgramWithDependencies { - program: initial_program, - dependencies, - } = program_with_dependencies; - let mut env_builder = ExecutorEnv::builder(); - let mut program_outputs = Vec::new(); - - let initial_call = ChainedCall { - program_id: initial_program.id(), - instruction_data, - pre_states, - pda_seeds: vec![], - }; - - let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program, None)]); - let mut chain_calls_counter = 0; - while let Some((chained_call, program, caller_program_id)) = chained_calls.pop_front() { - if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS { - return Err(LeeError::MaxChainedCallsDepthExceeded); - } - - let inner_receipt = execute_and_prove_program( - program, - caller_program_id, - &chained_call.pre_states, - &chained_call.instruction_data, - )?; - - let program_output: ProgramOutput = inner_receipt - .journal - .decode() - .map_err(|e| LeeError::ProgramOutputDeserializationError(e.to_string()))?; - - // TODO: remove clone - program_outputs.push(program_output.clone()); - - // Prove circuit. - env_builder.add_assumption(inner_receipt); - - for new_call in program_output.chained_calls.into_iter().rev() { - let next_program = dependencies.get(&new_call.program_id).ok_or( - InvalidProgramBehaviorError::UndeclaredProgramDependency { - program_id: new_call.program_id, - }, - )?; - chained_calls.push_front((new_call, next_program, Some(chained_call.program_id))); - } - - chain_calls_counter = chain_calls_counter - .checked_add(1) - .expect("we check the max depth at the beginning of the loop"); - } - - let circuit_input = PrivacyPreservingCircuitInput { - program_outputs, - account_identities, - program_id: program_with_dependencies.program.id(), - }; - - env_builder.write(&circuit_input).unwrap(); - let env = env_builder.build().unwrap(); - let prover = default_prover(); - let opts = ProverOpts::succinct(); - let prove_info = prover - .prove_with_opts(env, PRIVACY_PRESERVING_CIRCUIT_ELF, &opts) - .map_err(|e| LeeError::CircuitProvingError(e.to_string()))?; - - let proof = Proof(borsh::to_vec(&prove_info.receipt.inner)?); - - let circuit_output: PrivacyPreservingCircuitOutput = prove_info - .receipt - .journal - .decode() - .map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?; - - Ok((circuit_output, proof)) -} - -fn execute_and_prove_program( - program: &Program, - caller_program_id: Option, - pre_states: &[AccountWithMetadata], - instruction_data: &InstructionData, -) -> Result { - // Write inputs to the program - let mut env_builder = ExecutorEnv::builder(); - Program::write_inputs( - program.id(), - caller_program_id, - pre_states, - instruction_data, - &mut env_builder, - )?; - let env = env_builder.build().unwrap(); - - // Prove the program - let prover = default_prover(); - Ok(prover - .prove(env, program.elf()) - .map_err(|e| LeeError::ProgramProveFailed(e.to_string()))? - .receipt) -} - -#[cfg(test)] -mod tests { - #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] - - use lee_core::{ - Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, EphemeralSecretKey, Nullifier, - PrivacyPreservingCircuitOutput, SharedSecretKey, - account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, - program::{PdaSeed, PrivateAccountKind}, - }; - - use super::*; - use crate::{ - error::LeeError, - privacy_preserving_transaction::circuit::execute_and_prove, - program::Program, - state::{ - CommitmentSet, - tests::{test_private_account_keys_1, test_private_account_keys_2}, - }, - }; - - fn decrypt_kind( - output: &PrivacyPreservingCircuitOutput, - ssk: &SharedSecretKey, - idx: usize, - ) -> PrivateAccountKind { - let (kind, _) = EncryptionScheme::decrypt( - &output.encrypted_private_post_states[idx].ciphertext, - ssk, - &output.new_commitments[idx], - u32::try_from(idx).expect("idx fits in u32"), - ) - .unwrap(); - kind - } - - #[test] - fn proof_inner_roundtrip() { - // `Proof::from_inner(b).into_inner()` must return exactly `b`. Catches - // mutations of `into_inner` returning `vec![]`, `vec![0]`, or `vec![1]`, - // and of `from_inner` discarding its argument. - let bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF]; - assert_eq!(Proof::from_inner(bytes.clone()).into_inner(), bytes); - assert!(Proof::from_inner(vec![]).into_inner().is_empty()); - assert_eq!(Proof::from_inner(vec![0xFF]).into_inner(), vec![0xFF_u8]); - } - - #[test] - fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() { - let recipient_keys = test_private_account_keys_1(); - let program = Program::authenticated_transfer_program(); - let sender = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - - let recipient_account_id = - AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); - let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); - - let balance_to_move: u128 = 37; - - let expected_sender_post = Account { - program_owner: program.id(), - balance: 100 - balance_to_move, - nonce: Nonce::default(), - data: Data::default(), - }; - - let expected_recipient_post = Account { - program_owner: program.id(), - balance: balance_to_move, - nonce: Nonce::private_account_nonce_init(&recipient_account_id), - data: Data::default(), - }; - - let expected_sender_pre = sender.clone(); - - let init_nonce = Nonce::private_account_nonce_init(&recipient_account_id); - let esk = EphemeralSecretKey::new(&recipient_account_id, &[0; 32], &init_nonce); - let shared_secret = - SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &esk).0; - - let (output, proof) = execute_and_prove( - vec![sender, recipient], - Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { - amount: balance_to_move, - }) - .unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivateUnauthorized { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - npk: recipient_keys.npk(), - identifier: 0, - }, - ], - &Program::authenticated_transfer_program().into(), - ) - .unwrap(); - - assert!(proof.is_valid_for(&output)); - - let [sender_pre] = output.public_pre_states.try_into().unwrap(); - let [sender_post] = output.public_post_states.try_into().unwrap(); - assert_eq!(sender_pre, expected_sender_pre); - assert_eq!(sender_post, expected_sender_post); - assert_eq!(output.new_commitments.len(), 1); - assert_eq!(output.new_nullifiers.len(), 1); - assert_eq!(output.encrypted_private_post_states.len(), 1); - - let (_identifier, recipient_post) = EncryptionScheme::decrypt( - &output.encrypted_private_post_states[0].ciphertext, - &shared_secret, - &output.new_commitments[0], - 0, - ) - .unwrap(); - assert_eq!(recipient_post, expected_recipient_post); - } - - #[test] - fn prove_privacy_preserving_execution_circuit_fully_private() { - let program = Program::authenticated_transfer_program(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - - let sender_nonce = Nonce(0xdead_beef); - let sender_pre = AccountWithMetadata::new( - Account { - balance: 100, - nonce: sender_nonce, - program_owner: program.id(), - data: Data::default(), - }, - true, - AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - let sender_account_id = - AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); - let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account); - - let recipient_account_id = - AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); - let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); - let balance_to_move: u128 = 37; - - let mut commitment_set = CommitmentSet::with_capacity(2); - commitment_set.extend(std::slice::from_ref(&commitment_sender)); - let expected_new_nullifiers = vec![ - ( - Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk), - commitment_set.digest(), - ), - ( - Nullifier::for_account_initialization(&recipient_account_id), - DUMMY_COMMITMENT_HASH, - ), - ]; - - let program = Program::authenticated_transfer_program(); - - let expected_private_account_1 = Account { - program_owner: program.id(), - balance: 100 - balance_to_move, - nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), - ..Default::default() - }; - let expected_private_account_2 = Account { - program_owner: program.id(), - balance: balance_to_move, - nonce: Nonce::private_account_nonce_init(&recipient_account_id), - ..Default::default() - }; - let expected_new_commitments = vec![ - Commitment::new(&sender_account_id, &expected_private_account_1), - Commitment::new(&recipient_account_id, &expected_private_account_2), - ]; - - let sender_new_nonce = sender_nonce.private_account_nonce_increment(&sender_keys.nsk); - let sender_esk = EphemeralSecretKey::new(&sender_account_id, &[0; 32], &sender_new_nonce); - let shared_secret_1 = - SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &sender_esk).0; - - let recipient_init_nonce = Nonce::private_account_nonce_init(&recipient_account_id); - let recipient_esk = - EphemeralSecretKey::new(&recipient_account_id, &[0; 32], &recipient_init_nonce); - let shared_secret_2 = - SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &recipient_esk).0; - - let (output, proof) = execute_and_prove( - vec![sender_pre, recipient], - Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { - amount: balance_to_move, - }) - .unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: commitment_set - .get_proof_for(&commitment_sender) - .expect("sender's commitment must be in the set"), - identifier: 0, - }, - InputAccountIdentity::PrivateUnauthorized { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - npk: recipient_keys.npk(), - identifier: 0, - }, - ], - &program.into(), - ) - .unwrap(); - - assert!(proof.is_valid_for(&output)); - assert!(output.public_pre_states.is_empty()); - assert!(output.public_post_states.is_empty()); - assert_eq!(output.new_commitments, expected_new_commitments); - assert_eq!(output.new_nullifiers, expected_new_nullifiers); - assert_eq!(output.encrypted_private_post_states.len(), 2); - - let (_identifier, sender_post) = EncryptionScheme::decrypt( - &output.encrypted_private_post_states[0].ciphertext, - &shared_secret_1, - &expected_new_commitments[0], - 0, - ) - .unwrap(); - assert_eq!(sender_post, expected_private_account_1); - - let (_identifier, recipient_post) = EncryptionScheme::decrypt( - &output.encrypted_private_post_states[1].ciphertext, - &shared_secret_2, - &expected_new_commitments[1], - 1, - ) - .unwrap(); - assert_eq!(recipient_post, expected_private_account_2); - } - - #[test] - fn circuit_fails_when_chained_validity_windows_have_empty_intersection() { - let account_keys = test_private_account_keys_1(); - let pre = AccountWithMetadata::new( - Account::default(), - false, - AccountId::for_regular_private_account(&account_keys.npk(), &account_keys.vpk(), 0), - ); - - let validity_window_chain_caller = Program::validity_window_chain_caller(); - let validity_window = Program::validity_window(); - - let instruction = Program::serialize_instruction(( - Some(1_u64), - Some(4_u64), - validity_window.id(), - Some(4_u64), - Some(7_u64), - )) - .unwrap(); - - let program_with_deps = ProgramWithDependencies::new( - validity_window_chain_caller, - [(validity_window.id(), validity_window)].into(), - ); - - let result = execute_and_prove( - vec![pre], - instruction, - vec![InputAccountIdentity::PrivateUnauthorized { - vpk: account_keys.vpk(), - random_seed: [0; 32], - npk: account_keys.npk(), - identifier: 0, - }], - &program_with_deps, - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - /// A private PDA claimed with a non-default identifier produces a ciphertext that decrypts - /// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. - #[test] - fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() { - let program = Program::pda_claimer(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let seed = PdaSeed::new([42; 32]); - let identifier: u128 = 99; - let account_id = - AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier); - let init_nonce = Nonce::private_account_nonce_init(&account_id); - let esk = EphemeralSecretKey::new(&account_id, &[0; 32], &init_nonce); - let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; - - let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); - - let (output, _proof) = execute_and_prove( - vec![pre_state], - Program::serialize_instruction(seed).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { - vpk: keys.vpk(), - random_seed: [0; 32], - npk, - identifier, - seed: None, - }], - &program.clone().into(), - ) - .unwrap(); - - assert_eq!( - decrypt_kind(&output, &shared_secret, 0), - PrivateAccountKind::Pda { - program_id: program.id(), - seed, - identifier - }, - ); - } - - /// PDA init: initializes a new PDA under `authenticated_transfer`'s ownership. - /// The `auth_transfer_proxy` program chains to `authenticated_transfer` with `pda_seeds` - /// to establish authorization and the private PDA binding. - #[test] - fn private_pda_init() { - let program = Program::auth_transfer_proxy(); - let auth_transfer = Program::authenticated_transfer_program(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let seed = PdaSeed::new([42; 32]); - - // PDA (new, private PDA) - let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 0); - let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); - - let auth_id = auth_transfer.id(); - let program_with_deps = - ProgramWithDependencies::new(program, [(auth_id, auth_transfer)].into()); - - // is_withdraw=false triggers init path (1 pre-state) - let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, false)).unwrap(); - - let result = execute_and_prove( - vec![pda_pre], - instruction, - vec![InputAccountIdentity::PrivatePdaInit { - vpk: keys.vpk(), - random_seed: [0; 32], - npk, - identifier: 0, - seed: None, - }], - &program_with_deps, - ); - - let (output, _proof) = result.expect("PDA init should succeed"); - assert_eq!(output.new_commitments.len(), 1); - } - - /// PDA withdraw: chains to `authenticated_transfer` to move balance from PDA to recipient. - /// Uses a default PDA (amount=0) because testing with a pre-funded PDA requires a - /// two-tx sequence with membership proofs. - #[test] - fn private_pda_withdraw() { - let program = Program::auth_transfer_proxy(); - let auth_transfer = Program::authenticated_transfer_program(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let seed = PdaSeed::new([42; 32]); - - // PDA (new, private PDA) - let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 0); - let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); - - // Recipient (public) - let recipient_id = AccountId::new([88; 32]); - let recipient_pre = AccountWithMetadata::new( - Account { - program_owner: auth_transfer.id(), - balance: 10000, - ..Account::default() - }, - true, - recipient_id, - ); - - let auth_id = auth_transfer.id(); - let program_with_deps = - ProgramWithDependencies::new(program, [(auth_id, auth_transfer)].into()); - - // is_withdraw=true, amount=0 (PDA has no balance yet) - let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, true)).unwrap(); - - let result = execute_and_prove( - vec![pda_pre, recipient_pre], - instruction, - vec![ - InputAccountIdentity::PrivatePdaInit { - vpk: keys.vpk(), - random_seed: [0; 32], - npk, - identifier: 0, - seed: None, - }, - InputAccountIdentity::Public, - ], - &program_with_deps, - ); - - let (output, _proof) = result.expect("PDA withdraw should succeed"); - assert_eq!(output.new_commitments.len(), 1); - } - - /// Shared regular private account: receives funds via `authenticated_transfer` directly, - /// no custom program needed. This demonstrates the non-PDA shared account flow where - /// keys are derived from GMS via `derive_keys_for_shared_account`. The shared account - /// uses the standard unauthorized private account path and works with auth-transfer's - /// transfer path like any other private account. - #[test] - fn shared_account_receives_via_auth_transfer() { - let program = Program::authenticated_transfer_program(); - let shared_keys = test_private_account_keys_1(); - let shared_npk = shared_keys.npk(); - let shared_identifier: u128 = 42; - - // Sender: public account with balance, owned by auth-transfer - let sender_id = AccountId::new([99; 32]); - let sender = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 1000, - ..Account::default() - }, - true, - sender_id, - ); - - // Recipient: shared private account (new, unauthorized) - let shared_account_id = - AccountId::from((&shared_npk, &shared_keys.vpk(), shared_identifier)); - let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id); - - let balance_to_move: u128 = 100; - let instruction = - Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { - amount: balance_to_move, - }) - .unwrap(); - - let result = execute_and_prove( - vec![sender, recipient], - instruction, - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivateUnauthorized { - vpk: shared_keys.vpk(), - random_seed: [0; 32], - npk: shared_npk, - identifier: shared_identifier, - }, - ], - &program.into(), - ); - - let (output, _proof) = result.expect("shared account receive should succeed"); - // Sender is public (no commitment), recipient is private (1 commitment) - assert_eq!(output.new_commitments.len(), 1); - } - - /// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts - /// to `PrivateAccountKind::Regular` carrying the correct identifier. - #[test] - fn private_authorized_init_encrypts_regular_kind_with_identifier() { - let program = Program::authenticated_transfer_program(); - let keys = test_private_account_keys_1(); - let identifier: u128 = 99; - let account_id = - AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); - let nonce = Nonce::private_account_nonce_init(&account_id); - let esk = EphemeralSecretKey::new(&account_id, &[0; 32], &nonce); - let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; - let pre = AccountWithMetadata::new(Account::default(), true, account_id); - - let (output, _) = execute_and_prove( - vec![pre], - Program::serialize_instruction(authenticated_transfer_core::Instruction::Initialize) - .unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { - vpk: keys.vpk(), - random_seed: [0; 32], - nsk: keys.nsk, - identifier, - }], - &program.into(), - ) - .unwrap(); - - assert_eq!( - decrypt_kind(&output, &ssk, 0), - PrivateAccountKind::Regular(identifier) - ); - } - - /// Positive test to ensure that generating a cipher with - /// a viewing key allows to decrypt with it. - #[test] - fn circuit_note_is_decryptable_by_bound_viewing_key() { - let program = Program::authenticated_transfer_program(); - let keys = test_private_account_keys_1(); - let identifier: u128 = 99; - let account_id = - AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); - let pre = AccountWithMetadata::new(Account::default(), true, account_id); - - let (output, _) = execute_and_prove( - vec![pre], - Program::serialize_instruction(authenticated_transfer_core::Instruction::Initialize) - .unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { - vpk: keys.vpk(), - random_seed: [0; 32], - nsk: keys.nsk, - identifier, - }], - &program.into(), - ) - .unwrap(); - - let note = &output.encrypted_private_post_states[0]; - let shared_secret = SharedSecretKey::decapsulate(¬e.epk, &keys.d, &keys.z) - .expect("Bound viewing key must decapsulate the in-circuit epk."); - let (kind, account) = EncryptionScheme::decrypt( - ¬e.ciphertext, - &shared_secret, - &output.new_commitments[0], - 0, - ) - .expect("Note must decrypt using the shared secret."); - - assert_eq!(kind, PrivateAccountKind::Regular(identifier)); - assert_eq!( - Commitment::new(&account_id, &account), - output.new_commitments[0] - ); - } - - /// If the viewing keys supplied for initialization differ to the ones - /// used for generating account ids for pre-states, the PPC will catch - /// the mismatch. - #[test] - fn circuit_rejects_note_bound_to_foreign_viewing_key() { - let program = Program::authenticated_transfer_program(); - let keys = test_private_account_keys_1(); - let foreign_keys = test_private_account_keys_2(); - let identifier: u128 = 99; - // create an account id with one set of viewing keys - let account_id = - AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); - let pre = AccountWithMetadata::new(Account::default(), true, account_id); - - let result = execute_and_prove( - vec![pre], - Program::serialize_instruction(authenticated_transfer_core::Instruction::Initialize) - .unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { - // use a different vpk - vpk: foreign_keys.vpk(), - random_seed: [0; 32], - // but the same nsk - nsk: keys.nsk, - identifier, - }], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - /// `PrivateUnauthorized` with a non-default identifier produces a ciphertext that decrypts - /// to `PrivateAccountKind::Regular` carrying the correct identifier. - #[test] - fn private_unauthorized_init_encrypts_regular_kind_with_identifier() { - let program = Program::authenticated_transfer_program(); - let keys = test_private_account_keys_1(); - let identifier: u128 = 99; - let recipient_id = - AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); - let init_nonce = Nonce::private_account_nonce_init(&recipient_id); - let esk = EphemeralSecretKey::new(&recipient_id, &[0; 32], &init_nonce); - let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; - - let sender = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 1, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id); - - let (output, _) = execute_and_prove( - vec![sender, recipient], - Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { - amount: 1, - }) - .unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivateUnauthorized { - vpk: keys.vpk(), - random_seed: [0; 32], - npk: keys.npk(), - identifier, - }, - ], - &program.into(), - ) - .unwrap(); - - assert_eq!( - decrypt_kind(&output, &ssk, 0), - PrivateAccountKind::Regular(identifier) - ); - } - - /// `PrivateAuthorizedUpdate` with a non-default identifier produces a ciphertext that decrypts - /// to `PrivateAccountKind::Regular` carrying the correct identifier. - #[test] - fn private_authorized_update_encrypts_regular_kind_with_identifier() { - let program = Program::authenticated_transfer_program(); - let keys = test_private_account_keys_1(); - let identifier: u128 = 99; - let account_id = - AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); - let update_nonce = Nonce::default().private_account_nonce_increment(&keys.nsk); - let esk = EphemeralSecretKey::new(&account_id, &[0; 32], &update_nonce); - let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; - let account = Account { - program_owner: program.id(), - balance: 1, - ..Account::default() - }; - let commitment = Commitment::new(&account_id, &account); - let mut commitment_set = CommitmentSet::with_capacity(1); - commitment_set.extend(std::slice::from_ref(&commitment)); - - let sender = AccountWithMetadata::new(account, true, account_id); - let recipient = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); - - let (output, _) = execute_and_prove( - vec![sender, recipient], - Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { - amount: 1, - }) - .unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: keys.vpk(), - random_seed: [0; 32], - nsk: keys.nsk, - membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), - identifier, - }, - InputAccountIdentity::Public, - ], - &program.into(), - ) - .unwrap(); - - assert_eq!( - decrypt_kind(&output, &ssk, 0), - PrivateAccountKind::Regular(identifier) - ); - } - - /// `PrivatePdaUpdate` with a non-default identifier produces a ciphertext that decrypts - /// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. - #[test] - fn private_pda_update_encrypts_pda_kind_with_identifier() { - let program = Program::pda_spend_proxy(); - let auth_transfer = Program::authenticated_transfer_program(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let seed = PdaSeed::new([42; 32]); - let identifier: u128 = 99; - let auth_transfer_id = auth_transfer.id(); - let pda_id = - AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier); - let update_nonce = Nonce::default().private_account_nonce_increment(&keys.nsk); - let esk = EphemeralSecretKey::new(&pda_id, &[0; 32], &update_nonce); - let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; - let pda_account = Account { - program_owner: auth_transfer_id, - balance: 1, - ..Account::default() - }; - let pda_commitment = Commitment::new(&pda_id, &pda_account); - let mut commitment_set = CommitmentSet::with_capacity(1); - commitment_set.extend(std::slice::from_ref(&pda_commitment)); - - let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id); - let recipient_pre = - AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); - - let program_with_deps = ProgramWithDependencies::new( - program.clone(), - [(auth_transfer_id, auth_transfer)].into(), - ); - - let (output, _) = execute_and_prove( - vec![pda_pre, recipient_pre], - Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(), - vec![ - InputAccountIdentity::PrivatePdaUpdate { - vpk: keys.vpk(), - random_seed: [0; 32], - nsk: keys.nsk, - membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), - identifier, - seed: None, - }, - InputAccountIdentity::Public, - ], - &program_with_deps, - ) - .unwrap(); - - assert_eq!( - decrypt_kind(&output, &ssk, 0), - PrivateAccountKind::Pda { - program_id: program.id(), - seed, - identifier - }, - ); - } - - #[test] - fn private_pda_init_identifier_mismatch_fails() { - let program = Program::pda_claimer(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let seed = PdaSeed::new([42; 32]); - let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5); - let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); - - let result = execute_and_prove( - vec![pre_state], - Program::serialize_instruction(seed).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { - vpk: keys.vpk(), - random_seed: [0; 32], - npk, - identifier: 99, - seed: None, - }], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn private_pda_update_identifier_mismatch_fails() { - let program = Program::pda_spend_proxy(); - let auth_transfer = Program::authenticated_transfer_program(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let seed = PdaSeed::new([42; 32]); - let auth_transfer_id = auth_transfer.id(); - let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5); - let pda_account = Account { - program_owner: auth_transfer_id, - balance: 1, - ..Account::default() - }; - let pda_commitment = Commitment::new(&pda_id, &pda_account); - let mut commitment_set = CommitmentSet::with_capacity(1); - commitment_set.extend(std::slice::from_ref(&pda_commitment)); - - let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id); - let recipient_pre = - AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); - - let program_with_deps = - ProgramWithDependencies::new(program, [(auth_transfer_id, auth_transfer)].into()); - - let result = execute_and_prove( - vec![pda_pre, recipient_pre], - Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(), - vec![ - InputAccountIdentity::PrivatePdaUpdate { - vpk: keys.vpk(), - random_seed: [0; 32], - nsk: keys.nsk, - membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), - identifier: 99, - seed: None, - }, - InputAccountIdentity::Public, - ], - &program_with_deps, - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } -} diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit/mod.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit/mod.rs new file mode 100644 index 00000000..da8b2be4 --- /dev/null +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit/mod.rs @@ -0,0 +1,177 @@ +use std::collections::{HashMap, VecDeque}; + +use borsh::{BorshDeserialize, BorshSerialize}; +use lee_core::{ + InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + account::AccountWithMetadata, + program::{ChainedCall, InstructionData, ProgramId, ProgramOutput}, +}; +use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover}; + +use crate::{ + PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID, + error::{InvalidProgramBehaviorError, LeeError}, + program::Program, + state::MAX_NUMBER_CHAINED_CALLS, +}; + +/// Proof of the privacy preserving execution circuit. +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct Proof(pub(crate) Vec); + +impl Proof { + #[must_use] + pub fn into_inner(self) -> Vec { + self.0 + } + + #[must_use] + pub const fn from_inner(inner: Vec) -> Self { + Self(inner) + } + + pub(crate) fn is_valid_for(&self, circuit_output: &PrivacyPreservingCircuitOutput) -> bool { + let Ok(inner) = borsh::from_slice::(&self.0) else { + return false; + }; + let receipt = Receipt::new(inner, circuit_output.to_bytes()); + receipt.verify(PRIVACY_PRESERVING_CIRCUIT_ID).is_ok() + } +} + +#[derive(Clone)] +pub struct ProgramWithDependencies { + pub program: Program, + // TODO: avoid having a copy of the bytecode of each dependency. + pub dependencies: HashMap, +} + +impl ProgramWithDependencies { + #[must_use] + pub const fn new(program: Program, dependencies: HashMap) -> Self { + Self { + program, + dependencies, + } + } +} + +impl From for ProgramWithDependencies { + fn from(program: Program) -> Self { + Self::new(program, HashMap::new()) + } +} + +/// Generates a proof of the execution of a LEE program inside the privacy preserving execution +/// circuit. +pub fn execute_and_prove( + pre_states: Vec, + instruction_data: InstructionData, + account_identities: Vec, + program_with_dependencies: &ProgramWithDependencies, +) -> Result<(PrivacyPreservingCircuitOutput, Proof), LeeError> { + let ProgramWithDependencies { + program: initial_program, + dependencies, + } = program_with_dependencies; + let mut env_builder = ExecutorEnv::builder(); + let mut program_outputs = Vec::new(); + + let initial_call = ChainedCall { + program_id: initial_program.id(), + instruction_data, + pre_states, + pda_seeds: vec![], + }; + + let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program, None)]); + let mut chain_calls_counter = 0; + while let Some((chained_call, program, caller_program_id)) = chained_calls.pop_front() { + if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS { + return Err(LeeError::MaxChainedCallsDepthExceeded); + } + + let inner_receipt = execute_and_prove_program( + program, + caller_program_id, + &chained_call.pre_states, + &chained_call.instruction_data, + )?; + + let program_output: ProgramOutput = inner_receipt + .journal + .decode() + .map_err(|e| LeeError::ProgramOutputDeserializationError(e.to_string()))?; + + // TODO: remove clone + program_outputs.push(program_output.clone()); + + // Prove circuit. + env_builder.add_assumption(inner_receipt); + + for new_call in program_output.chained_calls.into_iter().rev() { + let next_program = dependencies.get(&new_call.program_id).ok_or( + InvalidProgramBehaviorError::UndeclaredProgramDependency { + program_id: new_call.program_id, + }, + )?; + chained_calls.push_front((new_call, next_program, Some(chained_call.program_id))); + } + + chain_calls_counter = chain_calls_counter + .checked_add(1) + .expect("we check the max depth at the beginning of the loop"); + } + + let circuit_input = PrivacyPreservingCircuitInput { + program_outputs, + account_identities, + program_id: program_with_dependencies.program.id(), + }; + + env_builder.write(&circuit_input).unwrap(); + let env = env_builder.build().unwrap(); + let prover = default_prover(); + let opts = ProverOpts::succinct(); + let prove_info = prover + .prove_with_opts(env, PRIVACY_PRESERVING_CIRCUIT_ELF, &opts) + .map_err(|e| LeeError::CircuitProvingError(e.to_string()))?; + + let proof = Proof(borsh::to_vec(&prove_info.receipt.inner)?); + + let circuit_output: PrivacyPreservingCircuitOutput = prove_info + .receipt + .journal + .decode() + .map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?; + + Ok((circuit_output, proof)) +} + +fn execute_and_prove_program( + program: &Program, + caller_program_id: Option, + pre_states: &[AccountWithMetadata], + instruction_data: &InstructionData, +) -> Result { + // Write inputs to the program + let mut env_builder = ExecutorEnv::builder(); + Program::write_inputs( + program.id(), + caller_program_id, + pre_states, + instruction_data, + &mut env_builder, + )?; + let env = env_builder.build().unwrap(); + + // Prove the program + let prover = default_prover(); + Ok(prover + .prove(env, program.elf()) + .map_err(|e| LeeError::ProgramProveFailed(e.to_string()))? + .receipt) +} + +#[cfg(test)] +mod tests; diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs new file mode 100644 index 00000000..005a9230 --- /dev/null +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit/tests.rs @@ -0,0 +1,722 @@ +#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] + +use lee_core::{ + Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, EphemeralSecretKey, Nullifier, + PrivacyPreservingCircuitOutput, SharedSecretKey, + account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, + program::{PdaSeed, PrivateAccountKind}, +}; + +use super::*; +use crate::{ + error::LeeError, + privacy_preserving_transaction::circuit::execute_and_prove, + program::Program, + state::{ + CommitmentSet, + tests::{test_private_account_keys_1, test_private_account_keys_2}, + }, +}; + +fn decrypt_kind( + output: &PrivacyPreservingCircuitOutput, + ssk: &SharedSecretKey, + idx: usize, +) -> PrivateAccountKind { + let (kind, _) = EncryptionScheme::decrypt( + &output.encrypted_private_post_states[idx].ciphertext, + ssk, + &output.new_commitments[idx], + u32::try_from(idx).expect("idx fits in u32"), + ) + .unwrap(); + kind +} + +#[test] +fn proof_inner_roundtrip() { + // `Proof::from_inner(b).into_inner()` must return exactly `b`. Catches + // mutations of `into_inner` returning `vec![]`, `vec![0]`, or `vec![1]`, + // and of `from_inner` discarding its argument. + let bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF]; + assert_eq!(Proof::from_inner(bytes.clone()).into_inner(), bytes); + assert!(Proof::from_inner(vec![]).into_inner().is_empty()); + assert_eq!(Proof::from_inner(vec![0xFF]).into_inner(), vec![0xFF_u8]); +} + +#[test] +fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() { + let recipient_keys = test_private_account_keys_1(); + let program = crate::test_methods::simple_balance_transfer(); + let sender = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + + let recipient_account_id = + AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); + let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); + + let balance_to_move: u128 = 37; + + let expected_sender_post = Account { + program_owner: program.id(), + balance: 100 - balance_to_move, + nonce: Nonce::default(), + data: Data::default(), + }; + + let expected_recipient_post = Account { + program_owner: program.id(), + balance: balance_to_move, + nonce: Nonce::private_account_nonce_init(&recipient_account_id), + data: Data::default(), + }; + + let expected_sender_pre = sender.clone(); + + let init_nonce = Nonce::private_account_nonce_init(&recipient_account_id); + let esk = EphemeralSecretKey::new(&recipient_account_id, &[0; 32], &init_nonce); + let shared_secret = SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &esk).0; + + let (output, proof) = execute_and_prove( + vec![sender, recipient], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + npk: recipient_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &crate::test_methods::simple_balance_transfer().into(), + ) + .unwrap(); + + assert!(proof.is_valid_for(&output)); + + let [sender_pre] = output.public_pre_states.try_into().unwrap(); + let [sender_post] = output.public_post_states.try_into().unwrap(); + assert_eq!(sender_pre, expected_sender_pre); + assert_eq!(sender_post, expected_sender_post); + assert_eq!(output.new_commitments.len(), 1); + assert_eq!(output.new_nullifiers.len(), 1); + assert_eq!(output.encrypted_private_post_states.len(), 1); + + let (_identifier, recipient_post) = EncryptionScheme::decrypt( + &output.encrypted_private_post_states[0].ciphertext, + &shared_secret, + &output.new_commitments[0], + 0, + ) + .unwrap(); + assert_eq!(recipient_post, expected_recipient_post); +} + +#[test] +fn prove_privacy_preserving_execution_circuit_fully_private() { + let program = crate::test_methods::simple_balance_transfer(); + let sender_keys = test_private_account_keys_1(); + let recipient_keys = test_private_account_keys_2(); + + let sender_nonce = Nonce(0xdead_beef); + let sender_pre = AccountWithMetadata::new( + Account { + balance: 100, + nonce: sender_nonce, + program_owner: program.id(), + data: Data::default(), + }, + true, + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); + let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account); + + let recipient_account_id = + AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); + let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); + let balance_to_move: u128 = 37; + + let mut commitment_set = CommitmentSet::with_capacity(2); + commitment_set.extend(std::slice::from_ref(&commitment_sender)); + let expected_new_nullifiers = vec![ + ( + Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk), + commitment_set.digest(), + ), + ( + Nullifier::for_account_initialization(&recipient_account_id), + DUMMY_COMMITMENT_HASH, + ), + ]; + + let program = crate::test_methods::simple_balance_transfer(); + + let expected_private_account_1 = Account { + program_owner: program.id(), + balance: 100 - balance_to_move, + nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), + ..Default::default() + }; + let expected_private_account_2 = Account { + program_owner: program.id(), + balance: balance_to_move, + nonce: Nonce::private_account_nonce_init(&recipient_account_id), + ..Default::default() + }; + let expected_new_commitments = vec![ + Commitment::new(&sender_account_id, &expected_private_account_1), + Commitment::new(&recipient_account_id, &expected_private_account_2), + ]; + + let esk_1 = EphemeralSecretKey::new( + &sender_account_id, + &[0; 32], + &sender_nonce.private_account_nonce_increment(&sender_keys.nsk), + ); + let shared_secret_1 = SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &esk_1).0; + + let init_nonce_2 = Nonce::private_account_nonce_init(&recipient_account_id); + let esk_2 = EphemeralSecretKey::new(&recipient_account_id, &[0; 32], &init_nonce_2); + let shared_secret_2 = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &esk_2).0; + + let (output, proof) = execute_and_prove( + vec![sender_pre, recipient], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: commitment_set + .get_proof_for(&commitment_sender) + .expect("sender's commitment must be in the set"), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + npk: recipient_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &program.into(), + ) + .unwrap(); + + assert!(proof.is_valid_for(&output)); + assert!(output.public_pre_states.is_empty()); + assert!(output.public_post_states.is_empty()); + assert_eq!(output.new_commitments, expected_new_commitments); + assert_eq!(output.new_nullifiers, expected_new_nullifiers); + assert_eq!(output.encrypted_private_post_states.len(), 2); + + let (_identifier, sender_post) = EncryptionScheme::decrypt( + &output.encrypted_private_post_states[0].ciphertext, + &shared_secret_1, + &expected_new_commitments[0], + 0, + ) + .unwrap(); + assert_eq!(sender_post, expected_private_account_1); + + let (_identifier, recipient_post) = EncryptionScheme::decrypt( + &output.encrypted_private_post_states[1].ciphertext, + &shared_secret_2, + &expected_new_commitments[1], + 1, + ) + .unwrap(); + assert_eq!(recipient_post, expected_private_account_2); +} + +#[test] +fn circuit_fails_when_chained_validity_windows_have_empty_intersection() { + let account_keys = test_private_account_keys_1(); + let pre = AccountWithMetadata::new( + Account::default(), + false, + AccountId::for_regular_private_account(&account_keys.npk(), &account_keys.vpk(), 0), + ); + + let validity_window_chain_caller = crate::test_methods::validity_window_chain_caller(); + let validity_window = crate::test_methods::validity_window(); + + let instruction = Program::serialize_instruction(( + Some(1_u64), + Some(4_u64), + validity_window.id(), + Some(4_u64), + Some(7_u64), + )) + .unwrap(); + + let program_with_deps = ProgramWithDependencies::new( + validity_window_chain_caller, + [(validity_window.id(), validity_window)].into(), + ); + + let result = execute_and_prove( + vec![pre], + instruction, + vec![InputAccountIdentity::PrivateUnauthorized { + vpk: account_keys.vpk(), + random_seed: [0; 32], + npk: account_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }], + &program_with_deps, + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +/// A private PDA claimed with a non-default identifier produces a ciphertext that decrypts +/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. +#[test] +fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() { + let program = crate::test_methods::pda_claimer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let identifier: u128 = 99; + let account_id = + AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier); + let init_nonce = Nonce::private_account_nonce_init(&account_id); + let esk = EphemeralSecretKey::new(&account_id, &[0; 32], &init_nonce); + let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; + + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let (output, _proof) = execute_and_prove( + vec![pre_state], + Program::serialize_instruction(seed).unwrap(), + vec![InputAccountIdentity::PrivatePdaInit { + vpk: keys.vpk(), + random_seed: [0; 32], + npk, + identifier, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }], + &program.clone().into(), + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &shared_secret, 0), + PrivateAccountKind::Pda { + program_id: program.id(), + seed, + identifier + }, + ); +} + +/// PDA init: initializes a new PDA under `simple_balance_transfer`'s ownership. +/// The `simple_transfer_proxy` program chains to `simple_balance_transfer` with `pda_seeds` +/// to establish authorization and the private PDA binding. +#[test] +fn private_pda_init() { + let program = crate::test_methods::simple_transfer_proxy(); + let simple_transfer = crate::test_methods::simple_balance_transfer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + // PDA (new, private PDA) + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 0); + let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); + + let auth_id = simple_transfer.id(); + let program_with_deps = + ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into()); + + // is_withdraw=false triggers init path (1 pre-state) + let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, false)).unwrap(); + + let result = execute_and_prove( + vec![pda_pre], + instruction, + vec![InputAccountIdentity::PrivatePdaInit { + vpk: keys.vpk(), + random_seed: [0; 32], + npk, + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }], + &program_with_deps, + ); + + let (output, _proof) = result.expect("PDA init should succeed"); + assert_eq!(output.new_commitments.len(), 1); +} + +/// PDA withdraw: chains to `simple_balance_transfer` to move balance from PDA to recipient. +/// Uses a default PDA (amount=0) because testing with a pre-funded PDA requires a +/// two-tx sequence with membership proofs. +#[test] +fn private_pda_withdraw() { + let program = crate::test_methods::simple_transfer_proxy(); + let simple_transfer = crate::test_methods::simple_balance_transfer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + // PDA (new, private PDA) + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 0); + let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); + + // Recipient (public) + let recipient_id = AccountId::new([88; 32]); + let recipient_pre = AccountWithMetadata::new( + Account { + program_owner: simple_transfer.id(), + balance: 10000, + ..Account::default() + }, + true, + recipient_id, + ); + + let auth_id = simple_transfer.id(); + let program_with_deps = + ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into()); + + // is_withdraw=true, amount=0 (PDA has no balance yet) + let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, true)).unwrap(); + + let result = execute_and_prove( + vec![pda_pre, recipient_pre], + instruction, + vec![ + InputAccountIdentity::PrivatePdaInit { + vpk: keys.vpk(), + random_seed: [0; 32], + npk, + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ); + + let (output, _proof) = result.expect("PDA withdraw should succeed"); + assert_eq!(output.new_commitments.len(), 1); +} + +/// Shared regular private account: receives funds via `authenticated_transfer` directly, +/// no custom program needed. This demonstrates the non-PDA shared account flow where +/// keys are derived from GMS via `derive_keys_for_shared_account`. The shared account +/// uses the standard unauthorized private account path and works with auth-transfer's +/// transfer path like any other private account. +#[test] +fn shared_account_receives_via_simple_transfer() { + let program = crate::test_methods::simple_balance_transfer(); + let shared_keys = test_private_account_keys_1(); + let shared_npk = shared_keys.npk(); + let shared_identifier: u128 = 42; + + // Sender: public account with balance, owned by auth-transfer + let sender_id = AccountId::new([99; 32]); + let sender = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 1000, + ..Account::default() + }, + true, + sender_id, + ); + + // Recipient: shared private account (new, unauthorized) + let shared_account_id = AccountId::from((&shared_npk, &shared_keys.vpk(), shared_identifier)); + let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id); + + let balance_to_move: u128 = 100; + let instruction = Program::serialize_instruction(balance_to_move).unwrap(); + + let result = execute_and_prove( + vec![sender, recipient], + instruction, + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + vpk: shared_keys.vpk(), + random_seed: [0; 32], + npk: shared_npk, + identifier: shared_identifier, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &program.into(), + ); + + let (output, _proof) = result.expect("shared account receive should succeed"); + // Sender is public (no commitment), recipient is private (1 commitment) + assert_eq!(output.new_commitments.len(), 1); +} + +/// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts +/// to `PrivateAccountKind::Regular` carrying the correct identifier. +#[test] +fn private_authorized_init_encrypts_regular_kind_with_identifier() { + let program = crate::test_methods::claimer(); + let keys = test_private_account_keys_1(); + let identifier: u128 = 99; + let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); + let esk = EphemeralSecretKey::new( + &account_id, + &[0; 32], + &Nonce::private_account_nonce_init(&account_id), + ); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; + let pre = AccountWithMetadata::new(Account::default(), true, account_id); + + let (output, _) = execute_and_prove( + vec![pre], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::PrivateAuthorizedInit { + vpk: keys.vpk(), + random_seed: [0; 32], + nsk: keys.nsk, + identifier, + commitment_root: DUMMY_COMMITMENT_HASH, + }], + &program.into(), + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &ssk, 0), + PrivateAccountKind::Regular(identifier) + ); +} + +/// `PrivateUnauthorized` with a non-default identifier produces a ciphertext that decrypts +/// to `PrivateAccountKind::Regular` carrying the correct identifier. +#[test] +fn private_unauthorized_init_encrypts_regular_kind_with_identifier() { + let program = crate::test_methods::claimer(); + let keys = test_private_account_keys_1(); + let identifier: u128 = 99; + let recipient_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); + let esk = EphemeralSecretKey::new( + &recipient_id, + &[0; 32], + &Nonce::private_account_nonce_init(&recipient_id), + ); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; + let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id); + + let (output, _) = execute_and_prove( + vec![recipient], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::PrivateUnauthorized { + vpk: keys.vpk(), + random_seed: [0; 32], + npk: keys.npk(), + identifier, + commitment_root: DUMMY_COMMITMENT_HASH, + }], + &program.into(), + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &ssk, 0), + PrivateAccountKind::Regular(identifier) + ); +} + +/// `PrivateAuthorizedUpdate` with a non-default identifier produces a ciphertext that decrypts +/// to `PrivateAccountKind::Regular` carrying the correct identifier. +#[test] +fn private_authorized_update_encrypts_regular_kind_with_identifier() { + let program = crate::test_methods::noop(); + let keys = test_private_account_keys_1(); + let identifier: u128 = 99; + let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), identifier); + let esk = EphemeralSecretKey::new( + &account_id, + &[0; 32], + &Nonce::default().private_account_nonce_increment(&keys.nsk), + ); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; + let account = Account { + program_owner: program.id(), + balance: 1, + ..Account::default() + }; + let commitment = Commitment::new(&account_id, &account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&commitment)); + + let sender = AccountWithMetadata::new(account, true, account_id); + + let (output, _) = execute_and_prove( + vec![sender], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: keys.vpk(), + random_seed: [0; 32], + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), + identifier, + }], + &program.into(), + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &ssk, 0), + PrivateAccountKind::Regular(identifier) + ); +} + +/// `PrivatePdaUpdate` with a non-default identifier produces a ciphertext that decrypts +/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. +#[test] +fn private_pda_update_encrypts_pda_kind_with_identifier() { + let program = crate::test_methods::pda_spend_proxy(); + let simple_transfer = crate::test_methods::simple_balance_transfer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let identifier: u128 = 99; + let simple_transfer_id = simple_transfer.id(); + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), identifier); + let esk = EphemeralSecretKey::new( + &pda_id, + &[0; 32], + &Nonce::default().private_account_nonce_increment(&keys.nsk), + ); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &esk).0; + let pda_account = Account { + program_owner: simple_transfer_id, + balance: 1, + ..Account::default() + }; + let pda_commitment = Commitment::new(&pda_id, &pda_account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&pda_commitment)); + + let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id); + let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); + + let program_with_deps = ProgramWithDependencies::new( + program.clone(), + [(simple_transfer_id, simple_transfer)].into(), + ); + + let (output, _) = execute_and_prove( + vec![pda_pre, recipient_pre], + Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + vpk: keys.vpk(), + random_seed: [0; 32], + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), + identifier, + seed: None, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &ssk, 0), + PrivateAccountKind::Pda { + program_id: program.id(), + seed, + identifier + }, + ); +} + +#[test] +fn private_pda_init_identifier_mismatch_fails() { + let program = crate::test_methods::pda_claimer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction(seed).unwrap(), + vec![InputAccountIdentity::PrivatePdaInit { + vpk: keys.vpk(), + random_seed: [0; 32], + npk, + identifier: 99, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn private_pda_update_identifier_mismatch_fails() { + let program = crate::test_methods::pda_spend_proxy(); + let simple_transfer = crate::test_methods::simple_balance_transfer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let simple_transfer_id = simple_transfer.id(); + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), 5); + let pda_account = Account { + program_owner: simple_transfer_id, + balance: 1, + ..Account::default() + }; + let pda_commitment = Commitment::new(&pda_id, &pda_account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&pda_commitment)); + + let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id); + let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); + + let program_with_deps = + ProgramWithDependencies::new(program, [(simple_transfer_id, simple_transfer)].into()); + + let result = execute_and_prove( + vec![pda_pre, recipient_pre], + Program::serialize_instruction((seed, 1_u128, simple_transfer_id, false)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + vpk: keys.vpk(), + random_seed: [0; 32], + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), + identifier: 99, + seed: None, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} diff --git a/lee/state_machine/src/program.rs b/lee/state_machine/src/program.rs deleted file mode 100644 index c4223810..00000000 --- a/lee/state_machine/src/program.rs +++ /dev/null @@ -1,591 +0,0 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use lee_core::{ - account::AccountWithMetadata, - program::{InstructionData, ProgramId, ProgramOutput}, -}; -use risc0_zkvm::{ExecutorEnv, ExecutorEnvBuilder, default_executor, serde::to_vec}; -use serde::Serialize; - -use crate::{ - error::LeeError, - program_methods::{ - AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID, - AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, BRIDGE_ELF, BRIDGE_ID, CLOCK_ELF, - CLOCK_ID, FAUCET_ELF, FAUCET_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, - VAULT_ID, - }, -}; - -/// Maximum number of cycles for a public execution. -/// TODO: Make this variable when fees are implemented. -const MAX_NUM_CYCLES_PUBLIC_EXECUTION: u64 = 1024 * 1024 * 32; // 32M cycles - -#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -pub struct Program { - id: ProgramId, - elf: Vec, -} - -impl Program { - pub fn new(bytecode: Vec) -> Result { - let binary = risc0_binfmt::ProgramBinary::decode(&bytecode) - .map_err(LeeError::InvalidProgramBytecode)?; - let id = binary - .compute_image_id() - .map_err(LeeError::InvalidProgramBytecode)? - .into(); - Ok(Self { elf: bytecode, id }) - } - - #[must_use] - pub const fn id(&self) -> ProgramId { - self.id - } - - #[must_use] - pub fn elf(&self) -> &[u8] { - &self.elf - } - - pub fn serialize_instruction( - instruction: T, - ) -> Result { - to_vec(&instruction).map_err(|e| LeeError::InstructionSerializationError(e.to_string())) - } - - pub(crate) fn execute( - &self, - caller_program_id: Option, - pre_states: &[AccountWithMetadata], - instruction_data: &InstructionData, - ) -> Result { - // Write inputs to the program - let mut env_builder = ExecutorEnv::builder(); - env_builder.session_limit(Some(MAX_NUM_CYCLES_PUBLIC_EXECUTION)); - Self::write_inputs( - self.id, - caller_program_id, - pre_states, - instruction_data, - &mut env_builder, - )?; - let env = env_builder.build().unwrap(); - - // Execute the program (without proving) - let executor = default_executor(); - let session_info = executor - .execute(env, self.elf()) - .map_err(|e| LeeError::ProgramExecutionFailed(e.to_string()))?; - - // Get outputs - let program_output = session_info - .journal - .decode() - .map_err(|e| LeeError::ProgramExecutionFailed(e.to_string()))?; - - Ok(program_output) - } - - /// Writes inputs to `env_builder` in the order expected by the programs. - pub(crate) fn write_inputs( - program_id: ProgramId, - caller_program_id: Option, - pre_states: &[AccountWithMetadata], - instruction_data: &[u32], - env_builder: &mut ExecutorEnvBuilder, - ) -> Result<(), LeeError> { - env_builder - .write(&program_id) - .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; - env_builder - .write(&caller_program_id) - .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; - let pre_states = pre_states.to_vec(); - env_builder - .write(&pre_states) - .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; - env_builder - .write(&instruction_data) - .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; - Ok(()) - } - - #[must_use] - pub fn authenticated_transfer_program() -> Self { - Self { - id: AUTHENTICATED_TRANSFER_ID, - elf: AUTHENTICATED_TRANSFER_ELF.to_vec(), - } - } - - #[must_use] - pub fn token() -> Self { - Self { - id: TOKEN_ID, - elf: TOKEN_ELF.to_vec(), - } - } - - #[must_use] - pub fn amm() -> Self { - Self { - id: AMM_ID, - elf: AMM_ELF.to_vec(), - } - } - - #[must_use] - pub fn clock() -> Self { - Self { - id: CLOCK_ID, - elf: CLOCK_ELF.to_vec(), - } - } - - #[must_use] - pub fn ata() -> Self { - Self { - id: ASSOCIATED_TOKEN_ACCOUNT_ID, - elf: ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec(), - } - } - - #[must_use] - pub fn vault() -> Self { - Self { - id: VAULT_ID, - elf: VAULT_ELF.to_vec(), - } - } - - #[must_use] - pub fn faucet() -> Self { - Self { - id: FAUCET_ID, - elf: FAUCET_ELF.to_vec(), - } - } - - #[must_use] - pub fn bridge() -> Self { - Self { - id: BRIDGE_ID, - elf: BRIDGE_ELF.to_vec(), - } - } -} - -// TODO: Testnet only. Refactor to prevent compilation on mainnet. -impl Program { - #[must_use] - pub fn pinata() -> Self { - Self { - id: PINATA_ID, - elf: PINATA_ELF.to_vec(), - } - } - - #[must_use] - pub fn pinata_token() -> Self { - use crate::program_methods::{PINATA_TOKEN_ELF, PINATA_TOKEN_ID}; - Self { - id: PINATA_TOKEN_ID, - elf: PINATA_TOKEN_ELF.to_vec(), - } - } -} - -#[cfg(test)] -mod tests { - use lee_core::account::{Account, AccountId, AccountWithMetadata}; - - use crate::{ - program::Program, - program_methods::{ - AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID, - AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, BRIDGE_ELF, BRIDGE_ID, - CLOCK_ELF, CLOCK_ID, FAUCET_ELF, FAUCET_ID, PINATA_ELF, PINATA_ID, PINATA_TOKEN_ELF, - PINATA_TOKEN_ID, TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID, - }, - }; - - impl Program { - /// A program that changes the nonce of an account. - #[must_use] - pub fn nonce_changer_program() -> Self { - use test_program_methods::{NONCE_CHANGER_ELF, NONCE_CHANGER_ID}; - - Self { - id: NONCE_CHANGER_ID, - elf: NONCE_CHANGER_ELF.to_vec(), - } - } - - /// A program that produces more output accounts than the inputs it received. - #[must_use] - pub fn extra_output_program() -> Self { - use test_program_methods::{EXTRA_OUTPUT_ELF, EXTRA_OUTPUT_ID}; - - Self { - id: EXTRA_OUTPUT_ID, - elf: EXTRA_OUTPUT_ELF.to_vec(), - } - } - - /// A program that produces less output accounts than the inputs it received. - #[must_use] - pub fn missing_output_program() -> Self { - use test_program_methods::{MISSING_OUTPUT_ELF, MISSING_OUTPUT_ID}; - - Self { - id: MISSING_OUTPUT_ID, - elf: MISSING_OUTPUT_ELF.to_vec(), - } - } - - /// A program that changes the program owner of an account to [0, 1, 2, 3, 4, 5, 6, 7]. - #[must_use] - pub fn program_owner_changer() -> Self { - use test_program_methods::{PROGRAM_OWNER_CHANGER_ELF, PROGRAM_OWNER_CHANGER_ID}; - - Self { - id: PROGRAM_OWNER_CHANGER_ID, - elf: PROGRAM_OWNER_CHANGER_ELF.to_vec(), - } - } - - /// A program that transfers balance without caring about authorizations. - #[must_use] - pub fn simple_balance_transfer() -> Self { - use test_program_methods::{SIMPLE_BALANCE_TRANSFER_ELF, SIMPLE_BALANCE_TRANSFER_ID}; - - Self { - id: SIMPLE_BALANCE_TRANSFER_ID, - elf: SIMPLE_BALANCE_TRANSFER_ELF.to_vec(), - } - } - - /// A program that modifies the data of an account. - #[must_use] - pub fn data_changer() -> Self { - use test_program_methods::{DATA_CHANGER_ELF, DATA_CHANGER_ID}; - - Self { - id: DATA_CHANGER_ID, - elf: DATA_CHANGER_ELF.to_vec(), - } - } - - /// A program that mints balance. - #[must_use] - pub fn minter() -> Self { - use test_program_methods::{MINTER_ELF, MINTER_ID}; - - Self { - id: MINTER_ID, - elf: MINTER_ELF.to_vec(), - } - } - - /// A program that burns balance. - #[must_use] - pub fn burner() -> Self { - use test_program_methods::{BURNER_ELF, BURNER_ID}; - - Self { - id: BURNER_ID, - elf: BURNER_ELF.to_vec(), - } - } - - #[must_use] - pub fn chain_caller() -> Self { - use test_program_methods::{CHAIN_CALLER_ELF, CHAIN_CALLER_ID}; - - Self { - id: CHAIN_CALLER_ID, - elf: CHAIN_CALLER_ELF.to_vec(), - } - } - - #[must_use] - pub fn claimer() -> Self { - use test_program_methods::{CLAIMER_ELF, CLAIMER_ID}; - - Self { - id: CLAIMER_ID, - elf: CLAIMER_ELF.to_vec(), - } - } - - #[must_use] - pub fn pda_claimer() -> Self { - use test_program_methods::{PDA_CLAIMER_ELF, PDA_CLAIMER_ID}; - - Self { - id: PDA_CLAIMER_ID, - elf: PDA_CLAIMER_ELF.to_vec(), - } - } - - #[must_use] - pub fn private_pda_delegator() -> Self { - use test_program_methods::{PRIVATE_PDA_DELEGATOR_ELF, PRIVATE_PDA_DELEGATOR_ID}; - - Self { - id: PRIVATE_PDA_DELEGATOR_ID, - elf: PRIVATE_PDA_DELEGATOR_ELF.to_vec(), - } - } - - #[must_use] - pub fn auth_transfer_proxy() -> Self { - use test_program_methods::{AUTH_TRANSFER_PROXY_ELF, AUTH_TRANSFER_PROXY_ID}; - - Self { - id: AUTH_TRANSFER_PROXY_ID, - elf: AUTH_TRANSFER_PROXY_ELF.to_vec(), - } - } - - #[must_use] - pub fn two_pda_claimer() -> Self { - use test_program_methods::{TWO_PDA_CLAIMER_ELF, TWO_PDA_CLAIMER_ID}; - - Self { - id: TWO_PDA_CLAIMER_ID, - elf: TWO_PDA_CLAIMER_ELF.to_vec(), - } - } - - #[must_use] - pub fn pda_spend_proxy() -> Self { - use test_program_methods::{PDA_SPEND_PROXY_ELF, PDA_SPEND_PROXY_ID}; - - Self { - id: PDA_SPEND_PROXY_ID, - elf: PDA_SPEND_PROXY_ELF.to_vec(), - } - } - - #[must_use] - pub fn changer_claimer() -> Self { - use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID}; - - Self { - id: CHANGER_CLAIMER_ID, - elf: CHANGER_CLAIMER_ELF.to_vec(), - } - } - - #[must_use] - pub fn noop() -> Self { - use test_program_methods::{NOOP_ELF, NOOP_ID}; - - Self { - id: NOOP_ID, - elf: NOOP_ELF.to_vec(), - } - } - - #[must_use] - pub fn auth_asserting_noop() -> Self { - use test_program_methods::{AUTH_ASSERTING_NOOP_ELF, AUTH_ASSERTING_NOOP_ID}; - - Self { - id: AUTH_ASSERTING_NOOP_ID, - elf: AUTH_ASSERTING_NOOP_ELF.to_vec(), - } - } - - #[must_use] - pub fn malicious_authorization_changer() -> Self { - use test_program_methods::{ - MALICIOUS_AUTHORIZATION_CHANGER_ELF, MALICIOUS_AUTHORIZATION_CHANGER_ID, - }; - - Self { - id: MALICIOUS_AUTHORIZATION_CHANGER_ID, - elf: MALICIOUS_AUTHORIZATION_CHANGER_ELF.to_vec(), - } - } - - #[must_use] - pub fn modified_transfer_program() -> Self { - use test_program_methods::{MODIFIED_TRANSFER_ELF, MODIFIED_TRANSFER_ID}; - Self { - id: MODIFIED_TRANSFER_ID, - elf: MODIFIED_TRANSFER_ELF.to_vec(), - } - } - - #[must_use] - pub fn validity_window() -> Self { - use test_program_methods::{VALIDITY_WINDOW_ELF, VALIDITY_WINDOW_ID}; - Self { - id: VALIDITY_WINDOW_ID, - elf: VALIDITY_WINDOW_ELF.to_vec(), - } - } - - #[must_use] - pub fn validity_window_chain_caller() -> Self { - use test_program_methods::{ - VALIDITY_WINDOW_CHAIN_CALLER_ELF, VALIDITY_WINDOW_CHAIN_CALLER_ID, - }; - Self { - id: VALIDITY_WINDOW_CHAIN_CALLER_ID, - elf: VALIDITY_WINDOW_CHAIN_CALLER_ELF.to_vec(), - } - } - - #[must_use] - pub fn flash_swap_initiator() -> Self { - use test_program_methods::FLASH_SWAP_INITIATOR_ELF; - Self::new(FLASH_SWAP_INITIATOR_ELF.to_vec()) - .expect("flash_swap_initiator must be a valid Risc0 program") - } - - #[must_use] - pub fn flash_swap_callback() -> Self { - use test_program_methods::FLASH_SWAP_CALLBACK_ELF; - Self::new(FLASH_SWAP_CALLBACK_ELF.to_vec()) - .expect("flash_swap_callback must be a valid Risc0 program") - } - - #[must_use] - pub fn malicious_self_program_id() -> Self { - use test_program_methods::MALICIOUS_SELF_PROGRAM_ID_ELF; - Self::new(MALICIOUS_SELF_PROGRAM_ID_ELF.to_vec()) - .expect("malicious_self_program_id must be a valid Risc0 program") - } - - #[must_use] - pub fn malicious_caller_program_id() -> Self { - use test_program_methods::MALICIOUS_CALLER_PROGRAM_ID_ELF; - Self::new(MALICIOUS_CALLER_PROGRAM_ID_ELF.to_vec()) - .expect("malicious_caller_program_id must be a valid Risc0 program") - } - - #[must_use] - pub fn time_locked_transfer() -> Self { - use test_program_methods::TIME_LOCKED_TRANSFER_ELF; - Self::new(TIME_LOCKED_TRANSFER_ELF.to_vec()).unwrap() - } - - #[must_use] - pub fn pinata_cooldown() -> Self { - use test_program_methods::PINATA_COOLDOWN_ELF; - Self::new(PINATA_COOLDOWN_ELF.to_vec()).unwrap() - } - - #[must_use] - pub fn malicious_injector() -> Self { - use test_program_methods::{MALICIOUS_INJECTOR_ELF, MALICIOUS_INJECTOR_ID}; - Self { - id: MALICIOUS_INJECTOR_ID, - elf: MALICIOUS_INJECTOR_ELF.to_vec(), - } - } - - #[must_use] - pub fn malicious_launderer() -> Self { - use test_program_methods::{MALICIOUS_LAUNDERER_ELF, MALICIOUS_LAUNDERER_ID}; - Self { - id: MALICIOUS_LAUNDERER_ID, - elf: MALICIOUS_LAUNDERER_ELF.to_vec(), - } - } - } - - #[test] - fn elf_returns_the_program_bytecode_constant() { - // `Program::elf` must return exactly the compile-time ELF, never an empty - // or placeholder slice. Catches mutations returning `Vec::leak(Vec::new())`, - // `Vec::leak(vec![0])`, or `Vec::leak(vec![1])`. - let at = Program::authenticated_transfer_program(); - assert!(!at.elf().is_empty()); - assert_eq!(at.elf(), AUTHENTICATED_TRANSFER_ELF); - - let token = Program::token(); - assert!(!token.elf().is_empty()); - assert_eq!(token.elf(), TOKEN_ELF); - } - - #[test] - fn program_execution() { - let program = Program::simple_balance_transfer(); - let balance_to_move: u128 = 11_223_344_556_677; - let instruction_data = Program::serialize_instruction(balance_to_move).unwrap(); - let sender = AccountWithMetadata::new( - Account { - balance: 77_665_544_332_211, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - let recipient = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); - - let expected_sender_post = Account { - balance: 77_665_544_332_211 - balance_to_move, - ..Account::default() - }; - let expected_recipient_post = Account { - balance: balance_to_move, - ..Account::default() - }; - let program_output = program - .execute(None, &[sender, recipient], &instruction_data) - .unwrap(); - - let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap(); - - assert_eq!(sender_post.account(), &expected_sender_post); - assert_eq!(recipient_post.account(), &expected_recipient_post); - } - - #[test] - fn builtin_programs() { - let auth_transfer_program = Program::authenticated_transfer_program(); - let token_program = Program::token(); - let vault_program = Program::vault(); - let faucet_program = Program::faucet(); - let bridge_program = Program::bridge(); - let pinata_program = Program::pinata(); - - assert_eq!(auth_transfer_program.id, AUTHENTICATED_TRANSFER_ID); - assert_eq!(auth_transfer_program.elf, AUTHENTICATED_TRANSFER_ELF); - assert_eq!(token_program.id, TOKEN_ID); - assert_eq!(token_program.elf, TOKEN_ELF); - assert_eq!(vault_program.id, VAULT_ID); - assert_eq!(vault_program.elf, VAULT_ELF); - assert_eq!(faucet_program.id, FAUCET_ID); - assert_eq!(faucet_program.elf, FAUCET_ELF); - assert_eq!(bridge_program.id, BRIDGE_ID); - assert_eq!(bridge_program.elf, BRIDGE_ELF); - assert_eq!(pinata_program.id, PINATA_ID); - assert_eq!(pinata_program.elf, PINATA_ELF); - } - - #[test] - fn builtin_program_ids_match_elfs() { - let cases: &[(&[u8], [u32; 8])] = &[ - (AMM_ELF, AMM_ID), - (AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID), - (ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID), - (CLOCK_ELF, CLOCK_ID), - (FAUCET_ELF, FAUCET_ID), - (BRIDGE_ELF, BRIDGE_ID), - (PINATA_ELF, PINATA_ID), - (PINATA_TOKEN_ELF, PINATA_TOKEN_ID), - (TOKEN_ELF, TOKEN_ID), - (VAULT_ELF, VAULT_ID), - ]; - for (elf, expected_id) in cases { - let program = Program::new(elf.to_vec()).unwrap(); - assert_eq!(program.id(), *expected_id); - } - } -} diff --git a/lee/state_machine/src/program/mod.rs b/lee/state_machine/src/program/mod.rs new file mode 100644 index 00000000..d481c1fa --- /dev/null +++ b/lee/state_machine/src/program/mod.rs @@ -0,0 +1,114 @@ +use std::borrow::Cow; + +use borsh::{BorshDeserialize, BorshSerialize}; +use lee_core::{ + account::AccountWithMetadata, + program::{InstructionData, ProgramId, ProgramOutput}, +}; +use risc0_zkvm::{ExecutorEnv, ExecutorEnvBuilder, default_executor, serde::to_vec}; +use serde::Serialize; + +use crate::error::LeeError; + +/// Maximum number of cycles for a public execution. +/// TODO: Make this variable when fees are implemented. +const MAX_NUM_CYCLES_PUBLIC_EXECUTION: u64 = 1024 * 1024 * 32; // 32M cycles + +#[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct Program { + id: ProgramId, + elf: Cow<'static, [u8]>, +} + +impl Program { + pub fn new(elf: Cow<'static, [u8]>) -> Result { + let binary = risc0_binfmt::ProgramBinary::decode(elf.as_ref()) + .map_err(LeeError::InvalidProgramBytecode)?; + let id = binary + .compute_image_id() + .map_err(LeeError::InvalidProgramBytecode)? + .into(); + Ok(Self { id, elf }) + } + + #[must_use] + pub const fn new_unchecked(id: ProgramId, elf: Cow<'static, [u8]>) -> Self { + Self { id, elf } + } + + #[must_use] + pub const fn id(&self) -> ProgramId { + self.id + } + + #[must_use] + pub fn elf(&self) -> &[u8] { + &self.elf + } + + pub fn serialize_instruction( + instruction: T, + ) -> Result { + to_vec(&instruction).map_err(|e| LeeError::InstructionSerializationError(e.to_string())) + } + + pub(crate) fn execute( + &self, + caller_program_id: Option, + pre_states: &[AccountWithMetadata], + instruction_data: &InstructionData, + ) -> Result { + // Write inputs to the program + let mut env_builder = ExecutorEnv::builder(); + env_builder.session_limit(Some(MAX_NUM_CYCLES_PUBLIC_EXECUTION)); + Self::write_inputs( + self.id, + caller_program_id, + pre_states, + instruction_data, + &mut env_builder, + )?; + let env = env_builder.build().unwrap(); + + // Execute the program (without proving) + let executor = default_executor(); + let session_info = executor + .execute(env, self.elf()) + .map_err(|e| LeeError::ProgramExecutionFailed(e.to_string()))?; + + // Get outputs + let program_output = session_info + .journal + .decode() + .map_err(|e| LeeError::ProgramExecutionFailed(e.to_string()))?; + + Ok(program_output) + } + + /// Writes inputs to `env_builder` in the order expected by the programs. + pub(crate) fn write_inputs( + program_id: ProgramId, + caller_program_id: Option, + pre_states: &[AccountWithMetadata], + instruction_data: &[u32], + env_builder: &mut ExecutorEnvBuilder, + ) -> Result<(), LeeError> { + env_builder + .write(&program_id) + .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; + env_builder + .write(&caller_program_id) + .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; + let pre_states = pre_states.to_vec(); + env_builder + .write(&pre_states) + .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; + env_builder + .write(&instruction_data) + .map_err(|e| LeeError::ProgramWriteInputFailed(e.to_string()))?; + Ok(()) + } +} + +#[cfg(test)] +mod tests; diff --git a/lee/state_machine/src/program/tests.rs b/lee/state_machine/src/program/tests.rs new file mode 100644 index 00000000..330bd0d6 --- /dev/null +++ b/lee/state_machine/src/program/tests.rs @@ -0,0 +1,36 @@ +use lee_core::account::{Account, AccountId, AccountWithMetadata}; + +use crate::program::Program; + +#[test] +fn program_execution() { + let program = crate::test_methods::simple_balance_transfer(); + let balance_to_move: u128 = 11_223_344_556_677; + let instruction_data = Program::serialize_instruction(balance_to_move).unwrap(); + let sender = AccountWithMetadata::new( + Account { + balance: 77_665_544_332_211, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + let recipient = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); + + let expected_sender_post = Account { + balance: 77_665_544_332_211 - balance_to_move, + ..Account::default() + }; + let expected_recipient_post = Account { + balance: balance_to_move, + ..Account::default() + }; + let program_output = program + .execute(None, &[sender, recipient], &instruction_data) + .unwrap(); + + let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap(); + + assert_eq!(sender_post.account(), &expected_sender_post); + assert_eq!(recipient_post.account(), &expected_recipient_post); +} diff --git a/lee/state_machine/src/public_transaction/transaction.rs b/lee/state_machine/src/public_transaction/transaction.rs index 1d1fee0d..e0d1fe1b 100644 --- a/lee/state_machine/src/public_transaction/transaction.rs +++ b/lee/state_machine/src/public_transaction/transaction.rs @@ -66,7 +66,6 @@ pub mod tests { use crate::{ AccountId, PrivateKey, PublicKey, PublicTransaction, Signature, V03State, error::LeeError, - program::Program, public_transaction::{Message, WitnessSet}, validated_state_diff::ValidatedStateDiff, }; @@ -82,7 +81,9 @@ pub mod tests { fn state_for_tests() -> V03State { let (_, _, addr1, addr2) = keys_for_tests(); let initial_data = [(addr1, 10000), (addr2, 20000)]; - V03State::new_with_genesis_accounts(&initial_data, vec![], 0) + V03State::new() + .with_public_account_balances(initial_data) + .with_programs([crate::test_methods::simple_balance_transfer()]) } fn transaction_for_tests() -> PublicTransaction { @@ -90,7 +91,7 @@ pub mod tests { let nonces = vec![0_u128.into(), 0_u128.into()]; let instruction = 1337; let message = Message::try_new( - Program::authenticated_transfer_program().id(), + crate::test_methods::simple_balance_transfer().id(), vec![addr1, addr2], nonces, instruction, @@ -168,7 +169,7 @@ pub mod tests { let nonces = vec![0_u128.into(), 0_u128.into()]; let instruction = 1337; let message = Message::try_new( - Program::authenticated_transfer_program().id(), + crate::test_methods::simple_balance_transfer().id(), vec![addr1, addr1], nonces, instruction, @@ -188,7 +189,7 @@ pub mod tests { let nonces = vec![0_u128.into()]; let instruction = 1337; let message = Message::try_new( - Program::authenticated_transfer_program().id(), + crate::test_methods::simple_balance_transfer().id(), vec![addr1, addr2], nonces, instruction, @@ -208,7 +209,7 @@ pub mod tests { let nonces = vec![0_u128.into(), 0_u128.into()]; let instruction = 1337; let message = Message::try_new( - Program::authenticated_transfer_program().id(), + crate::test_methods::simple_balance_transfer().id(), vec![addr1, addr2], nonces, instruction, @@ -229,7 +230,7 @@ pub mod tests { let nonces = vec![0_u128.into(), 1_u128.into()]; let instruction = 1337; let message = Message::try_new( - Program::authenticated_transfer_program().id(), + crate::test_methods::simple_balance_transfer().id(), vec![addr1, addr2], nonces, instruction, @@ -242,6 +243,21 @@ pub mod tests { assert!(matches!(result, Err(LeeError::InvalidInput(_)))); } + #[test] + fn empty_transaction_is_rejected() { + let state = state_for_tests(); + let message = Message::new_preserialized( + crate::test_methods::simple_balance_transfer().id(), + vec![], + vec![], + vec![0; 4], + ); + let witness_set = WitnessSet::from_raw_parts(vec![]); + let tx = PublicTransaction::new(message, witness_set); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); + assert!(matches!(result, Err(LeeError::InvalidInput(_)))); + } + #[test] fn program_id_must_belong_to_bulitin_program_ids() { let (key1, key2, addr1, addr2) = keys_for_tests(); diff --git a/lee/state_machine/src/state.rs b/lee/state_machine/src/state.rs deleted file mode 100644 index becb14ff..00000000 --- a/lee/state_machine/src/state.rs +++ /dev/null @@ -1,4751 +0,0 @@ -use std::collections::{BTreeSet, HashMap, HashSet}; - -use borsh::{BorshDeserialize, BorshSerialize}; -use clock_core::ClockAccountData; -pub use clock_core::{ - CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, - CLOCK_PROGRAM_ACCOUNT_IDS, -}; -use lee_core::{ - BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, - Timestamp, - account::{Account, AccountId, Nonce}, - program::ProgramId, -}; - -use crate::{ - error::LeeError, - merkle_tree::MerkleTree, - privacy_preserving_transaction::PrivacyPreservingTransaction, - program::Program, - program_deployment_transaction::ProgramDeploymentTransaction, - public_transaction::PublicTransaction, - validated_state_diff::{StateDiff, ValidatedStateDiff}, -}; - -pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; - -#[derive(Clone, BorshSerialize, BorshDeserialize)] -#[cfg_attr(test, derive(Debug, PartialEq, Eq))] -pub struct CommitmentSet { - merkle_tree: MerkleTree, - commitments: HashMap, - root_history: HashSet, -} - -impl CommitmentSet { - pub(crate) fn digest(&self) -> CommitmentSetDigest { - self.merkle_tree.root() - } - - /// Queries the `CommitmentSet` for a membership proof of commitment. - pub fn get_proof_for(&self, commitment: &Commitment) -> Option { - let index = *self.commitments.get(commitment)?; - - self.merkle_tree - .get_authentication_path_for(index) - .map(|path| (index, path)) - } - - /// Inserts a list of commitments to the `CommitmentSet`. - pub(crate) fn extend(&mut self, commitments: &[Commitment]) { - for commitment in commitments.iter().cloned() { - let index = self.merkle_tree.insert(commitment.to_byte_array()); - self.commitments.insert(commitment, index); - } - self.root_history.insert(self.digest()); - } - - fn contains(&self, commitment: &Commitment) -> bool { - self.commitments.contains_key(commitment) - } - - /// Initializes an empty `CommitmentSet` with a given capacity. - /// If the capacity is not a `power_of_two`, then capacity is taken - /// to be the next `power_of_two`. - pub(crate) fn with_capacity(capacity: usize) -> Self { - Self { - merkle_tree: MerkleTree::with_capacity(capacity), - commitments: HashMap::new(), - root_history: HashSet::new(), - } - } -} - -#[cfg_attr(test, derive(Debug, PartialEq, Eq))] -#[derive(Clone)] -struct NullifierSet(BTreeSet); - -impl NullifierSet { - const fn new() -> Self { - Self(BTreeSet::new()) - } - - fn extend(&mut self, new_nullifiers: &[Nullifier]) { - self.0.extend(new_nullifiers); - } - - fn contains(&self, nullifier: &Nullifier) -> bool { - self.0.contains(nullifier) - } -} - -impl BorshSerialize for NullifierSet { - fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - self.0.iter().collect::>().serialize(writer) - } -} - -impl BorshDeserialize for NullifierSet { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - let vec = Vec::::deserialize_reader(reader)?; - - let mut set = BTreeSet::new(); - for n in vec { - if !set.insert(n) { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "duplicate nullifier in NullifierSet", - )); - } - } - - Ok(Self(set)) - } -} - -#[derive(Clone, BorshSerialize, BorshDeserialize)] -#[cfg_attr(test, derive(Debug, PartialEq, Eq))] -pub struct V03State { - public_state: HashMap, - private_state: (CommitmentSet, NullifierSet), - programs: HashMap, -} - -impl Default for V03State { - fn default() -> Self { - let faucet_account_id = system_faucet_account_id(); - let faucet_account = system_faucet_account(); - let bridge_account_id = system_bridge_account_id(); - let bridge_account = system_bridge_account(); - let mut public_state = HashMap::new(); - public_state.insert(faucet_account_id, faucet_account); - public_state.insert(bridge_account_id, bridge_account); - - Self { - public_state, - private_state: (CommitmentSet::with_capacity(32), NullifierSet::new()), - programs: HashMap::new(), - } - } -} - -impl V03State { - #[must_use] - pub fn new() -> Self { - Self::default() - } - - #[must_use] - pub fn new_with_genesis_accounts( - initial_data: &[(AccountId, u128)], - initial_private_accounts: Vec<(Commitment, Nullifier)>, - genesis_timestamp: lee_core::Timestamp, - ) -> Self { - let faucet_account_id = system_faucet_account_id(); - let bridge_account_id = system_bridge_account_id(); - let authenticated_transfer_program = Program::authenticated_transfer_program(); - let mut public_state: HashMap<_, _> = initial_data - .iter() - .copied() - .map(|(account_id, balance)| { - let account = Account { - balance, - program_owner: authenticated_transfer_program.id(), - ..Account::default() - }; - (account_id, account) - }) - .collect(); - let faucet_account = system_faucet_account(); - let bridge_account = system_bridge_account(); - public_state.insert(faucet_account_id, faucet_account); - public_state.insert(bridge_account_id, bridge_account); - - let mut commitment_set = CommitmentSet::with_capacity(32); - commitment_set.extend(&[DUMMY_COMMITMENT]); - let (commitments, nullifiers): (Vec, Vec) = - initial_private_accounts.into_iter().unzip(); - commitment_set.extend(&commitments); - let mut nullifier_set = NullifierSet::new(); - nullifier_set.extend(&nullifiers); - let private_state = (commitment_set, nullifier_set); - - let mut this = Self { - public_state, - private_state, - programs: HashMap::new(), - }; - - this.insert_program(Program::clock()); - this.insert_clock_accounts(genesis_timestamp); - - this.insert_program(Program::authenticated_transfer_program()); - this.insert_program(Program::token()); - this.insert_program(Program::amm()); - this.insert_program(Program::ata()); - this.insert_program(Program::vault()); - this.insert_program(Program::faucet()); - this.insert_program(Program::bridge()); - - this - } - - fn insert_clock_accounts(&mut self, genesis_timestamp: lee_core::Timestamp) { - let data = ClockAccountData { - block_id: 0, - timestamp: genesis_timestamp, - } - .to_bytes(); - let clock_program_id = Program::clock().id(); - for account_id in CLOCK_PROGRAM_ACCOUNT_IDS { - self.public_state.insert( - account_id, - Account { - program_owner: clock_program_id, - data: data - .clone() - .try_into() - .expect("Clock account data should fit within accounts data"), - ..Account::default() - }, - ); - } - } - - pub(crate) fn insert_program(&mut self, program: Program) { - self.programs.insert(program.id(), program); - } - - pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) { - let StateDiff { - signer_account_ids, - public_diff, - new_commitments, - new_nullifiers, - program, - } = diff.into_state_diff(); - #[expect( - clippy::iter_over_hash_type, - reason = "Iteration order doesn't matter here" - )] - for (account_id, account) in public_diff { - *self.get_account_by_id_mut(account_id) = account; - } - for account_id in signer_account_ids { - self.get_account_by_id_mut(account_id) - .nonce - .public_account_nonce_increment(); - } - self.private_state.0.extend(&new_commitments); - self.private_state.1.extend(&new_nullifiers); - if let Some(program) = program { - self.insert_program(program); - } - } - - pub fn transition_from_public_transaction( - &mut self, - tx: &PublicTransaction, - block_id: BlockId, - timestamp: Timestamp, - ) -> Result<(), LeeError> { - let diff = ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp)?; - self.apply_state_diff(diff); - Ok(()) - } - - pub fn transition_from_privacy_preserving_transaction( - &mut self, - tx: &PrivacyPreservingTransaction, - block_id: BlockId, - timestamp: Timestamp, - ) -> Result<(), LeeError> { - let diff = - ValidatedStateDiff::from_privacy_preserving_transaction(tx, self, block_id, timestamp)?; - self.apply_state_diff(diff); - Ok(()) - } - - pub fn transition_from_program_deployment_transaction( - &mut self, - tx: &ProgramDeploymentTransaction, - ) -> Result<(), LeeError> { - let diff = ValidatedStateDiff::from_program_deployment_transaction(tx, self)?; - self.apply_state_diff(diff); - Ok(()) - } - - fn get_account_by_id_mut(&mut self, account_id: AccountId) -> &mut Account { - self.public_state.entry(account_id).or_default() - } - - #[must_use] - pub fn get_account_by_id(&self, account_id: AccountId) -> Account { - self.public_state - .get(&account_id) - .cloned() - .unwrap_or_else(Account::default) - } - - #[must_use] - pub fn get_proof_for_commitment(&self, commitment: &Commitment) -> Option { - self.private_state.0.get_proof_for(commitment) - } - - pub(crate) const fn programs(&self) -> &HashMap { - &self.programs - } - - #[must_use] - pub fn commitment_set_digest(&self) -> CommitmentSetDigest { - self.private_state.0.digest() - } - - pub(crate) fn check_commitments_are_new( - &self, - new_commitments: &[Commitment], - ) -> Result<(), LeeError> { - for commitment in new_commitments { - if self.private_state.0.contains(commitment) { - return Err(LeeError::InvalidInput("Commitment already seen".to_owned())); - } - } - Ok(()) - } - - pub(crate) fn check_nullifiers_are_valid( - &self, - new_nullifiers: &[(Nullifier, CommitmentSetDigest)], - ) -> Result<(), LeeError> { - for (nullifier, digest) in new_nullifiers { - if self.private_state.1.contains(nullifier) { - return Err(LeeError::InvalidInput("Nullifier already seen".to_owned())); - } - if !self.private_state.0.root_history.contains(digest) { - return Err(LeeError::InvalidInput( - "Unrecognized commitment set digest".to_owned(), - )); - } - } - Ok(()) - } -} - -// TODO: Testnet only. Refactor to prevent compilation on mainnet. -impl V03State { - pub fn add_pinata_program(&mut self, account_id: AccountId) { - self.insert_program(Program::pinata()); - - self.public_state.insert( - account_id, - Account { - program_owner: Program::pinata().id(), - balance: 1_500_000, - // Difficulty: 3 - data: vec![3; 33].try_into().expect("should fit"), - nonce: Nonce::default(), - }, - ); - } - - pub fn add_pinata_token_program(&mut self, account_id: AccountId) { - self.insert_program(Program::pinata_token()); - - self.public_state.insert( - account_id, - Account { - program_owner: Program::pinata_token().id(), - // Difficulty: 3 - data: vec![3; 33].try_into().expect("should fit"), - ..Account::default() - }, - ); - } -} - -#[cfg(any(test, feature = "test-utils"))] -impl V03State { - pub fn force_insert_account(&mut self, account_id: AccountId, account: Account) { - self.public_state.insert(account_id, account); - } -} - -fn system_faucet_account() -> Account { - Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: u128::MAX, - ..Account::default() - } -} - -fn system_bridge_account() -> Account { - Account { - program_owner: Program::authenticated_transfer_program().id(), - ..Account::default() - } -} - -#[must_use] -pub fn system_faucet_account_id() -> AccountId { - faucet_core::compute_faucet_account_id(Program::faucet().id()) -} - -#[must_use] -pub fn system_bridge_account_id() -> AccountId { - bridge_core::compute_bridge_account_id(Program::bridge().id()) -} - -#[cfg(test)] -pub mod tests { - #![expect( - clippy::arithmetic_side_effects, - clippy::shadow_unrelated, - reason = "We don't care about it in tests" - )] - - use std::collections::HashMap; - - use authenticated_transfer_core::Instruction as AuthTransferInstruction; - use lee_core::{ - BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey, - NullifierSecretKey, Timestamp, - account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, - encryption::ViewingPublicKey, - program::{ - BlockValidityWindow, ExecutionValidationError, PdaSeed, ProgramId, - TimestampValidityWindow, WrappedBalanceSum, - }, - }; - - use crate::{ - PublicKey, PublicTransaction, V03State, - error::{InvalidProgramBehaviorError, LeeError}, - execute_and_prove, - privacy_preserving_transaction::{ - PrivacyPreservingTransaction, - circuit::{self, ProgramWithDependencies}, - message::Message, - witness_set::WitnessSet, - }, - program::Program, - public_transaction, - signature::PrivateKey, - state::{ - CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, - CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS, system_bridge_account, - system_faucet_account, - }, - system_bridge_account_id, system_faucet_account_id, - }; - - impl V03State { - /// Include test programs in the builtin programs map. - #[must_use] - pub fn with_test_programs(mut self) -> Self { - self.insert_program(Program::nonce_changer_program()); - self.insert_program(Program::extra_output_program()); - self.insert_program(Program::missing_output_program()); - self.insert_program(Program::program_owner_changer()); - self.insert_program(Program::simple_balance_transfer()); - self.insert_program(Program::data_changer()); - self.insert_program(Program::minter()); - self.insert_program(Program::burner()); - self.insert_program(Program::chain_caller()); - self.insert_program(Program::amm()); - self.insert_program(Program::claimer()); - self.insert_program(Program::changer_claimer()); - self.insert_program(Program::validity_window()); - self.insert_program(Program::flash_swap_initiator()); - self.insert_program(Program::flash_swap_callback()); - self.insert_program(Program::malicious_self_program_id()); - self.insert_program(Program::malicious_caller_program_id()); - self.insert_program(Program::time_locked_transfer()); - self.insert_program(Program::pinata_cooldown()); - self - } - - #[must_use] - pub fn with_non_default_accounts_but_default_program_owners(mut self) -> Self { - let account_with_default_values_except_balance = Account { - balance: 100, - ..Account::default() - }; - let account_with_default_values_except_nonce = Account { - nonce: Nonce(37), - ..Account::default() - }; - let account_with_default_values_except_data = Account { - data: vec![0xca, 0xfe].try_into().unwrap(), - ..Account::default() - }; - self.force_insert_account( - AccountId::new([255; 32]), - account_with_default_values_except_balance, - ); - self.force_insert_account( - AccountId::new([254; 32]), - account_with_default_values_except_nonce, - ); - self.force_insert_account( - AccountId::new([253; 32]), - account_with_default_values_except_data, - ); - self - } - - #[must_use] - pub fn with_account_owned_by_burner_program(mut self) -> Self { - let account = Account { - program_owner: Program::burner().id(), - balance: 100, - ..Default::default() - }; - self.force_insert_account(AccountId::new([252; 32]), account); - self - } - - #[must_use] - pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self { - let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), 0); - let commitment = Commitment::new(&account_id, account); - self.private_state.0.extend(&[commitment]); - self - } - } - - pub struct TestPublicKeys { - pub signing_key: PrivateKey, - } - - impl TestPublicKeys { - pub fn account_id(&self) -> AccountId { - AccountId::from(&PublicKey::new_from_private_key(&self.signing_key)) - } - } - - pub struct TestPrivateKeys { - pub nsk: NullifierSecretKey, - pub d: [u8; 32], - pub z: [u8; 32], - } - - impl TestPrivateKeys { - pub fn npk(&self) -> NullifierPublicKey { - NullifierPublicKey::from(&self.nsk) - } - - pub fn vpk(&self) -> ViewingPublicKey { - ViewingPublicKey::from_seed(&self.d, &self.z) - } - } - - // โ”€โ”€ Flash Swap types (mirrors of guest types for host-side serialisation) โ”€โ”€ - - #[derive(serde::Serialize, serde::Deserialize)] - struct CallbackInstruction { - return_funds: bool, - token_program_id: ProgramId, - amount: u128, - } - - #[derive(serde::Serialize, serde::Deserialize)] - enum FlashSwapInstruction { - Initiate { - token_program_id: ProgramId, - callback_program_id: ProgramId, - amount_out: u128, - callback_instruction_data: Vec, - }, - InvariantCheck { - min_vault_balance: u128, - }, - } - - fn transfer_transaction( - from: AccountId, - from_key: &PrivateKey, - from_nonce: u128, - to: AccountId, - to_key: &PrivateKey, - to_nonce: u128, - balance: u128, - ) -> PublicTransaction { - let account_ids = vec![from, to]; - let nonces = vec![Nonce(from_nonce), Nonce(to_nonce)]; - let program_id = Program::authenticated_transfer_program().id(); - let message = public_transaction::Message::try_new( - program_id, - account_ids, - nonces, - AuthTransferInstruction::Transfer { amount: balance }, - ) - .unwrap(); - let witness_set = - public_transaction::WitnessSet::for_message(&message, &[from_key, to_key]); - PublicTransaction::new(message, witness_set) - } - - fn build_flash_swap_tx( - initiator: &Program, - vault_id: AccountId, - receiver_id: AccountId, - instruction: FlashSwapInstruction, - ) -> PublicTransaction { - let message = public_transaction::Message::try_new( - initiator.id(), - vec![vault_id, receiver_id], - vec![], // no signers โ€” vault is PDA-authorised - instruction, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - PublicTransaction::new(message, witness_set) - } - - #[test] - fn genesis_system_accounts_have_expected_contents() { - // System-account IDs must be distinct and non-default, and the genesis - // faucet/bridge accounts must carry their expected field values. Catches - // mutations that replace `system_faucet_account`/`system_bridge_account` - // with `Default::default()`, delete their `balance`/`program_owner` - // fields, or replace `system_bridge_account_id` with `Default::default()`. - let faucet_id = system_faucet_account_id(); - let bridge_id = system_bridge_account_id(); - assert_ne!(bridge_id, AccountId::default()); - assert_ne!(faucet_id, bridge_id); - - let state = V03State::new_with_genesis_accounts(&[], vec![], 0); - let default_owner = Account::default().program_owner; - - let faucet = state.get_account_by_id(faucet_id); - assert_eq!(faucet.balance, u128::MAX, "faucet must hold u128::MAX"); - assert_ne!( - faucet.program_owner, default_owner, - "faucet must have a non-default program_owner" - ); - - let bridge = state.get_account_by_id(bridge_id); - assert_ne!( - bridge.program_owner, default_owner, - "bridge must have a non-default program_owner" - ); - } - - #[test] - fn genesis_commitment_set_digest_differs_from_empty_state() { - // The genesis state inserts DUMMY_COMMITMENT, so its commitment-set digest - // must differ from a freshly-created empty state's all-zero root. Catches - // the mutation that replaces `commitment_set_digest` with `Default::default()`. - let genesis = V03State::new_with_genesis_accounts(&[], vec![], 0); - let empty = V03State::new(); - assert_ne!( - genesis.commitment_set_digest(), - empty.commitment_set_digest() - ); - } - - #[test] - fn new_with_genesis() { - let key1 = PrivateKey::try_new([1; 32]).unwrap(); - let key2 = PrivateKey::try_new([2; 32]).unwrap(); - let addr1 = AccountId::from(&PublicKey::new_from_private_key(&key1)); - let addr2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); - let initial_data = [(addr1, 100_u128), (addr2, 151_u128)]; - let authenticated_transfers_program = Program::authenticated_transfer_program(); - let clock_program = Program::clock(); - let expected_public_state = { - let mut this = HashMap::new(); - this.insert( - addr1, - Account { - balance: 100, - program_owner: authenticated_transfers_program.id(), - ..Account::default() - }, - ); - this.insert( - addr2, - Account { - balance: 151, - program_owner: authenticated_transfers_program.id(), - ..Account::default() - }, - ); - this.insert(system_faucet_account_id(), system_faucet_account()); - this.insert(system_bridge_account_id(), system_bridge_account()); - for account_id in CLOCK_PROGRAM_ACCOUNT_IDS { - this.insert( - account_id, - Account { - program_owner: clock_program.id(), - data: [0_u8; 16].to_vec().try_into().unwrap(), - ..Account::default() - }, - ); - } - this - }; - let expected_builtin_programs = { - let mut this = HashMap::new(); - this.insert( - authenticated_transfers_program.id(), - authenticated_transfers_program, - ); - this.insert(clock_program.id(), clock_program); - this.insert(Program::token().id(), Program::token()); - this.insert(Program::amm().id(), Program::amm()); - this.insert(Program::ata().id(), Program::ata()); - this.insert(Program::vault().id(), Program::vault()); - this.insert(Program::faucet().id(), Program::faucet()); - this.insert(Program::bridge().id(), Program::bridge()); - this - }; - - let state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); - - assert_eq!(state.public_state, expected_public_state); - assert_eq!(state.programs, expected_builtin_programs); - } - - #[test] - fn new_with_genesis_includes_nullifiers_for_private_accounts() { - let keys1 = test_private_account_keys_1(); - let keys2 = test_private_account_keys_2(); - - let account = Account { - balance: 100, - program_owner: Program::authenticated_transfer_program().id(), - ..Account::default() - }; - - let account_id1 = AccountId::for_regular_private_account(&keys1.npk(), &keys1.vpk(), 0); - let account_id2 = AccountId::for_regular_private_account(&keys2.npk(), &keys2.vpk(), 0); - - let init_commitment1 = Commitment::new(&account_id1, &account); - let init_commitment2 = Commitment::new(&account_id2, &account); - let init_nullifier1 = Nullifier::for_account_initialization(&account_id1); - let init_nullifier2 = Nullifier::for_account_initialization(&account_id2); - - let initial_private_accounts = vec![ - (init_commitment1, init_nullifier1), - (init_commitment2, init_nullifier2), - ]; - - let state = V03State::new_with_genesis_accounts(&[], initial_private_accounts, 0); - - assert!(state.private_state.1.contains(&init_nullifier1)); - assert!(state.private_state.1.contains(&init_nullifier2)); - } - - #[test] - fn insert_program() { - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - let program_to_insert = Program::simple_balance_transfer(); - let program_id = program_to_insert.id(); - assert!(!state.programs.contains_key(&program_id)); - - state.insert_program(program_to_insert); - - assert!(state.programs.contains_key(&program_id)); - } - - #[test] - fn get_account_by_account_id_non_default_account() { - let key = PrivateKey::try_new([1; 32]).unwrap(); - let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); - let initial_data = [(account_id, 100_u128)]; - let state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); - let expected_account = &state.public_state[&account_id]; - - let account = state.get_account_by_id(account_id); - - assert_eq!(&account, expected_account); - } - - #[test] - fn get_account_by_account_id_default_account() { - let addr2 = AccountId::new([0; 32]); - let state = V03State::new_with_genesis_accounts(&[], vec![], 0); - let expected_account = Account::default(); - - let account = state.get_account_by_id(addr2); - - assert_eq!(account, expected_account); - } - - #[test] - fn builtin_programs_getter() { - let state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - let builtin_programs = state.programs(); - - assert_eq!(builtin_programs, &state.programs); - } - - #[test] - fn transition_from_authenticated_transfer_program_invocation_default_account_destination() { - let key = PrivateKey::try_new([1; 32]).unwrap(); - let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); - let initial_data = [(account_id, 100)]; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); - let from = account_id; - let to_key = PrivateKey::try_new([2; 32]).unwrap(); - let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); - assert_eq!(state.get_account_by_id(to), Account::default()); - let balance_to_move = 5; - - let tx = transfer_transaction(from, &key, 0, to, &to_key, 0, balance_to_move); - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - - assert_eq!(state.get_account_by_id(from).balance, 95); - assert_eq!(state.get_account_by_id(to).balance, 5); - assert_eq!(state.get_account_by_id(from).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(to).nonce, Nonce(1)); - } - - #[test] - fn transition_from_authenticated_transfer_program_invocation_insuficient_balance() { - let key = PrivateKey::try_new([1; 32]).unwrap(); - let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); - let initial_data = [(account_id, 100)]; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); - let from = account_id; - let from_key = key; - let to_key = PrivateKey::try_new([2; 32]).unwrap(); - let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); - let balance_to_move = 101; - assert!(state.get_account_by_id(from).balance < balance_to_move); - - let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move); - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!(result, Err(LeeError::ProgramExecutionFailed(_)))); - assert_eq!(state.get_account_by_id(from).balance, 100); - assert_eq!(state.get_account_by_id(to).balance, 0); - assert_eq!(state.get_account_by_id(from).nonce, Nonce(0)); - assert_eq!(state.get_account_by_id(to).nonce, Nonce(0)); - } - - #[test] - fn transition_from_authenticated_transfer_program_invocation_non_default_account_destination() { - let key1 = PrivateKey::try_new([1; 32]).unwrap(); - let key2 = PrivateKey::try_new([2; 32]).unwrap(); - let account_id1 = AccountId::from(&PublicKey::new_from_private_key(&key1)); - let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); - let initial_data = [(account_id1, 100), (account_id2, 200)]; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); - let from = account_id2; - let from_key = key2; - let to = account_id1; - let to_key = key1; - assert_ne!(state.get_account_by_id(to), Account::default()); - let balance_to_move = 8; - - let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move); - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - - assert_eq!(state.get_account_by_id(from).balance, 192); - assert_eq!(state.get_account_by_id(to).balance, 108); - assert_eq!(state.get_account_by_id(from).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(to).nonce, Nonce(1)); - } - - #[test] - fn transition_from_sequence_of_authenticated_transfer_program_invocations() { - let key1 = PrivateKey::try_new([8; 32]).unwrap(); - let account_id1 = AccountId::from(&PublicKey::new_from_private_key(&key1)); - let key2 = PrivateKey::try_new([2; 32]).unwrap(); - let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); - let initial_data = [(account_id1, 100)]; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); - let key3 = PrivateKey::try_new([3; 32]).unwrap(); - let account_id3 = AccountId::from(&PublicKey::new_from_private_key(&key3)); - let balance_to_move = 5; - - let tx = transfer_transaction( - account_id1, - &key1, - 0, - account_id2, - &key2, - 0, - balance_to_move, - ); - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - let balance_to_move = 3; - let tx = transfer_transaction( - account_id2, - &key2, - 1, - account_id3, - &key3, - 0, - balance_to_move, - ); - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - - assert_eq!(state.get_account_by_id(account_id1).balance, 95); - assert_eq!(state.get_account_by_id(account_id2).balance, 2); - assert_eq!(state.get_account_by_id(account_id3).balance, 3); - assert_eq!(state.get_account_by_id(account_id1).nonce, Nonce(1)); - assert_eq!(state.get_account_by_id(account_id2).nonce, Nonce(2)); - assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(1)); - } - - fn clock_transaction(timestamp: lee_core::Timestamp) -> PublicTransaction { - let message = public_transaction::Message::try_new( - Program::clock().id(), - CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(), - vec![], - timestamp, - ) - .unwrap(); - PublicTransaction::new( - message, - public_transaction::WitnessSet::from_raw_parts(vec![]), - ) - } - - fn clock_account_data(state: &V03State, account_id: AccountId) -> (u64, lee_core::Timestamp) { - let data = state.get_account_by_id(account_id).data.into_inner(); - let parsed = clock_core::ClockAccountData::from_bytes(&data); - (parsed.block_id, parsed.timestamp) - } - - #[test] - fn clock_genesis_state_has_zero_block_id_and_genesis_timestamp() { - let genesis_timestamp = 1_000_000_u64; - let state = V03State::new_with_genesis_accounts(&[], vec![], genesis_timestamp); - - let (block_id, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); - - assert_eq!(block_id, 0); - assert_eq!(timestamp, genesis_timestamp); - } - - #[test] - fn clock_invocation_increments_block_id() { - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - let tx = clock_transaction(1234); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); - - let (block_id, _) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); - assert_eq!(block_id, 1); - } - - #[test] - fn clock_invocation_stores_timestamp_from_instruction() { - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - let block_timestamp = 1_700_000_000_000_u64; - - let tx = clock_transaction(block_timestamp); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); - - let (_, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); - assert_eq!(timestamp, block_timestamp); - } - - #[test] - fn clock_invocation_sequence_correctly_increments_block_id() { - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - for expected_block_id in 1_u64..=5 { - let tx = clock_transaction(expected_block_id * 1000); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); - - let (block_id, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); - assert_eq!(block_id, expected_block_id); - assert_eq!(timestamp, expected_block_id * 1000); - } - } - - #[test] - fn clock_10_account_not_updated_when_block_id_not_multiple_of_10() { - let genesis_timestamp = 0_u64; - let mut state = V03State::new_with_genesis_accounts(&[], vec![], genesis_timestamp); - - // Run 9 clock ticks (block_ids 1..=9), none of which are multiples of 10. - for tick in 1_u64..=9 { - let tx = clock_transaction(tick * 1000); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); - } - - let (block_id_10, timestamp_10) = clock_account_data(&state, CLOCK_10_PROGRAM_ACCOUNT_ID); - // The 10-block account should still reflect genesis state. - assert_eq!(block_id_10, 0); - assert_eq!(timestamp_10, genesis_timestamp); - } - - #[test] - fn clock_10_account_updated_when_block_id_is_multiple_of_10() { - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - // Run 10 clock ticks so block_id reaches 10. - for tick in 1_u64..=10 { - let tx = clock_transaction(tick * 1000); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); - } - - let (block_id_1, timestamp_1) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); - let (block_id_10, timestamp_10) = clock_account_data(&state, CLOCK_10_PROGRAM_ACCOUNT_ID); - assert_eq!(block_id_1, 10); - assert_eq!(block_id_10, 10); - assert_eq!(timestamp_10, timestamp_1); - } - - #[test] - fn clock_50_account_only_updated_at_multiples_of_50() { - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - // After 49 ticks the 50-block account should be unchanged. - for tick in 1_u64..=49 { - let tx = clock_transaction(tick * 1000); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); - } - let (block_id_50, _) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID); - assert_eq!(block_id_50, 0); - - // Tick 50 โ€” now the 50-block account should update. - let tx = clock_transaction(50 * 1000); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); - let (block_id_50, timestamp_50) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID); - assert_eq!(block_id_50, 50); - assert_eq!(timestamp_50, 50 * 1000); - } - - #[test] - fn all_three_clock_accounts_updated_at_multiple_of_50() { - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - // Advance to block 50 (a multiple of both 10 and 50). - for tick in 1_u64..=50 { - let tx = clock_transaction(tick * 1000); - state.transition_from_public_transaction(&tx, 0, 0).unwrap(); - } - - let (block_id_1, ts_1) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); - let (block_id_10, ts_10) = clock_account_data(&state, CLOCK_10_PROGRAM_ACCOUNT_ID); - let (block_id_50, ts_50) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID); - - assert_eq!(block_id_1, 50); - assert_eq!(block_id_10, 50); - assert_eq!(block_id_50, 50); - assert_eq!(ts_1, ts_10); - assert_eq!(ts_1, ts_50); - } - - #[test] - fn program_should_fail_if_modifies_nonces() { - let account_id = AccountId::new([1; 32]); - let initial_data = [(account_id, 100)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let account_ids = vec![account_id]; - let program_id = Program::nonce_changer_program().id(); - let message = - public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior( - InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::ModifiedNonce { account_id: err_account_id } - ) - )) if err_account_id == account_id - )); - } - - #[test] - fn program_should_fail_if_output_accounts_exceed_inputs() { - let initial_data = [(AccountId::new([1; 32]), 100)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let account_ids = vec![AccountId::new([1; 32])]; - let program_id = Program::extra_output_program().id(); - let message = - public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior( - InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::MismatchedPreStatePostStateLength { - pre_state_length, - post_state_length - } - ) - )) if pre_state_length == 1 && post_state_length == 2 - )); - } - - #[test] - fn program_should_fail_with_missing_output_accounts() { - let initial_data = [(AccountId::new([1; 32]), 100)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let account_ids = vec![AccountId::new([1; 32]), AccountId::new([2; 32])]; - let program_id = Program::missing_output_program().id(); - let message = - public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior( - InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::MismatchedPreStatePostStateLength { - pre_state_length, - post_state_length - } - ) - )) if pre_state_length == 2 && post_state_length == 1 - )); - } - - #[test] - fn program_should_fail_if_modifies_program_owner_with_only_non_default_program_owner() { - let initial_data = [(AccountId::new([1; 32]), 0)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let account_id = AccountId::new([1; 32]); - let account = state.get_account_by_id(account_id); - // Assert the target account only differs from the default account in the program owner - // field - assert_ne!(account.program_owner, Account::default().program_owner); - assert_eq!(account.balance, Account::default().balance); - assert_eq!(account.nonce, Account::default().nonce); - assert_eq!(account.data, Account::default().data); - let program_id = Program::program_owner_changer().id(); - let message = - public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } - ))) if err_account_id == account_id - )); - } - - #[test] - fn program_should_fail_if_modifies_program_owner_with_only_non_default_balance() { - let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0) - .with_test_programs() - .with_non_default_accounts_but_default_program_owners(); - let account_id = AccountId::new([255; 32]); - let account = state.get_account_by_id(account_id); - // Assert the target account only differs from the default account in balance field - assert_eq!(account.program_owner, Account::default().program_owner); - assert_ne!(account.balance, Account::default().balance); - assert_eq!(account.nonce, Account::default().nonce); - assert_eq!(account.data, Account::default().data); - let program_id = Program::program_owner_changer().id(); - let message = - public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } - ))) if err_account_id == account_id - )); - } - - #[test] - fn program_should_fail_if_modifies_program_owner_with_only_non_default_nonce() { - let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0) - .with_test_programs() - .with_non_default_accounts_but_default_program_owners(); - let account_id = AccountId::new([254; 32]); - let account = state.get_account_by_id(account_id); - // Assert the target account only differs from the default account in nonce field - assert_eq!(account.program_owner, Account::default().program_owner); - assert_eq!(account.balance, Account::default().balance); - assert_ne!(account.nonce, Account::default().nonce); - assert_eq!(account.data, Account::default().data); - let program_id = Program::program_owner_changer().id(); - let message = - public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } - ))) if err_account_id == account_id - )); - } - - #[test] - fn program_should_fail_if_modifies_program_owner_with_only_non_default_data() { - let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0) - .with_test_programs() - .with_non_default_accounts_but_default_program_owners(); - let account_id = AccountId::new([253; 32]); - let account = state.get_account_by_id(account_id); - // Assert the target account only differs from the default account in data field - assert_eq!(account.program_owner, Account::default().program_owner); - assert_eq!(account.balance, Account::default().balance); - assert_eq!(account.nonce, Account::default().nonce); - assert_ne!(account.data, Account::default().data); - let program_id = Program::program_owner_changer().id(); - let message = - public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } - ))) if err_account_id == account_id - )); - } - - #[test] - fn program_should_fail_if_transfers_balance_from_non_owned_account() { - let sender_account_id = AccountId::new([1; 32]); - let receiver_account_id = AccountId::new([2; 32]); - let initial_data = [(sender_account_id, 100)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let balance_to_move: u128 = 1; - let program_id = Program::simple_balance_transfer().id(); - assert_ne!( - state.get_account_by_id(sender_account_id).program_owner, - program_id - ); - let message = public_transaction::Message::try_new( - program_id, - vec![sender_account_id, receiver_account_id], - vec![], - balance_to_move, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::UnauthorizedBalanceDecrease { account_id: err_account_id, owner_program_id, executing_program_id } - ))) if err_account_id == sender_account_id && owner_program_id != program_id && executing_program_id == program_id - )); - } - - #[test] - fn program_should_fail_if_modifies_data_of_non_owned_account() { - let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0) - .with_test_programs() - .with_non_default_accounts_but_default_program_owners(); - let account_id = AccountId::new([255; 32]); - let program_id = Program::data_changer().id(); - - assert_ne!(state.get_account_by_id(account_id), Account::default()); - assert_ne!( - state.get_account_by_id(account_id).program_owner, - program_id - ); - let message = - public_transaction::Message::try_new(program_id, vec![account_id], vec![], vec![0]) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::UnauthorizedDataModification { account_id: err_account_id, executing_program_id } - ))) if err_account_id == account_id && executing_program_id == program_id - )); - } - - #[test] - fn program_should_fail_if_does_not_preserve_total_balance_by_minting() { - let initial_data = []; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let account_id = AccountId::new([1; 32]); - let program_id = Program::minter().id(); - - let message = - public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 2, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states } - ))) if total_balance_pre_states == 0.into() && total_balance_post_states == 1.into() - )); - } - - #[test] - fn program_should_fail_if_does_not_preserve_total_balance_by_burning() { - let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0) - .with_test_programs() - .with_account_owned_by_burner_program(); - let program_id = Program::burner().id(); - let account_id = AccountId::new([252; 32]); - assert_eq!( - state.get_account_by_id(account_id).program_owner, - program_id - ); - let balance_to_burn: u128 = 1; - assert!(state.get_account_by_id(account_id).balance > balance_to_burn); - - let message = public_transaction::Message::try_new( - program_id, - vec![account_id], - vec![], - balance_to_burn, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - let result = state.transition_from_public_transaction(&tx, 2, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states } - ))) if total_balance_pre_states == 100.into() && total_balance_post_states == 99.into() - )); - } - - fn test_public_account_keys_1() -> TestPublicKeys { - TestPublicKeys { - signing_key: PrivateKey::try_new([37; 32]).unwrap(), - } - } - - fn test_public_account_keys_2() -> TestPublicKeys { - TestPublicKeys { - signing_key: PrivateKey::try_new([38; 32]).unwrap(), - } - } - - pub fn test_private_account_keys_1() -> TestPrivateKeys { - TestPrivateKeys { - nsk: [13; 32], - d: [31; 32], - z: [32; 32], - } - } - - pub fn test_private_account_keys_2() -> TestPrivateKeys { - TestPrivateKeys { - nsk: [38; 32], - d: [83; 32], - z: [84; 32], - } - } - - fn shielded_balance_transfer_for_tests( - sender_keys: &TestPublicKeys, - recipient_keys: &TestPrivateKeys, - balance_to_move: u128, - state: &V03State, - ) -> PrivacyPreservingTransaction { - let sender = AccountWithMetadata::new( - state.get_account_by_id(sender_keys.account_id()), - true, - sender_keys.account_id(), - ); - - let sender_nonce = sender.account.nonce; - - let recipient = AccountWithMetadata::new( - Account::default(), - false, - (&recipient_keys.npk(), &recipient_keys.vpk(), 0), - ); - - let (output, proof) = circuit::execute_and_prove( - vec![sender, recipient], - Program::serialize_instruction(AuthTransferInstruction::Transfer { - amount: balance_to_move, - }) - .unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivateUnauthorized { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - npk: recipient_keys.npk(), - identifier: 0, - }, - ], - &Program::authenticated_transfer_program().into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![sender_keys.account_id()], - vec![sender_nonce], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[&sender_keys.signing_key]); - PrivacyPreservingTransaction::new(message, witness_set) - } - - fn private_balance_transfer_for_tests( - sender_keys: &TestPrivateKeys, - sender_private_account: &Account, - recipient_keys: &TestPrivateKeys, - balance_to_move: u128, - state: &V03State, - ) -> PrivacyPreservingTransaction { - let program = Program::authenticated_transfer_program(); - let sender_account_id = - AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); - let sender_commitment = Commitment::new(&sender_account_id, sender_private_account); - let sender_pre = AccountWithMetadata::new( - sender_private_account.clone(), - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - let recipient_pre = AccountWithMetadata::new( - Account::default(), - false, - (&recipient_keys.npk(), &recipient_keys.vpk(), 0), - ); - - let (output, proof) = circuit::execute_and_prove( - vec![sender_pre, recipient_pre], - Program::serialize_instruction(AuthTransferInstruction::Transfer { - amount: balance_to_move, - }) - .unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&sender_commitment) - .expect("sender's commitment must be in state"), - identifier: 0, - }, - InputAccountIdentity::PrivateUnauthorized { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - npk: recipient_keys.npk(), - identifier: 0, - }, - ], - &program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - - PrivacyPreservingTransaction::new(message, witness_set) - } - - fn deshielded_balance_transfer_for_tests( - sender_keys: &TestPrivateKeys, - sender_private_account: &Account, - recipient_account_id: &AccountId, - balance_to_move: u128, - state: &V03State, - ) -> PrivacyPreservingTransaction { - let program = Program::authenticated_transfer_program(); - let sender_account_id = - AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); - let sender_commitment = Commitment::new(&sender_account_id, sender_private_account); - let sender_pre = AccountWithMetadata::new( - sender_private_account.clone(), - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - let recipient_pre = AccountWithMetadata::new( - state.get_account_by_id(*recipient_account_id), - false, - *recipient_account_id, - ); - - let (output, proof) = circuit::execute_and_prove( - vec![sender_pre, recipient_pre], - Program::serialize_instruction(AuthTransferInstruction::Transfer { - amount: balance_to_move, - }) - .unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&sender_commitment) - .expect("sender's commitment must be in state"), - identifier: 0, - }, - InputAccountIdentity::Public, - ], - &program.into(), - ) - .unwrap(); - - let message = - Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - - PrivacyPreservingTransaction::new(message, witness_set) - } - - #[test] - fn transition_from_privacy_preserving_transaction_shielded() { - let sender_keys = test_public_account_keys_1(); - let recipient_keys = test_private_account_keys_1(); - - let mut state = - V03State::new_with_genesis_accounts(&[(sender_keys.account_id(), 200)], vec![], 0); - - let balance_to_move = 37; - - let tx = shielded_balance_transfer_for_tests( - &sender_keys, - &recipient_keys, - balance_to_move, - &state, - ); - - let expected_sender_post = { - let mut this = state.get_account_by_id(sender_keys.account_id()); - this.balance -= balance_to_move; - this.nonce.public_account_nonce_increment(); - this - }; - - let [expected_new_commitment] = tx.message().new_commitments.clone().try_into().unwrap(); - assert!(!state.private_state.0.contains(&expected_new_commitment)); - - state - .transition_from_privacy_preserving_transaction(&tx, 1, 0) - .unwrap(); - - let sender_post = state.get_account_by_id(sender_keys.account_id()); - assert_eq!(sender_post, expected_sender_post); - assert!(state.private_state.0.contains(&expected_new_commitment)); - - assert_eq!( - state.get_account_by_id(sender_keys.account_id()).balance, - 200 - balance_to_move - ); - } - - #[test] - fn transition_from_privacy_preserving_transaction_private() { - let sender_keys = test_private_account_keys_1(); - let sender_nonce = Nonce(0xdead_beef); - - let sender_private_account = Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: 100, - nonce: sender_nonce, - data: Data::default(), - }; - let recipient_keys = test_private_account_keys_2(); - - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0) - .with_private_account(&sender_keys, &sender_private_account); - - let balance_to_move = 37; - - let tx = private_balance_transfer_for_tests( - &sender_keys, - &sender_private_account, - &recipient_keys, - balance_to_move, - &state, - ); - - let sender_account_id = - AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); - let recipient_account_id = - AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); - let expected_new_commitment_1 = Commitment::new( - &sender_account_id, - &Account { - program_owner: Program::authenticated_transfer_program().id(), - nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), - balance: sender_private_account.balance - balance_to_move, - data: Data::default(), - }, - ); - - let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account); - let expected_new_nullifier = - Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk); - - let expected_new_commitment_2 = Commitment::new( - &recipient_account_id, - &Account { - program_owner: Program::authenticated_transfer_program().id(), - nonce: Nonce::private_account_nonce_init(&recipient_account_id), - balance: balance_to_move, - ..Account::default() - }, - ); - - let previous_public_state = state.public_state.clone(); - assert!(state.private_state.0.contains(&sender_pre_commitment)); - assert!(!state.private_state.0.contains(&expected_new_commitment_1)); - assert!(!state.private_state.0.contains(&expected_new_commitment_2)); - assert!(!state.private_state.1.contains(&expected_new_nullifier)); - - state - .transition_from_privacy_preserving_transaction(&tx, 1, 0) - .unwrap(); - - assert_eq!(state.public_state, previous_public_state); - assert!(state.private_state.0.contains(&sender_pre_commitment)); - assert!(state.private_state.0.contains(&expected_new_commitment_1)); - assert!(state.private_state.0.contains(&expected_new_commitment_2)); - assert!(state.private_state.1.contains(&expected_new_nullifier)); - } - - fn valid_private_transfer_tx_and_state() -> (V03State, PrivacyPreservingTransaction) { - let sender_keys = test_private_account_keys_1(); - let sender_private_account = Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: 100, - nonce: Nonce(0xdead_beef), - ..Account::default() - }; - let recipient_keys = test_private_account_keys_2(); - let state = V03State::new_with_genesis_accounts(&[], vec![], 0) - .with_private_account(&sender_keys, &sender_private_account); - let tx = private_balance_transfer_for_tests( - &sender_keys, - &sender_private_account, - &recipient_keys, - 37, - &state, - ); - (state, tx) - } - - /// After a valid fully-private tx is proven, tampering with a note's epk should - /// make the shielding proof invalid. - #[test] - fn privacy_tampered_epk_is_rejected() { - use crate::validated_state_diff::ValidatedStateDiff; - - let (state, mut tx) = valid_private_transfer_tx_and_state(); - - // Baseline: the untampered tx verifies - assert!( - ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0).is_ok(), - "the unmodified private transfer must verify" - ); - - // Flip a byte of the first note's epk - tx.message.encrypted_private_post_states[0].epk.0[0] ^= 0xFF; - - assert!( - matches!( - ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0), - Err(LeeError::InvalidPrivacyPreservingProof) - ), - "a tampered epk must be rejected by proof verification" - ); - } - - /// After a valid fully-private tx is proven, tampering with a note's view tag should - /// make the shielding proof invalid. - #[test] - fn privacy_tampered_view_tag_is_rejected() { - use crate::validated_state_diff::ValidatedStateDiff; - - let (state, mut tx) = valid_private_transfer_tx_and_state(); - - // Baseline: the untampered tx verifies. - assert!( - ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0).is_ok(), - "the unmodified private transfer must verify" - ); - - // Flip the first note's view_tag - tx.message.encrypted_private_post_states[0].view_tag ^= 0xFF; - - assert!( - matches!( - ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0), - Err(LeeError::InvalidPrivacyPreservingProof) - ), - "a tampered view_tag must be rejected by proof verification" - ); - } - - #[test] - fn transition_from_privacy_preserving_transaction_deshielded() { - let sender_keys = test_private_account_keys_1(); - let sender_nonce = Nonce(0xdead_beef); - - let sender_private_account = Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: 100, - nonce: sender_nonce, - data: Data::default(), - }; - let recipient_keys = test_public_account_keys_1(); - let recipient_initial_balance = 400; - let mut state = V03State::new_with_genesis_accounts( - &[(recipient_keys.account_id(), recipient_initial_balance)], - vec![], - 0, - ) - .with_private_account(&sender_keys, &sender_private_account); - - let balance_to_move = 37; - - let expected_recipient_post = { - let mut this = state.get_account_by_id(recipient_keys.account_id()); - this.balance += balance_to_move; - this - }; - - let tx = deshielded_balance_transfer_for_tests( - &sender_keys, - &sender_private_account, - &recipient_keys.account_id(), - balance_to_move, - &state, - ); - - let sender_account_id = - AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); - let expected_new_commitment = Commitment::new( - &sender_account_id, - &Account { - program_owner: Program::authenticated_transfer_program().id(), - nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), - balance: sender_private_account.balance - balance_to_move, - data: Data::default(), - }, - ); - - let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account); - let expected_new_nullifier = - Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk); - - assert!(state.private_state.0.contains(&sender_pre_commitment)); - assert!(!state.private_state.0.contains(&expected_new_commitment)); - assert!(!state.private_state.1.contains(&expected_new_nullifier)); - - state - .transition_from_privacy_preserving_transaction(&tx, 1, 0) - .unwrap(); - - let recipient_post = state.get_account_by_id(recipient_keys.account_id()); - assert_eq!(recipient_post, expected_recipient_post); - assert!(state.private_state.0.contains(&sender_pre_commitment)); - assert!(state.private_state.0.contains(&expected_new_commitment)); - assert!(state.private_state.1.contains(&expected_new_nullifier)); - assert_eq!( - state.get_account_by_id(recipient_keys.account_id()).balance, - recipient_initial_balance + balance_to_move - ); - } - - #[test] - fn burner_program_should_fail_in_privacy_preserving_circuit() { - let program = Program::burner(); - let public_account = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - - let result = execute_and_prove( - vec![public_account], - Program::serialize_instruction(10_u128).unwrap(), - vec![InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn minter_program_should_fail_in_privacy_preserving_circuit() { - let program = Program::minter(); - let public_account = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 0, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - - let result = execute_and_prove( - vec![public_account], - Program::serialize_instruction(10_u128).unwrap(), - vec![InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn nonce_changer_program_should_fail_in_privacy_preserving_circuit() { - let program = Program::nonce_changer_program(); - let public_account = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 0, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - - let result = execute_and_prove( - vec![public_account], - Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn data_changer_program_should_fail_for_non_owned_account_in_privacy_preserving_circuit() { - let program = Program::data_changer(); - let public_account = AccountWithMetadata::new( - Account { - program_owner: [0, 1, 2, 3, 4, 5, 6, 7], - balance: 0, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - - let result = execute_and_prove( - vec![public_account], - Program::serialize_instruction(vec![0]).unwrap(), - vec![InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn data_changer_program_should_fail_for_too_large_data_in_privacy_preserving_circuit() { - let program = Program::data_changer(); - let public_account = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 0, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - - let large_data: Vec = - vec![ - 0; - usize::try_from(lee_core::account::data::DATA_MAX_LENGTH.as_u64()) - .expect("DATA_MAX_LENGTH fits in usize") - + 1 - ]; - - let result = execute_and_prove( - vec![public_account], - Program::serialize_instruction(large_data).unwrap(), - vec![InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::ProgramProveFailed(_)))); - } - - #[test] - fn extra_output_program_should_fail_in_privacy_preserving_circuit() { - let program = Program::extra_output_program(); - let public_account = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 0, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - - let result = execute_and_prove( - vec![public_account], - Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn missing_output_program_should_fail_in_privacy_preserving_circuit() { - let program = Program::missing_output_program(); - let public_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 0, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - let public_account_2 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 0, - ..Account::default() - }, - true, - AccountId::new([1; 32]), - ); - - let result = execute_and_prove( - vec![public_account_1, public_account_2], - Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::Public, InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn program_owner_changer_should_fail_in_privacy_preserving_circuit() { - let program = Program::program_owner_changer(); - let public_account = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 0, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - - let result = execute_and_prove( - vec![public_account], - Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn transfer_from_non_owned_account_should_fail_in_privacy_preserving_circuit() { - let program = Program::simple_balance_transfer(); - let public_account_1 = AccountWithMetadata::new( - Account { - program_owner: [0, 1, 2, 3, 4, 5, 6, 7], - balance: 100, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - let public_account_2 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 0, - ..Account::default() - }, - true, - AccountId::new([1; 32]), - ); - - let result = execute_and_prove( - vec![public_account_1, public_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![InputAccountIdentity::Public, InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_visibility_masks_have_incorrect_lenght() { - let program = Program::simple_balance_transfer(); - let public_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - let public_account_2 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 0, - ..Account::default() - }, - true, - AccountId::new([1; 32]), - ); - - // Single account_identity entry for a circuit execution with two pre_state accounts. - let result = execute_and_prove( - vec![public_account_1, public_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_invalid_auth_keys_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - let private_account_2 = AccountWithMetadata::new( - Account::default(), - false, - (&recipient_keys.npk(), &recipient_keys.vpk(), 0), - ); - - // Setting the recipient nsk to authorize the sender. - // This should be set to the sender private account in a normal circumstance. - // `PrivateAuthorizedUpdate` derives npk from nsk and asserts equality with - // `pre_state.account_id`, so a mismatched nsk fails that check. - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: recipient_keys.nsk, - membership_proof: (0, vec![]), - identifier: 0, - }, - InputAccountIdentity::PrivateUnauthorized { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - npk: recipient_keys.npk(), - identifier: 0, - }, - ], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - let private_account_2 = AccountWithMetadata::new( - Account { - // Non default balance - balance: 1, - ..Account::default() - }, - false, - (&recipient_keys.npk(), &recipient_keys.vpk(), 0), - ); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), - identifier: 0, - }, - InputAccountIdentity::PrivateUnauthorized { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - npk: recipient_keys.npk(), - identifier: 0, - }, - ], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - let private_account_2 = AccountWithMetadata::new( - Account { - // Non default program_owner - program_owner: [0, 1, 2, 3, 4, 5, 6, 7], - ..Account::default() - }, - false, - (&recipient_keys.npk(), &recipient_keys.vpk(), 0), - ); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), - identifier: 0, - }, - InputAccountIdentity::PrivateUnauthorized { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - npk: recipient_keys.npk(), - identifier: 0, - }, - ], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - let private_account_2 = AccountWithMetadata::new( - Account { - // Non default data - data: b"hola mundo".to_vec().try_into().unwrap(), - ..Account::default() - }, - false, - (&recipient_keys.npk(), &recipient_keys.vpk(), 0), - ); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), - identifier: 0, - }, - InputAccountIdentity::PrivateUnauthorized { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - npk: recipient_keys.npk(), - identifier: 0, - }, - ], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - let private_account_2 = AccountWithMetadata::new( - Account { - // Non default nonce - nonce: Nonce(0xdead_beef), - ..Account::default() - }, - false, - (&recipient_keys.npk(), &recipient_keys.vpk(), 0), - ); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), - identifier: 0, - }, - InputAccountIdentity::PrivateUnauthorized { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - npk: recipient_keys.npk(), - identifier: 0, - }, - ], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_but_marked_as_authorized() - { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - let private_account_2 = AccountWithMetadata::new( - Account::default(), - // This should be set to false in normal circumstances - true, - (&recipient_keys.npk(), &recipient_keys.vpk(), 0), - ); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), - identifier: 0, - }, - InputAccountIdentity::PrivateUnauthorized { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - npk: recipient_keys.npk(), - identifier: 0, - }, - ], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - /// A private PDA account that no program claims via `Claim::Pda` and no caller authorizes via - /// `ChainedCall.pda_seeds` has no binding between its supplied npk and its `account_id`, - /// so the circuit must reject. Here `simple_balance_transfer` emits no claim for the - /// second account, leaving position 1 unbound. - #[test] - fn private_pda_without_binding_fails() { - let program = Program::simple_balance_transfer(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let public_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - AccountId::new([0; 32]), - ); - let private_pda_account = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); - - let result = execute_and_prove( - vec![public_account_1, private_pda_account], - Program::serialize_instruction(10_u128).unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { - vpk: keys.vpk(), - random_seed: [0; 32], - npk, - identifier: u128::MAX, - seed: None, - }, - ], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - /// Happy path: a program claims a new private PDA via `Claim::Pda(seed)`. The circuit - /// reads the npk for that `pre_state` from `private_account_keys` at the `pre_state`'s - /// position, derives `AccountId` via `AccountId::for_private_pda(program_id, seed, npk)`, and - /// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim - /// and binds the supplied npk to the `account_id`. - #[test] - fn private_pda_claim_succeeds() { - let program = Program::pda_claimer(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let seed = PdaSeed::new([42; 32]); - - let account_id = - AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), u128::MAX); - let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); - - let result = execute_and_prove( - vec![pre_state], - Program::serialize_instruction(seed).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { - vpk: keys.vpk(), - random_seed: [0; 32], - npk, - identifier: u128::MAX, - seed: None, - }], - &program.into(), - ); - - let (output, _proof) = result.expect("private PDA claim should succeed"); - assert_eq!(output.new_nullifiers.len(), 1); - assert_eq!(output.new_commitments.len(), 1); - assert_eq!(output.encrypted_private_post_states.len(), 1); - assert!(output.public_pre_states.is_empty()); - assert!(output.public_post_states.is_empty()); - } - - /// An npk is supplied that does not match the `pre_state`'s `account_id` under - /// `AccountId::for_private_pda(program, claim_seed, npk)`. The claim equality check rejects. - #[test] - fn private_pda_npk_mismatch_fails() { - // `keys_a` produces the `pre_state`'s `account_id` (the registered pair), `keys_b` is - // the mismatched pair supplied in `private_account_keys` for that pre_state. - let program = Program::pda_claimer(); - let keys_a = test_private_account_keys_1(); - let keys_b = test_private_account_keys_2(); - let npk_a = keys_a.npk(); - let npk_b = keys_b.npk(); - let seed = PdaSeed::new([42; 32]); - - // `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state. - // `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in - // the circuit must reject. - let account_id = - AccountId::for_private_pda(&program.id(), &seed, &npk_a, &keys_a.vpk(), u128::MAX); - let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); - - let result = execute_and_prove( - vec![pre_state], - Program::serialize_instruction(seed).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { - vpk: keys_b.vpk(), - random_seed: [0; 32], - npk: npk_b, - identifier: u128::MAX, - seed: None, - }], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - /// Happy path for the caller-seeds authorization of a private PDA. The delegator claims a - /// private PDA via `Claim::Pda(seed)`, then chains to a callee (`noop`) delegating the same - /// seed via `ChainedCall.pda_seeds`. In the callee's step, the `pre_state`'s authorization - /// is established via the private derivation - /// `AccountId::for_private_pda(delegator, seed, npk) == pre.account_id`. - #[test] - fn caller_pda_seeds_authorize_private_pda_for_callee() { - let delegator = Program::private_pda_delegator(); - let callee = Program::auth_asserting_noop(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let seed = PdaSeed::new([77; 32]); - - let account_id = - AccountId::for_private_pda(&delegator.id(), &seed, &npk, &keys.vpk(), u128::MAX); - let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); - - let callee_id = callee.id(); - let program_with_deps = - ProgramWithDependencies::new(delegator, [(callee_id, callee)].into()); - - let result = execute_and_prove( - vec![pre_state], - Program::serialize_instruction((seed, seed, callee_id)).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { - vpk: keys.vpk(), - random_seed: [0; 32], - npk, - identifier: u128::MAX, - seed: None, - }], - &program_with_deps, - ); - - let (output, _proof) = - result.expect("caller-seeds authorization of private PDA should succeed"); - assert_eq!(output.new_commitments.len(), 1); - assert_eq!(output.new_nullifiers.len(), 1); - } - - /// The delegator chains with a different seed than the one it claimed with. In the callee - /// step, neither public nor private caller-seeds authorization matches; `pre.is_authorized` - /// was set to `true` by the delegator but no proven source supports it, so the consistency - /// assertion rejects. - #[test] - fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() { - let delegator = Program::private_pda_delegator(); - let callee = Program::auth_asserting_noop(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let claim_seed = PdaSeed::new([77; 32]); - let wrong_delegated_seed = PdaSeed::new([88; 32]); - - let account_id = - AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, &keys.vpk(), u128::MAX); - let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); - - let callee_id = callee.id(); - let program_with_deps = - ProgramWithDependencies::new(delegator, [(callee_id, callee)].into()); - - let result = execute_and_prove( - vec![pre_state], - Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { - vpk: keys.vpk(), - random_seed: [0; 32], - npk, - identifier: u128::MAX, - seed: None, - }], - &program_with_deps, - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - /// Exploit-scenario pin. A single `(program_id, seed)` pair can derive a family of - /// `AccountId`s, one public PDA and one private PDA per distinct npk. Without the tx-wide - /// family-binding check, a program could claim `PDA_alice` (`alice_npk`) and - /// `PDA_bob` (`bob_npk`) under the same seed in one transaction, and once reuse - /// is supported a later chained call could delegate both to a callee via - /// `pda_seeds: [S]` and mix balances across them. The binding check rejects the setup - /// here: after the first claim records `(program, seed) โ†’ PDA_alice`, the second claim - /// tries to record `(program, seed) โ†’ PDA_bob` and panics. - #[test] - fn two_private_pda_claims_under_same_seed_are_rejected() { - let program = Program::two_pda_claimer(); - let keys_a = test_private_account_keys_1(); - let keys_b = test_private_account_keys_2(); - let seed = PdaSeed::new([55; 32]); - - let account_a = AccountId::for_private_pda( - &program.id(), - &seed, - &keys_a.npk(), - &keys_a.vpk(), - u128::MAX, - ); - let account_b = AccountId::for_private_pda( - &program.id(), - &seed, - &keys_b.npk(), - &keys_b.vpk(), - u128::MAX, - ); - - let pre_a = AccountWithMetadata::new(Account::default(), false, account_a); - let pre_b = AccountWithMetadata::new(Account::default(), false, account_b); - - let result = execute_and_prove( - vec![pre_a, pre_b], - Program::serialize_instruction(seed).unwrap(), - vec![ - InputAccountIdentity::PrivatePdaInit { - vpk: keys_a.vpk(), - random_seed: [0; 32], - npk: keys_a.npk(), - identifier: u128::MAX, - seed: None, - }, - InputAccountIdentity::PrivatePdaInit { - vpk: keys_b.vpk(), - random_seed: [0; 32], - npk: keys_b.npk(), - identifier: u128::MAX, - seed: None, - }, - ], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - /// A private PDA that is reused at top level without an external seed in the identity still - /// fails binding. The noop program emits no `Claim::Pda` and there is no caller - /// `ChainedCall.pda_seeds`, so position 0 is never bound and the assertion fires. - /// Supplying `seed: Some((seed, owner_program_id))` in the `PrivatePdaUpdate` identity is - /// the correct path for top-level reuse; this test pins the failure when no seed is provided. - #[test] - fn private_pda_top_level_reuse_rejected_by_binding_check() { - let program = Program::noop(); - let keys = test_private_account_keys_1(); - let npk = keys.npk(); - let seed = PdaSeed::new([99; 32]); - - // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = - // true, account_id derived via the private formula. - let account_id = - AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), u128::MAX); - let owned_pre_state = AccountWithMetadata::new( - Account { - program_owner: program.id(), - ..Account::default() - }, - true, - account_id, - ); - - let result = execute_and_prove( - vec![owned_pre_state], - Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivatePdaInit { - vpk: keys.vpk(), - random_seed: [0; 32], - npk, - identifier: u128::MAX, - seed: None, - }], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn private_accounts_can_only_be_initialized_once() { - let sender_keys = test_private_account_keys_1(); - let sender_nonce = Nonce(0xdead_beef); - - let sender_private_account = Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: 100, - nonce: sender_nonce, - data: Data::default(), - }; - let recipient_keys = test_private_account_keys_2(); - - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0) - .with_private_account(&sender_keys, &sender_private_account); - - let balance_to_move = 37; - let balance_to_move_2 = 30; - - let tx = private_balance_transfer_for_tests( - &sender_keys, - &sender_private_account, - &recipient_keys, - balance_to_move, - &state, - ); - - state - .transition_from_privacy_preserving_transaction(&tx, 1, 0) - .unwrap(); - - let sender_private_account = Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: 100, - nonce: sender_nonce, - data: Data::default(), - }; - - let tx = private_balance_transfer_for_tests( - &sender_keys, - &sender_private_account, - &recipient_keys, - balance_to_move_2, - &state, - ); - - let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); - - assert!(matches!(result, Err(LeeError::InvalidInput(_)))); - let LeeError::InvalidInput(error_message) = result.err().unwrap() else { - panic!("Incorrect message error"); - }; - let expected_error_message = "Nullifier already seen".to_owned(); - assert_eq!(error_message, expected_error_message); - } - - #[test] - fn circuit_should_fail_if_there_are_repeated_ids() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - - let result = execute_and_prove( - vec![private_account_1.clone(), private_account_1], - Program::serialize_instruction(100_u128).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: (1, vec![]), - identifier: 0, - }, - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: (1, vec![]), - identifier: 0, - }, - ], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn claiming_mechanism() { - let program = Program::authenticated_transfer_program(); - let from_key = PrivateKey::try_new([1; 32]).unwrap(); - let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); - let initial_balance = 100; - let initial_data = [(from, initial_balance)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let to_key = PrivateKey::try_new([2; 32]).unwrap(); - let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); - let amount: u128 = 37; - - // Check the recipient is an uninitialized account - assert_eq!(state.get_account_by_id(to), Account::default()); - - let expected_recipient_post = Account { - program_owner: program.id(), - balance: amount, - nonce: Nonce(1), - ..Account::default() - }; - - let message = public_transaction::Message::try_new( - program.id(), - vec![from, to], - vec![Nonce(0), Nonce(0)], - AuthTransferInstruction::Transfer { amount }, - ) - .unwrap(); - let witness_set = - public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]); - let tx = PublicTransaction::new(message, witness_set); - - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - - let recipient_post = state.get_account_by_id(to); - - assert_eq!(recipient_post, expected_recipient_post); - } - - #[test] - fn unauthorized_public_account_claiming_fails() { - let program = Program::authenticated_transfer_program(); - let account_key = PrivateKey::try_new([9; 32]).unwrap(); - let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key)); - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - assert_eq!(state.get_account_by_id(account_id), Account::default()); - - let message = public_transaction::Message::try_new( - program.id(), - vec![account_id], - vec![], - AuthTransferInstruction::Initialize, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 2, 0); - - assert!(matches!(result, Err(LeeError::InvalidProgramBehavior(_)))); - assert_eq!(state.get_account_by_id(account_id), Account::default()); - } - - #[test] - fn authorized_public_account_claiming_succeeds() { - let program = Program::authenticated_transfer_program(); - let account_key = PrivateKey::try_new([10; 32]).unwrap(); - let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key)); - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - assert_eq!(state.get_account_by_id(account_id), Account::default()); - - let message = public_transaction::Message::try_new( - program.id(), - vec![account_id], - vec![Nonce(0)], - AuthTransferInstruction::Initialize, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&account_key]); - let tx = PublicTransaction::new(message, witness_set); - - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - - assert_eq!( - state.get_account_by_id(account_id), - Account { - program_owner: program.id(), - nonce: Nonce(1), - ..Account::default() - } - ); - } - - #[test] - fn public_chained_call() { - let program = Program::chain_caller(); - let key = PrivateKey::try_new([1; 32]).unwrap(); - let from = AccountId::from(&PublicKey::new_from_private_key(&key)); - let to = AccountId::new([2; 32]); - let initial_balance = 1000; - let initial_data = [(from, initial_balance), (to, 0)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let from_key = key; - let amount: u128 = 37; - let instruction: (u128, ProgramId, u32, Option) = ( - amount, - Program::authenticated_transfer_program().id(), - 2, - None, - ); - - let expected_to_post = Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: amount * 2, // The `chain_caller` chains the program twice - ..Account::default() - }; - - let message = public_transaction::Message::try_new( - program.id(), - vec![to, from], // The chain_caller program permutes the account order in the chain - // call - vec![Nonce(0)], - instruction, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); - let tx = PublicTransaction::new(message, witness_set); - - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - - let from_post = state.get_account_by_id(from); - let to_post = state.get_account_by_id(to); - // The `chain_caller` program calls the program twice - assert_eq!(from_post.balance, initial_balance - 2 * amount); - assert_eq!(to_post, expected_to_post); - } - - #[test] - fn execution_fails_if_chained_calls_exceeds_depth() { - let program = Program::chain_caller(); - let key = PrivateKey::try_new([1; 32]).unwrap(); - let from = AccountId::from(&PublicKey::new_from_private_key(&key)); - let to = AccountId::new([2; 32]); - let initial_balance = 100; - let initial_data = [(from, initial_balance), (to, 0)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let from_key = key; - let amount: u128 = 0; - let instruction: (u128, ProgramId, u32, Option) = ( - amount, - Program::authenticated_transfer_program().id(), - u32::try_from(MAX_NUMBER_CHAINED_CALLS).expect("MAX_NUMBER_CHAINED_CALLS fits in u32") - + 1, - None, - ); - - let message = public_transaction::Message::try_new( - program.id(), - vec![to, from], // The chain_caller program permutes the account order in the chain - // call - vec![Nonce(0)], - instruction, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!( - result, - Err(LeeError::MaxChainedCallsDepthExceeded) - )); - } - - #[test] - fn execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() { - let chain_caller = Program::chain_caller(); - let pda_seed = PdaSeed::new([37; 32]); - let from = AccountId::for_public_pda(&chain_caller.id(), &pda_seed); - let to = AccountId::new([2; 32]); - let initial_balance = 1000; - let initial_data = [(from, initial_balance), (to, 0)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let amount: u128 = 58; - let instruction: (u128, ProgramId, u32, Option) = ( - amount, - Program::authenticated_transfer_program().id(), - 1, - Some(pda_seed), - ); - - let expected_to_post = Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: amount, // The `chain_caller` chains the program twice - ..Account::default() - }; - let message = public_transaction::Message::try_new( - chain_caller.id(), - vec![to, from], // The chain_caller program permutes the account order in the chain - // call - vec![], - instruction, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - - let from_post = state.get_account_by_id(from); - let to_post = state.get_account_by_id(to); - assert_eq!(from_post.balance, initial_balance - amount); - assert_eq!(to_post, expected_to_post); - } - - #[test] - fn claiming_mechanism_within_chain_call() { - // This test calls the authenticated transfer program through the chain_caller program. - // The transfer is made from an initialized sender to an uninitialized recipient. And - // it is expected that the recipient account is claimed by the authenticated transfer - // program and not the chained_caller program. - let chain_caller = Program::chain_caller(); - let auth_transfer = Program::authenticated_transfer_program(); - let from_key = PrivateKey::try_new([1; 32]).unwrap(); - let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); - let initial_balance = 100; - let initial_data = [(from, initial_balance)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let to_key = PrivateKey::try_new([2; 32]).unwrap(); - let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); - let amount: u128 = 37; - - // Check the recipient is an uninitialized account - assert_eq!(state.get_account_by_id(to), Account::default()); - - let expected_to_post = Account { - // The expected program owner is the authenticated transfer program - program_owner: auth_transfer.id(), - balance: amount, - nonce: Nonce(1), - ..Account::default() - }; - - // The transaction executes the chain_caller program, which internally calls the - // authenticated_transfer program - let instruction: (u128, ProgramId, u32, Option) = ( - amount, - Program::authenticated_transfer_program().id(), - 1, - None, - ); - let message = public_transaction::Message::try_new( - chain_caller.id(), - vec![to, from], // The chain_caller program permutes the account order in the chain - // call - vec![Nonce(0), Nonce(0)], - instruction, - ) - .unwrap(); - let witness_set = - public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]); - let tx = PublicTransaction::new(message, witness_set); - - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - - let from_post = state.get_account_by_id(from); - let to_post = state.get_account_by_id(to); - assert_eq!(from_post.balance, initial_balance - amount); - assert_eq!(to_post, expected_to_post); - } - - #[test] - fn unauthorized_public_account_claiming_fails_when_executed_privately() { - let program = Program::authenticated_transfer_program(); - let account_id = AccountId::new([11; 32]); - let public_account = AccountWithMetadata::new(Account::default(), false, account_id); - - let result = execute_and_prove( - vec![public_account], - Program::serialize_instruction(AuthTransferInstruction::Initialize).unwrap(), - vec![InputAccountIdentity::Public], - &program.into(), - ); - - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn authorized_public_account_claiming_succeeds_when_executed_privately() { - let program = Program::authenticated_transfer_program(); - let program_id = program.id(); - let sender_keys = test_private_account_keys_1(); - let sender_private_account = Account { - program_owner: program_id, - balance: 100, - ..Account::default() - }; - let sender_account_id = - AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); - let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account); - let sender_init_nullifier = Nullifier::for_account_initialization(&sender_account_id); - let mut state = V03State::new_with_genesis_accounts( - &[], - vec![(sender_commitment.clone(), sender_init_nullifier)], - 0, - ); - let sender_pre = AccountWithMetadata::new( - sender_private_account, - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap(); - let recipient_account_id = - AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key)); - let recipient_pre = - AccountWithMetadata::new(Account::default(), true, recipient_account_id); - - let balance = 37; - - let (output, proof) = execute_and_prove( - vec![sender_pre, recipient_pre], - Program::serialize_instruction(authenticated_transfer_core::Instruction::Transfer { - amount: balance, - }) - .unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&sender_commitment) - .expect("sender's commitment must be in state"), - identifier: 0, - }, - InputAccountIdentity::Public, - ], - &program.into(), - ) - .unwrap(); - - let message = - Message::try_from_circuit_output(vec![recipient_account_id], vec![Nonce(0)], output) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - state - .transition_from_privacy_preserving_transaction(&tx, 1, 0) - .unwrap(); - - let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk); - assert!(state.private_state.1.contains(&nullifier)); - - assert_eq!( - state.get_account_by_id(recipient_account_id), - Account { - program_owner: program_id, - balance, - nonce: Nonce(1), - ..Account::default() - } - ); - } - - #[test_case::test_case(1; "single call")] - #[test_case::test_case(2; "two calls")] - fn private_chained_call(number_of_calls: u32) { - // Arrange - let chain_caller = Program::chain_caller(); - let auth_transfers = Program::authenticated_transfer_program(); - let from_keys = test_private_account_keys_1(); - let to_keys = test_private_account_keys_2(); - let initial_balance = 100; - let from_account = AccountWithMetadata::new( - Account { - program_owner: auth_transfers.id(), - balance: initial_balance, - ..Account::default() - }, - true, - (&from_keys.npk(), &from_keys.vpk(), 0), - ); - let to_account = AccountWithMetadata::new( - Account { - program_owner: auth_transfers.id(), - ..Account::default() - }, - true, - (&to_keys.npk(), &to_keys.vpk(), 0), - ); - - let from_account_id = - AccountId::for_regular_private_account(&from_keys.npk(), &from_keys.vpk(), 0); - let to_account_id = - AccountId::for_regular_private_account(&to_keys.npk(), &to_keys.vpk(), 0); - let from_commitment = Commitment::new(&from_account_id, &from_account.account); - let to_commitment = Commitment::new(&to_account_id, &to_account.account); - let from_init_nullifier = Nullifier::for_account_initialization(&from_account_id); - let to_init_nullifier = Nullifier::for_account_initialization(&to_account_id); - let mut state = V03State::new_with_genesis_accounts( - &[], - vec![ - (from_commitment.clone(), from_init_nullifier), - (to_commitment.clone(), to_init_nullifier), - ], - 0, - ) - .with_test_programs(); - let amount: u128 = 37; - let instruction: (u128, ProgramId, u32, Option) = ( - amount, - Program::authenticated_transfer_program().id(), - number_of_calls, - None, - ); - - let mut dependencies = HashMap::new(); - - dependencies.insert(auth_transfers.id(), auth_transfers); - let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies); - - let from_new_nonce = Nonce::default().private_account_nonce_increment(&from_keys.nsk); - let to_new_nonce = Nonce::default().private_account_nonce_increment(&to_keys.nsk); - - let from_expected_post = Account { - balance: initial_balance - u128::from(number_of_calls) * amount, - nonce: from_new_nonce, - ..from_account.account.clone() - }; - let from_expected_commitment = Commitment::new(&from_account_id, &from_expected_post); - - let to_expected_post = Account { - balance: u128::from(number_of_calls) * amount, - nonce: to_new_nonce, - ..to_account.account.clone() - }; - let to_expected_commitment = Commitment::new(&to_account_id, &to_expected_post); - - // Act - let (output, proof) = execute_and_prove( - vec![to_account, from_account], - Program::serialize_instruction(instruction).unwrap(), - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: from_keys.vpk(), - random_seed: [0; 32], - nsk: from_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&from_commitment) - .expect("from's commitment must be in state"), - identifier: 0, - }, - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: to_keys.vpk(), - random_seed: [0; 32], - nsk: to_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&to_commitment) - .expect("to's commitment must be in state"), - identifier: 0, - }, - ], - &program_with_deps, - ) - .unwrap(); - - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let transaction = PrivacyPreservingTransaction::new(message, witness_set); - - state - .transition_from_privacy_preserving_transaction(&transaction, 1, 0) - .unwrap(); - - // Assert - assert!( - state - .get_proof_for_commitment(&from_expected_commitment) - .is_some() - ); - assert!( - state - .get_proof_for_commitment(&to_expected_commitment) - .is_some() - ); - } - - #[test] - fn pda_mechanism_with_pinata_token_program() { - let pinata_token = Program::pinata_token(); - let token = Program::token(); - - let pinata_definition_id = AccountId::new([1; 32]); - let pinata_token_definition_id = AccountId::new([2; 32]); - // Total supply of pinata token will be in an account under a PDA. - let pinata_token_holding_id = - AccountId::for_public_pda(&pinata_token.id(), &PdaSeed::new([0; 32])); - let winner_token_holding_id = AccountId::new([3; 32]); - - let expected_winner_account_holding = token_core::TokenHolding::Fungible { - definition_id: pinata_token_definition_id, - balance: 150, - }; - let expected_winner_token_holding_post = Account { - program_owner: token.id(), - data: Data::from(&expected_winner_account_holding), - ..Account::default() - }; - - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - state.add_pinata_token_program(pinata_definition_id); - - // Set up the token accounts directly (bypassing public transactions which - // would require signers for Claim::Authorized). The focus of this test is - // the PDA mechanism in the pinata program's chained call, not token creation. - let total_supply: u128 = 10_000_000; - let token_definition = token_core::TokenDefinition::Fungible { - name: String::from("PINATA"), - total_supply, - metadata_id: None, - }; - let token_holding = token_core::TokenHolding::Fungible { - definition_id: pinata_token_definition_id, - balance: total_supply, - }; - let winner_holding = token_core::TokenHolding::Fungible { - definition_id: pinata_token_definition_id, - balance: 0, - }; - state.force_insert_account( - pinata_token_definition_id, - Account { - program_owner: token.id(), - data: Data::from(&token_definition), - ..Account::default() - }, - ); - state.force_insert_account( - pinata_token_holding_id, - Account { - program_owner: token.id(), - data: Data::from(&token_holding), - ..Account::default() - }, - ); - state.force_insert_account( - winner_token_holding_id, - Account { - program_owner: token.id(), - data: Data::from(&winner_holding), - ..Account::default() - }, - ); - - // Submit a solution to the pinata program to claim the prize - let solution: u128 = 989_106; - let message = public_transaction::Message::try_new( - pinata_token.id(), - vec![ - pinata_definition_id, - pinata_token_holding_id, - winner_token_holding_id, - ], - vec![], - solution, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - state.transition_from_public_transaction(&tx, 1, 0).unwrap(); - - let winner_token_holding_post = state.get_account_by_id(winner_token_holding_id); - assert_eq!( - winner_token_holding_post, - expected_winner_token_holding_post - ); - } - - #[test] - fn claiming_mechanism_cannot_claim_initialied_accounts() { - let claimer = Program::claimer(); - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - let account_id = AccountId::new([2; 32]); - - // Insert an account with non-default program owner - state.force_insert_account( - account_id, - Account { - program_owner: [1, 2, 3, 4, 5, 6, 7, 8], - ..Account::default() - }, - ); - - let message = - public_transaction::Message::try_new(claimer.id(), vec![account_id], vec![], ()) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior( - InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id: err_account_id } - )) if err_account_id == account_id - )); - } - - /// This test ensures that even if a malicious program tries to perform overflow of balances - /// it will not be able to break the balance validation. - #[test] - fn malicious_program_cannot_break_balance_validation_if_not_in_genesis() { - let sender_key = PrivateKey::try_new([37; 32]).unwrap(); - let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); - let sender_init_balance: u128 = 10; - - let recipient_key = PrivateKey::try_new([42; 32]).unwrap(); - let recipient_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_key)); - let recipient_init_balance: u128 = 10; - - let mut state = V03State::new_with_genesis_accounts( - &[ - (sender_id, sender_init_balance), - (recipient_id, recipient_init_balance), - ], - vec![], - 0, - ); - - state.insert_program(Program::modified_transfer_program()); - - let balance_to_move: u128 = 4; - - let sender = AccountWithMetadata::new(state.get_account_by_id(sender_id), true, sender_id); - - let sender_nonce = sender.account.nonce; - - let _recipient = - AccountWithMetadata::new(state.get_account_by_id(recipient_id), false, sender_id); - - let message = public_transaction::Message::try_new( - Program::modified_transfer_program().id(), - vec![sender_id, recipient_id], - vec![sender_nonce], - balance_to_move, - ) - .unwrap(); - - let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); - let tx = PublicTransaction::new(message, witness_set); - let res = state.transition_from_public_transaction(&tx, 2, 0); - let expected_total_balance_pre_states = WrappedBalanceSum::from_balances( - [sender_init_balance, recipient_init_balance].into_iter(), - ) - .unwrap(); - let expected_total_balance_post_states = WrappedBalanceSum::from_balances( - [sender_init_balance, recipient_init_balance, u128::MAX, 1].into_iter(), - ) - .unwrap(); - assert!(matches!( - res, - Err(LeeError::InvalidProgramBehavior( - InvalidProgramBehaviorError::ExecutionValidationFailed( - ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states } - ) - )) if total_balance_pre_states == expected_total_balance_pre_states && total_balance_post_states == expected_total_balance_post_states - )); - - let sender_post = state.get_account_by_id(sender_id); - let recipient_post = state.get_account_by_id(recipient_id); - - let expected_sender_post = { - let mut this = state.get_account_by_id(sender_id); - this.balance = sender_init_balance; - this.nonce = Nonce(0); - this - }; - - let expected_recipient_post = { - let mut this = state.get_account_by_id(sender_id); - this.balance = recipient_init_balance; - this.nonce = Nonce(0); - this - }; - - assert_eq!(expected_sender_post, sender_post); - assert_eq!(expected_recipient_post, recipient_post); - } - - #[test] - fn private_authorized_uninitialized_account() { - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - // Set up keys for the authorized private account - let private_keys = test_private_account_keys_1(); - - // Create an authorized private account with default values (new account being initialized) - let authorized_account = AccountWithMetadata::new( - Account::default(), - true, - (&private_keys.npk(), &private_keys.vpk(), 0), - ); - - let program = Program::authenticated_transfer_program(); - - // Set up parameters for the new account - - let instruction = authenticated_transfer_core::Instruction::Initialize; - - // Execute and prove the circuit with the authorized account but no commitment proof - let (output, proof) = execute_and_prove( - vec![authorized_account], - Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { - vpk: private_keys.vpk(), - random_seed: [0; 32], - nsk: private_keys.nsk, - identifier: 0, - }], - &program.into(), - ) - .unwrap(); - - // Create message from circuit output - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - - let tx = PrivacyPreservingTransaction::new(message, witness_set); - let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); - assert!(result.is_ok()); - - let account_id = - AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0); - let nullifier = Nullifier::for_account_initialization(&account_id); - assert!(state.private_state.1.contains(&nullifier)); - } - - #[test] - fn private_unauthorized_uninitialized_account_can_still_be_claimed() { - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - - let private_keys = test_private_account_keys_1(); - // This is intentional: claim authorization was introduced to protect public accounts, - // especially PDAs. Private PDAs are not useful in practice because there is no way to - // operate them without the corresponding private keys, so unauthorized private claiming - // remains allowed. - let unauthorized_account = AccountWithMetadata::new( - Account::default(), - false, - (&private_keys.npk(), &private_keys.vpk(), 0), - ); - - let program = Program::claimer(); - - let (output, proof) = execute_and_prove( - vec![unauthorized_account], - Program::serialize_instruction(0_u128).unwrap(), - vec![InputAccountIdentity::PrivateUnauthorized { - vpk: private_keys.vpk(), - random_seed: [0; 32], - npk: private_keys.npk(), - identifier: 0, - }], - &program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - state - .transition_from_privacy_preserving_transaction(&tx, 1, 0) - .unwrap(); - - let account_id = - AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0); - let nullifier = Nullifier::for_account_initialization(&account_id); - assert!(state.private_state.1.contains(&nullifier)); - } - - #[test] - fn private_account_claimed_then_used_without_init_flag_should_fail() { - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - - // Set up keys for the private account - let private_keys = test_private_account_keys_1(); - - // Step 1: Create a new private account with authorization - let authorized_account = AccountWithMetadata::new( - Account::default(), - true, - (&private_keys.npk(), &private_keys.vpk(), 0), - ); - - let claimer_program = Program::claimer(); - - // Set up parameters for claiming the new account - - let instruction = authenticated_transfer_core::Instruction::Initialize; - - // Step 2: Execute claimer program to claim the account with authentication - let (output, proof) = execute_and_prove( - vec![authorized_account.clone()], - Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { - vpk: private_keys.vpk(), - random_seed: [0; 32], - nsk: private_keys.nsk, - identifier: 0, - }], - &claimer_program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - // Claim should succeed - assert!( - state - .transition_from_privacy_preserving_transaction(&tx, 1, 0) - .is_ok() - ); - - // Verify the account is now initialized (nullifier exists) - let account_id = - AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0); - let nullifier = Nullifier::for_account_initialization(&account_id); - assert!(state.private_state.1.contains(&nullifier)); - - // Prepare new state of account - let account_metadata = { - let mut acc = authorized_account; - acc.account.program_owner = Program::claimer().id(); - acc - }; - - let noop_program = Program::noop(); - - // Step 3: Try to execute noop program with authentication but without initialization - let res = execute_and_prove( - vec![account_metadata], - Program::serialize_instruction(()).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedInit { - vpk: private_keys.vpk(), - random_seed: [0; 32], - nsk: private_keys.nsk, - identifier: 0, - }], - &noop_program.into(), - ); - - assert!(matches!(res, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn public_changer_claimer_no_data_change_no_claim_succeeds() { - let initial_data = []; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let account_id = AccountId::new([1; 32]); - let program_id = Program::changer_claimer().id(); - // Don't change data (None) and don't claim (false) - let instruction: (Option>, bool) = (None, false); - - let message = - public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - // Should succeed - no changes made, no claim needed - assert!(result.is_ok()); - // Account should remain default/unclaimed - assert_eq!(state.get_account_by_id(account_id), Account::default()); - } - - #[test] - fn public_changer_claimer_data_change_no_claim_fails() { - let initial_data = []; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let account_id = AccountId::new([1; 32]); - let program_id = Program::changer_claimer().id(); - // Change data but don't claim (false) - should fail - let new_data = vec![1, 2, 3, 4, 5]; - let instruction: (Option>, bool) = (Some(new_data), false); - - let message = - public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - - // Should fail - cannot modify data without claiming the account - assert!(matches!( - result, - Err(LeeError::InvalidProgramBehavior( - InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { - account_id: err_account_id - } - )) if err_account_id == account_id - )); - } - - #[test] - fn private_changer_claimer_no_data_change_no_claim_succeeds() { - let program = Program::changer_claimer(); - let sender_keys = test_private_account_keys_1(); - let private_account = AccountWithMetadata::new( - Account::default(), - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - // Don't change data (None) and don't claim (false) - let instruction: (Option>, bool) = (None, false); - - let result = execute_and_prove( - vec![private_account], - Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), - identifier: 0, - }], - &program.into(), - ); - - // Should succeed - no changes made, no claim needed - assert!(result.is_ok()); - } - - #[test] - fn private_changer_claimer_data_change_no_claim_fails() { - let program = Program::changer_claimer(); - let sender_keys = test_private_account_keys_1(); - let private_account = AccountWithMetadata::new( - Account::default(), - true, - (&sender_keys.npk(), &sender_keys.vpk(), 0), - ); - // Change data but don't claim (false) - should fail - let new_data = vec![1, 2, 3, 4, 5]; - let instruction: (Option>, bool) = (Some(new_data), false); - - let result = execute_and_prove( - vec![private_account], - Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.vpk(), - random_seed: [0; 32], - nsk: sender_keys.nsk, - membership_proof: (0, vec![]), - identifier: 0, - }], - &program.into(), - ); - - // Should fail - cannot modify data without claiming the account - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test] - fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() { - // Arrange - let malicious_program = Program::malicious_authorization_changer(); - let auth_transfers = Program::authenticated_transfer_program(); - let sender_keys = test_public_account_keys_1(); - let recipient_keys = test_private_account_keys_1(); - - let sender_account = AccountWithMetadata::new( - Account { - program_owner: auth_transfers.id(), - balance: 100, - ..Default::default() - }, - false, - sender_keys.account_id(), - ); - let recipient_account = AccountWithMetadata::new( - Account::default(), - true, - (&recipient_keys.npk(), &recipient_keys.vpk(), 0), - ); - - let recipient_account_id = - AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); - let recipient_commitment = - Commitment::new(&recipient_account_id, &recipient_account.account); - let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_account_id); - let state = V03State::new_with_genesis_accounts( - &[(sender_account.account_id, sender_account.account.balance)], - vec![(recipient_commitment.clone(), recipient_init_nullifier)], - 0, - ) - .with_test_programs(); - - let balance_to_transfer = 10_u128; - let instruction = (balance_to_transfer, auth_transfers.id()); - - let mut dependencies = HashMap::new(); - dependencies.insert(auth_transfers.id(), auth_transfers); - let program_with_deps = ProgramWithDependencies::new(malicious_program, dependencies); - - // Act - execute the malicious program - this should fail during proving - let result = execute_and_prove( - vec![sender_account, recipient_account], - Program::serialize_instruction(instruction).unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: recipient_keys.vpk(), - random_seed: [0; 32], - nsk: recipient_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&recipient_commitment) - .expect("recipient's commitment must be in state"), - identifier: 0, - }, - ], - &program_with_deps, - ); - - // Assert - should fail because the malicious program tries to manipulate is_authorized - assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); - } - - #[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] - #[test_case::test_case((Some(1), Some(3)), 2; "inside range")] - #[test_case::test_case((Some(1), Some(3)), 0; "below range")] - #[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] - #[test_case::test_case((Some(1), Some(3)), 4; "above range")] - #[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] - #[test_case::test_case((Some(1), None), 10; "lower bound only - above")] - #[test_case::test_case((Some(1), None), 0; "lower bound only - below")] - #[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] - #[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] - #[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] - #[test_case::test_case((None, None), 0; "no bounds - always valid")] - #[test_case::test_case((None, None), 100; "no bounds - always valid 2")] - fn validity_window_works_in_public_transactions( - validity_window: (Option, Option), - block_id: BlockId, - ) { - let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap(); - let validity_window_program = Program::validity_window(); - let account_keys = test_public_account_keys_1(); - let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id()); - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - let tx = { - let account_ids = vec![pre.account_id]; - let nonces = vec![]; - let program_id = validity_window_program.id(); - let instruction = ( - block_validity_window, - TimestampValidityWindow::new_unbounded(), - ); - let message = - public_transaction::Message::try_new(program_id, account_ids, nonces, instruction) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - PublicTransaction::new(message, witness_set) - }; - let result = state.transition_from_public_transaction(&tx, block_id, 0); - let is_inside_validity_window = - match (block_validity_window.start(), block_validity_window.end()) { - (Some(s), Some(e)) => s <= block_id && block_id < e, - (Some(s), None) => s <= block_id, - (None, Some(e)) => block_id < e, - (None, None) => true, - }; - if is_inside_validity_window { - assert!(result.is_ok()); - } else { - assert!(matches!(result, Err(LeeError::OutOfValidityWindow))); - } - } - - #[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] - #[test_case::test_case((Some(1), Some(3)), 2; "inside range")] - #[test_case::test_case((Some(1), Some(3)), 0; "below range")] - #[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] - #[test_case::test_case((Some(1), Some(3)), 4; "above range")] - #[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] - #[test_case::test_case((Some(1), None), 10; "lower bound only - above")] - #[test_case::test_case((Some(1), None), 0; "lower bound only - below")] - #[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] - #[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] - #[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] - #[test_case::test_case((None, None), 0; "no bounds - always valid")] - #[test_case::test_case((None, None), 100; "no bounds - always valid 2")] - fn timestamp_validity_window_works_in_public_transactions( - validity_window: (Option, Option), - timestamp: Timestamp, - ) { - let timestamp_validity_window: TimestampValidityWindow = - validity_window.try_into().unwrap(); - let validity_window_program = Program::validity_window(); - let account_keys = test_public_account_keys_1(); - let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id()); - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - let tx = { - let account_ids = vec![pre.account_id]; - let nonces = vec![]; - let program_id = validity_window_program.id(); - let instruction = ( - BlockValidityWindow::new_unbounded(), - timestamp_validity_window, - ); - let message = - public_transaction::Message::try_new(program_id, account_ids, nonces, instruction) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - PublicTransaction::new(message, witness_set) - }; - let result = state.transition_from_public_transaction(&tx, 1, timestamp); - let is_inside_validity_window = match ( - timestamp_validity_window.start(), - timestamp_validity_window.end(), - ) { - (Some(s), Some(e)) => s <= timestamp && timestamp < e, - (Some(s), None) => s <= timestamp, - (None, Some(e)) => timestamp < e, - (None, None) => true, - }; - if is_inside_validity_window { - assert!(result.is_ok()); - } else { - assert!(matches!(result, Err(LeeError::OutOfValidityWindow))); - } - } - - #[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] - #[test_case::test_case((Some(1), Some(3)), 2; "inside range")] - #[test_case::test_case((Some(1), Some(3)), 0; "below range")] - #[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] - #[test_case::test_case((Some(1), Some(3)), 4; "above range")] - #[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] - #[test_case::test_case((Some(1), None), 10; "lower bound only - above")] - #[test_case::test_case((Some(1), None), 0; "lower bound only - below")] - #[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] - #[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] - #[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] - #[test_case::test_case((None, None), 0; "no bounds - always valid")] - #[test_case::test_case((None, None), 100; "no bounds - always valid 2")] - fn validity_window_works_in_privacy_preserving_transactions( - validity_window: (Option, Option), - block_id: BlockId, - ) { - let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap(); - let validity_window_program = Program::validity_window(); - let account_keys = test_private_account_keys_1(); - let pre = AccountWithMetadata::new( - Account::default(), - false, - (&account_keys.npk(), &account_keys.vpk(), 0), - ); - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - let tx = { - let instruction = ( - block_validity_window, - TimestampValidityWindow::new_unbounded(), - ); - let (output, proof) = circuit::execute_and_prove( - vec![pre], - Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateUnauthorized { - vpk: account_keys.vpk(), - random_seed: [0; 32], - npk: account_keys.npk(), - identifier: 0, - }], - &validity_window_program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - PrivacyPreservingTransaction::new(message, witness_set) - }; - let result = state.transition_from_privacy_preserving_transaction(&tx, block_id, 0); - let is_inside_validity_window = - match (block_validity_window.start(), block_validity_window.end()) { - (Some(s), Some(e)) => s <= block_id && block_id < e, - (Some(s), None) => s <= block_id, - (None, Some(e)) => block_id < e, - (None, None) => true, - }; - if is_inside_validity_window { - assert!(result.is_ok()); - } else { - assert!(matches!(result, Err(LeeError::OutOfValidityWindow))); - } - } - - #[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] - #[test_case::test_case((Some(1), Some(3)), 2; "inside range")] - #[test_case::test_case((Some(1), Some(3)), 0; "below range")] - #[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] - #[test_case::test_case((Some(1), Some(3)), 4; "above range")] - #[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] - #[test_case::test_case((Some(1), None), 10; "lower bound only - above")] - #[test_case::test_case((Some(1), None), 0; "lower bound only - below")] - #[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] - #[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] - #[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] - #[test_case::test_case((None, None), 0; "no bounds - always valid")] - #[test_case::test_case((None, None), 100; "no bounds - always valid 2")] - fn timestamp_validity_window_works_in_privacy_preserving_transactions( - validity_window: (Option, Option), - timestamp: Timestamp, - ) { - let timestamp_validity_window: TimestampValidityWindow = - validity_window.try_into().unwrap(); - let validity_window_program = Program::validity_window(); - let account_keys = test_private_account_keys_1(); - let pre = AccountWithMetadata::new( - Account::default(), - false, - (&account_keys.npk(), &account_keys.vpk(), 0), - ); - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - let tx = { - let instruction = ( - BlockValidityWindow::new_unbounded(), - timestamp_validity_window, - ); - let (output, proof) = circuit::execute_and_prove( - vec![pre], - Program::serialize_instruction(instruction).unwrap(), - vec![InputAccountIdentity::PrivateUnauthorized { - vpk: account_keys.vpk(), - random_seed: [0; 32], - npk: account_keys.npk(), - identifier: 0, - }], - &validity_window_program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - PrivacyPreservingTransaction::new(message, witness_set) - }; - let result = state.transition_from_privacy_preserving_transaction(&tx, 1, timestamp); - let is_inside_validity_window = match ( - timestamp_validity_window.start(), - timestamp_validity_window.end(), - ) { - (Some(s), Some(e)) => s <= timestamp && timestamp < e, - (Some(s), None) => s <= timestamp, - (None, Some(e)) => timestamp < e, - (None, None) => true, - }; - if is_inside_validity_window { - assert!(result.is_ok()); - } else { - assert!(matches!(result, Err(LeeError::OutOfValidityWindow))); - } - } - - fn time_locked_transfer_transaction( - from: AccountId, - from_key: &PrivateKey, - from_nonce: u128, - to: AccountId, - clock_account_id: AccountId, - amount: u128, - deadline: u64, - ) -> PublicTransaction { - let program_id = Program::time_locked_transfer().id(); - let message = public_transaction::Message::try_new( - program_id, - vec![from, to, clock_account_id], - vec![Nonce(from_nonce)], - (amount, deadline), - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key]); - PublicTransaction::new(message, witness_set) - } - - #[test] - fn time_locked_transfer_succeeds_when_deadline_has_passed() { - let recipient_id = AccountId::new([42; 32]); - let genesis_timestamp = 500_u64; - let mut state = - V03State::new_with_genesis_accounts(&[(recipient_id, 0)], vec![], genesis_timestamp) - .with_test_programs(); - let key1 = PrivateKey::try_new([1; 32]).unwrap(); - let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1)); - state.force_insert_account( - sender_id, - Account { - program_owner: Program::time_locked_transfer().id(), - balance: 100, - ..Account::default() - }, - ); - - let amount = 100_u128; - // Deadline in the past: transfer should succeed. - let deadline = 0_u64; - - let tx = time_locked_transfer_transaction( - sender_id, - &key1, - 0, - recipient_id, - CLOCK_01_PROGRAM_ACCOUNT_ID, - amount, - deadline, - ); - - let block_id = 1; - let timestamp = genesis_timestamp + 100; - state - .transition_from_public_transaction(&tx, block_id, timestamp) - .unwrap(); - - // Balances changed. - assert_eq!(state.get_account_by_id(sender_id).balance, 0); - assert_eq!(state.get_account_by_id(recipient_id).balance, 100); - } - - #[test] - fn time_locked_transfer_fails_when_deadline_is_in_the_future() { - let recipient_id = AccountId::new([42; 32]); - let genesis_timestamp = 500_u64; - let mut state = - V03State::new_with_genesis_accounts(&[(recipient_id, 0)], vec![], genesis_timestamp) - .with_test_programs(); - let key1 = PrivateKey::try_new([1; 32]).unwrap(); - let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1)); - state.force_insert_account( - sender_id, - Account { - program_owner: Program::time_locked_transfer().id(), - balance: 100, - ..Account::default() - }, - ); - - let amount = 100_u128; - // Far-future deadline: program should panic. - let deadline = u64::MAX; - - let tx = time_locked_transfer_transaction( - sender_id, - &key1, - 0, - recipient_id, - CLOCK_01_PROGRAM_ACCOUNT_ID, - amount, - deadline, - ); - - let block_id = 1; - let timestamp = genesis_timestamp + 100; - let result = state.transition_from_public_transaction(&tx, block_id, timestamp); - - assert!( - result.is_err(), - "Transfer should fail when deadline is in the future" - ); - // Balances unchanged. - assert_eq!(state.get_account_by_id(sender_id).balance, 100); - assert_eq!(state.get_account_by_id(recipient_id).balance, 0); - } - - fn pinata_cooldown_data(prize: u128, cooldown_ms: u64, last_claim_timestamp: u64) -> Vec { - let mut buf = Vec::with_capacity(32); - buf.extend_from_slice(&prize.to_le_bytes()); - buf.extend_from_slice(&cooldown_ms.to_le_bytes()); - buf.extend_from_slice(&last_claim_timestamp.to_le_bytes()); - buf - } - - fn pinata_cooldown_transaction( - pinata_id: AccountId, - winner_id: AccountId, - clock_account_id: AccountId, - ) -> PublicTransaction { - let program_id = Program::pinata_cooldown().id(); - let message = public_transaction::Message::try_new( - program_id, - vec![pinata_id, winner_id, clock_account_id], - vec![], - (), - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - PublicTransaction::new(message, witness_set) - } - - #[test] - fn pinata_cooldown_claim_succeeds_after_cooldown() { - let winner_id = AccountId::new([11; 32]); - let pinata_id = AccountId::new([99; 32]); - - let genesis_timestamp = 1000_u64; - let mut state = - V03State::new_with_genesis_accounts(&[(winner_id, 0)], vec![], genesis_timestamp) - .with_test_programs(); - - let prize = 50_u128; - let cooldown_ms = 500_u64; - // Last claim was at genesis, so any timestamp >= genesis + cooldown should work. - let last_claim_timestamp = genesis_timestamp; - - state.force_insert_account( - pinata_id, - Account { - program_owner: Program::pinata_cooldown().id(), - balance: 1000, - data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) - .try_into() - .unwrap(), - ..Account::default() - }, - ); - - let tx = pinata_cooldown_transaction(pinata_id, winner_id, CLOCK_01_PROGRAM_ACCOUNT_ID); - - let block_id = 1; - let block_timestamp = genesis_timestamp + cooldown_ms; - // Advance clock so the cooldown check reads an updated timestamp. - let clock_tx = clock_transaction(block_timestamp); - state - .transition_from_public_transaction(&clock_tx, block_id, block_timestamp) - .unwrap(); - - state - .transition_from_public_transaction(&tx, block_id, block_timestamp) - .unwrap(); - - assert_eq!(state.get_account_by_id(pinata_id).balance, 1000 - prize); - assert_eq!(state.get_account_by_id(winner_id).balance, prize); - } - - #[test] - fn pinata_cooldown_claim_fails_during_cooldown() { - let winner_id = AccountId::new([11; 32]); - let pinata_id = AccountId::new([99; 32]); - - let genesis_timestamp = 1000_u64; - let mut state = - V03State::new_with_genesis_accounts(&[(winner_id, 0)], vec![], genesis_timestamp) - .with_test_programs(); - - let prize = 50_u128; - let cooldown_ms = 500_u64; - let last_claim_timestamp = genesis_timestamp; - - state.force_insert_account( - pinata_id, - Account { - balance: 1000, - data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) - .try_into() - .unwrap(), - ..Account::default() - }, - ); - - let tx = pinata_cooldown_transaction(pinata_id, winner_id, CLOCK_01_PROGRAM_ACCOUNT_ID); - - let block_id = 1; - // Timestamp is only 100ms after last claim, well within the 500ms cooldown. - let block_timestamp = genesis_timestamp + 100; - let clock_tx = clock_transaction(block_timestamp); - state - .transition_from_public_transaction(&clock_tx, block_id, block_timestamp) - .unwrap(); - - let result = state.transition_from_public_transaction(&tx, block_id, block_timestamp); - - assert!(result.is_err(), "Claim should fail during cooldown period"); - assert_eq!(state.get_account_by_id(pinata_id).balance, 1000); - assert_eq!(state.get_account_by_id(winner_id).balance, 0); - } - - #[test] - fn state_serialization_roundtrip() { - let account_id_1 = AccountId::new([1; 32]); - let account_id_2 = AccountId::new([2; 32]); - let initial_data = [(account_id_1, 100_u128), (account_id_2, 151_u128)]; - let state = - V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); - let bytes = borsh::to_vec(&state).unwrap(); - let state_from_bytes: V03State = borsh::from_slice(&bytes).unwrap(); - assert_eq!(state, state_from_bytes); - } - - #[test] - fn flash_swap_successful() { - let initiator = Program::flash_swap_initiator(); - let callback = Program::flash_swap_callback(); - let token = Program::authenticated_transfer_program(); - - let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); - let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32])); - - let initial_balance: u128 = 1000; - let amount_out: u128 = 100; - - let vault_account = Account { - program_owner: token.id(), - balance: initial_balance, - ..Account::default() - }; - let receiver_account = Account { - program_owner: token.id(), - balance: 0, - ..Account::default() - }; - - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - state.force_insert_account(vault_id, vault_account); - state.force_insert_account(receiver_id, receiver_account); - - // Callback instruction: return funds - let cb_instruction = CallbackInstruction { - return_funds: true, - token_program_id: token.id(), - amount: amount_out, - }; - let cb_data = Program::serialize_instruction(cb_instruction).unwrap(); - - let instruction = FlashSwapInstruction::Initiate { - token_program_id: token.id(), - callback_program_id: callback.id(), - amount_out, - callback_instruction_data: cb_data, - }; - - let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction); - let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(result.is_ok(), "flash swap should succeed: {result:?}"); - - // Vault balance restored, receiver back to 0 - assert_eq!(state.get_account_by_id(vault_id).balance, initial_balance); - assert_eq!(state.get_account_by_id(receiver_id).balance, 0); - } - - #[test] - fn flash_swap_callback_keeps_funds_rollback() { - let initiator = Program::flash_swap_initiator(); - let callback = Program::flash_swap_callback(); - let token = Program::authenticated_transfer_program(); - - let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); - let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32])); - - let initial_balance: u128 = 1000; - let amount_out: u128 = 100; - - let vault_account = Account { - program_owner: token.id(), - balance: initial_balance, - ..Account::default() - }; - let receiver_account = Account { - program_owner: token.id(), - balance: 0, - ..Account::default() - }; - - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - state.force_insert_account(vault_id, vault_account); - state.force_insert_account(receiver_id, receiver_account); - - // Callback instruction: do NOT return funds - let cb_instruction = CallbackInstruction { - return_funds: false, - token_program_id: token.id(), - amount: amount_out, - }; - let cb_data = Program::serialize_instruction(cb_instruction).unwrap(); - - let instruction = FlashSwapInstruction::Initiate { - token_program_id: token.id(), - callback_program_id: callback.id(), - amount_out, - callback_instruction_data: cb_data, - }; - - let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction); - let result = state.transition_from_public_transaction(&tx, 1, 0); - - // Invariant check fails โ†’ entire tx rolls back - assert!( - result.is_err(), - "flash swap should fail when callback keeps funds" - ); - - // State unchanged (rollback) - assert_eq!(state.get_account_by_id(vault_id).balance, initial_balance); - assert_eq!(state.get_account_by_id(receiver_id).balance, 0); - } - - #[test] - fn flash_swap_self_call_targets_correct_program() { - // Zero-amount flash swap: the invariant self-call still runs and succeeds - // because vault balance doesn't decrease. - let initiator = Program::flash_swap_initiator(); - let callback = Program::flash_swap_callback(); - let token = Program::authenticated_transfer_program(); - - let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); - let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32])); - - let initial_balance: u128 = 1000; - - let vault_account = Account { - program_owner: token.id(), - balance: initial_balance, - ..Account::default() - }; - let receiver_account = Account { - program_owner: token.id(), - balance: 0, - ..Account::default() - }; - - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - state.force_insert_account(vault_id, vault_account); - state.force_insert_account(receiver_id, receiver_account); - - let cb_instruction = CallbackInstruction { - return_funds: true, - token_program_id: token.id(), - amount: 0, - }; - let cb_data = Program::serialize_instruction(cb_instruction).unwrap(); - - let instruction = FlashSwapInstruction::Initiate { - token_program_id: token.id(), - callback_program_id: callback.id(), - amount_out: 0, - callback_instruction_data: cb_data, - }; - - let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction); - let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!( - result.is_ok(), - "zero-amount flash swap should succeed: {result:?}" - ); - } - - #[test] - fn flash_swap_standalone_invariant_check_rejected() { - // Calling InvariantCheck directly (not as a chained self-call) should fail - // because caller_program_id will be None. - let initiator = Program::flash_swap_initiator(); - let token = Program::authenticated_transfer_program(); - - let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); - - let vault_account = Account { - program_owner: token.id(), - balance: 1000, - ..Account::default() - }; - - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - state.force_insert_account(vault_id, vault_account); - - let instruction = FlashSwapInstruction::InvariantCheck { - min_vault_balance: 1000, - }; - - let message = public_transaction::Message::try_new( - initiator.id(), - vec![vault_id], - vec![], - instruction, - ) - .unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!( - result.is_err(), - "standalone InvariantCheck should be rejected (caller_program_id is None)" - ); - } - - #[test] - fn malicious_self_program_id_rejected_in_public_execution() { - let program = Program::malicious_self_program_id(); - let acc_id = AccountId::new([99; 32]); - let account = Account::default(); - - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - state.force_insert_account(acc_id, account); - - let message = - public_transaction::Message::try_new(program.id(), vec![acc_id], vec![], ()).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!( - result.is_err(), - "program with wrong self_program_id in output should be rejected" - ); - } - - #[test] - fn malicious_caller_program_id_rejected_in_public_execution() { - let program = Program::malicious_caller_program_id(); - let acc_id = AccountId::new([99; 32]); - let account = Account::default(); - - let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); - state.force_insert_account(acc_id, account); - - let message = - public_transaction::Message::try_new(program.id(), vec![acc_id], vec![], ()).unwrap(); - let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); - let tx = PublicTransaction::new(message, witness_set); - - let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!( - result.is_err(), - "program with spoofed caller_program_id in output should be rejected" - ); - } - - #[test] - fn two_private_pda_family_members_receive_and_spend() { - let funder_keys = test_public_account_keys_1(); - let alice_keys = test_private_account_keys_1(); - let alice_npk = alice_keys.npk(); - - let proxy = Program::pda_spend_proxy(); - let auth_transfer = Program::authenticated_transfer_program(); - let proxy_id = proxy.id(); - let auth_transfer_id = auth_transfer.id(); - let seed = PdaSeed::new([42; 32]); - let amount: u128 = 100; - - let spend_with_deps = - ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer.clone())].into()); - - let funder_id = funder_keys.account_id(); - let alice_pda_0_id = - AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_keys.vpk(), 0); - let alice_pda_1_id = - AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_keys.vpk(), 1); - let recipient_id = test_public_account_keys_2().account_id(); - let recipient_signing_key = test_public_account_keys_2().signing_key; - - let mut state = V03State::new_with_genesis_accounts(&[(funder_id, 500)], vec![], 0); - - let alice_pda_0_account = Account { - program_owner: auth_transfer_id, - balance: amount, - nonce: Nonce::private_account_nonce_init(&alice_pda_0_id), - ..Account::default() - }; - let alice_pda_1_account = Account { - program_owner: auth_transfer_id, - balance: amount, - nonce: Nonce::private_account_nonce_init(&alice_pda_1_id), - ..Account::default() - }; - - // Fund alice_pda_0 via authenticated_transfer directly. - { - let funder_account = state.get_account_by_id(funder_id); - let funder_nonce = funder_account.nonce; - let (output, proof) = execute_and_prove( - vec![ - AccountWithMetadata::new(funder_account, true, funder_id), - AccountWithMetadata::new(Account::default(), false, alice_pda_0_id), - ], - Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) - .unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { - vpk: alice_keys.vpk(), - random_seed: [0; 32], - npk: alice_npk, - identifier: 0, - seed: Some((seed, proxy_id)), - }, - ], - &auth_transfer.clone().into(), - ) - .unwrap(); - let message = - Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output) - .unwrap(); - let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); - state - .transition_from_privacy_preserving_transaction( - &PrivacyPreservingTransaction::new(message, witness_set), - 1, - 0, - ) - .unwrap(); - } - - // Fund alice_pda_1 the same way with identifier 1. - { - let funder_account = state.get_account_by_id(funder_id); - let funder_nonce = funder_account.nonce; - let (output, proof) = execute_and_prove( - vec![ - AccountWithMetadata::new(funder_account, true, funder_id), - AccountWithMetadata::new(Account::default(), false, alice_pda_1_id), - ], - Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) - .unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaInit { - vpk: alice_keys.vpk(), - random_seed: [0; 32], - npk: alice_npk, - identifier: 1, - seed: Some((seed, proxy_id)), - }, - ], - &auth_transfer.into(), - ) - .unwrap(); - let message = - Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output) - .unwrap(); - let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); - state - .transition_from_privacy_preserving_transaction( - &PrivacyPreservingTransaction::new(message, witness_set), - 2, - 0, - ) - .unwrap(); - } - - let commitment_pda_0 = Commitment::new(&alice_pda_0_id, &alice_pda_0_account); - let commitment_pda_1 = Commitment::new(&alice_pda_1_id, &alice_pda_1_account); - - assert!(state.get_proof_for_commitment(&commitment_pda_0).is_some()); - assert!(state.get_proof_for_commitment(&commitment_pda_1).is_some()); - - // Alice spends alice_pda_0 into the public recipient. - { - let recipient_account = state.get_account_by_id(recipient_id); - let (output, proof) = execute_and_prove( - vec![ - AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id), - AccountWithMetadata::new(recipient_account, true, recipient_id), - ], - Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), - vec![ - InputAccountIdentity::PrivatePdaUpdate { - vpk: alice_keys.vpk(), - random_seed: [0; 32], - nsk: alice_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&commitment_pda_0) - .expect("pda_0 must be in state"), - identifier: 0, - seed: None, - }, - InputAccountIdentity::Public, - ], - &spend_with_deps, - ) - .unwrap(); - let message = - Message::try_from_circuit_output(vec![recipient_id], vec![Nonce(0)], output) - .unwrap(); - let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); - state - .transition_from_privacy_preserving_transaction( - &PrivacyPreservingTransaction::new(message, witness_set), - 3, - 0, - ) - .unwrap(); - } - - // Alice spends alice_pda_1 into the same public recipient. - { - let recipient_account = state.get_account_by_id(recipient_id); - let (output, proof) = execute_and_prove( - vec![ - AccountWithMetadata::new(alice_pda_1_account.clone(), true, alice_pda_1_id), - AccountWithMetadata::new(recipient_account, false, recipient_id), - ], - Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), - vec![ - InputAccountIdentity::PrivatePdaUpdate { - vpk: alice_keys.vpk(), - random_seed: [0; 32], - nsk: alice_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&commitment_pda_1) - .expect("pda_1 must be in state"), - identifier: 1, - seed: None, - }, - InputAccountIdentity::Public, - ], - &spend_with_deps, - ) - .unwrap(); - let message = - Message::try_from_circuit_output(vec![recipient_id], vec![], output).unwrap(); - let witness_set = WitnessSet::for_message(&message, proof, &[]); - state - .transition_from_privacy_preserving_transaction( - &PrivacyPreservingTransaction::new(message, witness_set), - 4, - 0, - ) - .unwrap(); - } - - assert_eq!(state.get_account_by_id(recipient_id).balance, 2 * amount); - - // Re-fund alice_pda_1 top-level via auth_transfer using PrivatePdaUpdate with an - // external seed. - let alice_pda_1_account_after_spend = Account { - program_owner: auth_transfer_id, - balance: 0, - nonce: alice_pda_1_account - .nonce - .private_account_nonce_increment(&alice_keys.nsk), - ..Account::default() - }; - let commitment_pda_1_after_spend = - Commitment::new(&alice_pda_1_id, &alice_pda_1_account_after_spend); - { - let recipient_account = state.get_account_by_id(recipient_id); - let recipient_nonce = recipient_account.nonce; - let (output, proof) = execute_and_prove( - vec![ - AccountWithMetadata::new(recipient_account, true, recipient_id), - AccountWithMetadata::new( - alice_pda_1_account_after_spend, - false, - alice_pda_1_id, - ), - ], - Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) - .unwrap(), - vec![ - InputAccountIdentity::Public, - InputAccountIdentity::PrivatePdaUpdate { - vpk: alice_keys.vpk(), - random_seed: [0; 32], - nsk: alice_keys.nsk, - membership_proof: state - .get_proof_for_commitment(&commitment_pda_1_after_spend) - .expect("pda_1 after spend must be in state"), - identifier: 1, - seed: Some((seed, proxy_id)), - }, - ], - &Program::authenticated_transfer_program().into(), - ) - .unwrap(); - let message = - Message::try_from_circuit_output(vec![recipient_id], vec![recipient_nonce], output) - .unwrap(); - let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); - state - .transition_from_privacy_preserving_transaction( - &PrivacyPreservingTransaction::new(message, witness_set), - 5, - 0, - ) - .unwrap(); - } - - assert_eq!(state.get_account_by_id(recipient_id).balance, amount); - } -} diff --git a/lee/state_machine/src/state/mod.rs b/lee/state_machine/src/state/mod.rs new file mode 100644 index 00000000..fcf71a21 --- /dev/null +++ b/lee/state_machine/src/state/mod.rs @@ -0,0 +1,320 @@ +use std::collections::{BTreeSet, HashMap, HashSet}; + +use borsh::{BorshDeserialize, BorshSerialize}; +use lee_core::{ + BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, + Timestamp, + account::{Account, AccountId}, + program::ProgramId, +}; + +use crate::{ + error::LeeError, + merkle_tree::MerkleTree, + privacy_preserving_transaction::PrivacyPreservingTransaction, + program::Program, + program_deployment_transaction::ProgramDeploymentTransaction, + public_transaction::PublicTransaction, + validated_state_diff::{StateDiff, ValidatedStateDiff}, +}; + +pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; + +#[derive(Clone, BorshSerialize, BorshDeserialize)] +#[cfg_attr(test, derive(Debug, PartialEq, Eq))] +pub struct CommitmentSet { + merkle_tree: MerkleTree, + commitments: HashMap, + root_history: HashSet, +} + +impl CommitmentSet { + pub(crate) fn digest(&self) -> CommitmentSetDigest { + self.merkle_tree.root() + } + + /// Queries the `CommitmentSet` for a membership proof of commitment. + pub fn get_proof_for(&self, commitment: &Commitment) -> Option { + let index = *self.commitments.get(commitment)?; + + self.merkle_tree + .get_authentication_path_for(index) + .map(|path| (index, path)) + } + + /// Inserts a list of commitments to the `CommitmentSet`. + pub(crate) fn extend(&mut self, commitments: &[Commitment]) { + for commitment in commitments.iter().cloned() { + let index = self.merkle_tree.insert(commitment.to_byte_array()); + self.commitments.insert(commitment, index); + } + self.root_history.insert(self.digest()); + } + + fn contains(&self, commitment: &Commitment) -> bool { + self.commitments.contains_key(commitment) + } + + /// Initializes an empty `CommitmentSet` with a given capacity. + /// If the capacity is not a `power_of_two`, then capacity is taken + /// to be the next `power_of_two`. + pub(crate) fn with_capacity(capacity: usize) -> Self { + Self { + merkle_tree: MerkleTree::with_capacity(capacity), + commitments: HashMap::new(), + root_history: HashSet::new(), + } + } +} + +#[cfg_attr(test, derive(Debug, PartialEq, Eq))] +#[derive(Clone)] +struct NullifierSet(BTreeSet); + +impl NullifierSet { + const fn new() -> Self { + Self(BTreeSet::new()) + } + + fn extend(&mut self, new_nullifiers: &[Nullifier]) { + self.0.extend(new_nullifiers); + } + + fn contains(&self, nullifier: &Nullifier) -> bool { + self.0.contains(nullifier) + } +} + +impl BorshSerialize for NullifierSet { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + self.0.iter().collect::>().serialize(writer) + } +} + +impl BorshDeserialize for NullifierSet { + fn deserialize_reader(reader: &mut R) -> std::io::Result { + let vec = Vec::::deserialize_reader(reader)?; + + let mut set = BTreeSet::new(); + for n in vec { + if !set.insert(n) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "duplicate nullifier in NullifierSet", + )); + } + } + + Ok(Self(set)) + } +} + +#[derive(Clone, BorshSerialize, BorshDeserialize)] +#[cfg_attr(test, derive(Debug, PartialEq, Eq))] +pub struct V03State { + public_state: HashMap, + private_state: (CommitmentSet, NullifierSet), + programs: HashMap, +} + +impl Default for V03State { + fn default() -> Self { + let mut commitment_set = CommitmentSet::with_capacity(32); + commitment_set.extend(&[DUMMY_COMMITMENT]); + let nullifier_set = NullifierSet::new(); + let private_state = (commitment_set, nullifier_set); + + Self { + public_state: HashMap::default(), + private_state, + programs: HashMap::default(), + } + } +} + +impl V03State { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Initializes state with given public account balances leaving other account fields at their + /// default values. + #[must_use] + pub fn with_public_account_balances( + mut self, + balances: impl IntoIterator, + ) -> Self { + let public_accounts = balances.into_iter().map(|(account_id, balance)| { + ( + account_id, + Account { + balance, + ..Account::default() + }, + ) + }); + self.public_state.extend(public_accounts); + self + } + + /// Initializes state with given public accounts. + #[must_use] + pub fn with_public_accounts( + mut self, + public_accounts: impl IntoIterator, + ) -> Self { + self.public_state.extend(public_accounts); + self + } + + /// Initializes state with given private accounts. + #[must_use] + pub fn with_private_accounts( + mut self, + private_accounts: impl IntoIterator, + ) -> Self { + let (commitments, nullifiers): (Vec, Vec) = + private_accounts.into_iter().unzip(); + self.private_state.0.extend(&commitments); + self.private_state.1.extend(&nullifiers); + self + } + + /// Initializes state with given builtin programs. + #[must_use] + pub fn with_programs(mut self, programs: impl IntoIterator) -> Self { + for program in programs { + self.insert_program(program); + } + self + } + + pub(crate) fn insert_program(&mut self, program: Program) { + self.programs.insert(program.id(), program); + } + + pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) { + let StateDiff { + signer_account_ids, + public_diff, + new_commitments, + new_nullifiers, + program, + } = diff.into_state_diff(); + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] + for (account_id, account) in public_diff { + *self.get_account_by_id_mut(account_id) = account; + } + for account_id in signer_account_ids { + self.get_account_by_id_mut(account_id) + .nonce + .public_account_nonce_increment(); + } + self.private_state.0.extend(&new_commitments); + self.private_state.1.extend(&new_nullifiers); + if let Some(program) = program { + self.insert_program(program); + } + } + + pub fn transition_from_public_transaction( + &mut self, + tx: &PublicTransaction, + block_id: BlockId, + timestamp: Timestamp, + ) -> Result<(), LeeError> { + let diff = ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp)?; + self.apply_state_diff(diff); + Ok(()) + } + + pub fn transition_from_privacy_preserving_transaction( + &mut self, + tx: &PrivacyPreservingTransaction, + block_id: BlockId, + timestamp: Timestamp, + ) -> Result<(), LeeError> { + let diff = + ValidatedStateDiff::from_privacy_preserving_transaction(tx, self, block_id, timestamp)?; + self.apply_state_diff(diff); + Ok(()) + } + + pub fn transition_from_program_deployment_transaction( + &mut self, + tx: &ProgramDeploymentTransaction, + ) -> Result<(), LeeError> { + let diff = ValidatedStateDiff::from_program_deployment_transaction(tx, self)?; + self.apply_state_diff(diff); + Ok(()) + } + + fn get_account_by_id_mut(&mut self, account_id: AccountId) -> &mut Account { + self.public_state.entry(account_id).or_default() + } + + #[must_use] + pub fn get_account_by_id(&self, account_id: AccountId) -> Account { + self.public_state + .get(&account_id) + .cloned() + .unwrap_or_else(Account::default) + } + + #[must_use] + pub fn get_proof_for_commitment(&self, commitment: &Commitment) -> Option { + self.private_state.0.get_proof_for(commitment) + } + + pub(crate) const fn programs(&self) -> &HashMap { + &self.programs + } + + #[must_use] + pub fn commitment_set_digest(&self) -> CommitmentSetDigest { + self.private_state.0.digest() + } + + pub(crate) fn check_commitments_are_new( + &self, + new_commitments: &[Commitment], + ) -> Result<(), LeeError> { + for commitment in new_commitments { + if self.private_state.0.contains(commitment) { + return Err(LeeError::InvalidInput("Commitment already seen".to_owned())); + } + } + Ok(()) + } + + pub(crate) fn check_nullifiers_are_valid( + &self, + new_nullifiers: &[(Nullifier, CommitmentSetDigest)], + ) -> Result<(), LeeError> { + for (nullifier, digest) in new_nullifiers { + if self.private_state.1.contains(nullifier) { + return Err(LeeError::InvalidInput("Nullifier already seen".to_owned())); + } + if !self.private_state.0.root_history.contains(digest) { + return Err(LeeError::InvalidInput( + "Unrecognized commitment set digest".to_owned(), + )); + } + } + Ok(()) + } +} + +#[cfg(any(test, feature = "test-utils"))] +impl V03State { + pub fn force_insert_account(&mut self, account_id: AccountId, account: Account) { + self.public_state.insert(account_id, account); + } +} + +#[cfg(test)] +pub mod tests; diff --git a/lee/state_machine/src/state/tests/authenticated_transfer.rs b/lee/state_machine/src/state/tests/authenticated_transfer.rs new file mode 100644 index 00000000..8d227fc3 --- /dev/null +++ b/lee/state_machine/src/state/tests/authenticated_transfer.rs @@ -0,0 +1,149 @@ +use super::*; + +#[test] +fn transition_from_authenticated_transfer_program_invocation_default_account_destination() { + let key = PrivateKey::try_new([1; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); + let initial_data = [( + account_id, + Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + ..Account::default() + }, + )]; + let mut state = V03State::new() + .with_public_accounts(initial_data) + .with_test_programs(); + let from = account_id; + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); + assert_eq!(state.get_account_by_id(to), Account::default()); + let balance_to_move = 5; + + let tx = transfer_transaction(from, &key, 0, to, &to_key, 0, balance_to_move); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + assert_eq!(state.get_account_by_id(from).balance, 95); + assert_eq!(state.get_account_by_id(to).balance, 5); + assert_eq!(state.get_account_by_id(from).nonce, Nonce(1)); + assert_eq!(state.get_account_by_id(to).nonce, Nonce(1)); +} + +#[test] +fn transition_from_authenticated_transfer_program_invocation_insuficient_balance() { + let key = PrivateKey::try_new([1; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); + let mut state = V03State::new() + .with_public_account_balances([(account_id, 100)]) + .with_test_programs(); + let from = account_id; + let from_key = key; + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); + let balance_to_move = 101; + assert!(state.get_account_by_id(from).balance < balance_to_move); + + let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move); + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!(result, Err(LeeError::ProgramExecutionFailed(_)))); + assert_eq!(state.get_account_by_id(from).balance, 100); + assert_eq!(state.get_account_by_id(to).balance, 0); + assert_eq!(state.get_account_by_id(from).nonce, Nonce(0)); + assert_eq!(state.get_account_by_id(to).nonce, Nonce(0)); +} + +#[test] +fn transition_from_authenticated_transfer_program_invocation_non_default_account_destination() { + let key1 = PrivateKey::try_new([1; 32]).unwrap(); + let key2 = PrivateKey::try_new([2; 32]).unwrap(); + let account_id1 = AccountId::from(&PublicKey::new_from_private_key(&key1)); + let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); + let initial_data = [ + ( + account_id1, + Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + ..Account::default() + }, + ), + ( + account_id2, + Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 200, + ..Account::default() + }, + ), + ]; + let mut state = V03State::new() + .with_public_accounts(initial_data) + .with_test_programs(); + let from = account_id2; + let from_key = key2; + let to = account_id1; + let to_key = key1; + assert_ne!(state.get_account_by_id(to), Account::default()); + let balance_to_move = 8; + + let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + assert_eq!(state.get_account_by_id(from).balance, 192); + assert_eq!(state.get_account_by_id(to).balance, 108); + assert_eq!(state.get_account_by_id(from).nonce, Nonce(1)); + assert_eq!(state.get_account_by_id(to).nonce, Nonce(1)); +} + +#[test] +fn transition_from_sequence_of_authenticated_transfer_program_invocations() { + let key1 = PrivateKey::try_new([8; 32]).unwrap(); + let account_id1 = AccountId::from(&PublicKey::new_from_private_key(&key1)); + let key2 = PrivateKey::try_new([2; 32]).unwrap(); + let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); + let initial_data = [( + account_id1, + Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + ..Account::default() + }, + )]; + let mut state = V03State::new() + .with_public_accounts(initial_data) + .with_test_programs(); + let key3 = PrivateKey::try_new([3; 32]).unwrap(); + let account_id3 = AccountId::from(&PublicKey::new_from_private_key(&key3)); + let balance_to_move = 5; + + let tx = transfer_transaction( + account_id1, + &key1, + 0, + account_id2, + &key2, + 0, + balance_to_move, + ); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + let balance_to_move = 3; + let tx = transfer_transaction( + account_id2, + &key2, + 1, + account_id3, + &key3, + 0, + balance_to_move, + ); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + assert_eq!(state.get_account_by_id(account_id1).balance, 95); + assert_eq!(state.get_account_by_id(account_id2).balance, 2); + assert_eq!(state.get_account_by_id(account_id3).balance, 3); + assert_eq!(state.get_account_by_id(account_id1).nonce, Nonce(1)); + assert_eq!(state.get_account_by_id(account_id2).nonce, Nonce(2)); + assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(1)); +} diff --git a/lee/state_machine/src/state/tests/changer_claimer.rs b/lee/state_machine/src/state/tests/changer_claimer.rs new file mode 100644 index 00000000..9a581c63 --- /dev/null +++ b/lee/state_machine/src/state/tests/changer_claimer.rs @@ -0,0 +1,116 @@ +use super::*; + +#[test] +fn public_changer_claimer_no_data_change_no_claim_succeeds() { + let initial_data = []; + let mut state = V03State::new() + .with_public_accounts(public_state_from_balances(&initial_data)) + .with_test_programs(); + let account_id = AccountId::new([1; 32]); + let program_id = crate::test_methods::changer_claimer().id(); + // Don't change data (None) and don't claim (false) + let instruction: (Option>, bool) = (None, false); + + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + // Should succeed - no changes made, no claim needed + assert!(result.is_ok()); + // Account should remain default/unclaimed + assert_eq!(state.get_account_by_id(account_id), Account::default()); +} + +#[test] +fn public_changer_claimer_data_change_no_claim_fails() { + let initial_data = []; + let mut state = V03State::new() + .with_public_accounts(public_state_from_balances(&initial_data)) + .with_test_programs(); + let account_id = AccountId::new([1; 32]); + let program_id = crate::test_methods::changer_claimer().id(); + // Change data but don't claim (false) - should fail + let new_data = vec![1, 2, 3, 4, 5]; + let instruction: (Option>, bool) = (Some(new_data), false); + + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + // Should fail - cannot modify data without claiming the account + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior( + InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { + account_id: err_account_id + } + )) if err_account_id == account_id + )); +} + +#[test] +fn private_changer_claimer_no_data_change_no_claim_succeeds() { + let program = crate::test_methods::changer_claimer(); + let sender_keys = test_private_account_keys_1(); + let private_account = AccountWithMetadata::new( + Account::default(), + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + // Don't change data (None) and don't claim (false) + let instruction: (Option>, bool) = (None, false); + + let result = execute_and_prove( + vec![private_account], + Program::serialize_instruction(instruction).unwrap(), + vec![InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }], + &program.into(), + ); + + // Should succeed - no changes made, no claim needed + assert!(result.is_ok()); +} + +#[test] +fn private_changer_claimer_data_change_no_claim_fails() { + let program = crate::test_methods::changer_claimer(); + let sender_keys = test_private_account_keys_1(); + let private_account = AccountWithMetadata::new( + Account::default(), + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + // Change data but don't claim (false) - should fail + let new_data = vec![1, 2, 3, 4, 5]; + let instruction: (Option>, bool) = (Some(new_data), false); + + let result = execute_and_prove( + vec![private_account], + Program::serialize_instruction(instruction).unwrap(), + vec![InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }], + &program.into(), + ); + + // Should fail - cannot modify data without claiming the account + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} diff --git a/lee/state_machine/src/state/tests/circuit.rs b/lee/state_machine/src/state/tests/circuit.rs new file mode 100644 index 00000000..c13a7370 --- /dev/null +++ b/lee/state_machine/src/state/tests/circuit.rs @@ -0,0 +1,1136 @@ +use super::*; + +#[test] +fn circuit_fails_if_visibility_masks_have_incorrect_lenght() { + let program = crate::test_methods::simple_balance_transfer(); + let public_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + let public_account_2 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([1; 32]), + ); + + // Single account_identity entry for a circuit execution with two pre_state accounts. + let result = execute_and_prove( + vec![public_account_1, public_account_2], + Program::serialize_instruction(10_u128).unwrap(), + vec![InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn circuit_fails_if_invalid_auth_keys_are_provided() { + let program = crate::test_methods::simple_balance_transfer(); + let sender_keys = test_private_account_keys_1(); + let recipient_keys = test_private_account_keys_2(); + let private_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let private_account_2 = AccountWithMetadata::new( + Account::default(), + false, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); + + // Setting the recipient nsk to authorize the sender. + // This should be set to the sender private account in a normal circumstance. + // `PrivateAuthorizedUpdate` derives npk from nsk and asserts equality with + // `pre_state.account_id`, so a mismatched nsk fails that check. + let result = execute_and_prove( + vec![private_account_1, private_account_2], + Program::serialize_instruction(10_u128).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: recipient_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + npk: recipient_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn circuit_should_fail_if_new_private_account_with_non_default_balance_is_provided() { + let program = crate::test_methods::simple_balance_transfer(); + let sender_keys = test_private_account_keys_1(); + let recipient_keys = test_private_account_keys_2(); + let private_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let private_account_2 = AccountWithMetadata::new( + Account { + // Non default balance + balance: 1, + ..Account::default() + }, + false, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); + + let result = execute_and_prove( + vec![private_account_1, private_account_2], + Program::serialize_instruction(10_u128).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + npk: recipient_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn circuit_should_fail_if_new_private_account_with_non_default_program_owner_is_provided() { + let program = crate::test_methods::simple_balance_transfer(); + let sender_keys = test_private_account_keys_1(); + let recipient_keys = test_private_account_keys_2(); + let private_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let private_account_2 = AccountWithMetadata::new( + Account { + // Non default program_owner + program_owner: [0, 1, 2, 3, 4, 5, 6, 7], + ..Account::default() + }, + false, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); + + let result = execute_and_prove( + vec![private_account_1, private_account_2], + Program::serialize_instruction(10_u128).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + npk: recipient_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn circuit_should_fail_if_new_private_account_with_non_default_data_is_provided() { + let program = crate::test_methods::simple_balance_transfer(); + let sender_keys = test_private_account_keys_1(); + let recipient_keys = test_private_account_keys_2(); + let private_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let private_account_2 = AccountWithMetadata::new( + Account { + // Non default data + data: b"hola mundo".to_vec().try_into().unwrap(), + ..Account::default() + }, + false, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); + + let result = execute_and_prove( + vec![private_account_1, private_account_2], + Program::serialize_instruction(10_u128).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + npk: recipient_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn circuit_should_fail_if_new_private_account_with_non_default_nonce_is_provided() { + let program = crate::test_methods::simple_balance_transfer(); + let sender_keys = test_private_account_keys_1(); + let recipient_keys = test_private_account_keys_2(); + let private_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let private_account_2 = AccountWithMetadata::new( + Account { + // Non default nonce + nonce: Nonce(0xdead_beef), + ..Account::default() + }, + false, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); + + let result = execute_and_prove( + vec![private_account_1, private_account_2], + Program::serialize_instruction(10_u128).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + npk: recipient_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn circuit_should_fail_if_new_private_account_is_provided_with_default_values_but_marked_as_authorized() + { + let program = crate::test_methods::simple_balance_transfer(); + let sender_keys = test_private_account_keys_1(); + let recipient_keys = test_private_account_keys_2(); + let private_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let private_account_2 = AccountWithMetadata::new( + Account::default(), + // This should be set to false in normal circumstances + true, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); + + let result = execute_and_prove( + vec![private_account_1, private_account_2], + Program::serialize_instruction(10_u128).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + npk: recipient_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +/// A private PDA account that no program claims via `Claim::Pda` and no caller authorizes via +/// `ChainedCall.pda_seeds` has no binding between its supplied npk and its `account_id`, +/// so the circuit must reject. Here `simple_balance_transfer` emits no claim for the +/// second account, leaving position 1 unbound. +#[test] +fn private_pda_without_binding_fails() { + let program = crate::test_methods::simple_balance_transfer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let public_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + let private_pda_account = + AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); + + let result = execute_and_prove( + vec![public_account_1, private_pda_account], + Program::serialize_instruction(10_u128).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + vpk: keys.vpk(), + random_seed: [0; 32], + npk, + identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }, + ], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +/// Happy path: a program claims a new private PDA via `Claim::Pda(seed)`. The circuit +/// reads the npk for that `pre_state` from `private_account_keys` at the `pre_state`'s +/// position, derives `AccountId` via `AccountId::for_private_pda(program_id, seed, npk)`, and +/// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim +/// and binds the supplied npk to the `account_id`. +#[test] +fn private_pda_claim_succeeds() { + let program = crate::test_methods::pda_claimer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), u128::MAX); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction(seed).unwrap(), + vec![InputAccountIdentity::PrivatePdaInit { + vpk: keys.vpk(), + random_seed: [0; 32], + npk, + identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }], + &program.into(), + ); + + let (output, _proof) = result.expect("private PDA claim should succeed"); + assert_eq!(output.new_nullifiers.len(), 1); + assert_eq!(output.new_commitments.len(), 1); + assert_eq!(output.encrypted_private_post_states.len(), 1); + assert!(output.public_pre_states.is_empty()); + assert!(output.public_post_states.is_empty()); +} + +/// An npk is supplied that does not match the `pre_state`'s `account_id` under +/// `AccountId::for_private_pda(program, claim_seed, npk)`. The claim equality check rejects. +#[test] +fn private_pda_npk_mismatch_fails() { + // `keys_a` produces the `pre_state`'s `account_id` (the registered pair), `keys_b` is + // the mismatched pair supplied in `private_account_keys` for that pre_state. + let program = crate::test_methods::pda_claimer(); + let keys_a = test_private_account_keys_1(); + let keys_b = test_private_account_keys_2(); + let npk_a = keys_a.npk(); + let npk_b = keys_b.npk(); + let seed = PdaSeed::new([42; 32]); + + // `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state. + // `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in + // the circuit must reject. + let account_id = + AccountId::for_private_pda(&program.id(), &seed, &npk_a, &keys_a.vpk(), u128::MAX); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction(seed).unwrap(), + vec![InputAccountIdentity::PrivatePdaInit { + vpk: keys_b.vpk(), + random_seed: [0; 32], + npk: npk_b, + identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +/// Happy path for the caller-seeds authorization of a private PDA. The delegator claims a +/// private PDA via `Claim::Pda(seed)`, then chains to a callee (`noop`) delegating the same +/// seed via `ChainedCall.pda_seeds`. In the callee's step, the `pre_state`'s authorization +/// is established via the private derivation +/// `AccountId::for_private_pda(delegator, seed, npk) == pre.account_id`. +#[test] +fn caller_pda_seeds_authorize_private_pda_for_callee() { + let delegator = crate::test_methods::private_pda_delegator(); + let callee = crate::test_methods::auth_asserting_noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([77; 32]); + + let account_id = + AccountId::for_private_pda(&delegator.id(), &seed, &npk, &keys.vpk(), u128::MAX); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let callee_id = callee.id(); + let program_with_deps = ProgramWithDependencies::new(delegator, [(callee_id, callee)].into()); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction((seed, seed, callee_id)).unwrap(), + vec![InputAccountIdentity::PrivatePdaInit { + vpk: keys.vpk(), + random_seed: [0; 32], + npk, + identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }], + &program_with_deps, + ); + + let (output, _proof) = + result.expect("caller-seeds authorization of private PDA should succeed"); + assert_eq!(output.new_commitments.len(), 1); + assert_eq!(output.new_nullifiers.len(), 1); +} + +/// The delegator chains with a different seed than the one it claimed with. In the callee +/// step, neither public nor private caller-seeds authorization matches; `pre.is_authorized` +/// was set to `true` by the delegator but no proven source supports it, so the consistency +/// assertion rejects. +#[test] +fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() { + let delegator = crate::test_methods::private_pda_delegator(); + let callee = crate::test_methods::auth_asserting_noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let claim_seed = PdaSeed::new([77; 32]); + let wrong_delegated_seed = PdaSeed::new([88; 32]); + + let account_id = + AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, &keys.vpk(), u128::MAX); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let callee_id = callee.id(); + let program_with_deps = ProgramWithDependencies::new(delegator, [(callee_id, callee)].into()); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), + vec![InputAccountIdentity::PrivatePdaInit { + vpk: keys.vpk(), + random_seed: [0; 32], + npk, + identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }], + &program_with_deps, + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +/// Exploit-scenario pin. A single `(program_id, seed)` pair can derive a family of +/// `AccountId`s, one public PDA and one private PDA per distinct npk. Without the tx-wide +/// family-binding check, a program could claim `PDA_alice` (`alice_npk`) and +/// `PDA_bob` (`bob_npk`) under the same seed in one transaction, and once reuse +/// is supported a later chained call could delegate both to a callee via +/// `pda_seeds: [S]` and mix balances across them. The binding check rejects the setup +/// here: after the first claim records `(program, seed) โ†’ PDA_alice`, the second claim +/// tries to record `(program, seed) โ†’ PDA_bob` and panics. +#[test] +fn two_private_pda_claims_under_same_seed_are_rejected() { + let program = crate::test_methods::two_pda_claimer(); + let keys_a = test_private_account_keys_1(); + let keys_b = test_private_account_keys_2(); + let seed = PdaSeed::new([55; 32]); + + let account_a = AccountId::for_private_pda( + &program.id(), + &seed, + &keys_a.npk(), + &keys_a.vpk(), + u128::MAX, + ); + let account_b = AccountId::for_private_pda( + &program.id(), + &seed, + &keys_b.npk(), + &keys_b.vpk(), + u128::MAX, + ); + + let pre_a = AccountWithMetadata::new(Account::default(), false, account_a); + let pre_b = AccountWithMetadata::new(Account::default(), false, account_b); + + let result = execute_and_prove( + vec![pre_a, pre_b], + Program::serialize_instruction(seed).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaInit { + vpk: keys_a.vpk(), + random_seed: [0; 32], + npk: keys_a.npk(), + identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }, + InputAccountIdentity::PrivatePdaInit { + vpk: keys_b.vpk(), + random_seed: [0; 32], + npk: keys_b.npk(), + identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }, + ], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +/// A private PDA that is reused at top level without an external seed in the identity still +/// fails binding. The noop program emits no `Claim::Pda` and there is no caller +/// `ChainedCall.pda_seeds`, so position 0 is never bound and the assertion fires. +/// Supplying `seed: Some((seed, owner_program_id))` in the `PrivatePdaUpdate` identity is +/// the correct path for top-level reuse; this test pins the failure when no seed is provided. +#[test] +fn private_pda_top_level_reuse_rejected_by_binding_check() { + let program = crate::test_methods::noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([99; 32]); + + // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = + // true, account_id derived via the private formula. + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, &keys.vpk(), u128::MAX); + let owned_pre_state = AccountWithMetadata::new( + Account { + program_owner: program.id(), + ..Account::default() + }, + true, + account_id, + ); + + let result = execute_and_prove( + vec![owned_pre_state], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::PrivatePdaInit { + vpk: keys.vpk(), + random_seed: [0; 32], + npk, + identifier: u128::MAX, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: None, + }], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn private_accounts_can_only_be_initialized_once() { + let sender_keys = test_private_account_keys_1(); + let sender_nonce = Nonce(0xdead_beef); + + let sender_private_account = Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + nonce: sender_nonce, + data: Data::default(), + }; + let recipient_keys = test_private_account_keys_2(); + + let mut state = V03State::new().with_private_account(&sender_keys, &sender_private_account); + + let balance_to_move = 37; + let balance_to_move_2 = 30; + + let tx = private_balance_transfer_for_tests( + &sender_keys, + &sender_private_account, + &recipient_keys, + balance_to_move, + &state, + ); + + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .unwrap(); + + let sender_private_account = Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + nonce: sender_nonce, + data: Data::default(), + }; + + let tx = private_balance_transfer_for_tests( + &sender_keys, + &sender_private_account, + &recipient_keys, + balance_to_move_2, + &state, + ); + + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); + + assert!(matches!(result, Err(LeeError::InvalidInput(_)))); + let LeeError::InvalidInput(error_message) = result.err().unwrap() else { + panic!("Incorrect message error"); + }; + let expected_error_message = "Nullifier already seen".to_owned(); + assert_eq!(error_message, expected_error_message); +} + +#[test] +fn circuit_should_fail_if_there_are_repeated_ids() { + let program = crate::test_methods::simple_balance_transfer(); + let sender_keys = test_private_account_keys_1(); + let private_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + + let result = execute_and_prove( + vec![private_account_1.clone(), private_account_1], + Program::serialize_instruction(100_u128).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: (1, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: (1, vec![]), + identifier: 0, + }, + ], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn private_authorized_uninitialized_account() { + let mut state = V03State::new().with_test_programs(); + + // Set up keys for the authorized private account + let private_keys = test_private_account_keys_1(); + + // Create an authorized private account with default values (new account being initialized) + let authorized_account = AccountWithMetadata::new( + Account::default(), + true, + (&private_keys.npk(), &private_keys.vpk(), 0), + ); + + let program = crate::test_methods::simple_balance_transfer(); + + // Set up parameters for the new account + + let instruction: u128 = 0; + + // Execute and prove the circuit with the authorized account but no commitment proof + let (output, proof) = execute_and_prove( + vec![authorized_account], + Program::serialize_instruction(instruction).unwrap(), + vec![InputAccountIdentity::PrivateAuthorizedInit { + vpk: private_keys.vpk(), + random_seed: [0; 32], + nsk: private_keys.nsk, + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }], + &program.into(), + ) + .unwrap(); + + // Create message from circuit output + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + + let tx = PrivacyPreservingTransaction::new(message, witness_set); + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); + assert!(result.is_ok()); + + let account_id = + AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0); + let nullifier = Nullifier::for_account_initialization(&account_id); + assert!(state.private_state.1.contains(&nullifier)); +} + +#[test] +fn private_unauthorized_uninitialized_account_can_still_be_claimed() { + let mut state = V03State::new().with_test_programs(); + + let private_keys = test_private_account_keys_1(); + // This is intentional: claim authorization was introduced to protect public accounts, + // especially PDAs. Private PDAs are not useful in practice because there is no way to + // operate them without the corresponding private keys, so unauthorized private claiming + // remains allowed. + let unauthorized_account = AccountWithMetadata::new( + Account::default(), + false, + (&private_keys.npk(), &private_keys.vpk(), 0), + ); + + let program = crate::test_methods::claimer(); + + let (output, proof) = execute_and_prove( + vec![unauthorized_account], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::PrivateUnauthorized { + vpk: private_keys.vpk(), + random_seed: [0; 32], + npk: private_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .unwrap(); + + let account_id = + AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0); + let nullifier = Nullifier::for_account_initialization(&account_id); + assert!(state.private_state.1.contains(&nullifier)); +} + +#[test] +fn private_account_claimed_then_used_without_init_flag_should_fail() { + let mut state = V03State::new().with_test_programs(); + + // Set up keys for the private account + let private_keys = test_private_account_keys_1(); + + // Step 1: Create a new private account with authorization + let authorized_account = AccountWithMetadata::new( + Account::default(), + true, + (&private_keys.npk(), &private_keys.vpk(), 0), + ); + + let claimer_program = crate::test_methods::claimer(); + + // Set up parameters for claiming the new account + + let instruction = (); + + // Step 2: Execute claimer program to claim the account with authentication + let (output, proof) = execute_and_prove( + vec![authorized_account.clone()], + Program::serialize_instruction(instruction).unwrap(), + vec![InputAccountIdentity::PrivateAuthorizedInit { + vpk: private_keys.vpk(), + random_seed: [0; 32], + nsk: private_keys.nsk, + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }], + &claimer_program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + // Claim should succeed + assert!( + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .is_ok() + ); + + // Verify the account is now initialized (nullifier exists) + let account_id = + AccountId::for_regular_private_account(&private_keys.npk(), &private_keys.vpk(), 0); + let nullifier = Nullifier::for_account_initialization(&account_id); + assert!(state.private_state.1.contains(&nullifier)); + + // Prepare new state of account + let account_metadata = { + let mut acc = authorized_account; + acc.account.program_owner = crate::test_methods::claimer().id(); + acc + }; + + let noop_program = crate::test_methods::noop(); + + // Step 3: Try to execute noop program with authentication but without initialization + let res = execute_and_prove( + vec![account_metadata], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::PrivateAuthorizedInit { + vpk: private_keys.vpk(), + random_seed: [0; 32], + nsk: private_keys.nsk, + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }], + &noop_program.into(), + ); + + assert!(matches!(res, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn two_private_pda_family_members_receive_and_spend() { + let funder_keys = test_public_account_keys_1(); + let alice_keys = test_private_account_keys_1(); + let alice_npk = alice_keys.npk(); + + let proxy = crate::test_methods::pda_spend_proxy(); + let simple_transfer = crate::test_methods::simple_balance_transfer(); + let proxy_id = proxy.id(); + let simple_transfer_id = simple_transfer.id(); + let seed = PdaSeed::new([42; 32]); + let amount: u128 = 100; + + let spend_with_deps = ProgramWithDependencies::new( + proxy, + [(simple_transfer_id, simple_transfer.clone())].into(), + ); + + let funder_id = funder_keys.account_id(); + let alice_pda_0_id = + AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_keys.vpk(), 0); + let alice_pda_1_id = + AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_keys.vpk(), 1); + let recipient_id = test_public_account_keys_2().account_id(); + let recipient_signing_key = test_public_account_keys_2().signing_key; + + let mut state = + V03State::new().with_public_accounts(public_state_from_balances(&[(funder_id, 500)])); + + let alice_pda_0_account = Account { + program_owner: simple_transfer_id, + balance: amount, + nonce: Nonce::private_account_nonce_init(&alice_pda_0_id), + ..Account::default() + }; + let alice_pda_1_account = Account { + program_owner: simple_transfer_id, + balance: amount, + nonce: Nonce::private_account_nonce_init(&alice_pda_1_id), + ..Account::default() + }; + + // Fund alice_pda_0 via authenticated_transfer directly. + { + let funder_account = state.get_account_by_id(funder_id); + let funder_nonce = funder_account.nonce; + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(funder_account, true, funder_id), + AccountWithMetadata::new(Account::default(), false, alice_pda_0_id), + ], + Program::serialize_instruction(amount).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + vpk: alice_keys.vpk(), + random_seed: [0; 32], + npk: alice_npk, + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: Some((seed, proxy_id)), + }, + ], + &simple_transfer.clone().into(), + ) + .unwrap(); + let message = + Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output).unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 1, + 0, + ) + .unwrap(); + } + + // Fund alice_pda_1 the same way with identifier 1. + { + let funder_account = state.get_account_by_id(funder_id); + let funder_nonce = funder_account.nonce; + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(funder_account, true, funder_id), + AccountWithMetadata::new(Account::default(), false, alice_pda_1_id), + ], + Program::serialize_instruction(amount).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + vpk: alice_keys.vpk(), + random_seed: [0; 32], + npk: alice_npk, + identifier: 1, + commitment_root: DUMMY_COMMITMENT_HASH, + seed: Some((seed, proxy_id)), + }, + ], + &simple_transfer.into(), + ) + .unwrap(); + let message = + Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output).unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 2, + 0, + ) + .unwrap(); + } + + let commitment_pda_0 = Commitment::new(&alice_pda_0_id, &alice_pda_0_account); + let commitment_pda_1 = Commitment::new(&alice_pda_1_id, &alice_pda_1_account); + + assert!(state.get_proof_for_commitment(&commitment_pda_0).is_some()); + assert!(state.get_proof_for_commitment(&commitment_pda_1).is_some()); + + // Alice spends alice_pda_0 into the public recipient. + { + let recipient_account = state.get_account_by_id(recipient_id); + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id), + AccountWithMetadata::new(recipient_account, true, recipient_id), + ], + Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + vpk: alice_keys.vpk(), + random_seed: [0; 32], + nsk: alice_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_0) + .expect("pda_0 must be in state"), + identifier: 0, + seed: None, + }, + InputAccountIdentity::Public, + ], + &spend_with_deps, + ) + .unwrap(); + let message = + Message::try_from_circuit_output(vec![recipient_id], vec![Nonce(0)], output).unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 3, + 0, + ) + .unwrap(); + } + + // Alice spends alice_pda_1 into the same public recipient. + { + let recipient_account = state.get_account_by_id(recipient_id); + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(alice_pda_1_account.clone(), true, alice_pda_1_id), + AccountWithMetadata::new(recipient_account, false, recipient_id), + ], + Program::serialize_instruction((seed, amount, simple_transfer_id)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + vpk: alice_keys.vpk(), + random_seed: [0; 32], + nsk: alice_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_1) + .expect("pda_1 must be in state"), + identifier: 1, + seed: None, + }, + InputAccountIdentity::Public, + ], + &spend_with_deps, + ) + .unwrap(); + let message = Message::try_from_circuit_output(vec![recipient_id], vec![], output).unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 4, + 0, + ) + .unwrap(); + } + + assert_eq!(state.get_account_by_id(recipient_id).balance, 2 * amount); + + // Re-fund alice_pda_1 top-level via simple_transfer using PrivatePdaUpdate with an + // external seed. + let alice_pda_1_account_after_spend = Account { + program_owner: simple_transfer_id, + balance: 0, + nonce: alice_pda_1_account + .nonce + .private_account_nonce_increment(&alice_keys.nsk), + ..Account::default() + }; + let commitment_pda_1_after_spend = + Commitment::new(&alice_pda_1_id, &alice_pda_1_account_after_spend); + { + let recipient_account = state.get_account_by_id(recipient_id); + let recipient_nonce = recipient_account.nonce; + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(recipient_account, true, recipient_id), + AccountWithMetadata::new(alice_pda_1_account_after_spend, false, alice_pda_1_id), + ], + Program::serialize_instruction(amount).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaUpdate { + vpk: alice_keys.vpk(), + random_seed: [0; 32], + nsk: alice_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_1_after_spend) + .expect("pda_1 after spend must be in state"), + identifier: 1, + seed: Some((seed, proxy_id)), + }, + ], + &crate::test_methods::simple_balance_transfer().into(), + ) + .unwrap(); + let message = + Message::try_from_circuit_output(vec![recipient_id], vec![recipient_nonce], output) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 5, + 0, + ) + .unwrap(); + } + + assert_eq!(state.get_account_by_id(recipient_id).balance, amount); +} diff --git a/lee/state_machine/src/state/tests/claiming.rs b/lee/state_machine/src/state/tests/claiming.rs new file mode 100644 index 00000000..acfe9834 --- /dev/null +++ b/lee/state_machine/src/state/tests/claiming.rs @@ -0,0 +1,607 @@ +use super::*; + +#[test] +fn claiming_mechanism() { + let program = crate::test_methods::simple_balance_transfer(); + let from_key = PrivateKey::try_new([1; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); + let initial_balance = 100; + let initial_data = [(from, initial_balance)]; + let mut state = V03State::new() + .with_public_accounts(public_state_from_balances(&initial_data)) + .with_test_programs(); + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); + let amount: u128 = 37; + + // Check the recipient is an uninitialized account + assert_eq!(state.get_account_by_id(to), Account::default()); + + let expected_recipient_post = Account { + program_owner: program.id(), + balance: amount, + nonce: Nonce(1), + ..Account::default() + }; + + let message = public_transaction::Message::try_new( + program.id(), + vec![from, to], + vec![Nonce(0), Nonce(0)], + amount, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + let recipient_post = state.get_account_by_id(to); + + assert_eq!(recipient_post, expected_recipient_post); +} + +#[test] +fn unauthorized_public_account_claiming_fails() { + let program = crate::test_methods::simple_balance_transfer(); + let account_key = PrivateKey::try_new([9; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key)); + let mut state = V03State::new().with_test_programs(); + + assert_eq!(state.get_account_by_id(account_id), Account::default()); + + let message = + public_transaction::Message::try_new(program.id(), vec![account_id], vec![], 0_u128) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 2, 0); + + assert!(matches!(result, Err(LeeError::InvalidProgramBehavior(_)))); + assert_eq!(state.get_account_by_id(account_id), Account::default()); +} + +#[test] +fn authorized_public_account_claiming_succeeds() { + let program = crate::test_methods::simple_balance_transfer(); + let account_key = PrivateKey::try_new([10; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key)); + let mut state = V03State::new().with_test_programs(); + + assert_eq!(state.get_account_by_id(account_id), Account::default()); + + let message = public_transaction::Message::try_new( + program.id(), + vec![account_id], + vec![Nonce(0)], + 0_u128, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&account_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + assert_eq!( + state.get_account_by_id(account_id), + Account { + program_owner: program.id(), + nonce: Nonce(1), + ..Account::default() + } + ); +} + +#[test] +fn public_chained_call() { + let program = crate::test_methods::chain_caller(); + let key = PrivateKey::try_new([1; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&key)); + let to = AccountId::new([2; 32]); + let initial_balance = 1000; + let initial_data = [(from, initial_balance), (to, 0)]; + let mut state = V03State::new() + .with_public_accounts(public_state_from_balances(&initial_data)) + .with_test_programs(); + let from_key = key; + let amount: u128 = 37; + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + crate::test_methods::simple_balance_transfer().id(), + 2, + None, + ); + + let expected_to_post = Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: amount * 2, // The `chain_caller` chains the program twice + ..Account::default() + }; + + let message = public_transaction::Message::try_new( + program.id(), + vec![to, from], // The chain_caller program permutes the account order in the chain + // call + vec![Nonce(0)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + let from_post = state.get_account_by_id(from); + let to_post = state.get_account_by_id(to); + // The `chain_caller` program calls the program twice + assert_eq!(from_post.balance, initial_balance - 2 * amount); + assert_eq!(to_post, expected_to_post); +} + +#[test] +fn execution_fails_if_chained_calls_exceeds_depth() { + let program = crate::test_methods::chain_caller(); + let key = PrivateKey::try_new([1; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&key)); + let to = AccountId::new([2; 32]); + let initial_balance = 100; + let initial_data = [(from, initial_balance), (to, 0)]; + let mut state = V03State::new() + .with_public_accounts(public_state_from_balances(&initial_data)) + .with_test_programs(); + let from_key = key; + let amount: u128 = 0; + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + crate::test_methods::simple_balance_transfer().id(), + u32::try_from(MAX_NUMBER_CHAINED_CALLS).expect("MAX_NUMBER_CHAINED_CALLS fits in u32") + 1, + None, + ); + + let message = public_transaction::Message::try_new( + program.id(), + vec![to, from], // The chain_caller program permutes the account order in the chain + // call + vec![Nonce(0)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!(matches!( + result, + Err(LeeError::MaxChainedCallsDepthExceeded) + )); +} + +#[test] +fn execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() { + let chain_caller = crate::test_methods::chain_caller(); + let pda_seed = PdaSeed::new([37; 32]); + let from = AccountId::for_public_pda(&chain_caller.id(), &pda_seed); + let to = AccountId::new([2; 32]); + let initial_balance = 1000; + let initial_data = [(from, initial_balance), (to, 0)]; + let mut state = V03State::new() + .with_public_accounts(public_state_from_balances(&initial_data)) + .with_test_programs(); + let amount: u128 = 58; + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + crate::test_methods::simple_balance_transfer().id(), + 1, + Some(pda_seed), + ); + + let expected_to_post = Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: amount, // The `chain_caller` chains the program twice + ..Account::default() + }; + let message = public_transaction::Message::try_new( + chain_caller.id(), + vec![to, from], // The chain_caller program permutes the account order in the chain + // call + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + let from_post = state.get_account_by_id(from); + let to_post = state.get_account_by_id(to); + assert_eq!(from_post.balance, initial_balance - amount); + assert_eq!(to_post, expected_to_post); +} + +#[test] +fn claiming_mechanism_within_chain_call() { + // This test calls the authenticated transfer program through the chain_caller program. + // The transfer is made from an initialized sender to an uninitialized recipient. And + // it is expected that the recipient account is claimed by the authenticated transfer + // program and not the chained_caller program. + let chain_caller = crate::test_methods::chain_caller(); + let simple_transfer = crate::test_methods::simple_balance_transfer(); + let from_key = PrivateKey::try_new([1; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); + let initial_balance = 100; + let initial_data = [(from, initial_balance)]; + let mut state = V03State::new() + .with_public_accounts(public_state_from_balances(&initial_data)) + .with_test_programs(); + let to_key = PrivateKey::try_new([2; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); + let amount: u128 = 37; + + // Check the recipient is an uninitialized account + assert_eq!(state.get_account_by_id(to), Account::default()); + + let expected_to_post = Account { + // The expected program owner is the authenticated transfer program + program_owner: simple_transfer.id(), + balance: amount, + nonce: Nonce(1), + ..Account::default() + }; + + // The transaction executes the chain_caller program, which internally calls the + // authenticated_transfer program + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + crate::test_methods::simple_balance_transfer().id(), + 1, + None, + ); + let message = public_transaction::Message::try_new( + chain_caller.id(), + vec![to, from], // The chain_caller program permutes the account order in the chain + // call + vec![Nonce(0), Nonce(0)], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + let from_post = state.get_account_by_id(from); + let to_post = state.get_account_by_id(to); + assert_eq!(from_post.balance, initial_balance - amount); + assert_eq!(to_post, expected_to_post); +} + +#[test] +fn unauthorized_public_account_claiming_fails_when_executed_privately() { + let program = crate::test_methods::simple_balance_transfer(); + let account_id = AccountId::new([11; 32]); + let public_account = AccountWithMetadata::new(Account::default(), false, account_id); + + let result = execute_and_prove( + vec![public_account], + Program::serialize_instruction(0_u128).unwrap(), + vec![InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn authorized_public_account_claiming_succeeds_when_executed_privately() { + let program = crate::test_methods::simple_balance_transfer(); + let program_id = program.id(); + let sender_keys = test_private_account_keys_1(); + let sender_private_account = Account { + program_owner: program_id, + balance: 100, + ..Account::default() + }; + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); + let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account); + let sender_init_nullifier = Nullifier::for_account_initialization(&sender_account_id); + let mut state = + V03State::new().with_private_accounts([(sender_commitment.clone(), sender_init_nullifier)]); + let sender_pre = AccountWithMetadata::new( + sender_private_account, + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap(); + let recipient_account_id = + AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key)); + let recipient_pre = AccountWithMetadata::new(Account::default(), true, recipient_account_id); + + let balance = 37; + + let (output, proof) = execute_and_prove( + vec![sender_pre, recipient_pre], + Program::serialize_instruction(balance).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program.into(), + ) + .unwrap(); + + let message = + Message::try_from_circuit_output(vec![recipient_account_id], vec![Nonce(0)], output) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .unwrap(); + + let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk); + assert!(state.private_state.1.contains(&nullifier)); + + assert_eq!( + state.get_account_by_id(recipient_account_id), + Account { + program_owner: program_id, + balance, + nonce: Nonce(1), + ..Account::default() + } + ); +} + +#[test_case::test_case(1; "single call")] +#[test_case::test_case(2; "two calls")] +fn private_chained_call(number_of_calls: u32) { + // Arrange + let chain_caller = crate::test_methods::chain_caller(); + let simple_transfers = crate::test_methods::simple_balance_transfer(); + let from_keys = test_private_account_keys_1(); + let to_keys = test_private_account_keys_2(); + let initial_balance = 100; + let from_account = AccountWithMetadata::new( + Account { + program_owner: simple_transfers.id(), + balance: initial_balance, + ..Account::default() + }, + true, + (&from_keys.npk(), &from_keys.vpk(), 0), + ); + let to_account = AccountWithMetadata::new( + Account { + program_owner: simple_transfers.id(), + ..Account::default() + }, + true, + (&to_keys.npk(), &to_keys.vpk(), 0), + ); + + let from_account_id = + AccountId::for_regular_private_account(&from_keys.npk(), &from_keys.vpk(), 0); + let to_account_id = AccountId::for_regular_private_account(&to_keys.npk(), &to_keys.vpk(), 0); + let from_commitment = Commitment::new(&from_account_id, &from_account.account); + let to_commitment = Commitment::new(&to_account_id, &to_account.account); + let from_init_nullifier = Nullifier::for_account_initialization(&from_account_id); + let to_init_nullifier = Nullifier::for_account_initialization(&to_account_id); + let mut state = V03State::new() + .with_private_accounts([ + (from_commitment.clone(), from_init_nullifier), + (to_commitment.clone(), to_init_nullifier), + ]) + .with_test_programs(); + let amount: u128 = 37; + let instruction: (u128, ProgramId, u32, Option) = ( + amount, + crate::test_methods::simple_balance_transfer().id(), + number_of_calls, + None, + ); + + let mut dependencies = HashMap::new(); + + dependencies.insert(simple_transfers.id(), simple_transfers); + let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies); + + let from_new_nonce = Nonce::default().private_account_nonce_increment(&from_keys.nsk); + let to_new_nonce = Nonce::default().private_account_nonce_increment(&to_keys.nsk); + + let from_expected_post = Account { + balance: initial_balance - u128::from(number_of_calls) * amount, + nonce: from_new_nonce, + ..from_account.account.clone() + }; + let from_expected_commitment = Commitment::new(&from_account_id, &from_expected_post); + + let to_expected_post = Account { + balance: u128::from(number_of_calls) * amount, + nonce: to_new_nonce, + ..to_account.account.clone() + }; + let to_expected_commitment = Commitment::new(&to_account_id, &to_expected_post); + + // Act + let (output, proof) = execute_and_prove( + vec![to_account, from_account], + Program::serialize_instruction(instruction).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: from_keys.vpk(), + random_seed: [0; 32], + nsk: from_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&from_commitment) + .expect("from's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: to_keys.vpk(), + random_seed: [0; 32], + nsk: to_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&to_commitment) + .expect("to's commitment must be in state"), + identifier: 0, + }, + ], + &program_with_deps, + ) + .unwrap(); + + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let transaction = PrivacyPreservingTransaction::new(message, witness_set); + + state + .transition_from_privacy_preserving_transaction(&transaction, 1, 0) + .unwrap(); + + // Assert + assert!( + state + .get_proof_for_commitment(&from_expected_commitment) + .is_some() + ); + assert!( + state + .get_proof_for_commitment(&to_expected_commitment) + .is_some() + ); +} + +#[test] +fn claiming_mechanism_cannot_claim_initialied_accounts() { + let claimer = crate::test_methods::claimer(); + let mut state = V03State::new().with_test_programs(); + let account_id = AccountId::new([2; 32]); + + // Insert an account with non-default program owner + state.force_insert_account( + account_id, + Account { + program_owner: [1, 2, 3, 4, 5, 6, 7, 8], + ..Account::default() + }, + ); + + let message = + public_transaction::Message::try_new(claimer.id(), vec![account_id], vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior( + InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id: err_account_id } + )) if err_account_id == account_id + )); +} + +/// This test ensures that even if a malicious program tries to perform overflow of balances +/// it will not be able to break the balance validation. +#[test] +fn malicious_program_cannot_break_balance_validation_if_not_in_genesis() { + let sender_key = PrivateKey::try_new([37; 32]).unwrap(); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); + let sender_init_balance: u128 = 10; + + let recipient_key = PrivateKey::try_new([42; 32]).unwrap(); + let recipient_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_key)); + let recipient_init_balance: u128 = 10; + + let modified_transfer_id = crate::test_methods::modified_transfer_program().id(); + + let mut state = V03State::new() + .with_public_accounts([ + ( + sender_id, + Account { + program_owner: modified_transfer_id, + balance: sender_init_balance, + ..Account::default() + }, + ), + ( + recipient_id, + Account { + program_owner: modified_transfer_id, + balance: recipient_init_balance, + ..Account::default() + }, + ), + ]) + .with_test_programs(); + + let balance_to_move: u128 = 4; + + let sender = AccountWithMetadata::new(state.get_account_by_id(sender_id), true, sender_id); + + let sender_nonce = sender.account.nonce; + + let _recipient = + AccountWithMetadata::new(state.get_account_by_id(recipient_id), false, sender_id); + + let message = public_transaction::Message::try_new( + modified_transfer_id, + vec![sender_id, recipient_id], + vec![sender_nonce], + balance_to_move, + ) + .unwrap(); + + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); + let tx = PublicTransaction::new(message, witness_set); + let res = state.transition_from_public_transaction(&tx, 2, 0); + let expected_total_balance_pre_states = + WrappedBalanceSum::from_balances([sender_init_balance, recipient_init_balance].into_iter()) + .unwrap(); + let expected_total_balance_post_states = WrappedBalanceSum::from_balances( + [sender_init_balance, recipient_init_balance, u128::MAX, 1].into_iter(), + ) + .unwrap(); + assert!(matches!( + res, + Err(LeeError::InvalidProgramBehavior( + InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states } + ) + )) if total_balance_pre_states == expected_total_balance_pre_states && total_balance_post_states == expected_total_balance_post_states + )); + + let sender_post = state.get_account_by_id(sender_id); + let recipient_post = state.get_account_by_id(recipient_id); + + let expected_sender_post = { + let mut this = state.get_account_by_id(sender_id); + this.balance = sender_init_balance; + this.nonce = Nonce(0); + this + }; + + let expected_recipient_post = { + let mut this = state.get_account_by_id(sender_id); + this.balance = recipient_init_balance; + this.nonce = Nonce(0); + this + }; + + assert_eq!(expected_sender_post, sender_post); + assert_eq!(expected_recipient_post, recipient_post); +} diff --git a/lee/state_machine/src/state/tests/flash_swap.rs b/lee/state_machine/src/state/tests/flash_swap.rs new file mode 100644 index 00000000..be8f1c10 --- /dev/null +++ b/lee/state_machine/src/state/tests/flash_swap.rs @@ -0,0 +1,235 @@ +use super::*; + +#[test] +fn flash_swap_successful() { + let initiator = crate::test_methods::flash_swap_initiator(); + let callback = crate::test_methods::flash_swap_callback(); + let token = crate::test_methods::simple_balance_transfer(); + + let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); + let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32])); + + let initial_balance: u128 = 1000; + let amount_out: u128 = 100; + + let vault_account = Account { + program_owner: token.id(), + balance: initial_balance, + ..Account::default() + }; + let receiver_account = Account { + program_owner: token.id(), + balance: 0, + ..Account::default() + }; + + let mut state = V03State::new().with_test_programs(); + state.force_insert_account(vault_id, vault_account); + state.force_insert_account(receiver_id, receiver_account); + + // Callback instruction: return funds + let cb_instruction = CallbackInstruction { + return_funds: true, + token_program_id: token.id(), + amount: amount_out, + }; + let cb_data = Program::serialize_instruction(cb_instruction).unwrap(); + + let instruction = FlashSwapInstruction::Initiate { + token_program_id: token.id(), + callback_program_id: callback.id(), + amount_out, + callback_instruction_data: cb_data, + }; + + let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction); + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!(result.is_ok(), "flash swap should succeed: {result:?}"); + + // Vault balance restored, receiver back to 0 + assert_eq!(state.get_account_by_id(vault_id).balance, initial_balance); + assert_eq!(state.get_account_by_id(receiver_id).balance, 0); +} + +#[test] +fn flash_swap_callback_keeps_funds_rollback() { + let initiator = crate::test_methods::flash_swap_initiator(); + let callback = crate::test_methods::flash_swap_callback(); + let token = crate::test_methods::simple_balance_transfer(); + + let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); + let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32])); + + let initial_balance: u128 = 1000; + let amount_out: u128 = 100; + + let vault_account = Account { + program_owner: token.id(), + balance: initial_balance, + ..Account::default() + }; + let receiver_account = Account { + program_owner: token.id(), + balance: 0, + ..Account::default() + }; + + let mut state = V03State::new().with_test_programs(); + state.force_insert_account(vault_id, vault_account); + state.force_insert_account(receiver_id, receiver_account); + + // Callback instruction: do NOT return funds + let cb_instruction = CallbackInstruction { + return_funds: false, + token_program_id: token.id(), + amount: amount_out, + }; + let cb_data = Program::serialize_instruction(cb_instruction).unwrap(); + + let instruction = FlashSwapInstruction::Initiate { + token_program_id: token.id(), + callback_program_id: callback.id(), + amount_out, + callback_instruction_data: cb_data, + }; + + let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction); + let result = state.transition_from_public_transaction(&tx, 1, 0); + + // Invariant check fails โ†’ entire tx rolls back + assert!( + result.is_err(), + "flash swap should fail when callback keeps funds" + ); + + // State unchanged (rollback) + assert_eq!(state.get_account_by_id(vault_id).balance, initial_balance); + assert_eq!(state.get_account_by_id(receiver_id).balance, 0); +} + +#[test] +fn flash_swap_self_call_targets_correct_program() { + // Zero-amount flash swap: the invariant self-call still runs and succeeds + // because vault balance doesn't decrease. + let initiator = crate::test_methods::flash_swap_initiator(); + let callback = crate::test_methods::flash_swap_callback(); + let token = crate::test_methods::simple_balance_transfer(); + + let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); + let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32])); + + let initial_balance: u128 = 1000; + + let vault_account = Account { + program_owner: token.id(), + balance: initial_balance, + ..Account::default() + }; + let receiver_account = Account { + program_owner: token.id(), + balance: 0, + ..Account::default() + }; + + let mut state = V03State::new().with_test_programs(); + state.force_insert_account(vault_id, vault_account); + state.force_insert_account(receiver_id, receiver_account); + + let cb_instruction = CallbackInstruction { + return_funds: true, + token_program_id: token.id(), + amount: 0, + }; + let cb_data = Program::serialize_instruction(cb_instruction).unwrap(); + + let instruction = FlashSwapInstruction::Initiate { + token_program_id: token.id(), + callback_program_id: callback.id(), + amount_out: 0, + callback_instruction_data: cb_data, + }; + + let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction); + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!( + result.is_ok(), + "zero-amount flash swap should succeed: {result:?}" + ); +} + +#[test] +fn flash_swap_standalone_invariant_check_rejected() { + // Calling InvariantCheck directly (not as a chained self-call) should fail + // because caller_program_id will be None. + let initiator = crate::test_methods::flash_swap_initiator(); + let token = crate::test_methods::simple_balance_transfer(); + + let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); + + let vault_account = Account { + program_owner: token.id(), + balance: 1000, + ..Account::default() + }; + + let mut state = V03State::new().with_test_programs(); + state.force_insert_account(vault_id, vault_account); + + let instruction = FlashSwapInstruction::InvariantCheck { + min_vault_balance: 1000, + }; + + let message = + public_transaction::Message::try_new(initiator.id(), vec![vault_id], vec![], instruction) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!( + result.is_err(), + "standalone InvariantCheck should be rejected (caller_program_id is None)" + ); +} + +#[test] +fn malicious_self_program_id_rejected_in_public_execution() { + let program = crate::test_methods::malicious_self_program_id(); + let acc_id = AccountId::new([99; 32]); + let account = Account::default(); + + let mut state = V03State::new().with_test_programs(); + state.force_insert_account(acc_id, account); + + let message = + public_transaction::Message::try_new(program.id(), vec![acc_id], vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!( + result.is_err(), + "program with wrong self_program_id in output should be rejected" + ); +} + +#[test] +fn malicious_caller_program_id_rejected_in_public_execution() { + let program = crate::test_methods::malicious_caller_program_id(); + let acc_id = AccountId::new([99; 32]); + let account = Account::default(); + + let mut state = V03State::new().with_test_programs(); + state.force_insert_account(acc_id, account); + + let message = + public_transaction::Message::try_new(program.id(), vec![acc_id], vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!( + result.is_err(), + "program with spoofed caller_program_id in output should be rejected" + ); +} diff --git a/lee/state_machine/src/state/tests/genesis.rs b/lee/state_machine/src/state/tests/genesis.rs new file mode 100644 index 00000000..f67628ee --- /dev/null +++ b/lee/state_machine/src/state/tests/genesis.rs @@ -0,0 +1,128 @@ +use super::*; + +#[test] +fn new_works() { + let key1 = PrivateKey::try_new([1; 32]).unwrap(); + let key2 = PrivateKey::try_new([2; 32]).unwrap(); + let addr1 = AccountId::from(&PublicKey::new_from_private_key(&key1)); + let addr2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); + let expected_public_state = { + let mut this = HashMap::new(); + this.insert( + addr1, + Account { + balance: 100, + ..Account::default() + }, + ); + this.insert( + addr2, + Account { + balance: 151, + ..Account::default() + }, + ); + this + }; + let expected_builtin_programs = HashMap::new(); + + let state = + V03State::new().with_public_account_balances([(addr1, 100_u128), (addr2, 151_u128)]); + + assert_eq!(state.public_state, expected_public_state); + assert_eq!(state.programs, expected_builtin_programs); +} + +#[test] +fn new_includes_nullifiers_for_private_accounts() { + let keys1 = test_private_account_keys_1(); + let keys2 = test_private_account_keys_2(); + + let account = Account { + balance: 100, + ..Account::default() + }; + + let account_id1 = AccountId::for_regular_private_account(&keys1.npk(), &keys1.vpk(), 0); + let account_id2 = AccountId::for_regular_private_account(&keys2.npk(), &keys2.vpk(), 0); + + let init_commitment1 = Commitment::new(&account_id1, &account); + let init_commitment2 = Commitment::new(&account_id2, &account); + let init_nullifier1 = Nullifier::for_account_initialization(&account_id1); + let init_nullifier2 = Nullifier::for_account_initialization(&account_id2); + + let initial_private_accounts = vec![ + (init_commitment1, init_nullifier1), + (init_commitment2, init_nullifier2), + ]; + + let state = V03State::new().with_private_accounts(initial_private_accounts); + + assert!(state.private_state.1.contains(&init_nullifier1)); + assert!(state.private_state.1.contains(&init_nullifier2)); +} + +#[test] +fn insert_program() { + let mut state = V03State::new(); + let program_to_insert = crate::test_methods::simple_balance_transfer(); + let program_id = program_to_insert.id(); + assert!(!state.programs.contains_key(&program_id)); + + state.insert_program(program_to_insert); + + assert!(state.programs.contains_key(&program_id)); +} + +#[test] +fn get_account_by_account_id_non_default_account() { + let key = PrivateKey::try_new([1; 32]).unwrap(); + let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); + let initial_data = [( + account_id, + Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + ..Account::default() + }, + )]; + let state = V03State::new().with_public_accounts(initial_data); + let expected_account = &state.public_state[&account_id]; + + let account = state.get_account_by_id(account_id); + + assert_eq!(&account, expected_account); +} + +#[test] +fn get_account_by_account_id_default_account() { + let addr2 = AccountId::new([0; 32]); + let state = V03State::new(); + let expected_account = Account::default(); + + let account = state.get_account_by_id(addr2); + + assert_eq!(account, expected_account); +} + +#[test] +fn builtin_programs_getter() { + let state = V03State::new(); + + let builtin_programs = state.programs(); + + assert_eq!(builtin_programs, &state.programs); +} + +#[test] +fn state_serialization_roundtrip() { + let account_id_1 = AccountId::new([1; 32]); + let account_id_2 = AccountId::new([2; 32]); + let initial_data = [(account_id_1, 100_u128), (account_id_2, 151_u128)]; + let state = V03State::new() + .with_public_accounts(public_state_from_balances(&initial_data)) + .with_test_programs(); + let bytes = borsh::to_vec(&state).unwrap(); + let state_from_bytes: V03State = borsh::from_slice(&bytes).unwrap(); + assert_eq!(state, state_from_bytes); +} diff --git a/lee/state_machine/src/state/tests/mod.rs b/lee/state_machine/src/state/tests/mod.rs new file mode 100644 index 00000000..55a99ead --- /dev/null +++ b/lee/state_machine/src/state/tests/mod.rs @@ -0,0 +1,425 @@ +#![expect( + clippy::arithmetic_side_effects, + clippy::shadow_unrelated, + reason = "We don't care about it in tests" +)] + +use std::collections::HashMap; + +use lee_core::{ + BlockId, Commitment, DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, + NullifierPublicKey, NullifierSecretKey, Timestamp, + account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, + encryption::ViewingPublicKey, + program::{ + BlockValidityWindow, ExecutionValidationError, MAX_NUMBER_CHAINED_CALLS, PdaSeed, + ProgramId, TimestampValidityWindow, WrappedBalanceSum, + }, +}; + +use crate::{ + PublicKey, PublicTransaction, V03State, + error::{InvalidProgramBehaviorError, LeeError}, + execute_and_prove, + privacy_preserving_transaction::{ + PrivacyPreservingTransaction, circuit::ProgramWithDependencies, message::Message, + witness_set::WitnessSet, + }, + program::Program, + public_transaction, + signature::PrivateKey, +}; + +mod authenticated_transfer; +mod changer_claimer; +mod circuit; +mod claiming; +mod flash_swap; +mod genesis; +mod privacy_preserving; +mod public_program_rules; +mod validity_window; + +impl V03State { + /// Include test programs in the builtin programs map. + #[must_use] + pub fn with_test_programs(mut self) -> Self { + self.insert_program(crate::test_methods::simple_balance_transfer()); + self.insert_program(crate::test_methods::nonce_changer()); + self.insert_program(crate::test_methods::extra_output()); + self.insert_program(crate::test_methods::missing_output()); + self.insert_program(crate::test_methods::program_owner_changer()); + self.insert_program(crate::test_methods::data_changer()); + self.insert_program(crate::test_methods::minter()); + self.insert_program(crate::test_methods::burner()); + self.insert_program(crate::test_methods::auth_asserting_noop()); + self.insert_program(crate::test_methods::private_pda_delegator()); + self.insert_program(crate::test_methods::pda_claimer()); + self.insert_program(crate::test_methods::two_pda_claimer()); + self.insert_program(crate::test_methods::noop()); + self.insert_program(crate::test_methods::chain_caller()); + self.insert_program(crate::test_methods::modified_transfer_program()); + self.insert_program(crate::test_methods::malicious_authorization_changer()); + self.insert_program(crate::test_methods::validity_window()); + self.insert_program(crate::test_methods::flash_swap_initiator()); + self.insert_program(crate::test_methods::flash_swap_callback()); + self.insert_program(crate::test_methods::malicious_self_program_id()); + self.insert_program(crate::test_methods::malicious_caller_program_id()); + self.insert_program(crate::test_methods::pda_spend_proxy()); + self.insert_program(crate::test_methods::claimer()); + self.insert_program(crate::test_methods::changer_claimer()); + self.insert_program(crate::test_methods::validity_window_chain_caller()); + self.insert_program(crate::test_methods::simple_transfer_proxy()); + self.insert_program(crate::test_methods::malicious_injector()); + self.insert_program(crate::test_methods::malicious_launderer()); + self.insert_program(crate::test_methods::modified_transfer_program()); + self + } + + #[must_use] + pub fn with_non_default_accounts_but_default_program_owners(mut self) -> Self { + let account_with_default_values_except_balance = Account { + balance: 100, + ..Account::default() + }; + let account_with_default_values_except_nonce = Account { + nonce: Nonce(37), + ..Account::default() + }; + let account_with_default_values_except_data = Account { + data: vec![0xca, 0xfe].try_into().unwrap(), + ..Account::default() + }; + self.force_insert_account( + AccountId::new([255; 32]), + account_with_default_values_except_balance, + ); + self.force_insert_account( + AccountId::new([254; 32]), + account_with_default_values_except_nonce, + ); + self.force_insert_account( + AccountId::new([253; 32]), + account_with_default_values_except_data, + ); + self + } + + #[must_use] + pub fn with_account_owned_by_burner_program(mut self) -> Self { + let account = Account { + program_owner: crate::test_methods::burner().id(), + balance: 100, + ..Default::default() + }; + self.force_insert_account(AccountId::new([252; 32]), account); + self + } + + #[must_use] + pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self { + let account_id = AccountId::for_regular_private_account(&keys.npk(), &keys.vpk(), 0); + let commitment = Commitment::new(&account_id, account); + self.private_state.0.extend(&[commitment]); + self + } +} + +pub struct TestPublicKeys { + pub signing_key: PrivateKey, +} + +impl TestPublicKeys { + pub fn account_id(&self) -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&self.signing_key)) + } +} + +pub struct TestPrivateKeys { + pub nsk: NullifierSecretKey, + pub d: [u8; 32], + pub z: [u8; 32], +} + +impl TestPrivateKeys { + pub fn npk(&self) -> NullifierPublicKey { + NullifierPublicKey::from(&self.nsk) + } + + pub fn vpk(&self) -> ViewingPublicKey { + ViewingPublicKey::from_seed(&self.d, &self.z) + } +} + +// โ”€โ”€ Flash Swap types (mirrors of guest types for host-side serialisation) โ”€โ”€ + +#[derive(serde::Serialize, serde::Deserialize)] +struct CallbackInstruction { + return_funds: bool, + token_program_id: ProgramId, + amount: u128, +} + +#[derive(serde::Serialize, serde::Deserialize)] +enum FlashSwapInstruction { + Initiate { + token_program_id: ProgramId, + callback_program_id: ProgramId, + amount_out: u128, + callback_instruction_data: Vec, + }, + InvariantCheck { + min_vault_balance: u128, + }, +} + +fn public_state_from_balances(initial_data: &[(AccountId, u128)]) -> HashMap { + initial_data + .iter() + .copied() + .map(|(account_id, balance)| { + ( + account_id, + Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance, + ..Account::default() + }, + ) + }) + .collect() +} + +fn transfer_transaction( + from: AccountId, + from_key: &PrivateKey, + from_nonce: u128, + to: AccountId, + to_key: &PrivateKey, + to_nonce: u128, + balance: u128, +) -> PublicTransaction { + let account_ids = vec![from, to]; + let nonces = vec![Nonce(from_nonce), Nonce(to_nonce)]; + let program_id = crate::test_methods::simple_balance_transfer().id(); + let message = + public_transaction::Message::try_new(program_id, account_ids, nonces, balance).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key, to_key]); + PublicTransaction::new(message, witness_set) +} + +fn build_flash_swap_tx( + initiator: &Program, + vault_id: AccountId, + receiver_id: AccountId, + instruction: FlashSwapInstruction, +) -> PublicTransaction { + let message = public_transaction::Message::try_new( + initiator.id(), + vec![vault_id, receiver_id], + vec![], // no signers โ€” vault is PDA-authorised + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set) +} + +fn test_public_account_keys_1() -> TestPublicKeys { + TestPublicKeys { + signing_key: PrivateKey::try_new([37; 32]).unwrap(), + } +} + +fn test_public_account_keys_2() -> TestPublicKeys { + TestPublicKeys { + signing_key: PrivateKey::try_new([38; 32]).unwrap(), + } +} + +pub fn test_private_account_keys_1() -> TestPrivateKeys { + TestPrivateKeys { + nsk: [13; 32], + d: [31; 32], + z: [32; 32], + } +} + +pub fn test_private_account_keys_2() -> TestPrivateKeys { + TestPrivateKeys { + nsk: [38; 32], + d: [83; 32], + z: [84; 32], + } +} + +fn shielded_balance_transfer_for_tests( + sender_keys: &TestPublicKeys, + recipient_keys: &TestPrivateKeys, + balance_to_move: u128, + state: &V03State, +) -> PrivacyPreservingTransaction { + let sender = AccountWithMetadata::new( + state.get_account_by_id(sender_keys.account_id()), + true, + sender_keys.account_id(), + ); + + let sender_nonce = sender.account.nonce; + + let recipient = AccountWithMetadata::new( + Account::default(), + false, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); + + let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove( + vec![sender, recipient], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + npk: recipient_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &crate::test_methods::simple_balance_transfer().into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output( + vec![sender_keys.account_id()], + vec![sender_nonce], + output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[&sender_keys.signing_key]); + PrivacyPreservingTransaction::new(message, witness_set) +} + +fn private_balance_transfer_for_tests( + sender_keys: &TestPrivateKeys, + sender_private_account: &Account, + recipient_keys: &TestPrivateKeys, + balance_to_move: u128, + state: &V03State, +) -> PrivacyPreservingTransaction { + let program = crate::test_methods::simple_balance_transfer(); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); + let sender_commitment = Commitment::new(&sender_account_id, sender_private_account); + let sender_pre = AccountWithMetadata::new( + sender_private_account.clone(), + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let recipient_pre = AccountWithMetadata::new( + Account::default(), + false, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); + + let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove( + vec![sender_pre, recipient_pre], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + npk: recipient_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }, + ], + &program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + + PrivacyPreservingTransaction::new(message, witness_set) +} + +fn deshielded_balance_transfer_for_tests( + sender_keys: &TestPrivateKeys, + sender_private_account: &Account, + recipient_account_id: &AccountId, + balance_to_move: u128, + state: &V03State, +) -> PrivacyPreservingTransaction { + let program = crate::test_methods::simple_balance_transfer(); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); + let sender_commitment = Commitment::new(&sender_account_id, sender_private_account); + let sender_pre = AccountWithMetadata::new( + sender_private_account.clone(), + true, + (&sender_keys.npk(), &sender_keys.vpk(), 0), + ); + let recipient_pre = AccountWithMetadata::new( + state.get_account_by_id(*recipient_account_id), + false, + *recipient_account_id, + ); + + let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove( + vec![sender_pre, recipient_pre], + Program::serialize_instruction(balance_to_move).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.vpk(), + random_seed: [0; 32], + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program.into(), + ) + .unwrap(); + + let message = + Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + + PrivacyPreservingTransaction::new(message, witness_set) +} + +fn valid_private_transfer_tx_and_state() -> (V03State, PrivacyPreservingTransaction) { + let sender_keys = test_private_account_keys_1(); + let sender_private_account = Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + nonce: Nonce(0xdead_beef), + ..Account::default() + }; + let recipient_keys = test_private_account_keys_2(); + let state = V03State::new().with_private_account(&sender_keys, &sender_private_account); + let tx = private_balance_transfer_for_tests( + &sender_keys, + &sender_private_account, + &recipient_keys, + 37, + &state, + ); + (state, tx) +} diff --git a/lee/state_machine/src/state/tests/privacy_preserving.rs b/lee/state_machine/src/state/tests/privacy_preserving.rs new file mode 100644 index 00000000..8c33e202 --- /dev/null +++ b/lee/state_machine/src/state/tests/privacy_preserving.rs @@ -0,0 +1,539 @@ +use super::*; + +#[test] +fn transition_from_privacy_preserving_transaction_shielded() { + let sender_keys = test_public_account_keys_1(); + let recipient_keys = test_private_account_keys_1(); + + let mut state = V03State::new().with_public_accounts([( + sender_keys.account_id(), + Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 200, + ..Account::default() + }, + )]); + + let balance_to_move = 37; + + let tx = + shielded_balance_transfer_for_tests(&sender_keys, &recipient_keys, balance_to_move, &state); + + let expected_sender_post = { + let mut this = state.get_account_by_id(sender_keys.account_id()); + this.balance -= balance_to_move; + this.nonce.public_account_nonce_increment(); + this + }; + + let [expected_new_commitment] = tx.message().new_commitments.clone().try_into().unwrap(); + assert!(!state.private_state.0.contains(&expected_new_commitment)); + + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .unwrap(); + + let sender_post = state.get_account_by_id(sender_keys.account_id()); + assert_eq!(sender_post, expected_sender_post); + assert!(state.private_state.0.contains(&expected_new_commitment)); + + assert_eq!( + state.get_account_by_id(sender_keys.account_id()).balance, + 200 - balance_to_move + ); +} + +#[test] +fn transition_from_privacy_preserving_transaction_private() { + let sender_keys = test_private_account_keys_1(); + let sender_nonce = Nonce(0xdead_beef); + + let sender_private_account = Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + nonce: sender_nonce, + data: Data::default(), + }; + let recipient_keys = test_private_account_keys_2(); + + let mut state = V03State::new().with_private_account(&sender_keys, &sender_private_account); + + let balance_to_move = 37; + + let tx = private_balance_transfer_for_tests( + &sender_keys, + &sender_private_account, + &recipient_keys, + balance_to_move, + &state, + ); + + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); + let recipient_account_id = + AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); + let expected_new_commitment_1 = Commitment::new( + &sender_account_id, + &Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), + balance: sender_private_account.balance - balance_to_move, + data: Data::default(), + }, + ); + + let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account); + let expected_new_nullifier = + Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk); + + let expected_new_commitment_2 = Commitment::new( + &recipient_account_id, + &Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + nonce: Nonce::private_account_nonce_init(&recipient_account_id), + balance: balance_to_move, + ..Account::default() + }, + ); + + let previous_public_state = state.public_state.clone(); + assert!(state.private_state.0.contains(&sender_pre_commitment)); + assert!(!state.private_state.0.contains(&expected_new_commitment_1)); + assert!(!state.private_state.0.contains(&expected_new_commitment_2)); + assert!(!state.private_state.1.contains(&expected_new_nullifier)); + + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .unwrap(); + + assert_eq!(state.public_state, previous_public_state); + assert!(state.private_state.0.contains(&sender_pre_commitment)); + assert!(state.private_state.0.contains(&expected_new_commitment_1)); + assert!(state.private_state.0.contains(&expected_new_commitment_2)); + assert!(state.private_state.1.contains(&expected_new_nullifier)); +} + +/// After a valid fully-private tx is proven, tampering with a note's epk should +/// make the shielding proof invalid. +#[test] +fn privacy_tampered_epk_is_rejected() { + use crate::validated_state_diff::ValidatedStateDiff; + + let (state, mut tx) = valid_private_transfer_tx_and_state(); + + // Baseline: the untampered tx verifies + assert!( + ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0).is_ok(), + "the unmodified private transfer must verify" + ); + + // Flip a byte of the first note's epk + tx.message.encrypted_private_post_states[0].epk.0[0] ^= 0xFF; + + assert!( + matches!( + ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0), + Err(LeeError::InvalidPrivacyPreservingProof) + ), + "a tampered epk must be rejected by proof verification" + ); +} + +/// After a valid fully-private tx is proven, tampering with a note's view tag should +/// make the shielding proof invalid. +#[test] +fn privacy_tampered_view_tag_is_rejected() { + use crate::validated_state_diff::ValidatedStateDiff; + + let (state, mut tx) = valid_private_transfer_tx_and_state(); + + // Baseline: the untampered tx verifies. + assert!( + ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0).is_ok(), + "the unmodified private transfer must verify" + ); + + // Flip the first note's view_tag + tx.message.encrypted_private_post_states[0].view_tag ^= 0xFF; + + assert!( + matches!( + ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0), + Err(LeeError::InvalidPrivacyPreservingProof) + ), + "a tampered view_tag must be rejected by proof verification" + ); +} + +#[test] +fn transition_from_privacy_preserving_transaction_deshielded() { + let sender_keys = test_private_account_keys_1(); + let sender_nonce = Nonce(0xdead_beef); + + let sender_private_account = Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + nonce: sender_nonce, + data: Data::default(), + }; + let recipient_keys = test_public_account_keys_1(); + let recipient_initial_balance = 400; + let mut state = V03State::new() + .with_public_accounts([( + recipient_keys.account_id(), + Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: recipient_initial_balance, + ..Account::default() + }, + )]) + .with_private_account(&sender_keys, &sender_private_account); + + let balance_to_move = 37; + + let expected_recipient_post = { + let mut this = state.get_account_by_id(recipient_keys.account_id()); + this.balance += balance_to_move; + this + }; + + let tx = deshielded_balance_transfer_for_tests( + &sender_keys, + &sender_private_account, + &recipient_keys.account_id(), + balance_to_move, + &state, + ); + + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.npk(), &sender_keys.vpk(), 0); + let expected_new_commitment = Commitment::new( + &sender_account_id, + &Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk), + balance: sender_private_account.balance - balance_to_move, + data: Data::default(), + }, + ); + + let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account); + let expected_new_nullifier = + Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk); + + assert!(state.private_state.0.contains(&sender_pre_commitment)); + assert!(!state.private_state.0.contains(&expected_new_commitment)); + assert!(!state.private_state.1.contains(&expected_new_nullifier)); + + state + .transition_from_privacy_preserving_transaction(&tx, 1, 0) + .unwrap(); + + let recipient_post = state.get_account_by_id(recipient_keys.account_id()); + assert_eq!(recipient_post, expected_recipient_post); + assert!(state.private_state.0.contains(&sender_pre_commitment)); + assert!(state.private_state.0.contains(&expected_new_commitment)); + assert!(state.private_state.1.contains(&expected_new_nullifier)); + assert_eq!( + state.get_account_by_id(recipient_keys.account_id()).balance, + recipient_initial_balance + balance_to_move + ); +} + +#[test] +fn burner_program_should_fail_in_privacy_preserving_circuit() { + let program = crate::test_methods::burner(); + let public_account = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 100, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + + let result = execute_and_prove( + vec![public_account], + Program::serialize_instruction(10_u128).unwrap(), + vec![InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn minter_program_should_fail_in_privacy_preserving_circuit() { + let program = crate::test_methods::minter(); + let public_account = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + + let result = execute_and_prove( + vec![public_account], + Program::serialize_instruction(10_u128).unwrap(), + vec![InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn nonce_changer_program_should_fail_in_privacy_preserving_circuit() { + let program = crate::test_methods::nonce_changer(); + let public_account = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + + let result = execute_and_prove( + vec![public_account], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn data_changer_program_should_fail_for_non_owned_account_in_privacy_preserving_circuit() { + let program = crate::test_methods::data_changer(); + let public_account = AccountWithMetadata::new( + Account { + program_owner: [0, 1, 2, 3, 4, 5, 6, 7], + balance: 0, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + + let result = execute_and_prove( + vec![public_account], + Program::serialize_instruction(vec![0]).unwrap(), + vec![InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn data_changer_program_should_fail_for_too_large_data_in_privacy_preserving_circuit() { + let program = crate::test_methods::data_changer(); + let public_account = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + + let large_data: Vec = + vec![ + 0; + usize::try_from(lee_core::account::data::DATA_MAX_LENGTH.as_u64()) + .expect("DATA_MAX_LENGTH fits in usize") + + 1 + ]; + + let result = execute_and_prove( + vec![public_account], + Program::serialize_instruction(large_data).unwrap(), + vec![InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::ProgramProveFailed(_)))); +} + +#[test] +fn extra_output_program_should_fail_in_privacy_preserving_circuit() { + let program = crate::test_methods::extra_output(); + let public_account = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + + let result = execute_and_prove( + vec![public_account], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn missing_output_program_should_fail_in_privacy_preserving_circuit() { + let program = crate::test_methods::missing_output(); + let public_account_1 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + let public_account_2 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([1; 32]), + ); + + let result = execute_and_prove( + vec![public_account_1, public_account_2], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::Public, InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn program_owner_changer_should_fail_in_privacy_preserving_circuit() { + let program = crate::test_methods::program_owner_changer(); + let public_account = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + + let result = execute_and_prove( + vec![public_account], + Program::serialize_instruction(()).unwrap(), + vec![InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn transfer_from_non_owned_account_should_fail_in_privacy_preserving_circuit() { + let program = crate::test_methods::simple_balance_transfer(); + let public_account_1 = AccountWithMetadata::new( + Account { + program_owner: [0, 1, 2, 3, 4, 5, 6, 7], + balance: 100, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + let public_account_2 = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 0, + ..Account::default() + }, + true, + AccountId::new([1; 32]), + ); + + let result = execute_and_prove( + vec![public_account_1, public_account_2], + Program::serialize_instruction(10_u128).unwrap(), + vec![InputAccountIdentity::Public, InputAccountIdentity::Public], + &program.into(), + ); + + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} + +#[test] +fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() { + // Arrange + let malicious_program = crate::test_methods::malicious_authorization_changer(); + let simple_transfers = crate::test_methods::simple_balance_transfer(); + let sender_keys = test_public_account_keys_1(); + let recipient_keys = test_private_account_keys_1(); + + let sender_account = AccountWithMetadata::new( + Account { + program_owner: simple_transfers.id(), + balance: 100, + ..Default::default() + }, + false, + sender_keys.account_id(), + ); + let recipient_account = AccountWithMetadata::new( + Account::default(), + true, + (&recipient_keys.npk(), &recipient_keys.vpk(), 0), + ); + + let recipient_account_id = + AccountId::for_regular_private_account(&recipient_keys.npk(), &recipient_keys.vpk(), 0); + let recipient_commitment = Commitment::new(&recipient_account_id, &recipient_account.account); + let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_account_id); + let state = V03State::new() + .with_public_accounts(public_state_from_balances(&[( + sender_account.account_id, + sender_account.account.balance, + )])) + .with_private_accounts([(recipient_commitment.clone(), recipient_init_nullifier)]) + .with_test_programs(); + + let balance_to_transfer = 10_u128; + let instruction = (balance_to_transfer, simple_transfers.id()); + + let mut dependencies = HashMap::new(); + dependencies.insert(simple_transfers.id(), simple_transfers); + let program_with_deps = ProgramWithDependencies::new(malicious_program, dependencies); + + // Act - execute the malicious program - this should fail during proving + let result = execute_and_prove( + vec![sender_account, recipient_account], + Program::serialize_instruction(instruction).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: recipient_keys.vpk(), + random_seed: [0; 32], + nsk: recipient_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&recipient_commitment) + .expect("recipient's commitment must be in state"), + identifier: 0, + }, + ], + &program_with_deps, + ); + + // Assert - should fail because the malicious program tries to manipulate is_authorized + assert!(matches!(result, Err(LeeError::CircuitProvingError(_)))); +} diff --git a/lee/state_machine/src/state/tests/public_program_rules.rs b/lee/state_machine/src/state/tests/public_program_rules.rs new file mode 100644 index 00000000..1c67aeff --- /dev/null +++ b/lee/state_machine/src/state/tests/public_program_rules.rs @@ -0,0 +1,325 @@ +use super::*; + +#[test] +fn program_should_fail_if_modifies_nonces() { + let account_id = AccountId::new([1; 32]); + let mut state = V03State::new() + .with_public_account_balances([(account_id, 100)]) + .with_test_programs(); + let account_ids = vec![account_id]; + let program_id = crate::test_methods::nonce_changer().id(); + let message = + public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior( + InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::ModifiedNonce { account_id: err_account_id } + ) + )) if err_account_id == account_id + )); +} + +#[test] +fn program_should_fail_if_output_accounts_exceed_inputs() { + let mut state = V03State::new() + .with_public_account_balances([(AccountId::new([1; 32]), 0)]) + .with_test_programs(); + let account_ids = vec![AccountId::new([1; 32])]; + let program_id = crate::test_methods::extra_output().id(); + let message = + public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior( + InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::MismatchedPreStatePostStateLength { + pre_state_length, + post_state_length + } + ) + )) if pre_state_length == 1 && post_state_length == 2 + )); +} + +#[test] +fn program_should_fail_with_missing_output_accounts() { + let mut state = V03State::new() + .with_public_account_balances([(AccountId::new([1; 32]), 100)]) + .with_test_programs(); + let account_ids = vec![AccountId::new([1; 32]), AccountId::new([2; 32])]; + let program_id = crate::test_methods::missing_output().id(); + let message = + public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior( + InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::MismatchedPreStatePostStateLength { + pre_state_length, + post_state_length + } + ) + )) if pre_state_length == 2 && post_state_length == 1 + )); +} + +#[test] +fn program_should_fail_if_modifies_program_owner_with_only_non_default_program_owner() { + let initial_data = [( + AccountId::new([1; 32]), + Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + ..Account::default() + }, + )]; + let mut state = V03State::new() + .with_public_accounts(initial_data) + .with_test_programs(); + let account_id = AccountId::new([1; 32]); + let account = state.get_account_by_id(account_id); + // Assert the target account only differs from the default account in the program owner + // field + assert_ne!(account.program_owner, Account::default().program_owner); + assert_eq!(account.balance, Account::default().balance); + assert_eq!(account.nonce, Account::default().nonce); + assert_eq!(account.data, Account::default().data); + let program_id = crate::test_methods::program_owner_changer().id(); + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } + ))) if err_account_id == account_id + )); +} + +#[test] +fn program_should_fail_if_modifies_program_owner_with_only_non_default_balance() { + let initial_data = HashMap::new(); + let mut state = V03State::new() + .with_public_accounts(initial_data) + .with_test_programs() + .with_non_default_accounts_but_default_program_owners(); + let account_id = AccountId::new([255; 32]); + let account = state.get_account_by_id(account_id); + // Assert the target account only differs from the default account in balance field + assert_eq!(account.program_owner, Account::default().program_owner); + assert_ne!(account.balance, Account::default().balance); + assert_eq!(account.nonce, Account::default().nonce); + assert_eq!(account.data, Account::default().data); + let program_id = crate::test_methods::program_owner_changer().id(); + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } + ))) if err_account_id == account_id + )); +} + +#[test] +fn program_should_fail_if_modifies_program_owner_with_only_non_default_nonce() { + let initial_data = HashMap::new(); + let mut state = V03State::new() + .with_public_accounts(initial_data) + .with_test_programs() + .with_non_default_accounts_but_default_program_owners(); + let account_id = AccountId::new([254; 32]); + let account = state.get_account_by_id(account_id); + // Assert the target account only differs from the default account in nonce field + assert_eq!(account.program_owner, Account::default().program_owner); + assert_eq!(account.balance, Account::default().balance); + assert_ne!(account.nonce, Account::default().nonce); + assert_eq!(account.data, Account::default().data); + let program_id = crate::test_methods::program_owner_changer().id(); + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } + ))) if err_account_id == account_id + )); +} + +#[test] +fn program_should_fail_if_modifies_program_owner_with_only_non_default_data() { + let initial_data = HashMap::new(); + let mut state = V03State::new() + .with_public_accounts(initial_data) + .with_test_programs() + .with_non_default_accounts_but_default_program_owners(); + let account_id = AccountId::new([253; 32]); + let account = state.get_account_by_id(account_id); + // Assert the target account only differs from the default account in data field + assert_eq!(account.program_owner, Account::default().program_owner); + assert_eq!(account.balance, Account::default().balance); + assert_eq!(account.nonce, Account::default().nonce); + assert_ne!(account.data, Account::default().data); + let program_id = crate::test_methods::program_owner_changer().id(); + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } + ))) if err_account_id == account_id + )); +} + +#[test] +fn program_should_fail_if_transfers_balance_from_non_owned_account() { + let sender_account_id = AccountId::new([1; 32]); + let receiver_account_id = AccountId::new([2; 32]); + let mut state = V03State::new() + .with_public_account_balances([(sender_account_id, 100)]) + .with_test_programs(); + let balance_to_move: u128 = 1; + let program_id = crate::test_methods::simple_balance_transfer().id(); + assert_ne!( + state.get_account_by_id(sender_account_id).program_owner, + program_id + ); + let message = public_transaction::Message::try_new( + program_id, + vec![sender_account_id, receiver_account_id], + vec![], + balance_to_move, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::UnauthorizedBalanceDecrease { account_id: err_account_id, owner_program_id, executing_program_id } + ))) if err_account_id == sender_account_id && owner_program_id != program_id && executing_program_id == program_id + )); +} + +#[test] +fn program_should_fail_if_modifies_data_of_non_owned_account() { + let initial_data = HashMap::new(); + let mut state = V03State::new() + .with_public_accounts(initial_data) + .with_test_programs() + .with_non_default_accounts_but_default_program_owners(); + let account_id = AccountId::new([255; 32]); + let program_id = crate::test_methods::data_changer().id(); + + assert_ne!(state.get_account_by_id(account_id), Account::default()); + assert_ne!( + state.get_account_by_id(account_id).program_owner, + program_id + ); + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], vec![0]) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::UnauthorizedDataModification { account_id: err_account_id, executing_program_id } + ))) if err_account_id == account_id && executing_program_id == program_id + )); +} + +#[test] +fn program_should_fail_if_does_not_preserve_total_balance_by_minting() { + let initial_data = HashMap::new(); + let mut state = V03State::new() + .with_public_accounts(initial_data) + .with_test_programs(); + let account_id = AccountId::new([1; 32]); + let program_id = crate::test_methods::minter().id(); + + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 2, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states } + ))) if total_balance_pre_states == 0.into() && total_balance_post_states == 1.into() + )); +} + +#[test] +fn program_should_fail_if_does_not_preserve_total_balance_by_burning() { + let initial_data = HashMap::new(); + let mut state = V03State::new() + .with_public_accounts(initial_data) + .with_test_programs() + .with_account_owned_by_burner_program(); + let program_id = crate::test_methods::burner().id(); + let account_id = AccountId::new([252; 32]); + assert_eq!( + state.get_account_by_id(account_id).program_owner, + program_id + ); + let balance_to_burn: u128 = 1; + assert!(state.get_account_by_id(account_id).balance > balance_to_burn); + + let message = + public_transaction::Message::try_new(program_id, vec![account_id], vec![], balance_to_burn) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + let result = state.transition_from_public_transaction(&tx, 2, 0); + + assert!(matches!( + result, + Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states } + ))) if total_balance_pre_states == 100.into() && total_balance_post_states == 99.into() + )); +} diff --git a/lee/state_machine/src/state/tests/validity_window.rs b/lee/state_machine/src/state/tests/validity_window.rs new file mode 100644 index 00000000..12e7c229 --- /dev/null +++ b/lee/state_machine/src/state/tests/validity_window.rs @@ -0,0 +1,237 @@ +use super::*; + +#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] +#[test_case::test_case((Some(1), Some(3)), 2; "inside range")] +#[test_case::test_case((Some(1), Some(3)), 0; "below range")] +#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] +#[test_case::test_case((Some(1), Some(3)), 4; "above range")] +#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] +#[test_case::test_case((Some(1), None), 10; "lower bound only - above")] +#[test_case::test_case((Some(1), None), 0; "lower bound only - below")] +#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] +#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] +#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] +#[test_case::test_case((None, None), 0; "no bounds - always valid")] +#[test_case::test_case((None, None), 100; "no bounds - always valid 2")] +fn validity_window_works_in_public_transactions( + validity_window: (Option, Option), + block_id: BlockId, +) { + let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap(); + let validity_window_program = crate::test_methods::validity_window(); + let account_keys = test_public_account_keys_1(); + let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id()); + let mut state = V03State::new().with_test_programs(); + let tx = { + let account_ids = vec![pre.account_id]; + let nonces = vec![]; + let program_id = validity_window_program.id(); + let instruction = ( + block_validity_window, + TimestampValidityWindow::new_unbounded(), + ); + let message = + public_transaction::Message::try_new(program_id, account_ids, nonces, instruction) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set) + }; + let result = state.transition_from_public_transaction(&tx, block_id, 0); + let is_inside_validity_window = + match (block_validity_window.start(), block_validity_window.end()) { + (Some(s), Some(e)) => s <= block_id && block_id < e, + (Some(s), None) => s <= block_id, + (None, Some(e)) => block_id < e, + (None, None) => true, + }; + if is_inside_validity_window { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(LeeError::OutOfValidityWindow))); + } +} + +#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] +#[test_case::test_case((Some(1), Some(3)), 2; "inside range")] +#[test_case::test_case((Some(1), Some(3)), 0; "below range")] +#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] +#[test_case::test_case((Some(1), Some(3)), 4; "above range")] +#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] +#[test_case::test_case((Some(1), None), 10; "lower bound only - above")] +#[test_case::test_case((Some(1), None), 0; "lower bound only - below")] +#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] +#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] +#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] +#[test_case::test_case((None, None), 0; "no bounds - always valid")] +#[test_case::test_case((None, None), 100; "no bounds - always valid 2")] +fn timestamp_validity_window_works_in_public_transactions( + validity_window: (Option, Option), + timestamp: Timestamp, +) { + let timestamp_validity_window: TimestampValidityWindow = validity_window.try_into().unwrap(); + let validity_window_program = crate::test_methods::validity_window(); + let account_keys = test_public_account_keys_1(); + let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id()); + let mut state = V03State::new().with_test_programs(); + let tx = { + let account_ids = vec![pre.account_id]; + let nonces = vec![]; + let program_id = validity_window_program.id(); + let instruction = ( + BlockValidityWindow::new_unbounded(), + timestamp_validity_window, + ); + let message = + public_transaction::Message::try_new(program_id, account_ids, nonces, instruction) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set) + }; + let result = state.transition_from_public_transaction(&tx, 1, timestamp); + let is_inside_validity_window = match ( + timestamp_validity_window.start(), + timestamp_validity_window.end(), + ) { + (Some(s), Some(e)) => s <= timestamp && timestamp < e, + (Some(s), None) => s <= timestamp, + (None, Some(e)) => timestamp < e, + (None, None) => true, + }; + if is_inside_validity_window { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(LeeError::OutOfValidityWindow))); + } +} + +#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] +#[test_case::test_case((Some(1), Some(3)), 2; "inside range")] +#[test_case::test_case((Some(1), Some(3)), 0; "below range")] +#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] +#[test_case::test_case((Some(1), Some(3)), 4; "above range")] +#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] +#[test_case::test_case((Some(1), None), 10; "lower bound only - above")] +#[test_case::test_case((Some(1), None), 0; "lower bound only - below")] +#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] +#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] +#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] +#[test_case::test_case((None, None), 0; "no bounds - always valid")] +#[test_case::test_case((None, None), 100; "no bounds - always valid 2")] +fn validity_window_works_in_privacy_preserving_transactions( + validity_window: (Option, Option), + block_id: BlockId, +) { + let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap(); + let validity_window_program = crate::test_methods::validity_window(); + let account_keys = test_private_account_keys_1(); + let pre = AccountWithMetadata::new( + Account::default(), + false, + (&account_keys.npk(), &account_keys.vpk(), 0), + ); + let mut state = V03State::new().with_test_programs(); + let tx = { + let instruction = ( + block_validity_window, + TimestampValidityWindow::new_unbounded(), + ); + let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove( + vec![pre], + Program::serialize_instruction(instruction).unwrap(), + vec![InputAccountIdentity::PrivateUnauthorized { + vpk: account_keys.vpk(), + random_seed: [0; 32], + npk: account_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }], + &validity_window_program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + PrivacyPreservingTransaction::new(message, witness_set) + }; + let result = state.transition_from_privacy_preserving_transaction(&tx, block_id, 0); + let is_inside_validity_window = + match (block_validity_window.start(), block_validity_window.end()) { + (Some(s), Some(e)) => s <= block_id && block_id < e, + (Some(s), None) => s <= block_id, + (None, Some(e)) => block_id < e, + (None, None) => true, + }; + if is_inside_validity_window { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(LeeError::OutOfValidityWindow))); + } +} + +#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")] +#[test_case::test_case((Some(1), Some(3)), 2; "inside range")] +#[test_case::test_case((Some(1), Some(3)), 0; "below range")] +#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")] +#[test_case::test_case((Some(1), Some(3)), 4; "above range")] +#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")] +#[test_case::test_case((Some(1), None), 10; "lower bound only - above")] +#[test_case::test_case((Some(1), None), 0; "lower bound only - below")] +#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")] +#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")] +#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")] +#[test_case::test_case((None, None), 0; "no bounds - always valid")] +#[test_case::test_case((None, None), 100; "no bounds - always valid 2")] +fn timestamp_validity_window_works_in_privacy_preserving_transactions( + validity_window: (Option, Option), + timestamp: Timestamp, +) { + let timestamp_validity_window: TimestampValidityWindow = validity_window.try_into().unwrap(); + let validity_window_program = crate::test_methods::validity_window(); + let account_keys = test_private_account_keys_1(); + let pre = AccountWithMetadata::new( + Account::default(), + false, + (&account_keys.npk(), &account_keys.vpk(), 0), + ); + let mut state = V03State::new().with_test_programs(); + let tx = { + let instruction = ( + BlockValidityWindow::new_unbounded(), + timestamp_validity_window, + ); + let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove( + vec![pre], + Program::serialize_instruction(instruction).unwrap(), + vec![InputAccountIdentity::PrivateUnauthorized { + vpk: account_keys.vpk(), + random_seed: [0; 32], + npk: account_keys.npk(), + identifier: 0, + commitment_root: DUMMY_COMMITMENT_HASH, + }], + &validity_window_program.into(), + ) + .unwrap(); + + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + PrivacyPreservingTransaction::new(message, witness_set) + }; + let result = state.transition_from_privacy_preserving_transaction(&tx, 1, timestamp); + let is_inside_validity_window = match ( + timestamp_validity_window.start(), + timestamp_validity_window.end(), + ) { + (Some(s), Some(e)) => s <= timestamp && timestamp < e, + (Some(s), None) => s <= timestamp, + (None, Some(e)) => timestamp < e, + (None, None) => true, + }; + if is_inside_validity_window { + assert!(result.is_ok()); + } else { + assert!(matches!(result, Err(LeeError::OutOfValidityWindow))); + } +} diff --git a/lee/state_machine/src/validated_state_diff.rs b/lee/state_machine/src/validated_state_diff.rs deleted file mode 100644 index 1953d93a..00000000 --- a/lee/state_machine/src/validated_state_diff.rs +++ /dev/null @@ -1,1019 +0,0 @@ -use std::{ - collections::{HashMap, HashSet, VecDeque}, - hash::Hash, -}; - -use lee_core::{ - BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp, - account::{Account, AccountId, AccountWithMetadata}, - program::{ - ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, compute_public_authorized_pdas, - validate_execution, - }, -}; -use log::debug; - -use crate::{ - V03State, ensure, - error::{InvalidProgramBehaviorError, LeeError}, - privacy_preserving_transaction::{ - PrivacyPreservingTransaction, circuit::Proof, message::Message, - }, - program::Program, - program_deployment_transaction::ProgramDeploymentTransaction, - public_transaction::PublicTransaction, - state::MAX_NUMBER_CHAINED_CALLS, -}; - -pub struct StateDiff { - pub signer_account_ids: Vec, - pub public_diff: HashMap, - pub new_commitments: Vec, - pub new_nullifiers: Vec, - pub program: Option, -} - -/// The validated output of executing or verifying a transaction, ready to be applied to the state. -/// -/// Can only be constructed by the transaction validation functions inside this crate, ensuring the -/// diff has been checked before any state mutation occurs. -pub struct ValidatedStateDiff(StateDiff); - -impl ValidatedStateDiff { - pub fn from_public_transaction( - tx: &PublicTransaction, - state: &V03State, - block_id: BlockId, - timestamp: Timestamp, - ) -> Result { - let message = tx.message(); - let witness_set = tx.witness_set(); - - // All account_ids must be different - ensure!( - message.account_ids.iter().collect::>().len() == message.account_ids.len(), - LeeError::InvalidInput("Duplicate account_ids found in message".into(),) - ); - - // Check exactly one nonce is provided for each signature - ensure!( - message.nonces.len() == witness_set.signatures_and_public_keys.len(), - LeeError::InvalidInput( - "Mismatch between number of nonces and signatures/public keys".into(), - ) - ); - - // Check the signatures are valid - ensure!( - witness_set.is_valid_for(message), - LeeError::InvalidInput("Invalid signature for given message and public key".into()) - ); - - let signer_account_ids = tx.signer_account_ids(); - // Check nonces corresponds to the current nonces on the public state. - for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) { - let current_nonce = state.get_account_by_id(*account_id).nonce; - ensure!( - current_nonce == *nonce, - LeeError::InvalidInput("Nonce mismatch".into()) - ); - } - - // Build pre_states for execution - let input_pre_states: Vec<_> = message - .account_ids - .iter() - .map(|account_id| { - AccountWithMetadata::new( - state.get_account_by_id(*account_id), - signer_account_ids.contains(account_id), - *account_id, - ) - }) - .collect(); - - let mut state_diff: HashMap = HashMap::new(); - - let initial_call = ChainedCall { - program_id: message.program_id, - instruction_data: message.instruction_data.clone(), - pre_states: input_pre_states, - pda_seeds: vec![], - }; - - #[expect( - clippy::items_after_statements, - reason = "More readable to keep it behind the place where it's used" - )] - #[derive(Debug)] - struct CallerData { - program_id: Option, - authorized_accounts: HashSet, - } - - let initial_caller_data = CallerData { - program_id: None, - authorized_accounts: signer_account_ids.iter().copied().collect(), - }; - - let mut chained_calls = - VecDeque::<(ChainedCall, CallerData)>::from_iter([(initial_call, initial_caller_data)]); - let mut chain_calls_counter = 0; - - while let Some((chained_call, caller_data)) = chained_calls.pop_front() { - ensure!( - chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS, - LeeError::MaxChainedCallsDepthExceeded - ); - - // Check that the `program_id` corresponds to a deployed program - let Some(program) = state.programs().get(&chained_call.program_id) else { - return Err(LeeError::InvalidInput("Unknown program".into())); - }; - - debug!( - "Program {:?} pre_states: {:?}, instruction_data: {:?}", - chained_call.program_id, chained_call.pre_states, chained_call.instruction_data - ); - let mut program_output = program.execute( - caller_data.program_id, - &chained_call.pre_states, - &chained_call.instruction_data, - )?; - debug!( - "Program {:?} output: {:?}", - chained_call.program_id, program_output - ); - - let authorized_pdas = - compute_public_authorized_pdas(caller_data.program_id, &chained_call.pda_seeds); - - // Account is authorized if it is either in the caller's authorized accounts or in the - // list of PDAs the caller has authorized. - let is_authorized = |account_id: &AccountId| { - authorized_pdas.contains(account_id) - || caller_data.authorized_accounts.contains(account_id) - }; - - for pre in &program_output.pre_states { - let account_id = pre.account_id; - // Check that the program output pre_states coincide with the values in the public - // state or with any modifications to those values during the chain of calls. - let expected_pre = state_diff - .get(&account_id) - .cloned() - .unwrap_or_else(|| state.get_account_by_id(account_id)); - ensure!( - pre.account == expected_pre, - InvalidProgramBehaviorError::InconsistentAccountPreState { - account_id, - expected: Box::new(expected_pre), - actual: Box::new(pre.account.clone()) - } - ); - - // Check that the program output pre_states marked as authorized are indeed - // authorized, and vice-versa. - let is_indeed_authorized = is_authorized(&account_id); - ensure!( - !pre.is_authorized || is_indeed_authorized, - InvalidProgramBehaviorError::InvalidAccountAuthorization { account_id } - ); - ensure!( - pre.is_authorized || !is_indeed_authorized, - InvalidProgramBehaviorError::AuthorizedAccountMarkedAsNotAuthorized { - account_id - } - ); - } - - // Verify that the program output's self_program_id matches the expected program ID. - ensure!( - program_output.self_program_id == chained_call.program_id, - InvalidProgramBehaviorError::MismatchedProgramId { - expected: chained_call.program_id, - actual: program_output.self_program_id - } - ); - - // Verify that the program output's caller_program_id matches the actual caller. - ensure!( - program_output.caller_program_id == caller_data.program_id, - InvalidProgramBehaviorError::MismatchedCallerProgramId { - expected: caller_data.program_id, - actual: program_output.caller_program_id, - } - ); - - // Verify execution corresponds to a well-behaved program. - // See the # Programs section for the definition of the `validate_execution` method. - validate_execution( - &program_output.pre_states, - &program_output.post_states, - chained_call.program_id, - ) - .map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?; - - // Verify validity window - ensure!( - program_output.block_validity_window.is_valid_for(block_id) - && program_output - .timestamp_validity_window - .is_valid_for(timestamp), - LeeError::OutOfValidityWindow - ); - - for (i, post) in program_output.post_states.iter_mut().enumerate() { - let Some(claim) = post.required_claim() else { - continue; - }; - let pre = &program_output.pre_states[i]; - let account_id = pre.account_id; - - // The invoked program can only claim accounts with default program id. - ensure!( - post.account().program_owner == DEFAULT_PROGRAM_ID, - InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id } - ); - - match claim { - Claim::Authorized => { - // The program can only claim accounts that were authorized by the signer. - ensure!( - pre.is_authorized, - InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id } - ); - } - Claim::Pda(seed) => { - // The program can only claim accounts that correspond to the PDAs it is - // authorized to claim. The public-execution path only sees public - // accounts, so the public-PDA derivation is the correct formula here. - let pda = AccountId::for_public_pda(&chained_call.program_id, &seed); - ensure!( - account_id == pda, - InvalidProgramBehaviorError::MismatchedPdaClaim { - expected: pda, - actual: account_id - } - ); - } - } - - post.account_mut().program_owner = chained_call.program_id; - } - - // Update the state diff - for (pre, post) in program_output - .pre_states - .iter() - .zip(program_output.post_states.iter()) - { - state_diff.insert(pre.account_id, post.account().clone()); - } - - // Source from `program_output.pre_states`, not `chained_call.pre_states`: - // the loop above already gates program_output's `is_authorized` via the - // `!pre.is_authorized || is_indeed_authorized` check, while `chained_call. - // pre_states` is caller-controlled and can be forged (audit-issue 91). - // - // Union with the caller's authorized set so that authorization is monotonically - // growing: once an account is authorized at any point in the chain it remains - // authorized for all subsequent calls. - let authorized_accounts: HashSet<_> = caller_data - .authorized_accounts - .into_iter() - .chain( - program_output - .pre_states - .iter() - .filter(|pre| pre.is_authorized) - .map(|pre| pre.account_id), - ) - .collect(); - for new_call in program_output.chained_calls.into_iter().rev() { - chained_calls.push_front(( - new_call, - CallerData { - program_id: Some(chained_call.program_id), - authorized_accounts: authorized_accounts.clone(), - }, - )); - } - - chain_calls_counter = chain_calls_counter - .checked_add(1) - .expect("we check the max depth at the beginning of the loop"); - } - - // Check that all modified uninitialized accounts where claimed - for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| { - let pre = state.get_account_by_id(*account_id); - if pre.program_owner != DEFAULT_PROGRAM_ID { - return None; - } - if pre == *post { - return None; - } - Some((*account_id, post)) - }) { - ensure!( - post.program_owner != DEFAULT_PROGRAM_ID, - InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id } - ); - } - - Ok(Self(StateDiff { - signer_account_ids, - public_diff: state_diff, - new_commitments: vec![], - new_nullifiers: vec![], - program: None, - })) - } - - pub fn from_privacy_preserving_transaction( - tx: &PrivacyPreservingTransaction, - state: &V03State, - block_id: BlockId, - timestamp: Timestamp, - ) -> Result { - let message = &tx.message; - let witness_set = &tx.witness_set; - - // 1. Commitments or nullifiers are non empty - ensure!( - !message.new_commitments.is_empty() || !message.new_nullifiers.is_empty(), - LeeError::InvalidInput( - "Empty commitments and empty nullifiers found in message".into(), - ) - ); - - // 2. Check there are no duplicate account_ids in the public_account_ids list. - ensure!( - n_unique(&message.public_account_ids) == message.public_account_ids.len(), - LeeError::InvalidInput("Duplicate account_ids found in message".into()) - ); - - // Check there are no duplicate nullifiers in the new_nullifiers list - ensure!( - n_unique( - &message - .new_nullifiers - .iter() - .map(|(n, _)| n) - .collect::>() - ) == message.new_nullifiers.len(), - LeeError::InvalidInput("Duplicate nullifiers found in message".into()) - ); - - // Check there are no duplicate commitments in the new_commitments list - ensure!( - n_unique(&message.new_commitments) == message.new_commitments.len(), - LeeError::InvalidInput("Duplicate commitments found in message".into()) - ); - - // 3. Nonce checks and Valid signatures - // Check exactly one nonce is provided for each signature - ensure!( - message.nonces.len() == witness_set.signatures_and_public_keys.len(), - LeeError::InvalidInput( - "Mismatch between number of nonces and signatures/public keys".into(), - ) - ); - - // Check the signatures are valid - ensure!( - witness_set.signatures_are_valid_for(message), - LeeError::InvalidInput("Invalid signature for given message and public key".into()) - ); - - let signer_account_ids = tx.signer_account_ids(); - // Check nonces corresponds to the current nonces on the public state. - for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) { - let current_nonce = state.get_account_by_id(*account_id).nonce; - ensure!( - current_nonce == *nonce, - LeeError::InvalidInput("Nonce mismatch".into()) - ); - } - - // Verify validity window - ensure!( - message.block_validity_window.is_valid_for(block_id) - && message.timestamp_validity_window.is_valid_for(timestamp), - LeeError::OutOfValidityWindow - ); - - // Build pre_states for proof verification - let public_pre_states: Vec<_> = message - .public_account_ids - .iter() - .map(|account_id| { - AccountWithMetadata::new( - state.get_account_by_id(*account_id), - signer_account_ids.contains(account_id), - *account_id, - ) - }) - .collect(); - - // 4. Proof verification - check_privacy_preserving_circuit_proof_is_valid( - &witness_set.proof, - &public_pre_states, - message, - )?; - - // 5. Commitment freshness - state.check_commitments_are_new(&message.new_commitments)?; - - // 6. Nullifier uniqueness - state.check_nullifiers_are_valid(&message.new_nullifiers)?; - - let public_diff = message - .public_account_ids - .iter() - .copied() - .zip(message.public_post_states.clone()) - .collect(); - let new_nullifiers = message - .new_nullifiers - .iter() - .copied() - .map(|(nullifier, _)| nullifier) - .collect(); - - Ok(Self(StateDiff { - signer_account_ids, - public_diff, - new_commitments: message.new_commitments.clone(), - new_nullifiers, - program: None, - })) - } - - pub fn from_program_deployment_transaction( - tx: &ProgramDeploymentTransaction, - state: &V03State, - ) -> Result { - // TODO: remove clone - let program = Program::new(tx.message.bytecode.clone())?; - if state.programs().contains_key(&program.id()) { - return Err(LeeError::ProgramAlreadyExists); - } - Ok(Self(StateDiff { - signer_account_ids: vec![], - public_diff: HashMap::new(), - new_commitments: vec![], - new_nullifiers: vec![], - program: Some(program), - })) - } - - /// Returns the public account changes produced by this transaction. - /// - /// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example - /// to enforce that system accounts are not modified by user transactions. - #[must_use] - pub fn public_diff(&self) -> HashMap { - self.0.public_diff.clone() - } - - pub(crate) fn into_state_diff(self) -> StateDiff { - self.0 - } -} - -fn check_privacy_preserving_circuit_proof_is_valid( - proof: &Proof, - public_pre_states: &[AccountWithMetadata], - message: &Message, -) -> Result<(), LeeError> { - let output = PrivacyPreservingCircuitOutput { - public_pre_states: public_pre_states.to_vec(), - public_post_states: message.public_post_states.clone(), - encrypted_private_post_states: message.encrypted_private_post_states.clone(), - new_commitments: message.new_commitments.clone(), - new_nullifiers: message.new_nullifiers.clone(), - block_validity_window: message.block_validity_window, - timestamp_validity_window: message.timestamp_validity_window, - }; - proof - .is_valid_for(&output) - .then_some(()) - .ok_or(LeeError::InvalidPrivacyPreservingProof) -} - -fn n_unique(data: &[T]) -> usize { - let set: HashSet<&T> = data.iter().collect(); - set.len() -} - -#[cfg(test)] -mod tests { - use lee_core::account::{AccountId, Nonce}; - - use crate::{ - PrivateKey, PublicKey, V03State, - error::{InvalidProgramBehaviorError, LeeError}, - program::Program, - public_transaction::{Message, WitnessSet}, - validated_state_diff::ValidatedStateDiff, - }; - - #[test] - fn public_diff_reflects_a_successful_transfer() { - // A successful native transfer must record the debited sender in - // `public_diff()`. Catches the mutation that replaces `public_diff` with - // `HashMap::new()` (which would hide every account change). - use authenticated_transfer_core::Instruction as AtInstruction; - - let from_key = PrivateKey::try_new([1_u8; 32]).unwrap(); - let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); - let to_key = PrivateKey::try_new([2_u8; 32]).unwrap(); - let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); - - let state = V03State::new_with_genesis_accounts(&[(from, 100)], vec![], 0); - let program_id = Program::authenticated_transfer_program().id(); - let message = Message::try_new( - program_id, - vec![from, to], - vec![Nonce(0), Nonce(0)], - AtInstruction::Transfer { amount: 5 }, - ) - .unwrap(); - let witness_set = WitnessSet::for_message(&message, &[&from_key, &to_key]); - let tx = crate::PublicTransaction::new(message, witness_set); - - let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0) - .expect("a valid native transfer must validate"); - let public_diff = diff.public_diff(); - - assert!( - public_diff.contains_key(&from), - "public_diff must contain the debited sender", - ); - assert_eq!( - public_diff[&from].balance, 95, - "sender balance in the diff must reflect the debit", - ); - } - - /// Privacy-path version of the authorization-injection attack. The test passes when the - /// attack is rejected and the victim's balance is left untouched. - /// - /// `execute_and_prove` succeeds because each inner receipt is individually valid and the - /// outer circuit faithfully commits whatever the attacker's program output says, including - /// `victim(is_authorized=true)`. The circuit has no access to chain state and cannot know - /// the victim never signed. - /// - /// The host-side validator is what catches the attack: it independently reconstructs - /// `public_pre_states` from chain state using `signer_account_ids.contains(victim_id) = false`, - /// so it expects `victim(is_authorized=false)`. The committed journal and the reconstructed - /// expected output diverge, `receipt.verify` fails, and `from_privacy_preserving_transaction` - /// returns an error before any state is applied. - #[test] - fn privacy_malicious_programs_cannot_drain_public_victim() { - use lee_core::{ - Commitment, InputAccountIdentity, - account::{Account, AccountWithMetadata}, - }; - - use crate::{ - PrivacyPreservingTransaction, - privacy_preserving_transaction::{ - circuit::{ProgramWithDependencies, execute_and_prove}, - message::Message, - witness_set::WitnessSet, - }, - state::{CommitmentSet, tests::test_private_account_keys_1}, - }; - - type InjectorInstruction = ( - lee_core::program::ProgramId, // p2_id - lee_core::program::ProgramId, // auth_transfer_id - [u8; 32], // victim_id_raw - u128, // victim_balance - u128, // victim_nonce - lee_core::program::ProgramId, // victim_program_owner - [u8; 32], // recipient_id_raw - u128, // amount - ); - - // Attacker controls a private account. - let attacker_keys = test_private_account_keys_1(); - let attacker_id = - AccountId::for_regular_private_account(&attacker_keys.npk(), &attacker_keys.vpk(), 0); - - let victim_id = AccountId::new([20_u8; 32]); - let recipient_id = AccountId::new([42_u8; 32]); - let victim_balance = 5_000_u128; - - // genesis sets program_owner = authenticated_transfer_program.id() on all accounts. - let mut state = V03State::new_with_genesis_accounts( - &[(victim_id, victim_balance), (recipient_id, 0)], - vec![], - 0, - ); - state.insert_program(Program::malicious_injector()); - state.insert_program(Program::malicious_launderer()); - - // Build attacker's private account and its local commitment tree. - let attacker_account = Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: 100, - ..Account::default() - }; - let attacker_commitment = Commitment::new(&attacker_id, &attacker_account); - let mut commitment_set = CommitmentSet::with_capacity(1); - commitment_set.extend(std::slice::from_ref(&attacker_commitment)); - let membership_proof = commitment_set - .get_proof_for(&attacker_commitment) - .expect("attacker commitment must be in the set"); - - let attacker_pre = AccountWithMetadata::new(attacker_account, true, attacker_id); - - let victim_account = state.get_account_by_id(victim_id); - let instruction: InjectorInstruction = ( - Program::malicious_launderer().id(), - Program::authenticated_transfer_program().id(), - *victim_id.value(), - victim_account.balance, - victim_account.nonce.0, - victim_account.program_owner, - *recipient_id.value(), - victim_balance, - ); - let instruction_data = Program::serialize_instruction(instruction).unwrap(); - - let p2 = Program::malicious_launderer(); - let at = Program::authenticated_transfer_program(); - let program_with_deps = ProgramWithDependencies::new( - Program::malicious_injector(), - [(p2.id(), p2), (at.id(), at)].into(), - ); - - // account_identities order must match self.pre_states as built by the circuit: - // [0] attacker โ€” first seen in P1's program_output.pre_states - // [1] victim โ€” first seen in authenticated_transfer's program_output.pre_states - // [2] recipient โ€” first seen in authenticated_transfer's program_output.pre_states - let account_identities = vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: attacker_keys.vpk(), - random_seed: [0; 32], - nsk: attacker_keys.nsk, - membership_proof, - identifier: 0, - }, - InputAccountIdentity::Public, // victim - InputAccountIdentity::Public, // recipient - ]; - - // execute_and_prove succeeds: all inner receipts are valid. - // The outer circuit commits victim(is_authorized=true) to its journal. - let (circuit_output, proof) = execute_and_prove( - vec![attacker_pre], - instruction_data, - account_identities, - &program_with_deps, - ) - .expect("execute_and_prove should succeed \u{2014} the programs execute correctly"); - - // public_account_ids lists the Public entries from account_identities, in order. - // The single ciphertext belongs to attacker's private account update. - let message = Message::try_from_circuit_output( - vec![victim_id, recipient_id], - vec![], // no public signers, no nonces - circuit_output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0); - - assert!( - matches!(result, Err(LeeError::InvalidPrivacyPreservingProof)), - "attack privacy transaction should be rejected with InvalidPrivacyPreservingProof" - ); - assert_eq!(state.get_account_by_id(victim_id).balance, victim_balance); - assert_eq!(state.get_account_by_id(recipient_id).balance, 0); - } - - /// Private-victim variant of the authorization-injection attack. The test passes when the - /// attack is rejected and the recipient's balance remains zero. - /// - /// After the circuit's Vacant branch accepts the injected `victim(is_authorized=true)` - /// verbatim, the attacker must choose how to declare the victim in `account_identities`. - /// There are two routes, both closed: - /// - /// - **mask=1 (`PrivateAuthorizedUpdate`)**: the circuit derives `account_id = - /// AccountId::for_regular_private_account(&npk_from(nsk), identifier)` and asserts it matches - /// `pre_state.account_id`. Passing this check requires the victim's `nsk`, which the attacker - /// does not have. `execute_and_prove` panics inside the ZKVM and no proof is produced. - /// - /// - **mask=0 (`Public`)**: the circuit places the account in `public_pre_states` and - /// `execute_and_prove` succeeds. The host-side validator then reconstructs - /// `public_pre_states` from chain state; `state.get_account_by_id(victim_id)` returns the - /// default account (balance=0) because the victim has no public state entry. The committed - /// journal and the reconstructed expected output diverge, `receipt.verify` fails, and - /// `from_privacy_preserving_transaction` returns an error before any state is applied. This - /// test exercises this route. - #[test] - fn privacy_malicious_programs_cannot_drain_private_victim() { - use lee_core::{ - Commitment, InputAccountIdentity, - account::{Account, AccountWithMetadata}, - }; - - use crate::{ - PrivacyPreservingTransaction, - privacy_preserving_transaction::{ - circuit::{ProgramWithDependencies, execute_and_prove}, - message::Message, - witness_set::WitnessSet, - }, - state::{ - CommitmentSet, - tests::{test_private_account_keys_1, test_private_account_keys_2}, - }, - }; - - type InjectorInstruction = ( - lee_core::program::ProgramId, // p2_id - lee_core::program::ProgramId, // auth_transfer_id - [u8; 32], // victim_id_raw - u128, // victim_balance - u128, // victim_nonce - lee_core::program::ProgramId, // victim_program_owner - [u8; 32], // recipient_id_raw - u128, // amount - ); - - // Attacker controls a private account. - let attacker_keys = test_private_account_keys_1(); - let attacker_id = - AccountId::for_regular_private_account(&attacker_keys.npk(), &attacker_keys.vpk(), 0); - - // Victim is a private account โ€” not registered in public chain state. - let victim_keys = test_private_account_keys_2(); - let victim_id = - AccountId::for_regular_private_account(&victim_keys.npk(), &victim_keys.vpk(), 0); - let victim_balance = 5_000_u128; - - let recipient_id = AccountId::new([42_u8; 32]); - - // Victim has no public state entry; only recipient is registered at genesis. - let mut state = V03State::new_with_genesis_accounts(&[(recipient_id, 0)], vec![], 0); - state.insert_program(Program::malicious_injector()); - state.insert_program(Program::malicious_launderer()); - - // Build attacker's private account and its local commitment tree. - let attacker_account = Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: 100, - ..Account::default() - }; - let attacker_commitment = Commitment::new(&attacker_id, &attacker_account); - let mut commitment_set = CommitmentSet::with_capacity(1); - commitment_set.extend(std::slice::from_ref(&attacker_commitment)); - let membership_proof = commitment_set - .get_proof_for(&attacker_commitment) - .expect("attacker commitment must be in the set"); - - let attacker_pre = AccountWithMetadata::new(attacker_account, true, attacker_id); - - // The attacker supplies the victim's account data directly โ€” it cannot be read from - // public state. The injected balance and program_owner allow authenticated_transfer - // to succeed inside the circuit, which has no access to chain state and cannot detect - // that these values are fabricated. - let instruction: InjectorInstruction = ( - Program::malicious_launderer().id(), - Program::authenticated_transfer_program().id(), - *victim_id.value(), - victim_balance, - 0_u128, // nonce - Program::authenticated_transfer_program().id(), // program_owner - *recipient_id.value(), - victim_balance, - ); - let instruction_data = Program::serialize_instruction(instruction).unwrap(); - - let p2 = Program::malicious_launderer(); - let at = Program::authenticated_transfer_program(); - let program_with_deps = ProgramWithDependencies::new( - Program::malicious_injector(), - [(p2.id(), p2), (at.id(), at)].into(), - ); - - // account_identities order must match self.pre_states as built by the circuit: - // [0] attacker โ€” first seen in P1's program_output.pre_states - // [1] victim โ€” first seen in authenticated_transfer's program_output.pre_states - // [2] recipient โ€” first seen in authenticated_transfer's program_output.pre_states - // - // Victim is marked Public: the attacker has no nsk for the victim's private account, - // so PrivateAuthorizedUpdate is not an option. - let account_identities = vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: attacker_keys.vpk(), - random_seed: [0; 32], - nsk: attacker_keys.nsk, - membership_proof, - identifier: 0, - }, - InputAccountIdentity::Public, // victim โ€” attacker lacks victim's nsk - InputAccountIdentity::Public, // recipient - ]; - - // execute_and_prove succeeds: authenticated_transfer runs against the injected - // victim(balance=5000, is_authorized=true) and produces valid inner receipts. - // The outer circuit commits victim(is_authorized=true) to public_pre_states. - let (circuit_output, proof) = execute_and_prove( - vec![attacker_pre], - instruction_data, - account_identities, - &program_with_deps, - ) - .expect("execute_and_prove should succeed \u{2014} the programs execute correctly"); - - // public_account_ids lists the Public entries from account_identities, in order. - // The single ciphertext belongs to attacker's private account update. - let message = Message::try_from_circuit_output( - vec![victim_id, recipient_id], - vec![], // no public signers, no nonces - circuit_output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0); - - assert!( - matches!(result, Err(LeeError::InvalidPrivacyPreservingProof)), - "attack on private victim should be rejected with InvalidPrivacyPreservingProof" - ); - // Victim has no public balance to check; confirming the recipient received nothing - // is sufficient to show no funds moved. - assert_eq!(state.get_account_by_id(recipient_id).balance, 0); - } - - /// Two malicious programs (injector + launderer) attempt to drain a victim's balance - /// without the victim signing anything. The test passes when the attack is rejected - /// and the victim's balance is left untouched. - /// - /// Attack flow: - /// Transaction (attacker signs) โ†’ P1 (`malicious_injector`) - /// โ†’ injects `victim(is_authorized=true)` into chained-call `pre_states` for P2 - /// P2 (`malicious_launderer`) - /// โ†’ outputs empty pre/post states, forwarding the forged flag to `authenticated_transfer` - /// โ†’ if `authorized_accounts` were built from the injected `pre_states`, - /// `{victim}.contains(victim)` would pass and the transfer would execute. - /// - /// The validator must reject this: `authorized_accounts` must be derived from the - /// parent program's own validated `program_output.pre_states`, not from the chained-call - /// input, so a forged `is_authorized=true` flag is never trusted. - #[test] - fn malicious_programs_cannot_drain_victim_without_signature() { - // p2_id, auth_transfer_id, victim_id_raw, victim_balance, victim_nonce, - // victim_program_owner, recipient_id_raw, amount. - // Primitives only โ€” AccountId/Account cannot round-trip through instruction_data - // via risc0_zkvm::serde (SerializeDisplay issue). - type InjectorInstruction = ( - lee_core::program::ProgramId, // p2_id - lee_core::program::ProgramId, // auth_transfer_id - [u8; 32], // victim_id_raw - u128, // victim_balance - u128, // victim_nonce - lee_core::program::ProgramId, // victim_program_owner - [u8; 32], // recipient_id_raw - u128, // amount - ); - - let attacker_key = PrivateKey::try_new([10; 32]).unwrap(); - let attacker_id = AccountId::from(&PublicKey::new_from_private_key(&attacker_key)); - - let victim_key = PrivateKey::try_new([20; 32]).unwrap(); - let victim_id = AccountId::from(&PublicKey::new_from_private_key(&victim_key)); - - let recipient_id = AccountId::new([42; 32]); - - let victim_balance = 5_000_u128; - let mut state = V03State::new_with_genesis_accounts( - &[ - (attacker_id, 100), - (victim_id, victim_balance), - (recipient_id, 0), - ], - vec![], - 0, - ); - - state.insert_program(Program::malicious_injector()); - state.insert_program(Program::malicious_launderer()); - - // Read victim state from chain, exactly as the attacker would. - let victim_account = state.get_account_by_id(victim_id); - - let instruction: InjectorInstruction = ( - Program::malicious_launderer().id(), - Program::authenticated_transfer_program().id(), - *victim_id.value(), - victim_account.balance, - victim_account.nonce.0, - victim_account.program_owner, - *recipient_id.value(), - victim_balance, - ); - - let message = Message::try_new( - Program::malicious_injector().id(), - vec![attacker_id], - vec![Nonce(0)], - instruction, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, &[&attacker_key]); - let tx = crate::PublicTransaction::new(message, witness_set); - - let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); - - assert!( - matches!( - result, - Err(LeeError::InvalidProgramBehavior( - InvalidProgramBehaviorError::InvalidAccountAuthorization { account_id } - )) if account_id == victim_id - ), - "attack transaction should be rejected with InvalidAccountAuthorization for the victim" - ); - - // Confirm the victim's balance is untouched. - let victim_balance_after = state.get_account_by_id(victim_id).balance; - let recipient_balance_after = state.get_account_by_id(recipient_id).balance; - - assert_eq!( - victim_balance_after, victim_balance, - "victim balance should be unchanged" - ); - assert_eq!( - recipient_balance_after, 0, - "recipient should receive nothing" - ); - } - - /// Regression test: a `PrivacyPreservingTransaction` carrying a structurally invalid - /// proof must be rejected with a clean `Err`. - #[test] - fn privacy_garbage_proof_is_rejected() { - use lee_core::{ - Commitment, - account::Account, - program::{BlockValidityWindow, TimestampValidityWindow}, - }; - - use crate::{ - PrivacyPreservingTransaction, - privacy_preserving_transaction::{ - circuit::Proof, message::Message, witness_set::WitnessSet, - }, - }; - - let state = V03State::new_with_genesis_accounts(&[], vec![], 0); - - // Minimal message that passes every check up to proof verification: a single - // commitment satisfies the non-empty requirement, no signers makes the - // nonce/signature checks vacuously true, and unbounded validity windows are valid - // for any block/timestamp. - let account_id = AccountId::from(&PublicKey::new_from_private_key( - &PrivateKey::try_new([1_u8; 32]).unwrap(), - )); - let commitment = Commitment::new(&account_id, &Account::default()); - let message = Message { - public_account_ids: vec![], - nonces: vec![], - public_post_states: vec![], - encrypted_private_post_states: vec![], - new_commitments: vec![commitment], - new_nullifiers: vec![], - block_validity_window: BlockValidityWindow::new_unbounded(), - timestamp_validity_window: TimestampValidityWindow::new_unbounded(), - }; - - // Garbage proof bytes: not a valid borsh-encoded `InnerReceipt`. - let garbage_proof = Proof::from_inner(vec![0xff_u8; 64]); - let witness_set = WitnessSet::for_message(&message, garbage_proof, &[]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0); - - match result { - Err(LeeError::InvalidPrivacyPreservingProof) => {} - Err(other) => panic!("expected InvalidPrivacyPreservingProof, got {other:?}"), - Ok(_) => panic!("garbage proof was accepted instead of rejected"), - } - } -} diff --git a/lee/state_machine/src/validated_state_diff/mod.rs b/lee/state_machine/src/validated_state_diff/mod.rs new file mode 100644 index 00000000..8fae4fee --- /dev/null +++ b/lee/state_machine/src/validated_state_diff/mod.rs @@ -0,0 +1,521 @@ +use std::{ + collections::{HashMap, HashSet, VecDeque}, + hash::Hash, +}; + +use lee_core::{ + BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp, + account::{Account, AccountId, AccountWithMetadata}, + program::{ + ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, compute_public_authorized_pdas, + validate_execution, + }, +}; +use log::debug; + +use crate::{ + V03State, ensure, + error::{InvalidProgramBehaviorError, LeeError}, + privacy_preserving_transaction::{ + PrivacyPreservingTransaction, circuit::Proof, message::Message, + }, + program::Program, + program_deployment_transaction::ProgramDeploymentTransaction, + public_transaction::PublicTransaction, + state::MAX_NUMBER_CHAINED_CALLS, +}; + +pub struct StateDiff { + pub signer_account_ids: Vec, + pub public_diff: HashMap, + pub new_commitments: Vec, + pub new_nullifiers: Vec, + pub program: Option, +} + +/// The validated output of executing or verifying a transaction, ready to be applied to the state. +/// +/// Can only be constructed by the transaction validation functions inside this crate, ensuring the +/// diff has been checked before any state mutation occurs. +pub struct ValidatedStateDiff(StateDiff); + +impl ValidatedStateDiff { + pub fn from_public_transaction( + tx: &PublicTransaction, + state: &V03State, + block_id: BlockId, + timestamp: Timestamp, + ) -> Result { + let signer_account_ids = authenticate_public_transaction_signers(tx, state)?; + let message = tx.message(); + + ensure!( + !message.account_ids.is_empty(), + LeeError::InvalidInput("Public transaction must have at least one account".into()) + ); + + // All account_ids must be different + ensure!( + message.account_ids.iter().collect::>().len() == message.account_ids.len(), + LeeError::InvalidInput("Duplicate account_ids found in message".into(),) + ); + + // Build pre_states for execution + let input_pre_states: Vec<_> = message + .account_ids + .iter() + .map(|account_id| { + AccountWithMetadata::new( + state.get_account_by_id(*account_id), + signer_account_ids.contains(account_id), + *account_id, + ) + }) + .collect(); + + let mut state_diff: HashMap = HashMap::new(); + + let initial_call = ChainedCall { + program_id: message.program_id, + instruction_data: message.instruction_data.clone(), + pre_states: input_pre_states, + pda_seeds: vec![], + }; + + let initial_caller_data = CallerData { + program_id: None, + authorized_accounts: signer_account_ids.iter().copied().collect(), + }; + + let mut chained_calls = + VecDeque::<(ChainedCall, CallerData)>::from_iter([(initial_call, initial_caller_data)]); + let mut chain_calls_counter = 0; + + while let Some((chained_call, caller_data)) = chained_calls.pop_front() { + ensure!( + chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS, + LeeError::MaxChainedCallsDepthExceeded + ); + + // Check that the `program_id` corresponds to a deployed program + let Some(program) = state.programs().get(&chained_call.program_id) else { + return Err(LeeError::InvalidInput("Unknown program".into())); + }; + + debug!( + "Program {:?} pre_states: {:?}, instruction_data: {:?}", + chained_call.program_id, chained_call.pre_states, chained_call.instruction_data + ); + let mut program_output = program.execute( + caller_data.program_id, + &chained_call.pre_states, + &chained_call.instruction_data, + )?; + debug!( + "Program {:?} output: {:?}", + chained_call.program_id, program_output + ); + + let authorized_pdas = + compute_public_authorized_pdas(caller_data.program_id, &chained_call.pda_seeds); + + // Account is authorized if it is either in the caller's authorized accounts or in the + // list of PDAs the caller has authorized. + let is_authorized = |account_id: &AccountId| { + authorized_pdas.contains(account_id) + || caller_data.authorized_accounts.contains(account_id) + }; + + for pre in &program_output.pre_states { + let account_id = pre.account_id; + // Check that the program output pre_states coincide with the values in the public + // state or with any modifications to those values during the chain of calls. + let expected_pre = state_diff + .get(&account_id) + .cloned() + .unwrap_or_else(|| state.get_account_by_id(account_id)); + ensure!( + pre.account == expected_pre, + InvalidProgramBehaviorError::InconsistentAccountPreState { + account_id, + expected: Box::new(expected_pre), + actual: Box::new(pre.account.clone()) + } + ); + + // Check that the program output pre_states marked as authorized are indeed + // authorized, and vice-versa. + let is_indeed_authorized = is_authorized(&account_id); + ensure!( + !pre.is_authorized || is_indeed_authorized, + InvalidProgramBehaviorError::InvalidAccountAuthorization { account_id } + ); + ensure!( + pre.is_authorized || !is_indeed_authorized, + InvalidProgramBehaviorError::AuthorizedAccountMarkedAsNotAuthorized { + account_id + } + ); + } + + // Verify that the program output's self_program_id matches the expected program ID. + ensure!( + program_output.self_program_id == chained_call.program_id, + InvalidProgramBehaviorError::MismatchedProgramId { + expected: chained_call.program_id, + actual: program_output.self_program_id + } + ); + + // Verify that the program output's caller_program_id matches the actual caller. + ensure!( + program_output.caller_program_id == caller_data.program_id, + InvalidProgramBehaviorError::MismatchedCallerProgramId { + expected: caller_data.program_id, + actual: program_output.caller_program_id, + } + ); + + // Verify execution corresponds to a well-behaved program. + // See the # Programs section for the definition of the `validate_execution` method. + validate_execution( + &program_output.pre_states, + &program_output.post_states, + chained_call.program_id, + ) + .map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?; + + // Verify validity window + ensure!( + program_output.block_validity_window.is_valid_for(block_id) + && program_output + .timestamp_validity_window + .is_valid_for(timestamp), + LeeError::OutOfValidityWindow + ); + + for (i, post) in program_output.post_states.iter_mut().enumerate() { + let Some(claim) = post.required_claim() else { + continue; + }; + let pre = &program_output.pre_states[i]; + let account_id = pre.account_id; + + // The invoked program can only claim accounts with default program id. + ensure!( + post.account().program_owner == DEFAULT_PROGRAM_ID, + InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id } + ); + + match claim { + Claim::Authorized => { + // The program can only claim accounts that were authorized by the signer. + ensure!( + pre.is_authorized, + InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id } + ); + } + Claim::Pda(seed) => { + // The program can only claim accounts that correspond to the PDAs it is + // authorized to claim. The public-execution path only sees public + // accounts, so the public-PDA derivation is the correct formula here. + let pda = AccountId::for_public_pda(&chained_call.program_id, &seed); + ensure!( + account_id == pda, + InvalidProgramBehaviorError::MismatchedPdaClaim { + expected: pda, + actual: account_id + } + ); + } + } + + post.account_mut().program_owner = chained_call.program_id; + } + + // Update the state diff + for (pre, post) in program_output + .pre_states + .iter() + .zip(program_output.post_states.iter()) + { + state_diff.insert(pre.account_id, post.account().clone()); + } + + // Source from `program_output.pre_states`, not `chained_call.pre_states`: + // the loop above already gates program_output's `is_authorized` via the + // `!pre.is_authorized || is_indeed_authorized` check, while `chained_call. + // pre_states` is caller-controlled and can be forged (audit-issue 91). + // + // Union with the caller's authorized set so that authorization is monotonically + // growing: once an account is authorized at any point in the chain it remains + // authorized for all subsequent calls. + let authorized_accounts: HashSet<_> = caller_data + .authorized_accounts + .into_iter() + .chain( + program_output + .pre_states + .iter() + .filter(|pre| pre.is_authorized) + .map(|pre| pre.account_id), + ) + .collect(); + for new_call in program_output.chained_calls.into_iter().rev() { + chained_calls.push_front(( + new_call, + CallerData { + program_id: Some(chained_call.program_id), + authorized_accounts: authorized_accounts.clone(), + }, + )); + } + + chain_calls_counter = chain_calls_counter + .checked_add(1) + .expect("we check the max depth at the beginning of the loop"); + } + + // Check that all modified uninitialized accounts where claimed + for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| { + let pre = state.get_account_by_id(*account_id); + if pre.program_owner != DEFAULT_PROGRAM_ID { + return None; + } + if pre == *post { + return None; + } + Some((*account_id, post)) + }) { + ensure!( + post.program_owner != DEFAULT_PROGRAM_ID, + InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id } + ); + } + + Ok(Self(StateDiff { + signer_account_ids, + public_diff: state_diff, + new_commitments: vec![], + new_nullifiers: vec![], + program: None, + })) + } + + pub fn from_privacy_preserving_transaction( + tx: &PrivacyPreservingTransaction, + state: &V03State, + block_id: BlockId, + timestamp: Timestamp, + ) -> Result { + let message = &tx.message; + let witness_set = &tx.witness_set; + + // 1. Commitments or nullifiers are non empty + ensure!( + !message.new_commitments.is_empty() || !message.new_nullifiers.is_empty(), + LeeError::InvalidInput( + "Empty commitments and empty nullifiers found in message".into(), + ) + ); + + // 2. Check there are no duplicate account_ids in the public_account_ids list. + ensure!( + n_unique(&message.public_account_ids) == message.public_account_ids.len(), + LeeError::InvalidInput("Duplicate account_ids found in message".into()) + ); + + // Check there are no duplicate nullifiers in the new_nullifiers list + ensure!( + n_unique( + &message + .new_nullifiers + .iter() + .map(|(n, _)| n) + .collect::>() + ) == message.new_nullifiers.len(), + LeeError::InvalidInput("Duplicate nullifiers found in message".into()) + ); + + // Check there are no duplicate commitments in the new_commitments list + ensure!( + n_unique(&message.new_commitments) == message.new_commitments.len(), + LeeError::InvalidInput("Duplicate commitments found in message".into()) + ); + + // 3. Nonce checks and Valid signatures + // Check exactly one nonce is provided for each signature + ensure!( + message.nonces.len() == witness_set.signatures_and_public_keys.len(), + LeeError::InvalidInput( + "Mismatch between number of nonces and signatures/public keys".into(), + ) + ); + + // Check the signatures are valid + ensure!( + witness_set.signatures_are_valid_for(message), + LeeError::InvalidInput("Invalid signature for given message and public key".into()) + ); + + let signer_account_ids = tx.signer_account_ids(); + // Check nonces corresponds to the current nonces on the public state. + for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) { + let current_nonce = state.get_account_by_id(*account_id).nonce; + ensure!( + current_nonce == *nonce, + LeeError::InvalidInput("Nonce mismatch".into()) + ); + } + + // Verify validity window + ensure!( + message.block_validity_window.is_valid_for(block_id) + && message.timestamp_validity_window.is_valid_for(timestamp), + LeeError::OutOfValidityWindow + ); + + // Build pre_states for proof verification + let public_pre_states: Vec<_> = message + .public_account_ids + .iter() + .map(|account_id| { + AccountWithMetadata::new( + state.get_account_by_id(*account_id), + signer_account_ids.contains(account_id), + *account_id, + ) + }) + .collect(); + + // 4. Proof verification + check_privacy_preserving_circuit_proof_is_valid( + &witness_set.proof, + &public_pre_states, + message, + )?; + + // 5. Commitment freshness + state.check_commitments_are_new(&message.new_commitments)?; + + // 6. Nullifier uniqueness + state.check_nullifiers_are_valid(&message.new_nullifiers)?; + + let public_diff = message + .public_account_ids + .iter() + .copied() + .zip(message.public_post_states.clone()) + .collect(); + let new_nullifiers = message + .new_nullifiers + .iter() + .copied() + .map(|(nullifier, _)| nullifier) + .collect(); + + Ok(Self(StateDiff { + signer_account_ids, + public_diff, + new_commitments: message.new_commitments.clone(), + new_nullifiers, + program: None, + })) + } + + pub fn from_program_deployment_transaction( + tx: &ProgramDeploymentTransaction, + state: &V03State, + ) -> Result { + // TODO: remove clone + let program = Program::new(tx.message.bytecode.clone().into())?; + if state.programs().contains_key(&program.id()) { + return Err(LeeError::ProgramAlreadyExists); + } + Ok(Self(StateDiff { + signer_account_ids: vec![], + public_diff: HashMap::new(), + new_commitments: vec![], + new_nullifiers: vec![], + program: Some(program), + })) + } + + /// Returns the public account changes produced by this transaction. + /// + /// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example + /// to enforce that system accounts are not modified by user transactions. + #[must_use] + pub fn public_diff(&self) -> HashMap { + self.0.public_diff.clone() + } + + pub(crate) fn into_state_diff(self) -> StateDiff { + self.0 + } +} + +#[derive(Debug)] +struct CallerData { + program_id: Option, + authorized_accounts: HashSet, +} + +fn authenticate_public_transaction_signers( + tx: &PublicTransaction, + state: &V03State, +) -> Result, LeeError> { + let message = tx.message(); + let witness_set = tx.witness_set(); + + ensure!( + message.nonces.len() == witness_set.signatures_and_public_keys.len(), + LeeError::InvalidInput( + "Mismatch between number of nonces and signatures/public keys".into(), + ) + ); + + ensure!( + witness_set.is_valid_for(message), + LeeError::InvalidInput("Invalid signature for given message and public key".into()) + ); + + let signer_account_ids = tx.signer_account_ids(); + for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) { + let current_nonce = state.get_account_by_id(*account_id).nonce; + ensure!( + current_nonce == *nonce, + LeeError::InvalidInput("Nonce mismatch".into()) + ); + } + + Ok(signer_account_ids) +} + +fn check_privacy_preserving_circuit_proof_is_valid( + proof: &Proof, + public_pre_states: &[AccountWithMetadata], + message: &Message, +) -> Result<(), LeeError> { + let output = PrivacyPreservingCircuitOutput { + public_pre_states: public_pre_states.to_vec(), + public_post_states: message.public_post_states.clone(), + encrypted_private_post_states: message.encrypted_private_post_states.clone(), + new_commitments: message.new_commitments.clone(), + new_nullifiers: message.new_nullifiers.clone(), + block_validity_window: message.block_validity_window, + timestamp_validity_window: message.timestamp_validity_window, + }; + proof + .is_valid_for(&output) + .then_some(()) + .ok_or(LeeError::InvalidPrivacyPreservingProof) +} + +fn n_unique(data: &[T]) -> usize { + let set: HashSet<&T> = data.iter().collect(); + set.len() +} + +#[cfg(test)] +mod tests; diff --git a/lee/state_machine/src/validated_state_diff/tests.rs b/lee/state_machine/src/validated_state_diff/tests.rs new file mode 100644 index 00000000..c471676c --- /dev/null +++ b/lee/state_machine/src/validated_state_diff/tests.rs @@ -0,0 +1,526 @@ +use std::collections::HashMap; + +use lee_core::account::{Account, AccountId, Nonce}; + +use crate::{ + PrivateKey, PublicKey, V03State, + error::{InvalidProgramBehaviorError, LeeError}, + program::Program, + public_transaction::{Message, WitnessSet}, + validated_state_diff::ValidatedStateDiff, +}; + +fn public_state_from_balances(initial_data: &[(AccountId, u128)]) -> HashMap { + initial_data + .iter() + .copied() + .map(|(account_id, balance)| { + ( + account_id, + Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance, + ..Account::default() + }, + ) + }) + .collect() +} + +#[test] +fn public_diff_reflects_a_successful_transfer() { + // A successful native transfer must record the debited sender in + // `public_diff()`. Catches the mutation that replaces `public_diff` with + // `HashMap::new()` (which would hide every account change). + let from_key = PrivateKey::try_new([1_u8; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); + let to_key = PrivateKey::try_new([2_u8; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); + + let state = V03State::new() + .with_public_accounts(public_state_from_balances(&[(from, 100)])) + .with_programs(std::iter::once( + crate::test_methods::simple_balance_transfer(), + )); + let program_id = crate::test_methods::simple_balance_transfer().id(); + let message = + Message::try_new(program_id, vec![from, to], vec![Nonce(0), Nonce(0)], 5_u128).unwrap(); + let witness_set = WitnessSet::for_message(&message, &[&from_key, &to_key]); + let tx = crate::PublicTransaction::new(message, witness_set); + + let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0) + .expect("a valid native transfer must validate"); + let public_diff = diff.public_diff(); + + assert!( + public_diff.contains_key(&from), + "public_diff must contain the debited sender", + ); + assert_eq!( + public_diff[&from].balance, 95, + "sender balance in the diff must reflect the debit", + ); +} + +/// Privacy-path version of the authorization-injection attack. The test passes when the +/// attack is rejected and the victim's balance is left untouched. +/// +/// `execute_and_prove` succeeds because each inner receipt is individually valid and the +/// outer circuit faithfully commits whatever the attacker's program output says, including +/// `victim(is_authorized=true)`. The circuit has no access to chain state and cannot know +/// the victim never signed. +/// +/// The host-side validator is what catches the attack: it independently reconstructs +/// `public_pre_states` from chain state using `signer_account_ids.contains(victim_id) = false`, +/// so it expects `victim(is_authorized=false)`. The committed journal and the reconstructed +/// expected output diverge, `receipt.verify` fails, and `from_privacy_preserving_transaction` +/// returns an error before any state is applied. +#[test] +fn privacy_malicious_programs_cannot_drain_public_victim() { + use lee_core::{ + Commitment, InputAccountIdentity, + account::{Account, AccountWithMetadata}, + }; + + use crate::{ + PrivacyPreservingTransaction, + privacy_preserving_transaction::{ + circuit::{ProgramWithDependencies, execute_and_prove}, + message::Message, + witness_set::WitnessSet, + }, + state::{CommitmentSet, tests::test_private_account_keys_1}, + }; + + type InjectorInstruction = ( + lee_core::program::ProgramId, // p2_id + lee_core::program::ProgramId, // simple_balance_transfer_id + [u8; 32], // victim_id_raw + u128, // victim_balance + u128, // victim_nonce + lee_core::program::ProgramId, // victim_program_owner + [u8; 32], // recipient_id_raw + u128, // amount + ); + + // Attacker controls a private account. + let attacker_keys = test_private_account_keys_1(); + let attacker_id = + AccountId::for_regular_private_account(&attacker_keys.npk(), &attacker_keys.vpk(), 0); + + let victim_id = AccountId::new([20_u8; 32]); + let recipient_id = AccountId::new([42_u8; 32]); + let victim_balance = 5_000_u128; + + // genesis sets program_owner = simple_balance_transfer_program.id() on all accounts. + let state = V03State::new() + .with_public_accounts(public_state_from_balances(&[ + (victim_id, victim_balance), + (recipient_id, 0), + ])) + .with_programs([ + crate::test_methods::simple_balance_transfer(), + crate::test_methods::malicious_injector(), + crate::test_methods::malicious_launderer(), + ]); + + // Build attacker's private account and its local commitment tree. + let attacker_account = Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + ..Account::default() + }; + let attacker_commitment = Commitment::new(&attacker_id, &attacker_account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&attacker_commitment)); + let membership_proof = commitment_set + .get_proof_for(&attacker_commitment) + .expect("attacker commitment must be in the set"); + + let attacker_pre = AccountWithMetadata::new(attacker_account, true, attacker_id); + + let victim_account = state.get_account_by_id(victim_id); + let instruction: InjectorInstruction = ( + crate::test_methods::malicious_launderer().id(), + crate::test_methods::simple_balance_transfer().id(), + *victim_id.value(), + victim_account.balance, + victim_account.nonce.0, + victim_account.program_owner, + *recipient_id.value(), + victim_balance, + ); + let instruction_data = Program::serialize_instruction(instruction).unwrap(); + + let p2 = crate::test_methods::malicious_launderer(); + let at = crate::test_methods::simple_balance_transfer(); + let program_with_deps = ProgramWithDependencies::new( + crate::test_methods::malicious_injector(), + [(p2.id(), p2), (at.id(), at)].into(), + ); + + // account_identities order must match self.pre_states as built by the circuit: + // [0] attacker โ€” first seen in P1's program_output.pre_states + // [1] victim โ€” first seen in simple_balance_transfer's program_output.pre_states + // [2] recipient โ€” first seen in simple_balance_transfer's program_output.pre_states + let account_identities = vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: attacker_keys.vpk(), + random_seed: [0; 32], + nsk: attacker_keys.nsk, + membership_proof, + identifier: 0, + }, + InputAccountIdentity::Public, // victim + InputAccountIdentity::Public, // recipient + ]; + + // execute_and_prove succeeds: all inner receipts are valid. + // The outer circuit commits victim(is_authorized=true) to its journal. + let (circuit_output, proof) = execute_and_prove( + vec![attacker_pre], + instruction_data, + account_identities, + &program_with_deps, + ) + .expect("execute_and_prove should succeed \u{2014} the programs execute correctly"); + + // public_account_ids lists the Public entries from account_identities, in order. + // The single ciphertext belongs to attacker's private account update. + let message = Message::try_from_circuit_output( + vec![victim_id, recipient_id], + vec![], // no public signers, no nonces + circuit_output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0); + + assert!( + matches!(result, Err(LeeError::InvalidPrivacyPreservingProof)), + "attack privacy transaction should be rejected with InvalidPrivacyPreservingProof" + ); + assert_eq!(state.get_account_by_id(victim_id).balance, victim_balance); + assert_eq!(state.get_account_by_id(recipient_id).balance, 0); +} + +/// Private-victim variant of the authorization-injection attack. The test passes when the +/// attack is rejected and the recipient's balance remains zero. +/// +/// After the circuit's Vacant branch accepts the injected `victim(is_authorized=true)` +/// verbatim, the attacker must choose how to declare the victim in `account_identities`. +/// There are two routes, both closed: +/// +/// - **mask=1 (`PrivateAuthorizedUpdate`)**: the circuit derives `account_id = +/// AccountId::for_regular_private_account(&npk_from(nsk), identifier)` and asserts it matches +/// `pre_state.account_id`. Passing this check requires the victim's `nsk`, which the attacker +/// does not have. `execute_and_prove` panics inside the ZKVM and no proof is produced. +/// +/// - **mask=0 (`Public`)**: the circuit places the account in `public_pre_states` and +/// `execute_and_prove` succeeds. The host-side validator then reconstructs `public_pre_states` +/// from chain state; `state.get_account_by_id(victim_id)` returns the default account (balance=0) +/// because the victim has no public state entry. The committed journal and the reconstructed +/// expected output diverge, `receipt.verify` fails, and `from_privacy_preserving_transaction` +/// returns an error before any state is applied. This test exercises this route. +#[test] +fn privacy_malicious_programs_cannot_drain_private_victim() { + use lee_core::{ + Commitment, InputAccountIdentity, + account::{Account, AccountWithMetadata}, + }; + + use crate::{ + PrivacyPreservingTransaction, + privacy_preserving_transaction::{ + circuit::{ProgramWithDependencies, execute_and_prove}, + message::Message, + witness_set::WitnessSet, + }, + state::{ + CommitmentSet, + tests::{test_private_account_keys_1, test_private_account_keys_2}, + }, + }; + + type InjectorInstruction = ( + lee_core::program::ProgramId, // p2_id + lee_core::program::ProgramId, // simple_balance_transfer_id + [u8; 32], // victim_id_raw + u128, // victim_balance + u128, // victim_nonce + lee_core::program::ProgramId, // victim_program_owner + [u8; 32], // recipient_id_raw + u128, // amount + ); + + // Attacker controls a private account. + let attacker_keys = test_private_account_keys_1(); + let attacker_id = + AccountId::for_regular_private_account(&attacker_keys.npk(), &attacker_keys.vpk(), 0); + + // Victim is a private account โ€” not registered in public chain state. + let victim_keys = test_private_account_keys_2(); + let victim_id = + AccountId::for_regular_private_account(&victim_keys.npk(), &victim_keys.vpk(), 0); + let victim_balance = 5_000_u128; + + let recipient_id = AccountId::new([42_u8; 32]); + + // Victim has no public state entry; only recipient is registered at genesis. + let state = V03State::new() + .with_public_accounts(public_state_from_balances(&[(recipient_id, 0)])) + .with_programs([ + crate::test_methods::simple_balance_transfer(), + crate::test_methods::malicious_injector(), + crate::test_methods::malicious_launderer(), + ]); + + // Build attacker's private account and its local commitment tree. + let attacker_account = Account { + program_owner: crate::test_methods::simple_balance_transfer().id(), + balance: 100, + ..Account::default() + }; + let attacker_commitment = Commitment::new(&attacker_id, &attacker_account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&attacker_commitment)); + let membership_proof = commitment_set + .get_proof_for(&attacker_commitment) + .expect("attacker commitment must be in the set"); + + let attacker_pre = AccountWithMetadata::new(attacker_account, true, attacker_id); + + // The attacker supplies the victim's account data directly โ€” it cannot be read from + // public state. The injected balance and program_owner allow simple_balance_transfer + // to succeed inside the circuit, which has no access to chain state and cannot detect + // that these values are fabricated. + let instruction: InjectorInstruction = ( + crate::test_methods::malicious_launderer().id(), + crate::test_methods::simple_balance_transfer().id(), + *victim_id.value(), + victim_balance, + 0_u128, // nonce + crate::test_methods::simple_balance_transfer().id(), // program_owner + *recipient_id.value(), + victim_balance, + ); + let instruction_data = Program::serialize_instruction(instruction).unwrap(); + + let p2 = crate::test_methods::malicious_launderer(); + let at = crate::test_methods::simple_balance_transfer(); + let program_with_deps = ProgramWithDependencies::new( + crate::test_methods::malicious_injector(), + [(p2.id(), p2), (at.id(), at)].into(), + ); + + // account_identities order must match self.pre_states as built by the circuit: + // [0] attacker โ€” first seen in P1's program_output.pre_states + // [1] victim โ€” first seen in simple_balance_transfer's program_output.pre_states + // [2] recipient โ€” first seen in simple_balance_transfer's program_output.pre_states + // + // Victim is marked Public: the attacker has no nsk for the victim's private account, + // so PrivateAuthorizedUpdate is not an option. + let account_identities = vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: attacker_keys.vpk(), + random_seed: [0; 32], + nsk: attacker_keys.nsk, + membership_proof, + identifier: 0, + }, + InputAccountIdentity::Public, // victim โ€” attacker lacks victim's nsk + InputAccountIdentity::Public, // recipient + ]; + + // execute_and_prove succeeds: simple_balance_transfer runs against the injected + // victim(balance=5000, is_authorized=true) and produces valid inner receipts. + // The outer circuit commits victim(is_authorized=true) to public_pre_states. + let (circuit_output, proof) = execute_and_prove( + vec![attacker_pre], + instruction_data, + account_identities, + &program_with_deps, + ) + .expect("execute_and_prove should succeed \u{2014} the programs execute correctly"); + + // public_account_ids lists the Public entries from account_identities, in order. + // The single ciphertext belongs to attacker's private account update. + let message = Message::try_from_circuit_output( + vec![victim_id, recipient_id], + vec![], // no public signers, no nonces + circuit_output, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0); + + assert!( + matches!(result, Err(LeeError::InvalidPrivacyPreservingProof)), + "attack on private victim should be rejected with InvalidPrivacyPreservingProof" + ); + // Victim has no public balance to check; confirming the recipient received nothing + // is sufficient to show no funds moved. + assert_eq!(state.get_account_by_id(recipient_id).balance, 0); +} + +/// Two malicious programs (injector + launderer) attempt to drain a victim's balance +/// without the victim signing anything. The test passes when the attack is rejected +/// and the victim's balance is left untouched. +/// +/// Attack flow: +/// Transaction (attacker signs) โ†’ P1 (`malicious_injector`) +/// โ†’ injects `victim(is_authorized=true)` into chained-call `pre_states` for P2 +/// P2 (`malicious_launderer`) +/// โ†’ outputs empty pre/post states, forwarding the forged flag to `simple_balance_transfer` +/// โ†’ if `authorized_accounts` were built from the injected `pre_states`, +/// `{victim}.contains(victim)` would pass and the transfer would execute. +/// +/// The validator must reject this: `authorized_accounts` must be derived from the +/// parent program's own validated `program_output.pre_states`, not from the chained-call +/// input, so a forged `is_authorized=true` flag is never trusted. +#[test] +fn malicious_programs_cannot_drain_victim_without_signature() { + // p2_id, simple_balance_transfer_id, victim_id_raw, victim_balance, victim_nonce, + // victim_program_owner, recipient_id_raw, amount. + // Primitives only โ€” AccountId/Account cannot round-trip through instruction_data + // via risc0_zkvm::serde (SerializeDisplay issue). + type InjectorInstruction = ( + lee_core::program::ProgramId, // p2_id + lee_core::program::ProgramId, // simple_balance_transfer_id + [u8; 32], // victim_id_raw + u128, // victim_balance + u128, // victim_nonce + lee_core::program::ProgramId, // victim_program_owner + [u8; 32], // recipient_id_raw + u128, // amount + ); + + let attacker_key = PrivateKey::try_new([10; 32]).unwrap(); + let attacker_id = AccountId::from(&PublicKey::new_from_private_key(&attacker_key)); + + let victim_key = PrivateKey::try_new([20; 32]).unwrap(); + let victim_id = AccountId::from(&PublicKey::new_from_private_key(&victim_key)); + + let recipient_id = AccountId::new([42; 32]); + + let victim_balance = 5_000_u128; + let state = V03State::new() + .with_public_accounts(public_state_from_balances(&[ + (attacker_id, 100), + (victim_id, victim_balance), + (recipient_id, 0), + ])) + .with_programs([ + crate::test_methods::simple_balance_transfer(), + crate::test_methods::malicious_injector(), + crate::test_methods::malicious_launderer(), + ]); + + // Read victim state from chain, exactly as the attacker would. + let victim_account = state.get_account_by_id(victim_id); + + let instruction: InjectorInstruction = ( + crate::test_methods::malicious_launderer().id(), + crate::test_methods::simple_balance_transfer().id(), + *victim_id.value(), + victim_account.balance, + victim_account.nonce.0, + victim_account.program_owner, + *recipient_id.value(), + victim_balance, + ); + + let message = Message::try_new( + crate::test_methods::malicious_injector().id(), + vec![attacker_id], + vec![Nonce(0)], + instruction, + ) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, &[&attacker_key]); + let tx = crate::PublicTransaction::new(message, witness_set); + + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); + + assert!( + matches!( + result, + Err(LeeError::InvalidProgramBehavior( + InvalidProgramBehaviorError::InvalidAccountAuthorization { account_id } + )) if account_id == victim_id + ), + "attack transaction should be rejected with InvalidAccountAuthorization for the victim" + ); + + // Confirm the victim's balance is untouched. + let victim_balance_after = state.get_account_by_id(victim_id).balance; + let recipient_balance_after = state.get_account_by_id(recipient_id).balance; + + assert_eq!( + victim_balance_after, victim_balance, + "victim balance should be unchanged" + ); + assert_eq!( + recipient_balance_after, 0, + "recipient should receive nothing" + ); +} + +/// Regression test: a `PrivacyPreservingTransaction` carrying a structurally invalid +/// proof must be rejected with a clean `Err`. +#[test] +fn privacy_garbage_proof_is_rejected() { + use lee_core::{ + Commitment, + account::Account, + program::{BlockValidityWindow, TimestampValidityWindow}, + }; + + use crate::{ + PrivacyPreservingTransaction, + privacy_preserving_transaction::{ + circuit::Proof, message::Message, witness_set::WitnessSet, + }, + }; + + let state = V03State::new(); + + // Minimal message that passes every check up to proof verification: a single + // commitment satisfies the non-empty requirement, no signers makes the + // nonce/signature checks vacuously true, and unbounded validity windows are valid + // for any block/timestamp. + let account_id = AccountId::from(&PublicKey::new_from_private_key( + &PrivateKey::try_new([1_u8; 32]).unwrap(), + )); + let commitment = Commitment::new(&account_id, &Account::default()); + let message = Message { + public_account_ids: vec![], + nonces: vec![], + public_post_states: vec![], + encrypted_private_post_states: vec![], + new_commitments: vec![commitment], + new_nullifiers: vec![], + block_validity_window: BlockValidityWindow::new_unbounded(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), + }; + + // Garbage proof bytes: not a valid borsh-encoded `InnerReceipt`. + let garbage_proof = Proof::from_inner(vec![0xff_u8; 64]); + let witness_set = WitnessSet::for_message(&message, garbage_proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0); + + match result { + Err(LeeError::InvalidPrivacyPreservingProof) => {} + Err(other) => panic!("expected InvalidPrivacyPreservingProof, got {other:?}"), + Ok(_) => panic!("garbage proof was accepted instead of rejected"), + } +} diff --git a/program_methods/Cargo.toml b/lee/state_machine/test_methods/Cargo.toml similarity index 88% rename from program_methods/Cargo.toml rename to lee/state_machine/test_methods/Cargo.toml index 573fd4e6..a2709e1f 100644 --- a/program_methods/Cargo.toml +++ b/lee/state_machine/test_methods/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "program_methods" +name = "test_methods" version = "0.1.0" edition = "2024" license = { workspace = true } diff --git a/program_methods/build.rs b/lee/state_machine/test_methods/build.rs similarity index 100% rename from program_methods/build.rs rename to lee/state_machine/test_methods/build.rs diff --git a/lee/state_machine/test_methods/guest/Cargo.toml b/lee/state_machine/test_methods/guest/Cargo.toml new file mode 100644 index 00000000..75d34081 --- /dev/null +++ b/lee/state_machine/test_methods/guest/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "test_methods_guests" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[lints] +workspace = true + +[dependencies] +lee_core.workspace = true + +risc0-zkvm.workspace = true +serde.workspace = true diff --git a/test_program_methods/guest/src/bin/auth_asserting_noop.rs b/lee/state_machine/test_methods/guest/src/bin/auth_asserting_noop.rs similarity index 100% rename from test_program_methods/guest/src/bin/auth_asserting_noop.rs rename to lee/state_machine/test_methods/guest/src/bin/auth_asserting_noop.rs diff --git a/test_program_methods/guest/src/bin/burner.rs b/lee/state_machine/test_methods/guest/src/bin/burner.rs similarity index 100% rename from test_program_methods/guest/src/bin/burner.rs rename to lee/state_machine/test_methods/guest/src/bin/burner.rs diff --git a/lee/state_machine/test_methods/guest/src/bin/chain_caller.rs b/lee/state_machine/test_methods/guest/src/bin/chain_caller.rs new file mode 100644 index 00000000..b812fc9e --- /dev/null +++ b/lee/state_machine/test_methods/guest/src/bin/chain_caller.rs @@ -0,0 +1,70 @@ +use lee_core::program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, read_lee_inputs, +}; +use risc0_zkvm::serde::to_vec; + +type Instruction = (u128, ProgramId, u32, Option); + +/// A program that calls another program `num_chain_calls` times. +/// It permutes the order of the input accounts on the subsequent call +/// The `ProgramId` in the instruction must be the `program_id` of the transfers +/// program. +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (balance, simple_transfer_id, num_chain_calls, pda_seed), + }, + instruction_words, + ) = read_lee_inputs::(); + + let Ok([recipient_pre, sender_pre]) = <[_; 2]>::try_from(pre_states) else { + return; + }; + + let instruction_data = to_vec(&balance).unwrap(); + + let mut running_recipient_pre = recipient_pre.clone(); + let mut running_sender_pre = sender_pre.clone(); + + if pda_seed.is_some() { + running_sender_pre.is_authorized = true; + } + + let mut chained_calls = Vec::new(); + for _i in 0..num_chain_calls { + let new_chained_call = ChainedCall { + program_id: simple_transfer_id, + instruction_data: instruction_data.clone(), + pre_states: vec![running_sender_pre.clone(), running_recipient_pre.clone()], /* <- Account order permutation here */ + pda_seeds: pda_seed.iter().copied().collect(), + }; + chained_calls.push(new_chained_call); + + running_sender_pre.account.balance = + match running_sender_pre.account.balance.checked_sub(balance) { + Some(new_balance) => new_balance, + None => return, + }; + running_recipient_pre.account.balance = + match running_recipient_pre.account.balance.checked_add(balance) { + Some(new_balance) => new_balance, + None => return, + }; + } + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![sender_pre.clone(), recipient_pre.clone()], + vec![ + AccountPostState::new(sender_pre.account), + AccountPostState::new(recipient_pre.account), + ], + ) + .with_chained_calls(chained_calls) + .write(); +} diff --git a/test_program_methods/guest/src/bin/changer_claimer.rs b/lee/state_machine/test_methods/guest/src/bin/changer_claimer.rs similarity index 100% rename from test_program_methods/guest/src/bin/changer_claimer.rs rename to lee/state_machine/test_methods/guest/src/bin/changer_claimer.rs diff --git a/test_program_methods/guest/src/bin/claimer.rs b/lee/state_machine/test_methods/guest/src/bin/claimer.rs similarity index 100% rename from test_program_methods/guest/src/bin/claimer.rs rename to lee/state_machine/test_methods/guest/src/bin/claimer.rs diff --git a/test_program_methods/guest/src/bin/data_changer.rs b/lee/state_machine/test_methods/guest/src/bin/data_changer.rs similarity index 100% rename from test_program_methods/guest/src/bin/data_changer.rs rename to lee/state_machine/test_methods/guest/src/bin/data_changer.rs diff --git a/test_program_methods/guest/src/bin/extra_output.rs b/lee/state_machine/test_methods/guest/src/bin/extra_output.rs similarity index 100% rename from test_program_methods/guest/src/bin/extra_output.rs rename to lee/state_machine/test_methods/guest/src/bin/extra_output.rs diff --git a/test_program_methods/guest/src/bin/flash_swap_callback.rs b/lee/state_machine/test_methods/guest/src/bin/flash_swap_callback.rs similarity index 95% rename from test_program_methods/guest/src/bin/flash_swap_callback.rs rename to lee/state_machine/test_methods/guest/src/bin/flash_swap_callback.rs index 5e1a30aa..28f6509f 100644 --- a/test_program_methods/guest/src/bin/flash_swap_callback.rs +++ b/lee/state_machine/test_methods/guest/src/bin/flash_swap_callback.rs @@ -62,10 +62,7 @@ fn main() { // Mark the receiver as authorized since it will be PDA-authorized in this chained call. let mut receiver_authorized = receiver_pre.clone(); receiver_authorized.is_authorized = true; - let transfer_instruction = - risc0_zkvm::serde::to_vec(&authenticated_transfer_core::Instruction::Transfer { - amount: instruction.amount, - }) + let transfer_instruction = risc0_zkvm::serde::to_vec(&instruction.amount) .expect("transfer instruction serialization"); chained_calls.push(ChainedCall { diff --git a/test_program_methods/guest/src/bin/flash_swap_initiator.rs b/lee/state_machine/test_methods/guest/src/bin/flash_swap_initiator.rs similarity index 97% rename from test_program_methods/guest/src/bin/flash_swap_initiator.rs rename to lee/state_machine/test_methods/guest/src/bin/flash_swap_initiator.rs index 15706c1e..699d7c57 100644 --- a/test_program_methods/guest/src/bin/flash_swap_initiator.rs +++ b/lee/state_machine/test_methods/guest/src/bin/flash_swap_initiator.rs @@ -122,10 +122,7 @@ fn main() { let mut vault_authorized = vault_pre.clone(); vault_authorized.is_authorized = true; let transfer_instruction = - risc0_zkvm::serde::to_vec(&authenticated_transfer_core::Instruction::Transfer { - amount: amount_out, - }) - .expect("transfer instruction serialization"); + risc0_zkvm::serde::to_vec(&amount_out).expect("transfer instruction serialization"); let call_1 = ChainedCall { program_id: token_program_id, pre_states: vec![vault_authorized, receiver_pre.clone()], diff --git a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs b/lee/state_machine/test_methods/guest/src/bin/malicious_authorization_changer.rs similarity index 92% rename from test_program_methods/guest/src/bin/malicious_authorization_changer.rs rename to lee/state_machine/test_methods/guest/src/bin/malicious_authorization_changer.rs index f1e3be6b..80bd8aaa 100644 --- a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs +++ b/lee/state_machine/test_methods/guest/src/bin/malicious_authorization_changer.rs @@ -32,8 +32,7 @@ fn main() { ..sender.clone() }; - let instruction_data = - to_vec(&authenticated_transfer_core::Instruction::Transfer { amount: balance }).unwrap(); + let instruction_data = to_vec(&balance).unwrap(); let chained_call = ChainedCall { program_id: transfer_program_id, diff --git a/test_program_methods/guest/src/bin/malicious_caller_program_id.rs b/lee/state_machine/test_methods/guest/src/bin/malicious_caller_program_id.rs similarity index 100% rename from test_program_methods/guest/src/bin/malicious_caller_program_id.rs rename to lee/state_machine/test_methods/guest/src/bin/malicious_caller_program_id.rs diff --git a/test_program_methods/guest/src/bin/malicious_injector.rs b/lee/state_machine/test_methods/guest/src/bin/malicious_injector.rs similarity index 100% rename from test_program_methods/guest/src/bin/malicious_injector.rs rename to lee/state_machine/test_methods/guest/src/bin/malicious_injector.rs diff --git a/test_program_methods/guest/src/bin/malicious_launderer.rs b/lee/state_machine/test_methods/guest/src/bin/malicious_launderer.rs similarity index 79% rename from test_program_methods/guest/src/bin/malicious_launderer.rs rename to lee/state_machine/test_methods/guest/src/bin/malicious_launderer.rs index 6794f0c0..5ec7989d 100644 --- a/test_program_methods/guest/src/bin/malicious_launderer.rs +++ b/lee/state_machine/test_methods/guest/src/bin/malicious_launderer.rs @@ -9,7 +9,7 @@ fn main() { self_program_id, caller_program_id, pre_states, - instruction: (auth_transfer_id, amount), + instruction: (simple_transfer_id, amount), }, instruction_words, ) = read_lee_inputs::(); @@ -18,13 +18,12 @@ fn main() { // authorization check at validated_state_diff.rs:158-182 runs over nothing. // Victim is never compared against caller_data.authorized_accounts = {attacker}. // - // The bug: authorized_accounts for authenticated_transfer is built from + // The bug: authorized_accounts for simple_transfer is built from // chained_call.pre_states (this call's inputs, set by P1), which contains // victim(is_authorized=true). So authorized_accounts = {victim}, and the // subsequent check passes. let auth_transfer_instruction = - risc0_zkvm::serde::to_vec(&authenticated_transfer_core::Instruction::Transfer { amount }) - .expect("serialization is infallible"); + risc0_zkvm::serde::to_vec(&amount).expect("serialization is infallible"); ProgramOutput::new( self_program_id, @@ -34,7 +33,7 @@ fn main() { vec![], ) .with_chained_calls(vec![ChainedCall { - program_id: auth_transfer_id, + program_id: simple_transfer_id, pre_states, instruction_data: auth_transfer_instruction, pda_seeds: vec![], diff --git a/test_program_methods/guest/src/bin/malicious_self_program_id.rs b/lee/state_machine/test_methods/guest/src/bin/malicious_self_program_id.rs similarity index 100% rename from test_program_methods/guest/src/bin/malicious_self_program_id.rs rename to lee/state_machine/test_methods/guest/src/bin/malicious_self_program_id.rs diff --git a/test_program_methods/guest/src/bin/minter.rs b/lee/state_machine/test_methods/guest/src/bin/minter.rs similarity index 100% rename from test_program_methods/guest/src/bin/minter.rs rename to lee/state_machine/test_methods/guest/src/bin/minter.rs diff --git a/test_program_methods/guest/src/bin/missing_output.rs b/lee/state_machine/test_methods/guest/src/bin/missing_output.rs similarity index 100% rename from test_program_methods/guest/src/bin/missing_output.rs rename to lee/state_machine/test_methods/guest/src/bin/missing_output.rs diff --git a/test_program_methods/guest/src/bin/modified_transfer.rs b/lee/state_machine/test_methods/guest/src/bin/modified_transfer.rs similarity index 100% rename from test_program_methods/guest/src/bin/modified_transfer.rs rename to lee/state_machine/test_methods/guest/src/bin/modified_transfer.rs diff --git a/test_program_methods/guest/src/bin/nonce_changer.rs b/lee/state_machine/test_methods/guest/src/bin/nonce_changer.rs similarity index 100% rename from test_program_methods/guest/src/bin/nonce_changer.rs rename to lee/state_machine/test_methods/guest/src/bin/nonce_changer.rs diff --git a/test_program_methods/guest/src/bin/noop.rs b/lee/state_machine/test_methods/guest/src/bin/noop.rs similarity index 100% rename from test_program_methods/guest/src/bin/noop.rs rename to lee/state_machine/test_methods/guest/src/bin/noop.rs diff --git a/test_program_methods/guest/src/bin/pda_claimer.rs b/lee/state_machine/test_methods/guest/src/bin/pda_claimer.rs similarity index 100% rename from test_program_methods/guest/src/bin/pda_claimer.rs rename to lee/state_machine/test_methods/guest/src/bin/pda_claimer.rs diff --git a/lee/state_machine/test_methods/guest/src/bin/pda_spend_proxy.rs b/lee/state_machine/test_methods/guest/src/bin/pda_spend_proxy.rs new file mode 100644 index 00000000..d8b9bb5c --- /dev/null +++ b/lee/state_machine/test_methods/guest/src/bin/pda_spend_proxy.rs @@ -0,0 +1,48 @@ +use lee_core::program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, read_lee_inputs, +}; +use risc0_zkvm::serde::to_vec; + +/// Proxy for spending from a private PDA via `simple_transfer`. +/// +/// `pre_states = [pda (authorized), recipient]`. Debits the PDA and credits the recipient. +/// The PDA-to-npk binding is established via `pda_seeds` in the chained call to `simple_transfer`. +type Instruction = (PdaSeed, u128, ProgramId); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (seed, amount, simple_transfer_id), + }, + instruction_words, + ) = read_lee_inputs::(); + + let Ok([first, second]) = <[_; 2]>::try_from(pre_states) else { + return; + }; + + assert!(first.is_authorized, "first pre_state must be authorized"); + + let first_post = AccountPostState::new(first.account.clone()); + let second_post = AccountPostState::new(second.account.clone()); + + let chained_call = ChainedCall { + program_id: simple_transfer_id, + instruction_data: to_vec(&amount).unwrap(), + pre_states: vec![first.clone(), second.clone()], + pda_seeds: vec![seed], + }; + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![first, second], + vec![first_post, second_post], + ) + .with_chained_calls(vec![chained_call]) + .write(); +} diff --git a/test_program_methods/guest/src/bin/private_pda_delegator.rs b/lee/state_machine/test_methods/guest/src/bin/private_pda_delegator.rs similarity index 100% rename from test_program_methods/guest/src/bin/private_pda_delegator.rs rename to lee/state_machine/test_methods/guest/src/bin/private_pda_delegator.rs diff --git a/test_program_methods/guest/src/bin/program_owner_changer.rs b/lee/state_machine/test_methods/guest/src/bin/program_owner_changer.rs similarity index 100% rename from test_program_methods/guest/src/bin/program_owner_changer.rs rename to lee/state_machine/test_methods/guest/src/bin/program_owner_changer.rs diff --git a/test_program_methods/guest/src/bin/simple_balance_transfer.rs b/lee/state_machine/test_methods/guest/src/bin/simple_balance_transfer.rs similarity index 57% rename from test_program_methods/guest/src/bin/simple_balance_transfer.rs rename to lee/state_machine/test_methods/guest/src/bin/simple_balance_transfer.rs index 29149272..addc4a19 100644 --- a/test_program_methods/guest/src/bin/simple_balance_transfer.rs +++ b/lee/state_machine/test_methods/guest/src/bin/simple_balance_transfer.rs @@ -1,4 +1,4 @@ -use lee_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_lee_inputs}; +use lee_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_lee_inputs}; type Instruction = u128; @@ -13,6 +13,21 @@ fn main() { instruction_words, ) = read_lee_inputs::(); + if let Ok([account_pre]) = <[_; 1]>::try_from(pre_states.clone()) { + let account_post = + AccountPostState::new_claimed_if_default(account_pre.account, Claim::Authorized); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + vec![account_post], + ) + .write(); + return; + } + let Ok([sender_pre, receiver_pre]) = <[_; 2]>::try_from(pre_states) else { return; }; @@ -34,8 +49,8 @@ fn main() { instruction_words, vec![sender_pre, receiver_pre], vec![ - AccountPostState::new(sender_post), - AccountPostState::new(receiver_post), + AccountPostState::new_claimed_if_default(sender_post, Claim::Authorized), + AccountPostState::new_claimed_if_default(receiver_post, Claim::Authorized), ], ) .write(); diff --git a/test_program_methods/guest/src/bin/auth_transfer_proxy.rs b/lee/state_machine/test_methods/guest/src/bin/simple_transfer_proxy.rs similarity index 65% rename from test_program_methods/guest/src/bin/auth_transfer_proxy.rs rename to lee/state_machine/test_methods/guest/src/bin/simple_transfer_proxy.rs index a7e2f5be..ce7f1d8e 100644 --- a/test_program_methods/guest/src/bin/auth_transfer_proxy.rs +++ b/lee/state_machine/test_methods/guest/src/bin/simple_transfer_proxy.rs @@ -2,23 +2,23 @@ use lee_core::program::{ AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, read_lee_inputs, }; -/// PDA authorization program that delegates balance operations to `authenticated_transfer`. +/// PDA authorization program that delegates balance operations to `simple_transfer`. /// -/// The PDA is owned by `authenticated_transfer`, not by this program. This program's role +/// The PDA is owned by `simple_transfer`, not by this program. This program's role /// is solely to provide PDA authorization via `pda_seeds` in chained calls. /// -/// Instruction: `(pda_seed, auth_transfer_id, amount, is_withdraw)`. +/// Instruction: `(pda_seed, simple_transfer_id, amount, is_withdraw)`. /// /// **Init** (`is_withdraw = false`, 1 pre-state `[pda]`): -/// Chains to `authenticated_transfer` with `instruction=0` (init path) and `pda_seeds=[seed]` -/// to initialize the PDA under `authenticated_transfer`'s ownership. +/// Chains to `simple_transfer` with `instruction=0` (init path) and `pda_seeds=[seed]` +/// to initialize the PDA under `simple_transfer`'s ownership. /// /// **Withdraw** (`is_withdraw = true`, 2 pre-states `[pda, recipient]`): -/// Chains to `authenticated_transfer` with the amount and `pda_seeds=[seed]` to authorize +/// Chains to `simple_transfer` with the amount and `pda_seeds=[seed]` to authorize /// the PDA for a balance transfer. The actual balance modification happens in -/// `authenticated_transfer`, not here. +/// `simple_transfer`, not here. /// -/// **Deposit**: done directly via `authenticated_transfer` (no need for this program). +/// **Deposit**: done directly via `simple_transfer` (no need for this program). type Instruction = (PdaSeed, ProgramId, u128, bool); #[expect( @@ -35,7 +35,7 @@ fn main() { self_program_id, caller_program_id, pre_states, - instruction: (pda_seed, auth_transfer_id, amount, is_withdraw), + instruction: (pda_seed, simple_transfer_id, amount, is_withdraw), }, instruction_words, ) = read_lee_inputs::(); @@ -46,19 +46,19 @@ fn main() { }; // Post-states stay unchanged in this program. The actual balance transfer - // happens in the chained call to authenticated_transfer. + // happens in the chained call to simple_transfer. let pda_post = AccountPostState::new(pda_pre.account.clone()); let recipient_post = AccountPostState::new(recipient_pre.account.clone()); - // Chain to authenticated_transfer with pda_seeds to authorize the PDA. + // Chain to simple_transfer with pda_seeds to authorize the PDA. // The circuit's resolve_authorization_and_record_bindings establishes the // private PDA (seed, npk) binding when pda_seeds match the private PDA derivation. let mut auth_pda_pre = pda_pre; auth_pda_pre.is_authorized = true; let auth_call = ChainedCall::new( - auth_transfer_id, + simple_transfer_id, vec![auth_pda_pre, recipient_pre], - &authenticated_transfer_core::Instruction::Transfer { amount }, + &amount, ) .with_pda_seeds(vec![pda_seed]); @@ -72,23 +72,19 @@ fn main() { .with_chained_calls(vec![auth_call]) .write(); } else { - // Init: initialize the PDA under authenticated_transfer's ownership. + // Init: initialize the PDA under simple_transfer's ownership. let Ok([pda_pre]) = <[_; 1]>::try_from(pre_states.clone()) else { panic!("expected exactly 1 pre_state for init: [pda]"); }; let pda_post = AccountPostState::new(pda_pre.account.clone()); - // Chain to authenticated_transfer with instruction=0 (init path) and pda_seeds - // to authorize the PDA. authenticated_transfer will claim it with Claim::Authorized. + // Chain to simple_transfer with instruction=0 (init path) and pda_seeds + // to authorize the PDA. simple_transfer will claim it with Claim::Authorized. let mut auth_pda_pre = pda_pre; auth_pda_pre.is_authorized = true; - let auth_call = ChainedCall::new( - auth_transfer_id, - vec![auth_pda_pre], - &authenticated_transfer_core::Instruction::Initialize, - ) - .with_pda_seeds(vec![pda_seed]); + let auth_call = ChainedCall::new(simple_transfer_id, vec![auth_pda_pre], &amount) + .with_pda_seeds(vec![pda_seed]); ProgramOutput::new( self_program_id, diff --git a/test_program_methods/guest/src/bin/two_pda_claimer.rs b/lee/state_machine/test_methods/guest/src/bin/two_pda_claimer.rs similarity index 100% rename from test_program_methods/guest/src/bin/two_pda_claimer.rs rename to lee/state_machine/test_methods/guest/src/bin/two_pda_claimer.rs diff --git a/test_program_methods/guest/src/bin/validity_window.rs b/lee/state_machine/test_methods/guest/src/bin/validity_window.rs similarity index 100% rename from test_program_methods/guest/src/bin/validity_window.rs rename to lee/state_machine/test_methods/guest/src/bin/validity_window.rs diff --git a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs b/lee/state_machine/test_methods/guest/src/bin/validity_window_chain_caller.rs similarity index 100% rename from test_program_methods/guest/src/bin/validity_window_chain_caller.rs rename to lee/state_machine/test_methods/guest/src/bin/validity_window_chain_caller.rs diff --git a/program_methods/src/lib.rs b/lee/state_machine/test_methods/src/lib.rs similarity index 100% rename from program_methods/src/lib.rs rename to lee/state_machine/test_methods/src/lib.rs diff --git a/lez/common/Cargo.toml b/lez/common/Cargo.toml index a559960d..8b2aa322 100644 --- a/lez/common/Cargo.toml +++ b/lez/common/Cargo.toml @@ -12,6 +12,8 @@ lee.workspace = true lee_core.workspace = true authenticated_transfer_core.workspace = true clock_core.workspace = true +programs.workspace = true +system_accounts.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/lez/common/src/lib.rs b/lez/common/src/lib.rs index cfbbbd9b..3cca327b 100644 --- a/lez/common/src/lib.rs +++ b/lez/common/src/lib.rs @@ -12,8 +12,6 @@ pub mod transaction; // TODO: Compile only for tests pub mod test_utils; -pub const PINATA_BASE58: &str = "EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7"; - #[derive( Default, Copy, diff --git a/lez/common/src/test_utils.rs b/lez/common/src/test_utils.rs index 179e9601..7afda3dd 100644 --- a/lez/common/src/test_utils.rs +++ b/lez/common/src/test_utils.rs @@ -44,7 +44,7 @@ pub fn produce_dummy_block( #[must_use] pub fn produce_dummy_empty_transaction() -> LeeTransaction { - let program_id = lee::program::Program::authenticated_transfer_program().id(); + let program_id = programs::authenticated_transfer().id(); let account_ids = vec![]; let nonces = vec![]; let message = lee::public_transaction::Message::try_new( @@ -72,7 +72,7 @@ pub fn create_transaction_native_token_transfer( ) -> LeeTransaction { let account_ids = vec![from, to]; let nonces = vec![nonce.into()]; - let program_id = lee::program::Program::authenticated_transfer_program().id(); + let program_id = programs::authenticated_transfer().id(); let message = lee::public_transaction::Message::try_new( program_id, account_ids, diff --git a/lez/common/src/transaction.rs b/lez/common/src/transaction.rs index 7fb32e39..b5aee648 100644 --- a/lez/common/src/transaction.rs +++ b/lez/common/src/transaction.rs @@ -80,10 +80,9 @@ impl LeeTransaction { ) -> Result { let diff = self.compute_state_diff(state, block_id, timestamp)?; - let restricted_modification_accounts = lee::CLOCK_PROGRAM_ACCOUNT_IDS - .iter() - .copied() - .chain(std::iter::once(lee::system_faucet_account_id())); + let restricted_modification_accounts = system_accounts::clock_account_ids() + .into_iter() + .chain(std::iter::once(system_accounts::faucet_account_id())); for account_id in restricted_modification_accounts { validate_doesnt_modify_account(state, &diff, account_id)?; } @@ -157,7 +156,7 @@ impl LeeTransaction { state: &V03State, diff: &ValidatedStateDiff, ) -> Result<(), lee::error::LeeError> { - let bridge_account_id = lee::system_bridge_account_id(); + let bridge_account_id = system_accounts::bridge_account_id(); let pre = state.get_account_by_id(bridge_account_id); let Some(post) = diff.public_diff().get(&bridge_account_id).cloned() else { return Ok(()); @@ -174,7 +173,7 @@ impl LeeTransaction { balance: pre.balance, ..post.clone() }; - (expected_pre == pre) && (pre.balance <= post.balance) + (expected_pre == pre) && (pre.balance < post.balance) }; if only_balance_increased { @@ -229,7 +228,7 @@ pub enum TransactionMalformationError { #[must_use] pub fn clock_invocation(timestamp: clock_core::Instruction) -> lee::PublicTransaction { let message = lee::public_transaction::Message::try_new( - lee::program::Program::clock().id(), + programs::clock().id(), clock_core::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(), vec![], timestamp, @@ -261,17 +260,14 @@ fn validate_doesnt_modify_account( #[cfg(test)] mod tests { - use lee::{ - AccountId, CLOCK_01_PROGRAM_ACCOUNT_ID, PrivateKey, PublicKey, V03State, - system_bridge_account_id, system_faucet_account_id, - }; + use lee::{AccountId, PrivateKey, PublicKey, V03State}; use crate::test_utils::create_transaction_native_token_transfer; #[test] fn system_account_ids_are_distinct_and_non_default() { - let faucet = system_faucet_account_id(); - let bridge = system_bridge_account_id(); + let faucet = system_accounts::faucet_account_id(); + let bridge = system_accounts::bridge_account_id(); assert_ne!(faucet, AccountId::default()); assert_ne!(bridge, AccountId::default()); assert_ne!(faucet, bridge); @@ -286,12 +282,12 @@ mod tests { // (an empty diff hides the modification). let sender_key = PrivateKey::try_new([5_u8; 32]).expect("valid key"); let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); - let state = V03State::new_with_genesis_accounts(&[(sender_id, 10_000)], vec![], 0); + let state = V03State::new().with_public_account_balances([(sender_id, 10_000)]); let tx = create_transaction_native_token_transfer( sender_id, 0, - CLOCK_01_PROGRAM_ACCOUNT_ID, + system_accounts::clock_account_ids()[0], 100, &sender_key, ); diff --git a/lez/configs/docker-all-in-one/indexer_config.json b/lez/configs/docker-all-in-one/indexer_config.json index f2005ff5..c1ff65b0 100644 --- a/lez/configs/docker-all-in-one/indexer_config.json +++ b/lez/configs/docker-all-in-one/indexer_config.json @@ -1,5 +1,4 @@ { - "home": "./indexer/service", "consensus_info_polling_interval": "1s", "bedrock_config": { "addr": "http://logos-blockchain-node-0:18080" diff --git a/lez/configs/docker-all-in-one/sequencer_config.json b/lez/configs/docker-all-in-one/sequencer_config.json index edb0132a..90b5d5f3 100644 --- a/lez/configs/docker-all-in-one/sequencer_config.json +++ b/lez/configs/docker-all-in-one/sequencer_config.json @@ -13,7 +13,6 @@ "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", "node_url": "http://logos-blockchain-node-0:18080" }, - "indexer_rpc_url": "ws://indexer_service:8779", "genesis": [ { "supply_bridge_account": { diff --git a/lez/docker/risc0-base.Dockerfile b/lez/docker/risc0-base.Dockerfile new file mode 100644 index 00000000..dfc6c869 --- /dev/null +++ b/lez/docker/risc0-base.Dockerfile @@ -0,0 +1,47 @@ +# Shared build base: cargo-chef toolchain + risc0 r0vm. +# +# This is the single source of truth for the r0vm install that the sequencer +# and indexer service images depend on. It is consumed as a named build context +# called `risc0_base` (the service Dockerfiles start with `FROM risc0_base`). +# +# Wiring: +# - docker-compose: `build.additional_contexts: { risc0_base: "service:risc0_base" }` +# - CI: built first and passed via `build-contexts: risc0_base=docker-image://...` +FROM lukemathwalker/cargo-chef:latest-rust-1.94.0-slim-trixie + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + pkg-config \ + libssl-dev \ + libclang-dev \ + clang \ + cmake \ + ninja-build \ + curl \ + unzip \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install r0vm +# Use quick install for x86-64 (risczero provides binaries only for this linux platform) +# Manual build for other platforms (including arm64 Linux) +RUN ARCH=$(uname -m); \ + if [ "$ARCH" = "x86_64" ]; then \ + echo "Using quick install for $ARCH"; \ + curl -L https://risczero.com/install | bash; \ + export PATH="/root/.cargo/bin:/root/.risc0/bin:${PATH}"; \ + rzup install; \ + else \ + echo "Using manual build for $ARCH"; \ + git clone --depth 1 --branch release-3.0 https://github.com/risc0/risc0.git; \ + git clone --depth 1 --branch risc0-1.94.1 https://github.com/risc0/rust.git; \ + cd /risc0; \ + cargo install --locked --path rzup; \ + rzup build --path /rust rust --verbose; \ + cargo install --locked --path risc0/cargo-risczero; \ + fi +ENV PATH="/root/.cargo/bin:/root/.risc0/bin:${PATH}" +RUN cp "$(which r0vm)" /usr/local/bin/r0vm +RUN test -x /usr/local/bin/r0vm +RUN r0vm --version diff --git a/lez/explorer_service/docker-compose.yml b/lez/explorer_service/docker-compose.yml index 7b699a39..fa884b0a 100644 --- a/lez/explorer_service/docker-compose.yml +++ b/lez/explorer_service/docker-compose.yml @@ -2,10 +2,10 @@ services: explorer_service: image: lez/explorer_service build: - context: .. - dockerfile: explorer_service/Dockerfile + context: ../.. + dockerfile: lez/explorer_service/Dockerfile container_name: explorer_service environment: - INDEXER_RPC_URL: ${INDEXER_RPC_URL:-http://localhost:8779} + INDEXER_RPC_URL: ${INDEXER_RPC_URL:-http://host.docker.internal:8779} ports: - "8080:8080" diff --git a/lez/explorer_service/src/components/account_nonce_list.rs b/lez/explorer_service/src/components/account_nonce_list.rs new file mode 100644 index 00000000..d8b30aff --- /dev/null +++ b/lez/explorer_service/src/components/account_nonce_list.rs @@ -0,0 +1,58 @@ +use indexer_service_protocol::AccountId; +use itertools::{EitherOrBoth, Itertools as _}; +use leptos::prelude::*; +use leptos_router::components::A; + +#[component] +pub fn AccountNonceList(account_ids: Vec, nonces: Vec) -> impl IntoView { + view! { +
+ {account_ids + .into_iter() + .zip_longest(nonces.into_iter()) + .map(|maybe_pair| { + match maybe_pair { + EitherOrBoth::Both(account_id, nonce) => { + let account_id_str = account_id.to_string(); + view! { + + } + } + EitherOrBoth::Left(account_id) => { + let account_id_str = account_id.to_string(); + view! { + + } + } + EitherOrBoth::Right(_) => { + view! { + + } + } + } + }) + .collect::>()} +
+ } +} diff --git a/lez/explorer_service/src/components/mod.rs b/lez/explorer_service/src/components/mod.rs index 306c79a8..3d0a4dae 100644 --- a/lez/explorer_service/src/components/mod.rs +++ b/lez/explorer_service/src/components/mod.rs @@ -1,7 +1,15 @@ +pub use account_nonce_list::AccountNonceList; pub use account_preview::AccountPreview; pub use block_preview::BlockPreview; +pub use search_results::SearchResultsView; +pub use transaction_details::{ + PrivacyPreservingTxDetails, ProgramDeploymentTxDetails, PublicTxDetails, +}; pub use transaction_preview::TransactionPreview; +pub mod account_nonce_list; pub mod account_preview; pub mod block_preview; +pub mod search_results; +pub mod transaction_details; pub mod transaction_preview; diff --git a/lez/explorer_service/src/components/search_results.rs b/lez/explorer_service/src/components/search_results.rs new file mode 100644 index 00000000..1033e515 --- /dev/null +++ b/lez/explorer_service/src/components/search_results.rs @@ -0,0 +1,93 @@ +use leptos::prelude::*; + +use super::{AccountPreview, BlockPreview, TransactionPreview}; +use crate::api::SearchResults; + +/// Search results view component +#[component] +pub fn SearchResultsView(results: SearchResults) -> impl IntoView { + let SearchResults { + blocks, + transactions, + accounts, + } = results; + let has_results = !blocks.is_empty() || !transactions.is_empty() || !accounts.is_empty(); + + view! { +
+

"Search Results"

+ {if has_results { + view! { +
+ {if blocks.is_empty() { + ().into_any() + } else { + view! { +
+

"Blocks"

+
+ {blocks + .into_iter() + .map(|block| { + view! { } + }) + .collect::>()} +
+
+ } + .into_any() + }} + + {if transactions.is_empty() { + ().into_any() + } else { + view! { +
+

"Transactions"

+
+ {transactions + .into_iter() + .map(|tx| { + view! { } + }) + .collect::>()} +
+
+ } + .into_any() + }} + + {if accounts.is_empty() { + ().into_any() + } else { + view! { +
+

"Accounts"

+
+ {accounts + .into_iter() + .map(|(id, account)| { + view! { + + } + }) + .collect::>()} +
+
+ } + .into_any() + }} + +
+ } + .into_any() + } else { + view! {
"No results found"
} + .into_any() + }} +
+ } +} diff --git a/lez/explorer_service/src/components/transaction_details.rs b/lez/explorer_service/src/components/transaction_details.rs new file mode 100644 index 00000000..c82f7d80 --- /dev/null +++ b/lez/explorer_service/src/components/transaction_details.rs @@ -0,0 +1,150 @@ +use indexer_service_protocol::{ + PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, + ProgramDeploymentTransaction, PublicMessage, PublicTransaction, WitnessSet, +}; +use leptos::prelude::*; + +use super::AccountNonceList; + +/// Public transaction details component +#[component] +pub fn PublicTxDetails(tx: PublicTransaction) -> impl IntoView { + let PublicTransaction { + hash: _, + message, + witness_set, + } = tx; + let PublicMessage { + program_id, + account_ids, + nonces, + instruction_data, + } = message; + let WitnessSet { + signatures_and_public_keys, + proof, + } = witness_set; + + let program_id_str = program_id.to_string(); + let proof_len = proof.map_or(0, |p| p.0.len()); + let signatures_count = signatures_and_public_keys.len(); + + view! { +
+

"Public Transaction Details"

+
+
+ "Program ID:" + {program_id_str} +
+
+ "Instruction Data:" + + {format!("{} u32 values", instruction_data.len())} + +
+
+ "Proof Size:" + {format!("{proof_len} bytes")} +
+
+ "Signatures:" + {signatures_count.to_string()} +
+
+ +

"Accounts"

+ +
+ } +} + +/// Privacy-preserving transaction details component +#[component] +pub fn PrivacyPreservingTxDetails(tx: PrivacyPreservingTransaction) -> impl IntoView { + let PrivacyPreservingTransaction { + hash: _, + message, + witness_set, + } = tx; + let PrivacyPreservingMessage { + public_account_ids, + nonces, + public_post_states: _, + encrypted_private_post_states, + new_commitments, + new_nullifiers, + block_validity_window, + timestamp_validity_window, + } = message; + let WitnessSet { + signatures_and_public_keys: _, + proof, + } = witness_set; + let proof_len = proof.map_or(0, |p| p.0.len()); + + view! { +
+

"Privacy-Preserving Transaction Details"

+
+
+ "Public Accounts:" + + {public_account_ids.len().to_string()} + +
+
+ "New Commitments:" + {new_commitments.len().to_string()} +
+
+ "Nullifiers:" + {new_nullifiers.len().to_string()} +
+
+ "Encrypted States:" + + {encrypted_private_post_states.len().to_string()} + +
+
+ "Proof Size:" + {format!("{proof_len} bytes")} +
+
+ "Block Validity Window:" + {block_validity_window.to_string()} +
+
+ "Timestamp Validity Window:" + {timestamp_validity_window.to_string()} +
+
+ +

"Public Accounts"

+ +
+ } +} + +/// Program deployment transaction details component +#[component] +pub fn ProgramDeploymentTxDetails(tx: ProgramDeploymentTransaction) -> impl IntoView { + let ProgramDeploymentTransaction { hash: _, message } = tx; + let ProgramDeploymentMessage { bytecode } = message; + + let bytecode_len = bytecode.len(); + view! { +
+

"Program Deployment Transaction Details"

+
+
+ "Bytecode Size:" + + {format!("{bytecode_len} bytes")} + +
+
+
+ } +} diff --git a/lez/explorer_service/src/pages/main_page.rs b/lez/explorer_service/src/pages/main_page.rs index 7e26e794..831182ce 100644 --- a/lez/explorer_service/src/pages/main_page.rs +++ b/lez/explorer_service/src/pages/main_page.rs @@ -6,8 +6,8 @@ use leptos_router::{ use web_sys::SubmitEvent; use crate::{ - api::{self, SearchResults}, - components::{AccountPreview, BlockPreview, TransactionPreview}, + api, + components::{BlockPreview, SearchResultsView}, }; const RECENT_BLOCKS_LIMIT: u64 = 10; @@ -138,93 +138,8 @@ pub fn MainPage() -> impl IntoView { .get() .and_then(|opt_results| opt_results) .map(|results| { - let SearchResults { - blocks, - transactions, - accounts, - } = results; - let has_results = !blocks.is_empty() - || !transactions.is_empty() - || !accounts.is_empty(); - view! { -
-

"Search Results"

- {if has_results { - view! { -
- {if blocks.is_empty() { - ().into_any() - } else { - view! { -
-

"Blocks"

-
- {blocks - .into_iter() - .map(|block| { - view! { } - }) - .collect::>()} -
-
- } - .into_any() - }} - - {if transactions.is_empty() { - ().into_any() - } else { - view! { -
-

"Transactions"

-
- {transactions - .into_iter() - .map(|tx| { - view! { } - }) - .collect::>()} -
-
- } - .into_any() - }} - - {if accounts.is_empty() { - ().into_any() - } else { - view! { -
-

"Accounts"

-
- {accounts - .into_iter() - .map(|(id, account)| { - view! { - - } - }) - .collect::>()} -
-
- } - .into_any() - }} - -
- } - .into_any() - } else { - view! {
"No results found"
} - .into_any() - }} -
- } - .into_any() - }) + view! { }.into_any() + }) }} diff --git a/lez/explorer_service/src/pages/transaction_page.rs b/lez/explorer_service/src/pages/transaction_page.rs index 0a3fc8e2..a99c269f 100644 --- a/lez/explorer_service/src/pages/transaction_page.rs +++ b/lez/explorer_service/src/pages/transaction_page.rs @@ -1,14 +1,13 @@ use std::str::FromStr as _; -use indexer_service_protocol::{ - HashType, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, - ProgramDeploymentTransaction, PublicMessage, PublicTransaction, Transaction, WitnessSet, -}; -use itertools::{EitherOrBoth, Itertools as _}; +use indexer_service_protocol::{HashType, Transaction}; use leptos::prelude::*; -use leptos_router::{components::A, hooks::use_params_map}; +use leptos_router::hooks::use_params_map; -use crate::api; +use crate::{ + api, + components::{PrivacyPreservingTxDetails, ProgramDeploymentTxDetails, PublicTxDetails}, +}; /// Transaction page component #[component] @@ -66,244 +65,21 @@ pub fn TransactionPage() -> impl IntoView { { match tx { - Transaction::Public(ptx) => { - let PublicTransaction { - hash: _, - message, - witness_set, - } = ptx; - let PublicMessage { - program_id, - account_ids, - nonces, - instruction_data, - } = message; - let WitnessSet { - signatures_and_public_keys, - proof, - } = witness_set; + Transaction::Public(ptx) => { + view! { }.into_any() + } + Transaction::PrivacyPreserving(pptx) => { + view! { }.into_any() + } + Transaction::ProgramDeployment(pdtx) => { + view! { }.into_any() + } + } + } - let program_id_str = program_id.to_string(); - let proof_len = proof.map_or(0, |p| p.0.len()); - let signatures_count = signatures_and_public_keys.len(); - - view! { -
-

"Public Transaction Details"

-
-
- "Program ID:" - {program_id_str} -
-
- "Instruction Data:" - - {format!("{} u32 values", instruction_data.len())} - -
-
- "Proof Size:" - {format!("{proof_len} bytes")} -
-
- "Signatures:" - {signatures_count.to_string()} -
-
- -

"Accounts"

-
- {account_ids - .into_iter() - .zip_longest(nonces.into_iter()) - .map(|maybe_pair| { - match maybe_pair { - EitherOrBoth::Both(account_id, nonce) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Left(account_id) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Right(_) => { - view! { - - } - } - } - }) - .collect::>()} -
-
- } - .into_any() + } - Transaction::PrivacyPreserving(pptx) => { - let PrivacyPreservingTransaction { - hash: _, - message, - witness_set, - } = pptx; - let PrivacyPreservingMessage { - public_account_ids, - nonces, - public_post_states: _, - encrypted_private_post_states, - new_commitments, - new_nullifiers, - block_validity_window, - timestamp_validity_window, - } = message; - let WitnessSet { - signatures_and_public_keys: _, - proof, - } = witness_set; - let proof_len = proof.map_or(0, |p| p.0.len()); - view! { -
-

"Privacy-Preserving Transaction Details"

-
-
- "Public Accounts:" - - {public_account_ids.len().to_string()} - -
-
- "New Commitments:" - {new_commitments.len().to_string()} -
-
- "Nullifiers:" - {new_nullifiers.len().to_string()} -
-
- "Encrypted States:" - - {encrypted_private_post_states.len().to_string()} - -
-
- "Proof Size:" - {format!("{proof_len} bytes")} -
-
- "Block Validity Window:" - {block_validity_window.to_string()} -
-
- "Timestamp Validity Window:" - {timestamp_validity_window.to_string()} -
-
- -

"Public Accounts"

-
- {public_account_ids - .into_iter() - .zip_longest(nonces.into_iter()) - .map(|maybe_pair| { - match maybe_pair { - EitherOrBoth::Both(account_id, nonce) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Left(account_id) => { - let account_id_str = account_id.to_string(); - view! { - - } - } - EitherOrBoth::Right(_) => { - view! { - - } - } - } - }) - .collect::>()} -
-
- } - .into_any() - } - Transaction::ProgramDeployment(pdtx) => { - let ProgramDeploymentTransaction { - hash: _, - message, - } = pdtx; - let ProgramDeploymentMessage { bytecode } = message; - - let bytecode_len = bytecode.len(); - view! { -
-

"Program Deployment Transaction Details"

-
-
- "Bytecode Size:" - - {format!("{bytecode_len} bytes")} - -
-
-
- } - .into_any() - } - }} - - - } - .into_any() + .into_any() } Err(e) => { view! { diff --git a/lez/indexer/core/Cargo.toml b/lez/indexer/core/Cargo.toml index c6cc5fc6..758acdd6 100644 --- a/lez/indexer/core/Cargo.toml +++ b/lez/indexer/core/Cargo.toml @@ -16,6 +16,7 @@ storage.workspace = true testnet_initial_state.workspace = true anyhow.workspace = true +arc-swap.workspace = true log.workspace = true serde.workspace = true humantime-serde.workspace = true diff --git a/lez/indexer/core/src/block_store.rs b/lez/indexer/core/src/block_store.rs index f00c94c5..c67148bd 100644 --- a/lez/indexer/core/src/block_store.rs +++ b/lez/indexer/core/src/block_store.rs @@ -211,6 +211,60 @@ mod tests { use super::*; + struct TestFixture { + storage: IndexerStore, + from: AccountId, + to: AccountId, + _home: tempfile::TempDir, + } + + #[expect( + clippy::arithmetic_side_effects, + reason = "test helper with bounded inputs" + )] + async fn store_with_transfer_blocks( + block_count: u64, + prev_hash: Option, + ) -> TestFixture { + let home = tempdir().unwrap(); + let storage = IndexerStore::open_db(home.path()).unwrap(); + + let initial_accounts = initial_pub_accounts_private_keys(); + let from = initial_accounts[0].account_id; + let to = initial_accounts[1].account_id; + let sign_key = initial_accounts[0].pub_sign_key.clone(); + + let mut prev_hash = prev_hash; + for i in 0..block_count { + let tx = common::test_utils::create_transaction_native_token_transfer( + from, + u128::from(i), + to, + 10, + &sign_key, + ); + let block_id = i + 1; + + let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); + prev_hash = Some(next_block.header.hash); + + storage + .put_block( + next_block, + HeaderId::from([u8::try_from(i + 1).unwrap(); 32]), + ) + .await + .unwrap(); + } + + TestFixture { + storage, + from, + to, + _home: home, + } + } + #[test] fn correct_startup() { let home = tempdir().unwrap(); @@ -225,7 +279,6 @@ mod tests { #[tokio::test] async fn state_transition() { let home = tempdir().unwrap(); - let storage = IndexerStore::open_db(home.as_ref()).unwrap(); let initial_accounts = initial_pub_accounts_private_keys(); @@ -233,7 +286,6 @@ mod tests { let to = initial_accounts[1].account_id; let sign_key = initial_accounts[0].pub_sign_key.clone(); - // Submit genesis block let clock_tx = LeeTransaction::Public(clock_invocation(0)); let genesis_block_data = HashableBlockData { block_id: 1, @@ -249,15 +301,13 @@ mod tests { .await .unwrap(); - for i in 0..10 { + for i in 0..10_u128 { let tx = common::test_utils::create_transaction_native_token_transfer( from, i, to, 10, &sign_key, ); let block_id = u64::try_from(i + 1).unwrap(); - let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); prev_hash = Some(next_block.header.hash); - storage .put_block( next_block, @@ -276,48 +326,23 @@ mod tests { #[tokio::test] async fn account_state_at_block() { - let home = tempdir().unwrap(); + let TestFixture { + storage, + from, + to, + _home, + } = store_with_transfer_blocks(10, None).await; - let storage = IndexerStore::open_db(home.as_ref()).unwrap(); - - let mut prev_hash = None; - - let initial_accounts = initial_pub_accounts_private_keys(); - let from = initial_accounts[0].account_id; - let to = initial_accounts[1].account_id; - let sign_key = initial_accounts[0].pub_sign_key.clone(); - - for i in 0..10 { - let tx = common::test_utils::create_transaction_native_token_transfer( - from, i, to, 10, &sign_key, - ); - let block_id = u64::try_from(i + 1).unwrap(); - - let next_block = common::test_utils::produce_dummy_block(block_id, prev_hash, vec![tx]); - prev_hash = Some(next_block.header.hash); - - storage - .put_block( - next_block, - HeaderId::from([u8::try_from(i + 1).unwrap(); 32]), - ) - .await - .unwrap(); - } - - // Genesis block: no transfers applied yet. let acc1_at_1 = storage.account_state_at_block(&from, 1).unwrap(); let acc2_at_1 = storage.account_state_at_block(&to, 1).unwrap(); assert_eq!(acc1_at_1.balance, 9990); assert_eq!(acc2_at_1.balance, 20010); - // After block 5: 4 transfers of 10 applied (one each in blocks 2..=5). let acc1_at_5 = storage.account_state_at_block(&from, 5).unwrap(); let acc2_at_5 = storage.account_state_at_block(&to, 5).unwrap(); assert_eq!(acc1_at_5.balance, 9950); assert_eq!(acc2_at_5.balance, 20050); - // After final block 9: 8 transfers applied; should match current state. let acc1_at_9 = storage.account_state_at_block(&from, 9).unwrap(); let acc2_at_9 = storage.account_state_at_block(&to, 9).unwrap(); assert_eq!(acc1_at_9.balance, 9910); diff --git a/lez/indexer/core/src/config.rs b/lez/indexer/core/src/config.rs index 6a019828..cb7f3dfe 100644 --- a/lez/indexer/core/src/config.rs +++ b/lez/indexer/core/src/config.rs @@ -1,9 +1,4 @@ -use std::{ - fs::File, - io::BufReader, - path::{Path, PathBuf}, - time::Duration, -}; +use std::{fs::File, io::BufReader, path::Path, time::Duration}; use anyhow::{Context as _, Result}; use common::config::BasicAuth; @@ -21,8 +16,6 @@ pub struct ClientConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IndexerConfig { - /// Home dir of indexer storage. - pub home: PathBuf, #[serde(with = "humantime_serde")] pub consensus_info_polling_interval: Duration, pub bedrock_config: ClientConfig, diff --git a/lez/indexer/core/src/lib.rs b/lez/indexer/core/src/lib.rs index b0416905..0d595fc0 100644 --- a/lez/indexer/core/src/lib.rs +++ b/lez/indexer/core/src/lib.rs @@ -1,6 +1,7 @@ -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use anyhow::Result; +use arc_swap::ArcSwap; use common::block::Block; // ToDo: Remove after testnet use futures::StreamExt as _; @@ -10,21 +11,30 @@ use logos_blockchain_zone_sdk::{ CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer, }; -use crate::{block_store::IndexerStore, config::IndexerConfig}; +use crate::{ + block_store::IndexerStore, + config::IndexerConfig, + status::{IndexerStatus, IndexerSyncStatus}, +}; pub mod block_store; pub mod config; +pub mod status; #[derive(Clone)] pub struct IndexerCore { pub zone_indexer: Arc>, pub config: IndexerConfig, pub store: IndexerStore, + /// Live ingestion status; updated by the ingest stream, read by `status`. + pub status: Arc>, } impl IndexerCore { - pub fn new(config: IndexerConfig) -> Result { - let home = config.home.join("rocksdb"); + pub fn new(config: IndexerConfig, storage_dir: &Path) -> Result { + // Namespace the DB by channel so indexers on different channels can + // share a storage dir without their RocksDB state colliding. + let home = storage_dir.join(format!("rocksdb-{}", config.channel_id)); let basic_auth = config.bedrock_config.auth.clone().map(Into::into); let node = NodeHttpClient::new( @@ -37,9 +47,29 @@ impl IndexerCore { zone_indexer: Arc::new(zone_indexer), config, store: IndexerStore::open_db(&home)?, + status: Arc::new(ArcSwap::from_pointee(IndexerSyncStatus::starting())), }) } + /// Snapshot of the current ingestion status (sync state + indexed tip). + /// + /// Combines the ingest loop's live status with the L2 tip read fresh from the + /// store, so callers (FFI/RPC) can tell "catching up" from "failed". + #[must_use] + pub fn status(&self) -> IndexerStatus { + let sync = IndexerSyncStatus::clone(&self.status.load()); + let indexed_block_id = self.store.get_last_block_id().ok().flatten(); + IndexerStatus { + sync, + indexed_block_id, + } + } + + /// Atomically publish a new ingestion status for readers of `status`. + fn set_status(&self, status: IndexerSyncStatus) { + self.status.store(Arc::new(status)); + } + pub fn subscribe_parse_block_stream(&self) -> impl futures::Stream> + '_ { let poll_interval = self.config.consensus_info_polling_interval; let initial_cursor = self @@ -60,14 +90,30 @@ impl IndexerCore { let stream = match self.zone_indexer.next_messages(cursor).await { Ok(s) => s, Err(err) => { + // `next_messages` reads L1 consensus info internally, so + // this also covers an unreachable/misconfigured L1 node. error!("Failed to start zone-sdk next_messages stream: {err}"); + self.set_status(IndexerSyncStatus::error(format!( + "cannot reach L1 / read channel: {err}" + ))); tokio::time::sleep(poll_interval).await; continue; } }; let mut stream = std::pin::pin!(stream); + // Flip to Syncing on the first message of this cycle (not merely on + // a successful poll) so the steady-state CaughtUp status doesn't + // flicker. Until then the state stays Starting (cold-start scan of + // empty L1 history) or CaughtUp (idle). + let mut announced_syncing = false; + while let Some((msg, slot)) = stream.next().await { + if !announced_syncing { + self.set_status(IndexerSyncStatus::syncing()); + announced_syncing = true; + } + let zone_block = match msg { ZoneMessage::Block(b) => b, // Non-block messages don't carry a cursor position; the @@ -105,7 +151,11 @@ impl IndexerCore { yield Ok(block); } - // Stream ended (caught up to LIB). Sleep then poll again. + // Stream drained: caught up to LIB as of this cycle. Clears any + // prior error (e.g. a transient L1 disconnect that left no + // backlog, so the `Syncing` branch above never ran). Sleep then + // poll again. + self.set_status(IndexerSyncStatus::caught_up()); tokio::time::sleep(poll_interval).await; } } diff --git a/lez/indexer/core/src/status.rs b/lez/indexer/core/src/status.rs new file mode 100644 index 00000000..1193e124 --- /dev/null +++ b/lez/indexer/core/src/status.rs @@ -0,0 +1,103 @@ +use serde::Serialize; + +/// Coarse lifecycle state of the indexer's ingestion loop, so a client can tell +/// "still catching up" apart from "something went wrong". +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum IndexerSyncState { + /// Booted; no ingestion cycle has run yet. + Starting, + /// Streaming finalized messages toward the L1 frontier. + Syncing, + /// Drained the stream up to LIB; idle until new blocks finalize. + CaughtUp, + /// The last cycle failed (e.g. the L1 node is unreachable). See `last_error`. + Error, +} + +/// Live ingestion status owned by the ingest loop: the coarse `state` plus the +/// reason when it is `Error`. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IndexerSyncStatus { + pub state: IndexerSyncState, + pub last_error: Option, +} + +impl IndexerSyncStatus { + /// Initial status before any ingestion cycle has run. + pub(crate) const fn starting() -> Self { + Self { + state: IndexerSyncState::Starting, + last_error: None, + } + } + + /// Actively streaming finalized messages toward the L1 frontier. + pub(crate) const fn syncing() -> Self { + Self { + state: IndexerSyncState::Syncing, + last_error: None, + } + } + + /// Drained the stream up to LIB; idle until new blocks finalize. + pub(crate) const fn caught_up() -> Self { + Self { + state: IndexerSyncState::CaughtUp, + last_error: None, + } + } + + /// The last cycle failed; `reason` explains why. + pub(crate) const fn error(reason: String) -> Self { + Self { + state: IndexerSyncState::Error, + last_error: Some(reason), + } + } +} + +/// Full status snapshot returned to callers (FFI/RPC): the live [`IndexerSyncStatus`] +/// plus the L2 tip (`indexed_block_id`) read fresh from the store at query time. +/// +/// The tip is tracked by the store, not the ingest loop, so it lives here on the +/// returned snapshot rather than inside the shared [`IndexerSyncStatus`]. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IndexerStatus { + #[serde(flatten)] + pub sync: IndexerSyncStatus, + pub indexed_block_id: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn indexer_status_serializes_to_flat_object() { + let status = IndexerStatus { + sync: IndexerSyncStatus::error("boom".to_owned()), + indexed_block_id: Some(7), + }; + let value = serde_json::to_value(&status).expect("serialize"); + assert_eq!( + value, + serde_json::json!({ + "state": "error", + "lastError": "boom", + "indexedBlockId": 7, + }) + ); + } + + #[test] + fn caught_up_clears_error() { + let value = serde_json::to_value(IndexerSyncStatus::caught_up()).expect("serialize"); + assert_eq!( + value, + serde_json::json!({ "state": "caught_up", "lastError": null }) + ); + } +} diff --git a/lez/indexer/ffi/Cargo.toml b/lez/indexer/ffi/Cargo.toml index 66f6a518..a1615b75 100644 --- a/lez/indexer/ffi/Cargo.toml +++ b/lez/indexer/ffi/Cargo.toml @@ -6,15 +6,14 @@ version = "0.1.0" [dependencies] lee.workspace = true -indexer_service.workspace = true -indexer_service_rpc = { workspace = true, features = ["client"] } -indexer_service_protocol.workspace = true +indexer_core.workspace = true +indexer_service_protocol = { workspace = true, features = ["convert"] } -url.workspace = true +env_logger.workspace = true log = { workspace = true } tokio = { features = ["rt-multi-thread"], workspace = true } -jsonrpsee.workspace = true -anyhow.workspace = true +futures.workspace = true +serde_json.workspace = true [build-dependencies] cbindgen = "0.29" diff --git a/lez/indexer/ffi/build.rs b/lez/indexer/ffi/build.rs index 92c95407..4d2faae8 100644 --- a/lez/indexer/ffi/build.rs +++ b/lez/indexer/ffi/build.rs @@ -6,6 +6,8 @@ fn main() { cbindgen::Builder::new() .with_crate(crate_dir) .with_language(cbindgen::Language::C) + .with_cpp_compat(true) + .with_pragma_once(true) .generate() .expect("Unable to generate bindings") .write_to_file("indexer_ffi.h"); diff --git a/lez/indexer/ffi/cbindgen.toml b/lez/indexer/ffi/cbindgen.toml deleted file mode 100644 index 79f622b7..00000000 --- a/lez/indexer/ffi/cbindgen.toml +++ /dev/null @@ -1,2 +0,0 @@ -language = "C" # For increased compatibility -no_includes = true diff --git a/lez/indexer/ffi/indexer_ffi.h b/lez/indexer/ffi/indexer_ffi.h index 84aaeae7..8347ad3c 100644 --- a/lez/indexer/ffi/indexer_ffi.h +++ b/lez/indexer/ffi/indexer_ffi.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include @@ -22,26 +24,6 @@ typedef enum FfiBedrockStatus { Finalized, } FfiBedrockStatus; -typedef struct Option_u64 Option_u64; - -typedef struct IndexerServiceFFI { - void *indexer_handle; - void *indexer_client; -} IndexerServiceFFI; - -/** - * Simple wrapper around a pointer to a value or an error. - * - * Pointer is not guaranteed. You should check the error field before - * dereferencing the pointer. - */ -typedef struct PointerResult_IndexerServiceFFI__OperationStatus { - struct IndexerServiceFFI *value; - enum OperationStatus error; -} PointerResult_IndexerServiceFFI__OperationStatus; - -typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult; - typedef enum PointerKind_Tag { Owned, Borrowed, @@ -72,15 +54,19 @@ typedef struct Runtime { } Runtime; /** - * Simple wrapper around a pointer to a value or an error. + * FFI-owned indexer. * - * Pointer is not guaranteed. You should check the error field before - * dereferencing the pointer. + * - An [`IndexerCore`] used to answer queries + * - The background task [`JoinHandle`] that drives ingestion (consuming the block stream so the + * store stays populated) + * - The [`Runtime`] used to run async queries against the store (either owned or borrowed), + * already FFI-safe. */ -typedef struct PointerResult_Runtime__OperationStatus { - struct Runtime *value; - enum OperationStatus error; -} PointerResult_Runtime__OperationStatus; +typedef struct IndexerServiceFFI { + void *core; + void *ingest_handle; + struct Runtime runtime; +} IndexerServiceFFI; /** * Simple wrapper around a pointer to a value or an error. @@ -88,10 +74,26 @@ typedef struct PointerResult_Runtime__OperationStatus { * Pointer is not guaranteed. You should check the error field before * dereferencing the pointer. */ -typedef struct PointerResult_Option_u64_____OperationStatus { - struct Option_u64 *value; +typedef struct PointerResult_IndexerServiceFFI__OperationStatus { + struct IndexerServiceFFI *value; enum OperationStatus error; -} PointerResult_Option_u64_____OperationStatus; +} PointerResult_IndexerServiceFFI__OperationStatus; + +typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult; + +/** + * Result of [`query_last_block`], returned **inline** (no heap allocation, so + * there is no corresponding `free_*` to call). + * + * `block_id` is only meaningful when `error` is `Ok` *and* `is_some` is + * `true`. An `Ok` result with `is_some == false` means the indexer has no + * finalized block yet (an empty chain) โ€” which is distinct from an error. + */ +typedef struct LastBlockIdResult { + uint64_t block_id; + bool is_some; + enum OperationStatus error; +} LastBlockIdResult; typedef uint64_t FfiBlockId; @@ -404,14 +406,22 @@ typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus { enum OperationStatus error; } PointerResult_FfiVec_FfiTransaction_____OperationStatus; +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + /** * Creates and starts an indexer based on the provided * configuration file path. * * # Arguments * + * - `runtime`: A runtime for the indexer to run on, or null to have the indexer create and own + * one. * - `config_path`: A pointer to a string representing the path to the configuration file. - * - `port`: Number representing a port, on which indexers RPC will start. + * - `storage_dir`: A pointer to a string naming the directory under which the indexer stores its + * state (`RocksDB`), or null/empty to use the current directory. The host (e.g. a Logos module's + * instance persistence path) owns this location. * * # Returns * @@ -420,17 +430,13 @@ typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus { * * # Safety * The caller must ensure that: - * - `runtime` is a valid pointer to a `tokio::runtime::Runtime` instance. + * - `runtime` is either null or a valid pointer to a [`Runtime`] that outlives the indexer. * - `config_path` is a valid pointer to a null-terminated C string. + * - `storage_dir` is either null or a valid pointer to a null-terminated C string. */ InitializedIndexerServiceFFIResult start_indexer(const struct Runtime *runtime, const char *config_path, - uint16_t port); - -/** - * Creates a new [`tokio::runtime::Runtime`]. - */ -struct PointerResult_Runtime__OperationStatus new_runtime(void); + const char *storage_dir); /** * Stops and frees the resources associated with the given indexer service. @@ -452,6 +458,20 @@ struct PointerResult_Runtime__OperationStatus new_runtime(void); */ enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer); +/** + * Initializes logging for the indexer at `level`. + * + * - `level` is a null-terminated string (`off`/`error`/`warn`/`info`/`debug`/ `trace`, + * case-insensitive); null or unparseable falls back to `info`. + * + * Only the `indexer_ffi` and `indexer_core` targets are enabled! + * + * # Safety + * - `level` must be a valid null-terminated C string, or null. + * - First call to this function wins; subsequent calls are no-ops. + */ +void init_logger(const char *level); + /** * # Safety * It's up to the caller to pass a proper pointer, if somehow from c/c++ side @@ -469,16 +489,40 @@ void free_cstring(char *block); * * # Returns * - * A `PointerResult, OperationStatus>` indicating success or failure. + * A [`LastBlockIdResult`] indicating success or failure. The block id is + * returned inline; nothing needs to be freed. * * # Safety * * The caller must ensure that: - * - `runtime` is a valid pointer to a [`Runtime`] instance. * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_Option_u64_____OperationStatus query_last_block(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer); +struct LastBlockIdResult query_last_block(const struct IndexerServiceFFI *indexer); + +/** + * Query the indexer's current sync status as a JSON C-string. + * + * The JSON schema is owned by `indexer_core` (`IndexerStatus`): an object with + * `state` (`starting`/`syncing`/`caught_up`/`error`), `indexedBlockId`, and + * `lastError`. Lets a client distinguish "still catching up" from "something + * went wrong". + * + * # Arguments + * + * - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. + * + * # Returns + * + * A heap-allocated, null-terminated JSON string that the caller MUST free with + * `free_cstring`. Returns null on error (null `indexer` pointer or a + * serialization failure). + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. + */ +char *query_status(const struct IndexerServiceFFI *indexer); /** * Query the block by id from indexer. @@ -495,15 +539,13 @@ struct PointerResult_Option_u64_____OperationStatus query_last_block(const struc * # Safety * * The caller must ensure that: - * - `runtime` is a valid pointer to a [`Runtime`] instance. * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct IndexerServiceFFI *indexer, FfiBlockId block_id); /** - * Query the block by id from indexer. + * Query the block by hash from indexer. * * # Arguments * @@ -517,11 +559,9 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct Runti * # Safety * * The caller must ensure that: - * - `runtime` is a valid pointer to a [`Runtime`] instance. * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct IndexerServiceFFI *indexer, FfiHashType hash); /** @@ -539,15 +579,13 @@ struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const stru * # Safety * * The caller must ensure that: - * - `runtime` is a valid pointer to a [`Runtime`] instance. * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. */ -struct PointerResult_FfiAccount__OperationStatus query_account(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiAccount__OperationStatus query_account(const struct IndexerServiceFFI *indexer, FfiAccountId account_id); /** - * Query the trasnaction by hash from indexer. + * Query the transaction by hash from indexer. * * # Arguments * @@ -562,10 +600,8 @@ struct PointerResult_FfiAccount__OperationStatus query_account(const struct Runt * * The caller must ensure that: * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. - * - `runtime` is a valid pointer to a [`Runtime`] instance. */ -struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct IndexerServiceFFI *indexer, FfiHashType hash); /** @@ -585,10 +621,8 @@ struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transact * * The caller must ensure that: * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. - * - `runtime` is a valid pointer to a [`Runtime`] instance. */ -struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct IndexerServiceFFI *indexer, struct FfiOption_u64 before, uint64_t limit); @@ -604,16 +638,14 @@ struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const s * * # Returns * - * A `PointerResult, OperationStatus>` indicating success or failure. + * A `PointerResult, OperationStatus>` indicating success or failure. * * # Safety * * The caller must ensure that: * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. - * - `runtime` is a valid pointer to a [`Runtime`] instance. */ -struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct Runtime *runtime, - const struct IndexerServiceFFI *indexer, +struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct IndexerServiceFFI *indexer, FfiAccountId account_id, uint64_t offset, uint64_t limit); @@ -621,9 +653,14 @@ struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transaction /** * Frees the resources associated with the given ffi account. * + * Takes ownership of the whole allocation produced by a `query_*` call: the + * outer `Box` (the `PointerResult.value` pointer) *and* its inner + * data buffer. Passing the struct by value previously freed only the inner + * buffer and leaked the outer box. + * * # Arguments * - * - `val`: An instance of `FfiAccount`. + * - `val`: The `*mut FfiAccount` returned in `PointerResult.value`. * * # Returns * @@ -632,12 +669,18 @@ struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transaction * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiAccount`. + * - `val` is a pointer to an `FfiAccount` produced by this library and not yet freed. */ -void free_ffi_account(struct FfiAccount val); +void free_ffi_account(struct FfiAccount *val); /** - * Frees the resources associated with the given ffi block. + * Frees the resources owned by an `FfiBlock` value. + * + * This frees the block's transaction bodies (the only heap-owning field); the + * header/status fields are `Copy`. It operates on the struct by value because + * it is an element-level helper, used both for the vector path + * ([`free_ffi_block_vec`]) and the optional path ([`free_ffi_block_opt`]) โ€” in + * neither case is an `FfiBlock` itself wrapped in its own outer box. * * # Arguments * @@ -650,16 +693,20 @@ void free_ffi_account(struct FfiAccount val); * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiBlock`. + * - `val` is a valid instance of `FfiBlock` produced by this library and not yet freed. */ void free_ffi_block(struct FfiBlock val); /** * Frees the resources associated with the given ffi block option. * + * Takes ownership of the whole allocation produced by a `query_*` call: the + * outer `Box` (the `PointerResult.value` pointer), the inner + * `Box` (when present), and that block's transaction bodies. + * * # Arguments * - * - `val`: An instance of `FfiBlockOpt`. + * - `val`: The `*mut FfiBlockOpt` returned in `PointerResult.value`. * * # Returns * @@ -668,16 +715,20 @@ void free_ffi_block(struct FfiBlock val); * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiBlockOpt`. + * - `val` is a pointer to an `FfiBlockOpt` produced by this library and not yet freed. */ -void free_ffi_block_opt(FfiBlockOpt val); +void free_ffi_block_opt(FfiBlockOpt *val); /** * Frees the resources associated with the given ffi block vector. * + * Takes ownership of the whole allocation produced by a `query_*` call: the + * outer `Box>` (the `PointerResult.value` pointer), the + * vector's backing buffer, and every block within it. + * * # Arguments * - * - `val`: An instance of `FfiVec`. + * - `val`: The `*mut FfiVec` returned in `PointerResult.value`. * * # Returns * @@ -686,9 +737,9 @@ void free_ffi_block_opt(FfiBlockOpt val); * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiVec`. + * - `val` is a pointer to an `FfiVec` produced by this library and not yet freed. */ -void free_ffi_block_vec(struct FfiVec_FfiBlock val); +void free_ffi_block_vec(struct FfiVec_FfiBlock *val); /** * Frees the resources associated with the given ffi transaction. @@ -711,9 +762,13 @@ void free_ffi_transaction(struct FfiTransaction val); /** * Frees the resources associated with the given ffi transaction option. * + * Takes ownership of the whole allocation produced by a `query_*` call: the + * outer `Box>` (the `PointerResult.value` pointer), + * the inner `Box` (when present), and its body. + * * # Arguments * - * - `val`: An instance of `FfiOption`. + * - `val`: The `*mut FfiOption` returned in `PointerResult.value`. * * # Returns * @@ -722,16 +777,21 @@ void free_ffi_transaction(struct FfiTransaction val); * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiOption`. + * - `val` is a pointer to an `FfiOption` produced by this library and not yet + * freed. */ -void free_ffi_transaction_opt(struct FfiOption_FfiTransaction val); +void free_ffi_transaction_opt(struct FfiOption_FfiTransaction *val); /** * Frees the resources associated with the given vector of ffi transactions. * + * Takes ownership of the whole allocation produced by a `query_*` call: the + * outer `Box>` (the `PointerResult.value` pointer), the + * vector's backing buffer, and every transaction within it. + * * # Arguments * - * - `val`: An instance of `FfiVec`. + * - `val`: The `*mut FfiVec` returned in `PointerResult.value`. * * # Returns * @@ -740,10 +800,14 @@ void free_ffi_transaction_opt(struct FfiOption_FfiTransaction val); * # Safety * * The caller must ensure that: - * - `val` is a valid instance of `FfiVec`. + * - `val` is a pointer to an `FfiVec` produced by this library and not yet freed. */ -void free_ffi_transaction_vec(struct FfiVec_FfiTransaction val); +void free_ffi_transaction_vec(struct FfiVec_FfiTransaction *val); bool is_ok(const enum OperationStatus *self); bool is_error(const enum OperationStatus *self); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus diff --git a/lez/indexer/ffi/src/api/client.rs b/lez/indexer/ffi/src/api/client.rs deleted file mode 100644 index 825a57de..00000000 --- a/lez/indexer/ffi/src/api/client.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::net::SocketAddr; - -use url::Url; - -use crate::OperationStatus; - -#[derive(Debug, Clone, Copy)] -pub enum UrlProtocol { - Http, - Ws, -} - -impl std::fmt::Display for UrlProtocol { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Http => write!(f, "http"), - Self::Ws => write!(f, "ws"), - } - } -} - -pub(crate) fn addr_to_url(protocol: UrlProtocol, addr: SocketAddr) -> Result { - // Convert 0.0.0.0 to 127.0.0.1 for client connections - // When binding to port 0, the server binds to 0.0.0.0: - // but clients need to connect to 127.0.0.1: to work reliably - let url_string = if addr.ip().is_unspecified() { - format!("{protocol}://127.0.0.1:{}", addr.port()) - } else { - format!("{protocol}://{addr}") - }; - - url_string.parse().map_err(|e| { - log::error!("Could not parse indexer url: {e}"); - OperationStatus::InitializationError - }) -} diff --git a/lez/indexer/ffi/src/api/lifecycle.rs b/lez/indexer/ffi/src/api/lifecycle.rs index d124901f..f668f3ee 100644 --- a/lez/indexer/ffi/src/api/lifecycle.rs +++ b/lez/indexer/ffi/src/api/lifecycle.rs @@ -1,14 +1,9 @@ use std::{ffi::c_char, path::PathBuf}; -use crate::{ - IndexerServiceFFI, Runtime, - api::{ - PointerResult, - client::{UrlProtocol, addr_to_url}, - }, - client::{IndexerClient, IndexerClientTrait as _}, - errors::OperationStatus, -}; +use futures::StreamExt as _; +use indexer_core::{IndexerCore, config::IndexerConfig}; + +use crate::{IndexerServiceFFI, Runtime, api::PointerResult, errors::OperationStatus}; pub type InitializedIndexerServiceFFIResult = PointerResult; @@ -17,8 +12,12 @@ pub type InitializedIndexerServiceFFIResult = PointerResult InitializedIndexerServiceFFIResult { - // SAFETY: The caller must ensure the validness of the `runtime` and `config_path` pointers. - unsafe { setup_indexer(runtime, config_path, port) }.map_or_else( + // SAFETY: The caller must ensure the validness of the pointer arguments. + unsafe { setup_indexer(runtime, config_path, storage_dir) }.map_or_else( InitializedIndexerServiceFFIResult::from_error, InitializedIndexerServiceFFIResult::from_value, ) } -/// Creates a new [`tokio::runtime::Runtime`]. -#[unsafe(no_mangle)] -pub extern "C" fn new_runtime() -> PointerResult { - Runtime::new().map_or_else( - |_e| PointerResult::from_error(OperationStatus::InitializationError), - PointerResult::from_value, - ) -} - /// Initializes and starts an indexer based on the provided /// configuration file path. /// /// # Arguments /// +/// - `runtime`: A runtime for the indexer to run on, or null to create and own one. /// - `config_path`: A pointer to a string representing the path to the configuration file. -/// - `port`: Number representing a port, on which indexers RPC will start. +/// - `storage_dir`: A pointer to a string naming the storage directory, or null/empty for `.`. /// /// # Returns /// @@ -66,12 +58,13 @@ pub extern "C" fn new_runtime() -> PointerResult { /// /// # Safety /// The caller must ensure that: -/// - `runtime` is a valid pointer to a `tokio::runtime::Runtime` instance. +/// - `runtime` is either null or a valid pointer to a [`Runtime`] that outlives the indexer. /// - `config_path` is a valid pointer to a null-terminated C string. +/// - `storage_dir` is either null or a valid pointer to a null-terminated C string. unsafe fn setup_indexer( runtime: *const Runtime, config_path: *const c_char, - port: u16, + storage_dir: *const c_char, ) -> Result { let user_config_path = PathBuf::from( unsafe { std::ffi::CStr::from_ptr(config_path) } @@ -81,31 +74,64 @@ unsafe fn setup_indexer( OperationStatus::InitializationError })?, ); - let config = indexer_service::IndexerConfig::from_path(&user_config_path).map_err(|e| { + let config = IndexerConfig::from_path(&user_config_path).map_err(|e| { log::error!("Failed to read config: {e}"); OperationStatus::InitializationError })?; - // SAFETY: The caller must ensure that `runtime` is a valid pointer to a - // `tokio::runtime::Runtime` instance. - let runtime = unsafe { &*runtime }; + // The host owns where state lives. An empty/null `storage_dir` falls back to + // the current directory (matches the standalone service's `--data-dir` + // default), but a Logos module passes its instance persistence path. + let storage_dir = if storage_dir.is_null() { + PathBuf::from(".") + } else { + let storage_dir = unsafe { std::ffi::CStr::from_ptr(storage_dir) } + .to_str() + .map_err(|e| { + log::error!("Could not convert the storage dir to string: {e}"); + OperationStatus::InitializationError + })?; + if storage_dir.is_empty() { + PathBuf::from(".") + } else { + PathBuf::from(storage_dir) + } + }; - let indexer_handle = runtime - .block_on(indexer_service::run_server(config, port)) - .map_err(|e| { - log::error!("Could not start indexer service: {e}"); + // Use the caller's runtime if one was supplied, otherwise create (and own) + // our own. The `Runtime` wrapper drops the underlying tokio runtime only + // when we own it; a borrowed one is left to its external owner. + let runtime = if runtime.is_null() { + Runtime::new().map_err(|e| { + log::error!("Could not create tokio runtime: {e}"); OperationStatus::InitializationError - })?; + })? + } else { + // SAFETY: the caller guarantees `runtime` is valid and outlives the indexer. + let caller = unsafe { &*runtime }; + unsafe { Runtime::from_borrowed(caller.as_ref()) } + }; - let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())?; - let indexer_client = runtime - .block_on(IndexerClient::new(&indexer_url)) - .map_err(|e| { - log::error!("Could not start indexer client: {e}"); - OperationStatus::InitializationError - })?; + let core = IndexerCore::new(config, &storage_dir).map_err(|e| { + log::error!("Could not initialize indexer core: {e}"); + OperationStatus::InitializationError + })?; - Ok(IndexerServiceFFI::new(indexer_handle, indexer_client)) + // The block stream writes each parsed block into the store as a side effect + // of being polled, so we spawn a task that simply drains it. There are no + // subscribers โ€” queries read the store directly via `core()`. + let ingest_core = core.clone(); + let ingest_handle = runtime.spawn(async move { + let mut block_stream = std::pin::pin!(ingest_core.subscribe_parse_block_stream()); + while let Some(result) = block_stream.next().await { + if let Err(e) = result { + log::error!("Indexer ingestion error: {e:#}"); + } + } + log::warn!("Indexer block stream ended"); + }); + + Ok(IndexerServiceFFI::new(core, ingest_handle, runtime)) } /// Stops and frees the resources associated with the given indexer service. diff --git a/lez/indexer/ffi/src/api/logging.rs b/lez/indexer/ffi/src/api/logging.rs new file mode 100644 index 00000000..06c41688 --- /dev/null +++ b/lez/indexer/ffi/src/api/logging.rs @@ -0,0 +1,32 @@ +use std::ffi::{CStr, c_char}; + +use log::LevelFilter; + +/// Initializes logging for the indexer at `level`. +/// +/// - `level` is a null-terminated string (`off`/`error`/`warn`/`info`/`debug`/ `trace`, +/// case-insensitive); null or unparseable falls back to `info`. +/// +/// Only the `indexer_ffi` and `indexer_core` targets are enabled! +/// +/// # Safety +/// - `level` must be a valid null-terminated C string, or null. +/// - First call to this function wins; subsequent calls are no-ops. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn init_logger(level: *const c_char) { + let level = if level.is_null() { + LevelFilter::Info + } else { + unsafe { CStr::from_ptr(level) } + .to_str() + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(LevelFilter::Info) + }; + + let _dontcare = env_logger::Builder::new() + .filter_level(LevelFilter::Off) + .filter_module("indexer_ffi", level) + .filter_module("indexer_core", level) + .try_init(); +} diff --git a/lez/indexer/ffi/src/api/mod.rs b/lez/indexer/ffi/src/api/mod.rs index ea2b91d7..2e4be797 100644 --- a/lez/indexer/ffi/src/api/mod.rs +++ b/lez/indexer/ffi/src/api/mod.rs @@ -1,7 +1,7 @@ pub use result::PointerResult; -pub mod client; pub mod lifecycle; +pub mod logging; pub mod memory; pub mod query; pub mod result; diff --git a/lez/indexer/ffi/src/api/query.rs b/lez/indexer/ffi/src/api/query.rs index f10de598..1943f6d4 100644 --- a/lez/indexer/ffi/src/api/query.rs +++ b/lez/indexer/ffi/src/api/query.rs @@ -1,8 +1,9 @@ -use indexer_service_protocol::{AccountId, HashType}; -use indexer_service_rpc::RpcClient as _; +use std::ffi::{CString, c_char}; + +use indexer_service_protocol::AccountId; use crate::{ - IndexerServiceFFI, Runtime, + IndexerServiceFFI, api::{ PointerResult, types::{ @@ -15,6 +16,45 @@ use crate::{ errors::OperationStatus, }; +/// Result of [`query_last_block`], returned **inline** (no heap allocation, so +/// there is no corresponding `free_*` to call). +/// +/// `block_id` is only meaningful when `error` is `Ok` *and* `is_some` is +/// `true`. An `Ok` result with `is_some == false` means the indexer has no +/// finalized block yet (an empty chain) โ€” which is distinct from an error. +#[repr(C)] +pub struct LastBlockIdResult { + pub block_id: u64, + pub is_some: bool, + pub error: OperationStatus, +} + +impl LastBlockIdResult { + const fn error(error: OperationStatus) -> Self { + Self { + block_id: 0, + is_some: false, + error, + } + } + + const fn none() -> Self { + Self { + block_id: 0, + is_some: false, + error: OperationStatus::Ok, + } + } + + const fn some(block_id: u64) -> Self { + Self { + block_id, + is_some: true, + error: OperationStatus::Ok, + } + } +} + /// Query the last block id from indexer. /// /// # Arguments @@ -23,34 +63,79 @@ use crate::{ /// /// # Returns /// -/// A `PointerResult, OperationStatus>` indicating success or failure. +/// A [`LastBlockIdResult`] indicating success or failure. The block id is +/// returned inline; nothing needs to be freed. /// /// # Safety /// /// The caller must ensure that: -/// - `runtime` is a valid pointer to a [`Runtime`] instance. /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] -pub unsafe extern "C" fn query_last_block( - runtime: *const Runtime, - indexer: *const IndexerServiceFFI, -) -> PointerResult, OperationStatus> { +pub unsafe extern "C" fn query_last_block(indexer: *const IndexerServiceFFI) -> LastBlockIdResult { if indexer.is_null() { log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); - return PointerResult::from_error(OperationStatus::NullPointer); + return LastBlockIdResult::error(OperationStatus::NullPointer); } let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; + indexer.core().store.get_last_block_id().map_or_else( + |e| { + log::error!("Failed to query last block id: {e:#}"); + LastBlockIdResult::error(OperationStatus::ClientError) + }, + |opt| opt.map_or_else(LastBlockIdResult::none, LastBlockIdResult::some), + ) +} - runtime - .block_on(client.get_last_finalized_block_id()) - .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), - PointerResult::from_value, - ) +/// Query the indexer's current sync status as a JSON C-string. +/// +/// The JSON schema is owned by `indexer_core` (`IndexerStatus`): an object with +/// `state` (`starting`/`syncing`/`caught_up`/`error`), `indexedBlockId`, and +/// `lastError`. Lets a client distinguish "still catching up" from "something +/// went wrong". +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. +/// +/// # Returns +/// +/// A heap-allocated, null-terminated JSON string that the caller MUST free with +/// `free_cstring`. Returns null on error (null `indexer` pointer or a +/// serialization failure). +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_status(indexer: *const IndexerServiceFFI) -> *mut c_char { + if indexer.is_null() { + log::error!( + "Attempted to query status on a null indexer pointer. This is a bug. Aborting." + ); + return std::ptr::null_mut(); + } + + let indexer = unsafe { &*indexer }; + let status = indexer.core().status(); + + let json = match serde_json::to_string(&status) { + Ok(json) => json, + Err(e) => { + log::error!("Failed to serialize indexer status: {e}"); + return std::ptr::null_mut(); + } + }; + + CString::new(json).map_or_else( + |e| { + log::error!("Indexer status JSON contained an interior nul byte: {e}"); + std::ptr::null_mut() + }, + CString::into_raw, + ) } /// Query the block by id from indexer. @@ -67,11 +152,9 @@ pub unsafe extern "C" fn query_last_block( /// # Safety /// /// The caller must ensure that: -/// - `runtime` is a valid pointer to a [`Runtime`] instance. /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_block( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, block_id: FfiBlockId, ) -> PointerResult { @@ -82,24 +165,23 @@ pub unsafe extern "C" fn query_block( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; + indexer.core().store.get_block_at_id(block_id).map_or_else( + |e| { + log::error!("Failed to query block by id: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, + |block_opt| { + let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| { + let block: indexer_service_protocol::Block = block.into(); + FfiBlockOpt::from_value(block.into()) + }); - runtime - .block_on(client.get_block_by_id(block_id)) - .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), - |block_opt| { - let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| { - FfiBlockOpt::from_value(block.into()) - }); - - PointerResult::from_value(block_ffi) - }, - ) + PointerResult::from_value(block_ffi) + }, + ) } -/// Query the block by id from indexer. +/// Query the block by hash from indexer. /// /// # Arguments /// @@ -113,11 +195,9 @@ pub unsafe extern "C" fn query_block( /// # Safety /// /// The caller must ensure that: -/// - `runtime` is a valid pointer to a [`Runtime`] instance. /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_block_by_hash( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, hash: FfiHashType, ) -> PointerResult { @@ -128,15 +208,18 @@ pub unsafe extern "C" fn query_block_by_hash( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - - runtime - .block_on(client.get_block_by_hash(HashType(hash.data))) + indexer + .core() + .store + .get_block_by_hash(hash.data) .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), + |e| { + log::error!("Failed to query block by hash: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, |block_opt| { let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| { + let block: indexer_service_protocol::Block = block.into(); FfiBlockOpt::from_value(block.into()) }); @@ -159,11 +242,9 @@ pub unsafe extern "C" fn query_block_by_hash( /// # Safety /// /// The caller must ensure that: -/// - `runtime` is a valid pointer to a [`Runtime`] instance. /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_account( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, account_id: FfiAccountId, ) -> PointerResult { @@ -174,23 +255,29 @@ pub unsafe extern "C" fn query_account( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - - runtime - .block_on(client.get_account(AccountId { - value: account_id.data, - })) + // `account_current_state` is the only async store call; drive it on the + // runtime the indexer was started on. + let account_id = AccountId { + value: account_id.data, + }; + indexer + .runtime() + .block_on( + indexer + .core() + .store + .account_current_state(&account_id.into()), + ) .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), - |acc| { - let acc_lee: lee::Account = acc.try_into().expect("Source is in blocks, must fit"); - PointerResult::from_value(acc_lee.into()) + |e| { + log::error!("Failed to query account: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) }, + |account| PointerResult::from_value(account.into()), ) } -/// Query the trasnaction by hash from indexer. +/// Query the transaction by hash from indexer. /// /// # Arguments /// @@ -205,10 +292,8 @@ pub unsafe extern "C" fn query_account( /// /// The caller must ensure that: /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. -/// - `runtime` is a valid pointer to a [`Runtime`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_transaction( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, hash: FfiHashType, ) -> PointerResult, OperationStatus> { @@ -219,15 +304,18 @@ pub unsafe extern "C" fn query_transaction( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - - runtime - .block_on(client.get_transaction(HashType(hash.data))) + indexer + .core() + .store + .get_transaction_by_hash(hash.data) .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), + |e| { + log::error!("Failed to query transaction: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, |tx_opt| { let tx_ffi = tx_opt.map_or_else(FfiOption::::from_none, |tx| { + let tx: indexer_service_protocol::Transaction = tx.into(); FfiOption::::from_value(tx.into()) }); @@ -252,10 +340,8 @@ pub unsafe extern "C" fn query_transaction( /// /// The caller must ensure that: /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. -/// - `runtime` is a valid pointer to a [`Runtime`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_block_vec( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, before: FfiOption, limit: u64, @@ -267,21 +353,26 @@ pub unsafe extern "C" fn query_block_vec( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - let before_std = before.is_some.then(|| unsafe { *before.value }); - runtime - .block_on(client.get_blocks(before_std, limit)) + indexer + .core() + .store + .get_block_batch(before_std, limit) .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), + |e| { + log::error!("Failed to query block batch: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, |block_vec| { PointerResult::from_value( block_vec .into_iter() - .map(Into::into) - .collect::>() + .map(|block| { + let block: indexer_service_protocol::Block = block.into(); + block.into() + }) + .collect::>() .into(), ) }, @@ -299,16 +390,14 @@ pub unsafe extern "C" fn query_block_vec( /// /// # Returns /// -/// A `PointerResult, OperationStatus>` indicating success or failure. +/// A `PointerResult, OperationStatus>` indicating success or failure. /// /// # Safety /// /// The caller must ensure that: /// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. -/// - `runtime` is a valid pointer to a [`Runtime`] instance. #[unsafe(no_mangle)] pub unsafe extern "C" fn query_transactions_by_account( - runtime: *const Runtime, indexer: *const IndexerServiceFFI, account_id: FfiAccountId, offset: u64, @@ -321,25 +410,24 @@ pub unsafe extern "C" fn query_transactions_by_account( let indexer = unsafe { &*indexer }; - let client = indexer.client(); - let runtime = unsafe { &*runtime }; - - runtime - .block_on(client.get_transactions_by_account( - AccountId { - value: account_id.data, - }, - offset, - limit, - )) + indexer + .core() + .store + .get_transactions_by_account(account_id.data, offset, limit) .map_or_else( - |_| PointerResult::from_error(OperationStatus::ClientError), + |e| { + log::error!("Failed to query transactions by account: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, |tx_vec| { PointerResult::from_value( tx_vec .into_iter() - .map(Into::into) - .collect::>() + .map(|tx| { + let tx: indexer_service_protocol::Transaction = tx.into(); + tx.into() + }) + .collect::>() .into(), ) }, diff --git a/lez/indexer/ffi/src/api/types/account.rs b/lez/indexer/ffi/src/api/types/account.rs index 2309b84b..f2eb8e58 100644 --- a/lez/indexer/ffi/src/api/types/account.rs +++ b/lez/indexer/ffi/src/api/types/account.rs @@ -100,9 +100,14 @@ impl From<&FfiAccount> for indexer_service_protocol::Account { /// Frees the resources associated with the given ffi account. /// +/// Takes ownership of the whole allocation produced by a `query_*` call: the +/// outer `Box` (the `PointerResult.value` pointer) *and* its inner +/// data buffer. Passing the struct by value previously freed only the inner +/// buffer and leaked the outer box. +/// /// # Arguments /// -/// - `val`: An instance of `FfiAccount`. +/// - `val`: The `*mut FfiAccount` returned in `PointerResult.value`. /// /// # Returns /// @@ -111,9 +116,15 @@ impl From<&FfiAccount> for indexer_service_protocol::Account { /// # Safety /// /// The caller must ensure that: -/// - `val` is a valid instance of `FfiAccount`. +/// - `val` is a pointer to an `FfiAccount` produced by this library and not yet freed. #[unsafe(no_mangle)] -pub unsafe extern "C" fn free_ffi_account(val: FfiAccount) { - let orig_val: indexer_service_protocol::Account = val.into(); +pub unsafe extern "C" fn free_ffi_account(val: *mut FfiAccount) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + // Reclaim the outer box, then convert to drop the inner data buffer. + let boxed = unsafe { Box::from_raw(val) }; + let orig_val: indexer_service_protocol::Account = (*boxed).into(); drop(orig_val); } diff --git a/lez/indexer/ffi/src/api/types/block.rs b/lez/indexer/ffi/src/api/types/block.rs index e7ae0760..b652a7fe 100644 --- a/lez/indexer/ffi/src/api/types/block.rs +++ b/lez/indexer/ffi/src/api/types/block.rs @@ -2,7 +2,7 @@ use indexer_service_protocol::{BedrockStatus, Block, BlockHeader, HashType, Sign use crate::api::types::{ FfiBlockId, FfiHashType, FfiOption, FfiSignature, FfiTimestamp, FfiVec, - transaction::free_ffi_transaction_vec, vectors::FfiBlockBody, + transaction::free_transaction_vec_value, vectors::FfiBlockBody, }; #[repr(C)] @@ -91,7 +91,13 @@ impl From for BedrockStatus { } } -/// Frees the resources associated with the given ffi block. +/// Frees the resources owned by an `FfiBlock` value. +/// +/// This frees the block's transaction bodies (the only heap-owning field); the +/// header/status fields are `Copy`. It operates on the struct by value because +/// it is an element-level helper, used both for the vector path +/// ([`free_ffi_block_vec`]) and the optional path ([`free_ffi_block_opt`]) โ€” in +/// neither case is an `FfiBlock` itself wrapped in its own outer box. /// /// # Arguments /// @@ -104,7 +110,7 @@ impl From for BedrockStatus { /// # Safety /// /// The caller must ensure that: -/// - `val` is a valid instance of `FfiBlock`. +/// - `val` is a valid instance of `FfiBlock` produced by this library and not yet freed. #[unsafe(no_mangle)] pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) { // We don't really need all the casts, but just in case @@ -121,16 +127,18 @@ pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) { #[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")] let _: BedrockStatus = val.bedrock_status.into(); - unsafe { - free_ffi_transaction_vec(ffi_tx_ffi_vec); - }; + free_transaction_vec_value(ffi_tx_ffi_vec); } /// Frees the resources associated with the given ffi block option. /// +/// Takes ownership of the whole allocation produced by a `query_*` call: the +/// outer `Box` (the `PointerResult.value` pointer), the inner +/// `Box` (when present), and that block's transaction bodies. +/// /// # Arguments /// -/// - `val`: An instance of `FfiBlockOpt`. +/// - `val`: The `*mut FfiBlockOpt` returned in `PointerResult.value`. /// /// # Returns /// @@ -139,37 +147,32 @@ pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) { /// # Safety /// /// The caller must ensure that: -/// - `val` is a valid instance of `FfiBlockOpt`. +/// - `val` is a pointer to an `FfiBlockOpt` produced by this library and not yet freed. #[unsafe(no_mangle)] -pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) { - if val.is_some { - let value = unsafe { Box::from_raw(val.value) }; - - // We don't really need all the casts, but just in case - // All except `ffi_tx_ffi_vec` is Copy types, so no need for Drop - let _ = BlockHeader { - block_id: value.header.block_id, - prev_block_hash: HashType(value.header.prev_block_hash.data), - hash: HashType(value.header.hash.data), - timestamp: value.header.timestamp, - signature: Signature(value.header.signature.data), - }; - let ffi_tx_ffi_vec = value.body; - - #[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")] - let _: BedrockStatus = value.bedrock_status.into(); - +pub unsafe extern "C" fn free_ffi_block_opt(val: *mut FfiBlockOpt) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + // Reclaim the outer box, then the inner block box (if any). + let opt = unsafe { Box::from_raw(val) }; + if opt.is_some { + let block = unsafe { Box::from_raw(opt.value) }; unsafe { - free_ffi_transaction_vec(ffi_tx_ffi_vec); - }; + free_ffi_block(*block); + } } } /// Frees the resources associated with the given ffi block vector. /// +/// Takes ownership of the whole allocation produced by a `query_*` call: the +/// outer `Box>` (the `PointerResult.value` pointer), the +/// vector's backing buffer, and every block within it. +/// /// # Arguments /// -/// - `val`: An instance of `FfiVec`. +/// - `val`: The `*mut FfiVec` returned in `PointerResult.value`. /// /// # Returns /// @@ -178,10 +181,16 @@ pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) { /// # Safety /// /// The caller must ensure that: -/// - `val` is a valid instance of `FfiVec`. +/// - `val` is a pointer to an `FfiVec` produced by this library and not yet freed. #[unsafe(no_mangle)] -pub unsafe extern "C" fn free_ffi_block_vec(val: FfiVec) { - let ffi_block_std_vec: Vec<_> = val.into(); +pub unsafe extern "C" fn free_ffi_block_vec(val: *mut FfiVec) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + // Reclaim the outer box, then the backing buffer and each block. + let boxed = unsafe { Box::from_raw(val) }; + let ffi_block_std_vec: Vec<_> = (*boxed).into(); for block in ffi_block_std_vec { unsafe { free_ffi_block(block); diff --git a/lez/indexer/ffi/src/api/types/transaction.rs b/lez/indexer/ffi/src/api/types/transaction.rs index ca733ed3..d5cb9035 100644 --- a/lez/indexer/ffi/src/api/types/transaction.rs +++ b/lez/indexer/ffi/src/api/types/transaction.rs @@ -463,9 +463,13 @@ pub unsafe extern "C" fn free_ffi_transaction(val: FfiTransaction) { /// Frees the resources associated with the given ffi transaction option. /// +/// Takes ownership of the whole allocation produced by a `query_*` call: the +/// outer `Box>` (the `PointerResult.value` pointer), +/// the inner `Box` (when present), and its body. +/// /// # Arguments /// -/// - `val`: An instance of `FfiOption`. +/// - `val`: The `*mut FfiOption` returned in `PointerResult.value`. /// /// # Returns /// @@ -474,48 +478,32 @@ pub unsafe extern "C" fn free_ffi_transaction(val: FfiTransaction) { /// # Safety /// /// The caller must ensure that: -/// - `val` is a valid instance of `FfiOption`. +/// - `val` is a pointer to an `FfiOption` produced by this library and not yet +/// freed. #[unsafe(no_mangle)] -pub unsafe extern "C" fn free_ffi_transaction_opt(val: FfiOption) { - if val.is_some { - let value = unsafe { Box::from_raw(val.value) }; - - match value.kind { - FfiTransactionKind::Public => { - let body = unsafe { Box::from_raw(value.body.public_body) }; - let std_body: PublicTransaction = body.into(); - drop(std_body); - } - FfiTransactionKind::Private => { - let body = unsafe { Box::from_raw(value.body.private_body) }; - let std_body: PrivacyPreservingTransaction = body.into(); - drop(std_body); - } - FfiTransactionKind::ProgramDeploy => { - let body = unsafe { Box::from_raw(value.body.program_deployment_body) }; - let std_body: ProgramDeploymentTransaction = body.into(); - drop(std_body); - } +pub unsafe extern "C" fn free_ffi_transaction_opt(val: *mut FfiOption) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + // Reclaim the outer box, then the inner transaction box (if any). + let opt = unsafe { Box::from_raw(val) }; + if opt.is_some { + let tx = unsafe { Box::from_raw(opt.value) }; + unsafe { + free_ffi_transaction(*tx); } } } -/// Frees the resources associated with the given vector of ffi transactions. +/// Frees the resources owned by an `FfiVec` value (the backing +/// buffer and each transaction), without owning an outer box. /// -/// # Arguments -/// -/// - `val`: An instance of `FfiVec`. -/// -/// # Returns -/// -/// void. -/// -/// # Safety -/// -/// The caller must ensure that: -/// - `val` is a valid instance of `FfiVec`. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn free_ffi_transaction_vec(val: FfiVec) { +/// This is the element-level helper shared by the block free path +/// ([`crate::api::types::block::free_ffi_block`], whose body is a transaction +/// vector held by value) and the public [`free_ffi_transaction_vec`] entry +/// point (which first reclaims the outer box). +pub(crate) fn free_transaction_vec_value(val: FfiVec) { let ffi_tx_std_vec: Vec<_> = val.into(); for tx in ffi_tx_std_vec { unsafe { @@ -524,6 +512,35 @@ pub unsafe extern "C" fn free_ffi_transaction_vec(val: FfiVec) { } } +/// Frees the resources associated with the given vector of ffi transactions. +/// +/// Takes ownership of the whole allocation produced by a `query_*` call: the +/// outer `Box>` (the `PointerResult.value` pointer), the +/// vector's backing buffer, and every transaction within it. +/// +/// # Arguments +/// +/// - `val`: The `*mut FfiVec` returned in `PointerResult.value`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a pointer to an `FfiVec` produced by this library and not yet freed. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_transaction_vec(val: *mut FfiVec) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + // Reclaim the outer box, then the backing buffer and each transaction. + let boxed = unsafe { Box::from_raw(val) }; + free_transaction_vec_value(*boxed); +} + fn cast_validity_window(window: ValidityWindow) -> [u64; 2] { [ window.0.0.unwrap_or_default(), diff --git a/lez/indexer/ffi/src/client.rs b/lez/indexer/ffi/src/client.rs deleted file mode 100644 index f05b350e..00000000 --- a/lez/indexer/ffi/src/client.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::{ops::Deref, sync::Arc}; - -use anyhow::{Context as _, Result}; -use log::info; -pub use url::Url; - -pub trait IndexerClientTrait: Clone { - async fn new(indexer_url: &Url) -> Result; -} - -#[derive(Clone)] -pub struct IndexerClient(Arc); - -impl IndexerClientTrait for IndexerClient { - async fn new(indexer_url: &Url) -> Result { - info!("Connecting to Indexer at {indexer_url}"); - - let client = jsonrpsee::ws_client::WsClientBuilder::default() - .build(indexer_url) - .await - .context("Failed to create websocket client")?; - - Ok(Self(Arc::new(client))) - } -} - -impl Deref for IndexerClient { - type Target = jsonrpsee::ws_client::WsClient; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/lez/indexer/ffi/src/indexer.rs b/lez/indexer/ffi/src/indexer.rs index e8707697..0b6b874a 100644 --- a/lez/indexer/ffi/src/indexer.rs +++ b/lez/indexer/ffi/src/indexer.rs @@ -1,95 +1,66 @@ -use std::{ffi::c_void, net::SocketAddr}; +use std::ffi::c_void; -use indexer_service::IndexerHandle; +use indexer_core::IndexerCore; +use tokio::task::JoinHandle; -use crate::client::IndexerClient; +use crate::Runtime; +/// FFI-owned indexer. +/// +/// - An [`IndexerCore`] used to answer queries +/// - The background task [`JoinHandle`] that drives ingestion (consuming the block stream so the +/// store stays populated) +/// - The [`Runtime`] used to run async queries against the store (either owned or borrowed), +/// already FFI-safe. #[repr(C)] pub struct IndexerServiceFFI { - indexer_handle: *mut c_void, - indexer_client: *mut c_void, + core: *mut c_void, + ingest_handle: *mut c_void, + runtime: Runtime, } impl IndexerServiceFFI { #[must_use] - pub fn new( - indexer_handle: indexer_service::IndexerHandle, - indexer_client: IndexerClient, - ) -> Self { + pub fn new(core: IndexerCore, ingest_handle: JoinHandle<()>, runtime: Runtime) -> Self { Self { - // Box the complex types and convert to opaque pointers - indexer_handle: Box::into_raw(Box::new(indexer_handle)).cast::(), - indexer_client: Box::into_raw(Box::new(indexer_client)).cast::(), + core: Box::into_raw(Box::new(core)).cast::(), + ingest_handle: Box::into_raw(Box::new(ingest_handle)).cast::(), + runtime, } } - /// Helper to take ownership back. + /// Borrow the [`IndexerCore`] to run a query against its store. #[must_use] - pub fn into_parts(mut self) -> (Box, Box) { - let Self { - indexer_handle, - indexer_client, - } = &mut self; - - let indexer_handle_boxed = unsafe { Box::from_raw(indexer_handle.cast::()) }; - let indexer_client_boxed = unsafe { Box::from_raw(indexer_client.cast::()) }; - - // Assigning nulls to prevent double free on drop, since ownership is transferred to caller - *indexer_handle = std::ptr::null_mut(); - *indexer_client = std::ptr::null_mut(); - - (indexer_handle_boxed, indexer_client_boxed) - } - - /// Helper to get indexer handle addr. - #[must_use] - pub const fn addr(&self) -> SocketAddr { - let indexer_handle = unsafe { - self.indexer_handle - .cast::() - .as_ref() - .expect("Indexer Handle must be non-null pointer") - }; - - indexer_handle.addr() - } - - /// Helper to get indexer handle ref. - #[must_use] - pub const fn handle(&self) -> &IndexerHandle { + pub const fn core(&self) -> &IndexerCore { unsafe { - self.indexer_handle - .cast::() + self.core + .cast::() .as_ref() - .expect("Indexer Handle must be non-null pointer") + .expect("IndexerCore must be a non-null pointer") } } - /// Helper to get indexer client ref. + /// Borrow the runtime to `block_on` an async store query. #[must_use] - pub const fn client(&self) -> &IndexerClient { - unsafe { - self.indexer_client - .cast::() - .as_ref() - .expect("Indexer Client must be non-null pointer") - } + pub const fn runtime(&self) -> &Runtime { + &self.runtime } } -// Implement Drop to prevent memory leaks impl Drop for IndexerServiceFFI { fn drop(&mut self) { - let Self { - indexer_handle, - indexer_client, - } = self; + if !self.ingest_handle.is_null() { + let handle = unsafe { Box::from_raw(self.ingest_handle.cast::>()) }; + // stop the background ingestion task before tearing down the core. + handle.abort(); + drop(handle); + } + if !self.core.is_null() { + drop(unsafe { Box::from_raw(self.core.cast::()) }); + } - if !indexer_handle.is_null() { - drop(unsafe { Box::from_raw(indexer_handle.cast::()) }); - } - if !indexer_client.is_null() { - drop(unsafe { Box::from_raw(indexer_client.cast::()) }); - } + // `runtime` field is dropped automatically on return here: + // - if runtime was owned, it is shutdown at this point + // - if it was borrowed, it continues to live within the external owner } } diff --git a/lez/indexer/ffi/src/lib.rs b/lez/indexer/ffi/src/lib.rs index 9e34b111..0ca197c7 100644 --- a/lez/indexer/ffi/src/lib.rs +++ b/lez/indexer/ffi/src/lib.rs @@ -5,7 +5,6 @@ pub use indexer::IndexerServiceFFI; pub use runtime::Runtime; pub mod api; -mod client; mod errors; mod indexer; mod runtime; diff --git a/lez/indexer/service/Dockerfile b/lez/indexer/service/Dockerfile index 3da1eb7f..7499875e 100644 --- a/lez/indexer/service/Dockerfile +++ b/lez/indexer/service/Dockerfile @@ -1,41 +1,5 @@ -# Chef stage - uses pre-built cargo-chef image -FROM lukemathwalker/cargo-chef:latest-rust-1.94.0-slim-trixie AS chef - -# Install build dependencies -RUN apt-get update && apt-get install -y \ - build-essential \ - pkg-config \ - libssl-dev \ - libclang-dev \ - clang \ - cmake \ - ninja-build \ - curl \ - git \ - && rm -rf /var/lib/apt/lists/* - -# Install r0vm -# Use quick install for x86-64 (risczero provides binaries only for this linux platform) -# Manual build for other platforms (including arm64 Linux) -RUN ARCH=$(uname -m); \ - if [ "$ARCH" = "x86_64" ]; then \ - echo "Using quick install for $ARCH"; \ - curl -L https://risczero.com/install | bash; \ - export PATH="/root/.cargo/bin:/root/.risc0/bin:${PATH}"; \ - rzup install; \ - else \ - echo "Using manual build for $ARCH"; \ - git clone --depth 1 --branch release-3.0 https://github.com/risc0/risc0.git; \ - git clone --depth 1 --branch r0.1.91.1 https://github.com/risc0/rust.git; \ - cd /risc0; \ - cargo install --path rzup; \ - rzup build --path /rust rust --verbose; \ - cargo install --path risc0/cargo-risczero; \ - fi -ENV PATH="/root/.cargo/bin:/root/.risc0/bin:${PATH}" -RUN cp "$(which r0vm)" /usr/local/bin/r0vm -RUN test -x /usr/local/bin/r0vm -RUN r0vm --version +# Chef stage +FROM risc0_base AS chef WORKDIR /indexer_service @@ -69,6 +33,11 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry/index \ # Runtime stage - minimal image FROM debian:trixie-slim +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + # Create non-root user for security RUN useradd -m -u 1000 -s /bin/bash indexer_service_user && \ mkdir -p /indexer_service /etc/indexer_service /var/lib/indexer_service && \ @@ -102,4 +71,4 @@ ENV RUST_LOG=info USER indexer_service_user WORKDIR /indexer_service -CMD ["indexer_service", "/etc/indexer_service/indexer_config.json"] +CMD ["indexer_service", "/etc/indexer_service/indexer_config.json", "--data-dir", "/var/lib/indexer_service"] diff --git a/lez/indexer/service/configs/indexer_config.json b/lez/indexer/service/configs/debug/indexer_config.json similarity index 73% rename from lez/indexer/service/configs/indexer_config.json rename to lez/indexer/service/configs/debug/indexer_config.json index f6a0e07c..85227700 100644 --- a/lez/indexer/service/configs/indexer_config.json +++ b/lez/indexer/service/configs/debug/indexer_config.json @@ -1,8 +1,7 @@ { - "home": ".", "consensus_info_polling_interval": "1s", "bedrock_config": { - "addr": "http://localhost:8080" + "addr": "http://localhost:18080" }, "channel_id": "0101010101010101010101010101010101010101010101010101010101010101" -} \ No newline at end of file +} diff --git a/lez/indexer/service/configs/docker/indexer_config.json b/lez/indexer/service/configs/docker/indexer_config.json new file mode 100644 index 00000000..f083ca27 --- /dev/null +++ b/lez/indexer/service/configs/docker/indexer_config.json @@ -0,0 +1,7 @@ +{ + "consensus_info_polling_interval": "1s", + "bedrock_config": { + "addr": "http://host.docker.internal:18080" + }, + "channel_id": "0101010101010101010101010101010101010101010101010101010101010101" +} diff --git a/lez/indexer/service/docker-compose.yml b/lez/indexer/service/docker-compose.yml index e9189cfc..c32067de 100644 --- a/lez/indexer/service/docker-compose.yml +++ b/lez/indexer/service/docker-compose.yml @@ -1,15 +1,26 @@ services: + # Build-only: shared base image (toolchain + r0vm) referenced as the + # `risc0_base` named context below. It has no long-running command, so it + # only gets built โ€” it exits immediately if started. + risc0_base: + image: lez/risc0_base + build: + context: ../../.. + dockerfile: lez/docker/risc0-base.Dockerfile + indexer_service: image: lez/indexer_service build: - context: ../.. + context: ../../.. dockerfile: lez/indexer/service/Dockerfile + additional_contexts: + risc0_base: "service:risc0_base" container_name: indexer_service ports: - "8779:8779" volumes: # Mount configuration - - ./configs/indexer_config.json:/etc/indexer_service/indexer_config.json + - ./configs/docker/indexer_config.json:/etc/indexer_service/indexer_config.json # Mount data volume - indexer_data:/var/lib/indexer_service diff --git a/lez/indexer/service/src/lib.rs b/lez/indexer/service/src/lib.rs index b0a6e516..b1c57163 100644 --- a/lez/indexer/service/src/lib.rs +++ b/lez/indexer/service/src/lib.rs @@ -1,4 +1,4 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, path::Path}; use anyhow::{Context as _, Result}; pub use indexer_core::config::*; @@ -65,9 +65,13 @@ impl Drop for IndexerHandle { } } -pub async fn run_server(config: IndexerConfig, port: u16) -> Result { +pub async fn run_server( + config: IndexerConfig, + storage_dir: &Path, + port: u16, +) -> Result { #[cfg(feature = "mock-responses")] - let _ = config; + let _ = (config, storage_dir); let server = Server::builder() .build(SocketAddr::from(([0, 0, 0, 0], port))) @@ -82,8 +86,8 @@ pub async fn run_server(config: IndexerConfig, port: u16) -> Result Result<()> { env_logger::init(); - let Args { config_path, port } = Args::parse(); + let Args { + config_path, + port, + data_dir, + } = Args::parse(); let cancellation_token = listen_for_shutdown_signal(); let config = indexer_service::IndexerConfig::from_path(&config_path)?; - let indexer_handle = indexer_service::run_server(config, port).await?; + let indexer_handle = indexer_service::run_server(config, data_dir.as_path(), port).await?; tokio::select! { () = cancellation_token.cancelled() => { diff --git a/lez/indexer/service/src/mock_service.rs b/lez/indexer/service/src/mock_service.rs index d9ab9484..7bf6c528 100644 --- a/lez/indexer/service/src/mock_service.rs +++ b/lez/indexer/service/src/mock_service.rs @@ -330,6 +330,76 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { } } +fn mock_public_tx( + tx_hash: HashType, + block_id: BlockId, + tx_idx: u64, + account_ids: &[AccountId], +) -> Transaction { + Transaction::Public(PublicTransaction { + hash: tx_hash, + message: PublicMessage { + program_id: ProgramId([1_u32; 8]), + account_ids: vec![ + account_ids[tx_idx as usize % account_ids.len()], + account_ids[(tx_idx as usize + 1) % account_ids.len()], + ], + nonces: vec![block_id as u128, (block_id + 1) as u128], + instruction_data: vec![1, 2, 3, 4], + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: None, + }, + }) +} + +fn mock_privacy_preserving_tx( + tx_hash: HashType, + block_id: BlockId, + tx_idx: u64, + account_ids: &[AccountId], +) -> Transaction { + Transaction::PrivacyPreserving(PrivacyPreservingTransaction { + hash: tx_hash, + message: PrivacyPreservingMessage { + public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]], + nonces: vec![block_id as u128], + public_post_states: vec![Account { + program_owner: ProgramId([1_u32; 8]), + balance: 500, + data: Data(vec![0xdd, 0xee]), + nonce: block_id as u128, + }], + encrypted_private_post_states: vec![EncryptedAccountData { + ciphertext: indexer_service_protocol::Ciphertext(vec![0x01, 0x02, 0x03, 0x04]), + epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), + view_tag: 42, + }], + new_commitments: vec![Commitment([block_id as u8; 32])], + new_nullifiers: vec![( + indexer_service_protocol::Nullifier([tx_idx as u8; 32]), + CommitmentSetDigest([0xff; 32]), + )], + block_validity_window: ValidityWindow((None, None)), + timestamp_validity_window: ValidityWindow((None, None)), + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: Some(indexer_service_protocol::Proof(vec![0; 32])), + }, + }) +} + +fn mock_program_deployment_tx(tx_hash: HashType) -> Transaction { + Transaction::ProgramDeployment(ProgramDeploymentTransaction { + hash: tx_hash, + message: ProgramDeploymentMessage { + bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], + }, + }) +} + fn build_mock_block( block_id: BlockId, prev_hash: HashType, @@ -344,7 +414,6 @@ fn build_mock_block( HashType(hash) }; - // Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and ProgramDeployment) let num_txs = 2 + (block_id % 3); let mut block_transactions = Vec::new(); @@ -356,65 +425,10 @@ fn build_mock_block( HashType(hash) }; - // Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment let tx = match (block_id + tx_idx) % 5 { - // Public transactions (most common) - 0 | 1 => Transaction::Public(PublicTransaction { - hash: tx_hash, - message: PublicMessage { - program_id: ProgramId([1_u32; 8]), - account_ids: vec![ - account_ids[tx_idx as usize % account_ids.len()], - account_ids[(tx_idx as usize + 1) % account_ids.len()], - ], - nonces: vec![block_id as u128, (block_id + 1) as u128], - instruction_data: vec![1, 2, 3, 4], - }, - witness_set: WitnessSet { - signatures_and_public_keys: vec![], - proof: None, - }, - }), - // PrivacyPreserving transactions - 2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction { - hash: tx_hash, - message: PrivacyPreservingMessage { - public_account_ids: vec![account_ids[tx_idx as usize % account_ids.len()]], - nonces: vec![block_id as u128], - public_post_states: vec![Account { - program_owner: ProgramId([1_u32; 8]), - balance: 500, - data: Data(vec![0xdd, 0xee]), - nonce: block_id as u128, - }], - encrypted_private_post_states: vec![EncryptedAccountData { - ciphertext: indexer_service_protocol::Ciphertext(vec![ - 0x01, 0x02, 0x03, 0x04, - ]), - epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), - view_tag: 42, - }], - new_commitments: vec![Commitment([block_id as u8; 32])], - new_nullifiers: vec![( - indexer_service_protocol::Nullifier([tx_idx as u8; 32]), - CommitmentSetDigest([0xff; 32]), - )], - block_validity_window: ValidityWindow((None, None)), - timestamp_validity_window: ValidityWindow((None, None)), - }, - witness_set: WitnessSet { - signatures_and_public_keys: vec![], - proof: Some(indexer_service_protocol::Proof(vec![0; 32])), - }, - }), - // ProgramDeployment transactions (rare) - _ => Transaction::ProgramDeployment(ProgramDeploymentTransaction { - hash: tx_hash, - message: ProgramDeploymentMessage { - bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic - * number */ - }, - }), + 0 | 1 => mock_public_tx(tx_hash, block_id, tx_idx, account_ids), + 2 | 3 => mock_privacy_preserving_tx(tx_hash, block_id, tx_idx, account_ids), + _ => mock_program_deployment_tx(tx_hash), }; block_transactions.push(tx); diff --git a/lez/indexer/service/src/service.rs b/lez/indexer/service/src/service.rs index a959b80c..7a8ed90f 100644 --- a/lez/indexer/service/src/service.rs +++ b/lez/indexer/service/src/service.rs @@ -1,4 +1,4 @@ -use std::{pin::pin, sync::Arc}; +use std::{path::Path, pin::pin, sync::Arc}; use anyhow::{Context as _, Result, bail}; use arc_swap::ArcSwap; @@ -19,8 +19,8 @@ pub struct IndexerService { } impl IndexerService { - pub fn new(config: IndexerConfig) -> Result { - let indexer = IndexerCore::new(config)?; + pub fn new(config: IndexerConfig, storage_dir: &Path) -> Result { + let indexer = IndexerCore::new(config, storage_dir)?; let subscription_service = SubscriptionService::spawn_new(indexer.clone()); Ok(Self { diff --git a/lez/keycard_wallet/src/lib.rs b/lez/keycard_wallet/src/lib.rs index 73486392..a3ecd140 100644 --- a/lez/keycard_wallet/src/lib.rs +++ b/lez/keycard_wallet/src/lib.rs @@ -134,10 +134,6 @@ impl KeycardWallet { }) } - #[expect( - clippy::arithmetic_side_effects, - reason = "64 - s_stripped.len() is safe: s_stripped.len() โ‰ค 31 because py_signature.len() is in [32, 63]" - )] pub fn sign_message_for_path( &self, py: Python, @@ -150,33 +146,9 @@ impl KeycardWallet { .call_method1("sign_message_for_path", (message, path))? .extract()?; - // The keycard Python library strips leading zeros from S when S < 2^(8k) for some k. - // Left-pad S back to 32 bytes so the full signature is always 64 bytes (R || S). - let py_signature = if py_signature.len() < 64 { - if py_signature.len() < 32 { - return Err(PyErr::new::(format!( - "signature from keycard too short: {} bytes", - py_signature.len() - ))); - } - let s_stripped = &py_signature[32..]; - let mut padded = [0_u8; 64]; - padded[..32].copy_from_slice(&py_signature[..32]); - padded[(64 - s_stripped.len())..].copy_from_slice(s_stripped); - padded.to_vec() - } else { - py_signature + let sig = Signature { + value: normalize_keycard_signature(py_signature)?, }; - - let signature: [u8; 64] = py_signature.try_into().map_err(|vec: Vec| { - PyErr::new::(format!( - "Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})", - vec.len(), - vec - )) - })?; - - let sig = Signature { value: signature }; let pub_key = self.get_public_key_for_path(py, path)?; if !sig.is_valid_for(message, &pub_key) { return Err(PyErr::new::( @@ -224,32 +196,8 @@ impl KeycardWallet { .call_method1("get_private_keys_for_path", (path,))? .extract()?; - let raw_nsk = Zeroizing::new(raw_nsk); - let raw_vsk = Zeroizing::new(raw_vsk); - - let nsk = { - if raw_nsk.len() != 32 { - return Err(PyErr::new::(format!( - "expected 32-byte NSK from keycard, got {} bytes", - raw_nsk.len() - ))); - } - let mut arr = Zeroizing::new([0_u8; 32]); - arr.copy_from_slice(&raw_nsk); - arr - }; - - let vsk = { - if raw_vsk.len() != 64 { - return Err(PyErr::new::(format!( - "expected 64-byte VSK from keycard, got {} bytes", - raw_vsk.len() - ))); - } - let mut arr = Zeroizing::new([0_u8; 64]); - arr.copy_from_slice(&raw_vsk); - arr - }; + let nsk = zeroizing_fixed_bytes::<32>("nullifier secret key", Zeroizing::new(raw_nsk))?; + let vsk = zeroizing_fixed_bytes::<64>("view secret key", Zeroizing::new(raw_vsk))?; Ok((nsk, vsk)) } @@ -269,6 +217,55 @@ impl KeycardWallet { } } +/// The keycard Python library strips leading zeros from S when S < 2^(8k) for some k. +/// Left-pad S back to 32 bytes so the full signature is always 64 bytes (R || S). +#[expect( + clippy::arithmetic_side_effects, + reason = "64 - s_stripped.len() is safe: s_stripped.len() โ‰ค 31 because py_signature.len() is in [32, 63]" +)] +fn normalize_keycard_signature(py_signature: Vec) -> PyResult<[u8; 64]> { + if py_signature.len() < 64 { + if py_signature.len() < 32 { + return Err(PyErr::new::(format!( + "signature from keycard too short: {} bytes", + py_signature.len() + ))); + } + let s_stripped = &py_signature[32..]; + let mut padded = [0_u8; 64]; + padded[..32].copy_from_slice(&py_signature[..32]); + padded[(64 - s_stripped.len())..].copy_from_slice(s_stripped); + Ok(padded) + } else { + py_signature.try_into().map_err(|vec: Vec| { + PyErr::new::(format!( + "Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})", + vec.len(), + vec + )) + }) + } +} + +#[expect( + clippy::needless_pass_by_value, + reason = "Zeroizing> is consumed to ensure the source is zeroed on drop" +)] +fn zeroizing_fixed_bytes( + label: &str, + raw: Zeroizing>, +) -> PyResult> { + if raw.len() != N { + return Err(PyErr::new::(format!( + "expected {N}-byte {label} from keycard, got {} bytes", + raw.len() + ))); + } + let mut arr = Zeroizing::new([0_u8; N]); + arr.copy_from_slice(&raw); + Ok(arr) +} + fn pairing_file_path() -> Option { let home = std::env::var("LEE_WALLET_HOME_DIR") .map(PathBuf::from) diff --git a/lez/keycard_wallet/src/python_path.rs b/lez/keycard_wallet/src/python_path.rs index 99ed936e..61196ad5 100644 --- a/lez/keycard_wallet/src/python_path.rs +++ b/lez/keycard_wallet/src/python_path.rs @@ -2,8 +2,7 @@ use std::{env, path::PathBuf}; use pyo3::{prelude::*, types::PyList}; -/// Adds the project's `python/` directory and venv site-packages to Python's sys.path. -pub fn add_python_path(py: Python<'_>) -> PyResult<()> { +fn collect_python_paths() -> Vec { let current_dir = env::current_dir().expect("Failed to get current working directory"); let python_base = env::var("VIRTUAL_ENV") @@ -11,7 +10,7 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { .and_then(|v| PathBuf::from(v).parent().map(PathBuf::from)) .unwrap_or_else(|| current_dir.clone()); - let mut paths_to_add: Vec = vec![ + let mut paths = vec![ python_base .join("lez") .join("keycard_wallet") @@ -23,24 +22,28 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { .join("keycard-py"), ]; - // If a virtualenv is active, add its site-packages so that dependencies - // installed in the venv (e.g. smartcard, ecdsa) are importable by the - // pyo3 embedded interpreter, which does not inherit sys.path from the - // shell's `python3` executable. + // pyo3's embedded interpreter does not inherit sys.path from the shell, + // so venv site-packages must be added explicitly. if let Ok(venv) = env::var("VIRTUAL_ENV") { let lib = PathBuf::from(&venv).join("lib"); if let Ok(entries) = std::fs::read_dir(&lib) { for entry in entries.flatten() { let site_packages = entry.path().join("site-packages"); if site_packages.exists() { - paths_to_add.push(site_packages); + paths.push(site_packages); } } } } - // Sanity check โ€” warns early if a path doesn't exist - for path in &paths_to_add { + paths +} + +/// Adds the project's `python/` directory and venv site-packages to Python's sys.path. +pub fn add_python_path(py: Python<'_>) -> PyResult<()> { + let paths = collect_python_paths(); + + for path in &paths { if !path.exists() { log::info!("Warning: Python path does not exist: {}", path.display()); } @@ -50,10 +53,9 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { let binding = sys.getattr("path")?; let sys_path = binding.cast::()?; - for path in &paths_to_add { + for path in &paths { let path_str = path.to_str().expect("Invalid path"); - // Avoid duplicating the path let already_present = sys_path .iter() .any(|p| p.extract::<&str>().is_ok_and(|s| s == path_str)); diff --git a/lez/programs/Cargo.toml b/lez/programs/Cargo.toml new file mode 100644 index 00000000..06038d7f --- /dev/null +++ b/lez/programs/Cargo.toml @@ -0,0 +1,101 @@ +[package] +name = "programs" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[[bin]] +name = "amm" +path = "amm/src/main.rs" +required-features = ["programs"] + +[[bin]] +name = "associated_token_account" +path = "associated_token_account/src/main.rs" +required-features = ["programs"] + +[[bin]] +name = "authenticated_transfer" +path = "authenticated_transfer/src/main.rs" +required-features = ["programs"] + +[[bin]] +name = "bridge" +path = "bridge/src/main.rs" +required-features = ["programs"] + +[[bin]] +name = "clock" +path = "clock/src/main.rs" +required-features = ["programs"] + +[[bin]] +name = "faucet" +path = "faucet/src/main.rs" +required-features = ["programs"] + +[[bin]] +name = "pinata" +path = "pinata/src/main.rs" +required-features = ["programs"] + +[[bin]] +name = "pinata_token" +path = "pinata_token/src/main.rs" +required-features = ["programs"] + +[[bin]] +name = "token" +path = "token/src/main.rs" +required-features = ["programs"] + +[[bin]] +name = "vault" +path = "vault/src/main.rs" +required-features = ["programs"] + +[features] +# TODO: Uncomment once https://github.com/risc0/risc0/issues/3772 is resolved. +# default = ["artifacts"] + +# This feature is only usable for library target. +# Activating this will cause library to include built binary artifacts and pack them into `Program` structs. +artifacts = ["dep:build_utils", "dep:lee"] + +# Import dependencies required for program binaries. +# You don't need this if you only want to use the library target. +programs = [ + "dep:lee_core", + "dep:risc0-zkvm", + "dep:amm_program", + "dep:associated_token_account_program", + "dep:token_program", + "dep:amm_core", + "dep:associated_token_account_core", + "dep:authenticated_transfer_core", + "dep:bridge_core", + "dep:clock_core", + "dep:faucet_core", + "dep:token_core", + "dep:vault_core", +] + +[dependencies] +lee = { workspace = true, optional = true } +lee_core = { workspace = true, optional = true } +risc0-zkvm = { workspace = true, optional = true } +amm_core = { workspace = true, optional = true } +associated_token_account_core = { workspace = true, optional = true } +authenticated_transfer_core = { workspace = true, optional = true } +bridge_core = { workspace = true, optional = true } +clock_core = { workspace = true, optional = true } +faucet_core = { workspace = true, optional = true } +token_core = { workspace = true, optional = true } +vault_core = { workspace = true, optional = true } + +amm_program = { path = "amm", optional = true } +associated_token_account_program = { path = "associated_token_account", optional = true } +token_program = { path = "token", optional = true } + +[build-dependencies] +build_utils = { workspace = true, optional = true } diff --git a/lez/programs/README.md b/lez/programs/README.md new file mode 100644 index 00000000..b52f6663 --- /dev/null +++ b/lez/programs/README.md @@ -0,0 +1,22 @@ +# Programs + +This crate serves two purposes at once: + +1. Provide one entrypoint for `cargo risczero build` to build guest binaries used in LEZ in one shot. +2. Provide access to the built binaries wrapped with `Program` type. + +## Binaries + +This crate contains binaries taken from sub-directories: one per each program. This binaries are meant to be compiled with `cargo risczero build`. No other use is intended for them. + +## Library + +You may import this crate as a library but it will only make sense if you enable `artifacts` feature flag. +Enabling this flag will make crate expect that [`binaries`](#binaries) where already built and put in the right place (use `just build-artifacts` for that). + +## Why not just `risc0_build::embed_methods()` ? + +Because this will either provide non-deterministic guest build or requires Docker. +And forcing to use Docker to build the project is not an option for us especially because we also build Docker images for our services, which would mean we would have to call docker from docker (and this is not really feasible). + +`risc0_build::embed_methods()` works well when you don't need deterministic build or Docker is not a problem. This is the case for our tests and we use it there. diff --git a/programs/amm/Cargo.toml b/lez/programs/amm/Cargo.toml similarity index 90% rename from programs/amm/Cargo.toml rename to lez/programs/amm/Cargo.toml index 5a4be879..171efbd9 100644 --- a/programs/amm/Cargo.toml +++ b/lez/programs/amm/Cargo.toml @@ -14,3 +14,4 @@ amm_core.workspace = true [dev-dependencies] lee = { workspace = true, features = ["test-utils"] } +programs = { workspace = true } diff --git a/programs/amm/core/Cargo.toml b/lez/programs/amm/core/Cargo.toml similarity index 100% rename from programs/amm/core/Cargo.toml rename to lez/programs/amm/core/Cargo.toml diff --git a/programs/amm/core/src/lib.rs b/lez/programs/amm/core/src/lib.rs similarity index 100% rename from programs/amm/core/src/lib.rs rename to lez/programs/amm/core/src/lib.rs diff --git a/programs/amm/src/add.rs b/lez/programs/amm/src/add.rs similarity index 100% rename from programs/amm/src/add.rs rename to lez/programs/amm/src/add.rs diff --git a/programs/amm/src/lib.rs b/lez/programs/amm/src/lib.rs similarity index 100% rename from programs/amm/src/lib.rs rename to lez/programs/amm/src/lib.rs diff --git a/program_methods/guest/src/bin/amm.rs b/lez/programs/amm/src/main.rs similarity index 100% rename from program_methods/guest/src/bin/amm.rs rename to lez/programs/amm/src/main.rs diff --git a/programs/amm/src/new_definition.rs b/lez/programs/amm/src/new_definition.rs similarity index 100% rename from programs/amm/src/new_definition.rs rename to lez/programs/amm/src/new_definition.rs diff --git a/programs/amm/src/remove.rs b/lez/programs/amm/src/remove.rs similarity index 100% rename from programs/amm/src/remove.rs rename to lez/programs/amm/src/remove.rs diff --git a/programs/amm/src/swap.rs b/lez/programs/amm/src/swap.rs similarity index 100% rename from programs/amm/src/swap.rs rename to lez/programs/amm/src/swap.rs diff --git a/programs/amm/src/tests.rs b/lez/programs/amm/src/tests.rs similarity index 96% rename from programs/amm/src/tests.rs rename to lez/programs/amm/src/tests.rs index d93bedb2..e98c33a0 100644 --- a/programs/amm/src/tests.rs +++ b/lez/programs/amm/src/tests.rs @@ -4,9 +4,7 @@ use amm_core::{ PoolDefinition, compute_liquidity_token_pda, compute_liquidity_token_pda_seed, compute_pool_pda, compute_vault_pda, compute_vault_pda_seed, }; -use lee::{ - PrivateKey, PublicKey, PublicTransaction, V03State, program::Program, public_transaction, -}; +use lee::{PrivateKey, PublicKey, PublicTransaction, V03State, public_transaction}; use lee_core::{ account::{Account, AccountId, AccountWithMetadata, Data}, program::{ChainedCall, ProgramId}, @@ -1295,14 +1293,14 @@ impl BalanceForExeTests { impl IdForExeTests { fn pool_definition_id() -> AccountId { amm_core::compute_pool_pda( - Program::amm().id(), + programs::amm().id(), Self::token_a_definition_id(), Self::token_b_definition_id(), ) } fn token_lp_definition_id() -> AccountId { - amm_core::compute_liquidity_token_pda(Program::amm().id(), Self::pool_definition_id()) + amm_core::compute_liquidity_token_pda(programs::amm().id(), Self::pool_definition_id()) } fn token_a_definition_id() -> AccountId { @@ -1333,7 +1331,7 @@ impl IdForExeTests { fn vault_a_id() -> AccountId { amm_core::compute_vault_pda( - Program::amm().id(), + programs::amm().id(), Self::pool_definition_id(), Self::token_a_definition_id(), ) @@ -1341,7 +1339,7 @@ impl IdForExeTests { fn vault_b_id() -> AccountId { amm_core::compute_vault_pda( - Program::amm().id(), + programs::amm().id(), Self::pool_definition_id(), Self::token_b_definition_id(), ) @@ -1351,7 +1349,7 @@ impl IdForExeTests { impl AccountsForExeTests { fn user_token_a_holding() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1363,7 +1361,7 @@ impl AccountsForExeTests { fn user_token_b_holding() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1375,7 +1373,7 @@ impl AccountsForExeTests { fn pool_definition_init() -> Account { Account { - program_owner: Program::amm().id(), + program_owner: programs::amm().id(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: IdForExeTests::token_a_definition_id(), @@ -1395,7 +1393,7 @@ impl AccountsForExeTests { fn token_a_definition_account() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("test"), @@ -1408,7 +1406,7 @@ impl AccountsForExeTests { fn token_b_definition_acc() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("test"), @@ -1421,7 +1419,7 @@ impl AccountsForExeTests { fn token_lp_definition_acc() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("LP Token"), @@ -1434,7 +1432,7 @@ impl AccountsForExeTests { fn vault_a_init() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1446,7 +1444,7 @@ impl AccountsForExeTests { fn vault_b_init() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1458,7 +1456,7 @@ impl AccountsForExeTests { fn user_token_lp_holding() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_lp_definition_id(), @@ -1470,7 +1468,7 @@ impl AccountsForExeTests { fn vault_a_swap_1() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1482,7 +1480,7 @@ impl AccountsForExeTests { fn vault_b_swap_1() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1494,7 +1492,7 @@ impl AccountsForExeTests { fn pool_definition_swap_1() -> Account { Account { - program_owner: Program::amm().id(), + program_owner: programs::amm().id(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: IdForExeTests::token_a_definition_id(), @@ -1514,7 +1512,7 @@ impl AccountsForExeTests { fn user_token_a_holding_swap_1() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1526,7 +1524,7 @@ impl AccountsForExeTests { fn user_token_b_holding_swap_1() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1538,7 +1536,7 @@ impl AccountsForExeTests { fn vault_a_swap_2() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1550,7 +1548,7 @@ impl AccountsForExeTests { fn vault_b_swap_2() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1562,7 +1560,7 @@ impl AccountsForExeTests { fn pool_definition_swap_2() -> Account { Account { - program_owner: Program::amm().id(), + program_owner: programs::amm().id(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: IdForExeTests::token_a_definition_id(), @@ -1582,7 +1580,7 @@ impl AccountsForExeTests { fn user_token_a_holding_swap_2() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1594,7 +1592,7 @@ impl AccountsForExeTests { fn user_token_b_holding_swap_2() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1606,7 +1604,7 @@ impl AccountsForExeTests { fn vault_a_add() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1618,7 +1616,7 @@ impl AccountsForExeTests { fn vault_b_add() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1630,7 +1628,7 @@ impl AccountsForExeTests { fn pool_definition_add() -> Account { Account { - program_owner: Program::amm().id(), + program_owner: programs::amm().id(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: IdForExeTests::token_a_definition_id(), @@ -1650,7 +1648,7 @@ impl AccountsForExeTests { fn user_token_a_holding_add() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1662,7 +1660,7 @@ impl AccountsForExeTests { fn user_token_b_holding_add() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1674,7 +1672,7 @@ impl AccountsForExeTests { fn user_token_lp_holding_add() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_lp_definition_id(), @@ -1686,7 +1684,7 @@ impl AccountsForExeTests { fn token_lp_definition_add() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("LP Token"), @@ -1699,7 +1697,7 @@ impl AccountsForExeTests { fn vault_a_remove() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1711,7 +1709,7 @@ impl AccountsForExeTests { fn vault_b_remove() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1723,7 +1721,7 @@ impl AccountsForExeTests { fn pool_definition_remove() -> Account { Account { - program_owner: Program::amm().id(), + program_owner: programs::amm().id(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: IdForExeTests::token_a_definition_id(), @@ -1743,7 +1741,7 @@ impl AccountsForExeTests { fn user_token_a_holding_remove() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1755,7 +1753,7 @@ impl AccountsForExeTests { fn user_token_b_holding_remove() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1767,7 +1765,7 @@ impl AccountsForExeTests { fn user_token_lp_holding_remove() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_lp_definition_id(), @@ -1779,7 +1777,7 @@ impl AccountsForExeTests { fn token_lp_definition_remove() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("LP Token"), @@ -1792,7 +1790,7 @@ impl AccountsForExeTests { fn token_lp_definition_init_inactive() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("LP Token"), @@ -1805,7 +1803,7 @@ impl AccountsForExeTests { fn vault_a_init_inactive() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1817,7 +1815,7 @@ impl AccountsForExeTests { fn vault_b_init_inactive() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1829,7 +1827,7 @@ impl AccountsForExeTests { fn pool_definition_inactive() -> Account { Account { - program_owner: Program::amm().id(), + program_owner: programs::amm().id(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: IdForExeTests::token_a_definition_id(), @@ -1849,7 +1847,7 @@ impl AccountsForExeTests { fn user_token_a_holding_new_init() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_a_definition_id(), @@ -1861,7 +1859,7 @@ impl AccountsForExeTests { fn user_token_b_holding_new_init() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_b_definition_id(), @@ -1873,7 +1871,7 @@ impl AccountsForExeTests { fn user_token_lp_holding_new_init() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_lp_definition_id(), @@ -1885,7 +1883,7 @@ impl AccountsForExeTests { fn token_lp_definition_new_init() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenDefinition::Fungible { name: String::from("LP Token"), @@ -1898,7 +1896,7 @@ impl AccountsForExeTests { fn pool_definition_new_init() -> Account { Account { - program_owner: Program::amm().id(), + program_owner: programs::amm().id(), balance: 0_u128, data: Data::from(&PoolDefinition { definition_token_a_id: IdForExeTests::token_a_definition_id(), @@ -1918,7 +1916,7 @@ impl AccountsForExeTests { fn user_token_lp_holding_init_zero() -> Account { Account { - program_owner: Program::token().id(), + program_owner: programs::token().id(), balance: 0_u128, data: Data::from(&TokenHolding::Fungible { definition_id: IdForExeTests::token_lp_definition_id(), @@ -3035,68 +3033,73 @@ fn new_definition_lp_symmetric_amounts() { } fn state_for_amm_tests() -> V03State { - let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); - state.force_insert_account( - IdForExeTests::pool_definition_id(), - AccountsForExeTests::pool_definition_init(), - ); - state.force_insert_account( - IdForExeTests::token_a_definition_id(), - AccountsForExeTests::token_a_definition_account(), - ); - state.force_insert_account( - IdForExeTests::token_b_definition_id(), - AccountsForExeTests::token_b_definition_acc(), - ); - state.force_insert_account( - IdForExeTests::token_lp_definition_id(), - AccountsForExeTests::token_lp_definition_acc(), - ); - state.force_insert_account( - IdForExeTests::user_token_a_id(), - AccountsForExeTests::user_token_a_holding(), - ); - state.force_insert_account( - IdForExeTests::user_token_b_id(), - AccountsForExeTests::user_token_b_holding(), - ); - state.force_insert_account( - IdForExeTests::user_token_lp_id(), - AccountsForExeTests::user_token_lp_holding(), - ); - state.force_insert_account( - IdForExeTests::vault_a_id(), - AccountsForExeTests::vault_a_init(), - ); - state.force_insert_account( - IdForExeTests::vault_b_id(), - AccountsForExeTests::vault_b_init(), - ); + let public_state = [ + ( + IdForExeTests::pool_definition_id(), + AccountsForExeTests::pool_definition_init(), + ), + ( + IdForExeTests::token_a_definition_id(), + AccountsForExeTests::token_a_definition_account(), + ), + ( + IdForExeTests::token_b_definition_id(), + AccountsForExeTests::token_b_definition_acc(), + ), + ( + IdForExeTests::token_lp_definition_id(), + AccountsForExeTests::token_lp_definition_acc(), + ), + ( + IdForExeTests::user_token_a_id(), + AccountsForExeTests::user_token_a_holding(), + ), + ( + IdForExeTests::user_token_b_id(), + AccountsForExeTests::user_token_b_holding(), + ), + ( + IdForExeTests::user_token_lp_id(), + AccountsForExeTests::user_token_lp_holding(), + ), + ( + IdForExeTests::vault_a_id(), + AccountsForExeTests::vault_a_init(), + ), + ( + IdForExeTests::vault_b_id(), + AccountsForExeTests::vault_b_init(), + ), + ]; - state + V03State::new() + .with_public_accounts(public_state) + .with_programs([programs::amm(), programs::token()]) } fn state_for_amm_tests_with_new_def() -> V03State { - let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); - state.force_insert_account( - IdForExeTests::token_a_definition_id(), - AccountsForExeTests::token_a_definition_account(), - ); - state.force_insert_account( - IdForExeTests::token_b_definition_id(), - AccountsForExeTests::token_b_definition_acc(), - ); - state.force_insert_account( - IdForExeTests::user_token_a_id(), - AccountsForExeTests::user_token_a_holding(), - ); - state.force_insert_account( - IdForExeTests::user_token_b_id(), - AccountsForExeTests::user_token_b_holding(), - ); - state + let public_state = [ + ( + IdForExeTests::token_a_definition_id(), + AccountsForExeTests::token_a_definition_account(), + ), + ( + IdForExeTests::token_b_definition_id(), + AccountsForExeTests::token_b_definition_acc(), + ), + ( + IdForExeTests::user_token_a_id(), + AccountsForExeTests::user_token_a_holding(), + ), + ( + IdForExeTests::user_token_b_id(), + AccountsForExeTests::user_token_b_holding(), + ), + ]; + + V03State::new() + .with_public_accounts(public_state) + .with_programs([programs::amm(), programs::token()]) } #[test] @@ -3110,7 +3113,7 @@ fn simple_amm_remove() { }; let message = public_transaction::Message::try_new( - Program::amm().id(), + programs::amm().id(), vec![ IdForExeTests::pool_definition_id(), IdForExeTests::vault_a_id(), @@ -3183,11 +3186,11 @@ fn simple_amm_new_definition_inactive_initialized_pool_and_uninit_user_lp() { let instruction = amm_core::Instruction::NewDefinition { token_a_amount: BalanceForExeTests::vault_a_balance_init(), token_b_amount: BalanceForExeTests::vault_b_balance_init(), - amm_program_id: Program::amm().id(), + amm_program_id: programs::amm().id(), }; let message = public_transaction::Message::try_new( - Program::amm().id(), + programs::amm().id(), vec![ IdForExeTests::pool_definition_id(), IdForExeTests::vault_a_id(), @@ -3268,11 +3271,11 @@ fn simple_amm_new_definition_inactive_initialized_pool_init_user_lp() { let instruction = amm_core::Instruction::NewDefinition { token_a_amount: BalanceForExeTests::vault_a_balance_init(), token_b_amount: BalanceForExeTests::vault_b_balance_init(), - amm_program_id: Program::amm().id(), + amm_program_id: programs::amm().id(), }; let message = public_transaction::Message::try_new( - Program::amm().id(), + programs::amm().id(), vec![ IdForExeTests::pool_definition_id(), IdForExeTests::vault_a_id(), @@ -3340,11 +3343,11 @@ fn simple_amm_new_definition_uninitialized_pool() { let instruction = amm_core::Instruction::NewDefinition { token_a_amount: BalanceForExeTests::vault_a_balance_init(), token_b_amount: BalanceForExeTests::vault_b_balance_init(), - amm_program_id: Program::amm().id(), + amm_program_id: programs::amm().id(), }; let message = public_transaction::Message::try_new( - Program::amm().id(), + programs::amm().id(), vec![ IdForExeTests::pool_definition_id(), IdForExeTests::vault_a_id(), @@ -3407,7 +3410,7 @@ fn simple_amm_add() { }; let message = public_transaction::Message::try_new( - Program::amm().id(), + programs::amm().id(), vec![ IdForExeTests::pool_definition_id(), IdForExeTests::vault_a_id(), @@ -3469,7 +3472,7 @@ fn simple_amm_swap_1() { }; let message = public_transaction::Message::try_new( - Program::amm().id(), + programs::amm().id(), vec![ IdForExeTests::pool_definition_id(), IdForExeTests::vault_a_id(), @@ -3519,7 +3522,7 @@ fn simple_amm_swap_2() { token_definition_id_in: IdForExeTests::token_a_definition_id(), }; let message = public_transaction::Message::try_new( - Program::amm().id(), + programs::amm().id(), vec![ IdForExeTests::pool_definition_id(), IdForExeTests::vault_a_id(), diff --git a/lez/programs/associated_token_account/Cargo.toml b/lez/programs/associated_token_account/Cargo.toml new file mode 100644 index 00000000..494b179f --- /dev/null +++ b/lez/programs/associated_token_account/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "associated_token_account_program" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[dependencies] +lee_core.workspace = true +token_core.workspace = true +associated_token_account_core.workspace = true diff --git a/programs/associated_token_account/core/Cargo.toml b/lez/programs/associated_token_account/core/Cargo.toml similarity index 81% rename from programs/associated_token_account/core/Cargo.toml rename to lez/programs/associated_token_account/core/Cargo.toml index 9c80e897..9b2ff668 100644 --- a/programs/associated_token_account/core/Cargo.toml +++ b/lez/programs/associated_token_account/core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ata_core" +name = "associated_token_account_core" version = "0.1.0" edition = "2024" license = { workspace = true } diff --git a/programs/associated_token_account/core/src/lib.rs b/lez/programs/associated_token_account/core/src/lib.rs similarity index 100% rename from programs/associated_token_account/core/src/lib.rs rename to lez/programs/associated_token_account/core/src/lib.rs diff --git a/programs/associated_token_account/src/burn.rs b/lez/programs/associated_token_account/src/burn.rs similarity index 88% rename from programs/associated_token_account/src/burn.rs rename to lez/programs/associated_token_account/src/burn.rs index 2aae0dd2..09d1645a 100644 --- a/programs/associated_token_account/src/burn.rs +++ b/lez/programs/associated_token_account/src/burn.rs @@ -16,8 +16,12 @@ pub fn burn_from_associated_token_account( let definition_id = TokenHolding::try_from(&holder_ata.account.data) .expect("Holder ATA must hold a valid token") .definition_id(); - let seed = - ata_core::verify_ata_and_get_seed(&holder_ata, &owner, definition_id, ata_program_id); + let seed = associated_token_account_core::verify_ata_and_get_seed( + &holder_ata, + &owner, + definition_id, + ata_program_id, + ); let post_states = vec![ AccountPostState::new(owner.account.clone()), diff --git a/programs/associated_token_account/src/create.rs b/lez/programs/associated_token_account/src/create.rs similarity index 95% rename from programs/associated_token_account/src/create.rs rename to lez/programs/associated_token_account/src/create.rs index c910ab5b..4e1b2074 100644 --- a/programs/associated_token_account/src/create.rs +++ b/lez/programs/associated_token_account/src/create.rs @@ -11,7 +11,7 @@ pub fn create_associated_token_account( ) -> (Vec, Vec) { // No authorization check needed: create is idempotent, so anyone can call it safely. let token_program_id = token_definition.account.program_owner; - let ata_seed = ata_core::verify_ata_and_get_seed( + let ata_seed = associated_token_account_core::verify_ata_and_get_seed( &ata_account, &owner, token_definition.account_id, diff --git a/programs/associated_token_account/src/lib.rs b/lez/programs/associated_token_account/src/lib.rs similarity index 73% rename from programs/associated_token_account/src/lib.rs rename to lez/programs/associated_token_account/src/lib.rs index 13740f0a..fc11902f 100644 --- a/programs/associated_token_account/src/lib.rs +++ b/lez/programs/associated_token_account/src/lib.rs @@ -1,6 +1,6 @@ //! The Associated Token Account Program implementation. -pub use ata_core as core; +pub use associated_token_account_core as core; pub mod burn; pub mod create; diff --git a/program_methods/guest/src/bin/associated_token_account.rs b/lez/programs/associated_token_account/src/main.rs similarity index 85% rename from program_methods/guest/src/bin/associated_token_account.rs rename to lez/programs/associated_token_account/src/main.rs index a32bbf9b..2eaa2f3c 100644 --- a/program_methods/guest/src/bin/associated_token_account.rs +++ b/lez/programs/associated_token_account/src/main.rs @@ -1,4 +1,4 @@ -use ata_core::Instruction; +use associated_token_account_core::Instruction; use lee_core::program::{ProgramInput, ProgramOutput, read_lee_inputs}; fn main() { @@ -19,7 +19,7 @@ fn main() { let [owner, token_definition, ata_account] = pre_states .try_into() .expect("Create instruction requires exactly three accounts"); - ata_program::create::create_associated_token_account( + associated_token_account_program::create::create_associated_token_account( owner, token_definition, ata_account, @@ -33,7 +33,7 @@ fn main() { let [owner, sender_ata, recipient] = pre_states .try_into() .expect("Transfer instruction requires exactly three accounts"); - ata_program::transfer::transfer_from_associated_token_account( + associated_token_account_program::transfer::transfer_from_associated_token_account( owner, sender_ata, recipient, @@ -48,7 +48,7 @@ fn main() { let [owner, holder_ata, token_definition] = pre_states .try_into() .expect("Burn instruction requires exactly three accounts"); - ata_program::burn::burn_from_associated_token_account( + associated_token_account_program::burn::burn_from_associated_token_account( owner, holder_ata, token_definition, diff --git a/programs/associated_token_account/src/tests.rs b/lez/programs/associated_token_account/src/tests.rs similarity index 97% rename from programs/associated_token_account/src/tests.rs rename to lez/programs/associated_token_account/src/tests.rs index 7717eae1..f244f6cd 100644 --- a/programs/associated_token_account/src/tests.rs +++ b/lez/programs/associated_token_account/src/tests.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use ata_core::{compute_ata_seed, get_associated_token_account_id}; +use associated_token_account_core::{compute_ata_seed, get_associated_token_account_id}; use lee_core::account::{Account, AccountId, AccountWithMetadata, Data}; use token_core::{TokenDefinition, TokenHolding}; diff --git a/programs/associated_token_account/src/transfer.rs b/lez/programs/associated_token_account/src/transfer.rs similarity index 88% rename from programs/associated_token_account/src/transfer.rs rename to lez/programs/associated_token_account/src/transfer.rs index cd76292f..dbe38803 100644 --- a/programs/associated_token_account/src/transfer.rs +++ b/lez/programs/associated_token_account/src/transfer.rs @@ -16,8 +16,12 @@ pub fn transfer_from_associated_token_account( let definition_id = TokenHolding::try_from(&sender_ata.account.data) .expect("Sender ATA must hold a valid token") .definition_id(); - let seed = - ata_core::verify_ata_and_get_seed(&sender_ata, &owner, definition_id, ata_program_id); + let seed = associated_token_account_core::verify_ata_and_get_seed( + &sender_ata, + &owner, + definition_id, + ata_program_id, + ); let post_states = vec![ AccountPostState::new(owner.account.clone()), diff --git a/lez/programs/authenticated_transfer/Cargo.toml b/lez/programs/authenticated_transfer/Cargo.toml new file mode 100644 index 00000000..d586acdc --- /dev/null +++ b/lez/programs/authenticated_transfer/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "authenticated_transfer_program" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[dependencies] +authenticated_transfer_core.workspace = true +lee_core.workspace = true diff --git a/programs/authenticated_transfer/core/Cargo.toml b/lez/programs/authenticated_transfer/core/Cargo.toml similarity index 100% rename from programs/authenticated_transfer/core/Cargo.toml rename to lez/programs/authenticated_transfer/core/Cargo.toml diff --git a/programs/authenticated_transfer/core/src/lib.rs b/lez/programs/authenticated_transfer/core/src/lib.rs similarity index 100% rename from programs/authenticated_transfer/core/src/lib.rs rename to lez/programs/authenticated_transfer/core/src/lib.rs diff --git a/program_methods/guest/src/bin/authenticated_transfer.rs b/lez/programs/authenticated_transfer/src/main.rs similarity index 100% rename from program_methods/guest/src/bin/authenticated_transfer.rs rename to lez/programs/authenticated_transfer/src/main.rs diff --git a/lez/programs/bridge/Cargo.toml b/lez/programs/bridge/Cargo.toml new file mode 100644 index 00000000..d7762f1f --- /dev/null +++ b/lez/programs/bridge/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bridge_program" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[dependencies] +bridge_core.workspace = true +vault_core.workspace = true +authenticated_transfer_core.workspace = true +lee_core.workspace = true diff --git a/programs/bridge/core/Cargo.toml b/lez/programs/bridge/core/Cargo.toml similarity index 100% rename from programs/bridge/core/Cargo.toml rename to lez/programs/bridge/core/Cargo.toml diff --git a/programs/bridge/core/src/lib.rs b/lez/programs/bridge/core/src/lib.rs similarity index 100% rename from programs/bridge/core/src/lib.rs rename to lez/programs/bridge/core/src/lib.rs diff --git a/program_methods/guest/src/bin/bridge.rs b/lez/programs/bridge/src/main.rs similarity index 100% rename from program_methods/guest/src/bin/bridge.rs rename to lez/programs/bridge/src/main.rs diff --git a/lez/programs/build.rs b/lez/programs/build.rs new file mode 100644 index 00000000..b26b3bda --- /dev/null +++ b/lez/programs/build.rs @@ -0,0 +1,6 @@ +fn main() -> Result<(), Box> { + #[cfg(feature = "artifacts")] + build_utils::include_artifacts("lez/programs")?; + + Ok(()) +} diff --git a/lez/programs/clock/Cargo.toml b/lez/programs/clock/Cargo.toml new file mode 100644 index 00000000..0c51a69d --- /dev/null +++ b/lez/programs/clock/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "clock_program" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[dependencies] +clock_core.workspace = true +lee_core.workspace = true diff --git a/programs/clock/core/Cargo.toml b/lez/programs/clock/core/Cargo.toml similarity index 100% rename from programs/clock/core/Cargo.toml rename to lez/programs/clock/core/Cargo.toml diff --git a/programs/clock/core/src/lib.rs b/lez/programs/clock/core/src/lib.rs similarity index 100% rename from programs/clock/core/src/lib.rs rename to lez/programs/clock/core/src/lib.rs diff --git a/program_methods/guest/src/bin/clock.rs b/lez/programs/clock/src/main.rs similarity index 100% rename from program_methods/guest/src/bin/clock.rs rename to lez/programs/clock/src/main.rs diff --git a/lez/programs/faucet/Cargo.toml b/lez/programs/faucet/Cargo.toml new file mode 100644 index 00000000..c1f4f426 --- /dev/null +++ b/lez/programs/faucet/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "faucet_program" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[dependencies] +lee_core.workspace = true +faucet_core.workspace = true +vault_core.workspace = true +authenticated_transfer_core.workspace = true diff --git a/programs/faucet/core/Cargo.toml b/lez/programs/faucet/core/Cargo.toml similarity index 100% rename from programs/faucet/core/Cargo.toml rename to lez/programs/faucet/core/Cargo.toml diff --git a/programs/faucet/core/src/lib.rs b/lez/programs/faucet/core/src/lib.rs similarity index 100% rename from programs/faucet/core/src/lib.rs rename to lez/programs/faucet/core/src/lib.rs diff --git a/program_methods/guest/src/bin/faucet.rs b/lez/programs/faucet/src/main.rs similarity index 100% rename from program_methods/guest/src/bin/faucet.rs rename to lez/programs/faucet/src/main.rs diff --git a/lez/programs/pinata/Cargo.toml b/lez/programs/pinata/Cargo.toml new file mode 100644 index 00000000..7a6ad121 --- /dev/null +++ b/lez/programs/pinata/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "pinata_program" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[lints] +workspace = true + +[dependencies] +lee_core.workspace = true + +risc0-zkvm.workspace = true diff --git a/program_methods/guest/src/bin/pinata.rs b/lez/programs/pinata/src/main.rs similarity index 100% rename from program_methods/guest/src/bin/pinata.rs rename to lez/programs/pinata/src/main.rs diff --git a/programs/associated_token_account/Cargo.toml b/lez/programs/pinata_token/Cargo.toml similarity index 63% rename from programs/associated_token_account/Cargo.toml rename to lez/programs/pinata_token/Cargo.toml index b6be7155..476409ec 100644 --- a/programs/associated_token_account/Cargo.toml +++ b/lez/programs/pinata_token/Cargo.toml @@ -1,10 +1,14 @@ [package] -name = "ata_program" +name = "pinata_token_program" version = "0.1.0" edition = "2024" license = { workspace = true } +[lints] +workspace = true + [dependencies] lee_core.workspace = true token_core.workspace = true -ata_core.workspace = true + +risc0-zkvm.workspace = true diff --git a/program_methods/guest/src/bin/pinata_token.rs b/lez/programs/pinata_token/src/main.rs similarity index 100% rename from program_methods/guest/src/bin/pinata_token.rs rename to lez/programs/pinata_token/src/main.rs diff --git a/lez/programs/src/lib.rs b/lez/programs/src/lib.rs new file mode 100644 index 00000000..4acade0f --- /dev/null +++ b/lez/programs/src/lib.rs @@ -0,0 +1,137 @@ +//! This crate provides [`Program`]s and associated utilities used by LEZ. + +#[cfg(feature = "artifacts")] +pub use inner::*; + +#[cfg(feature = "artifacts")] +mod inner { + + use std::borrow::Cow; + + use guests::{ + AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID, + AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, BRIDGE_ELF, BRIDGE_ID, CLOCK_ELF, + CLOCK_ID, FAUCET_ELF, FAUCET_ID, PINATA_ELF, PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, + TOKEN_ELF, TOKEN_ID, VAULT_ELF, VAULT_ID, + }; + use lee::program::Program; + + mod guests { + include!(concat!(env!("OUT_DIR"), "/lez/programs/mod.rs")); + } + + #[must_use] + #[inline] + pub const fn authenticated_transfer() -> Program { + Program::new_unchecked( + AUTHENTICATED_TRANSFER_ID, + Cow::Borrowed(AUTHENTICATED_TRANSFER_ELF), + ) + } + + #[must_use] + #[inline] + pub const fn token() -> Program { + Program::new_unchecked(TOKEN_ID, Cow::Borrowed(TOKEN_ELF)) + } + + #[must_use] + #[inline] + pub const fn pinata() -> Program { + Program::new_unchecked(PINATA_ID, Cow::Borrowed(PINATA_ELF)) + } + + // TODO: Not used anywhere? + #[must_use] + #[inline] + pub const fn pinata_token() -> Program { + Program::new_unchecked(PINATA_TOKEN_ID, Cow::Borrowed(PINATA_TOKEN_ELF)) + } + + #[must_use] + #[inline] + pub const fn amm() -> Program { + Program::new_unchecked(AMM_ID, Cow::Borrowed(AMM_ELF)) + } + + #[must_use] + #[inline] + pub const fn clock() -> Program { + Program::new_unchecked(CLOCK_ID, Cow::Borrowed(CLOCK_ELF)) + } + + #[must_use] + #[inline] + pub const fn ata() -> Program { + Program::new_unchecked( + ASSOCIATED_TOKEN_ACCOUNT_ID, + Cow::Borrowed(ASSOCIATED_TOKEN_ACCOUNT_ELF), + ) + } + + #[must_use] + #[inline] + pub const fn vault() -> Program { + Program::new_unchecked(VAULT_ID, Cow::Borrowed(VAULT_ELF)) + } + + #[must_use] + #[inline] + pub const fn faucet() -> Program { + Program::new_unchecked(FAUCET_ID, Cow::Borrowed(FAUCET_ELF)) + } + + #[must_use] + #[inline] + pub const fn bridge() -> Program { + Program::new_unchecked(BRIDGE_ID, Cow::Borrowed(BRIDGE_ELF)) + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn builtin_programs() { + let auth_transfer_program = authenticated_transfer(); + let token_program = token(); + let vault_program = vault(); + let faucet_program = faucet(); + let bridge_program = bridge(); + let pinata_program = pinata(); + + assert_eq!(auth_transfer_program.id(), AUTHENTICATED_TRANSFER_ID); + assert_eq!(auth_transfer_program.elf(), AUTHENTICATED_TRANSFER_ELF); + assert_eq!(token_program.id(), TOKEN_ID); + assert_eq!(token_program.elf(), TOKEN_ELF); + assert_eq!(vault_program.id(), VAULT_ID); + assert_eq!(vault_program.elf(), VAULT_ELF); + assert_eq!(faucet_program.id(), FAUCET_ID); + assert_eq!(faucet_program.elf(), FAUCET_ELF); + assert_eq!(bridge_program.id(), BRIDGE_ID); + assert_eq!(bridge_program.elf(), BRIDGE_ELF); + assert_eq!(pinata_program.id(), PINATA_ID); + assert_eq!(pinata_program.elf(), PINATA_ELF); + } + + #[test] + fn builtin_program_ids_match_elfs() { + let cases: &[(&[u8], [u32; 8])] = &[ + (AMM_ELF, AMM_ID), + (AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID), + (ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID), + (CLOCK_ELF, CLOCK_ID), + (FAUCET_ELF, FAUCET_ID), + (BRIDGE_ELF, BRIDGE_ID), + (PINATA_ELF, PINATA_ID), + (PINATA_TOKEN_ELF, PINATA_TOKEN_ID), + (TOKEN_ELF, TOKEN_ID), + (VAULT_ELF, VAULT_ID), + ]; + for (elf, expected_id) in cases { + let program = Program::new((*elf).into()).unwrap(); + assert_eq!(program.id(), *expected_id); + } + } + } +} diff --git a/programs/token/Cargo.toml b/lez/programs/token/Cargo.toml similarity index 100% rename from programs/token/Cargo.toml rename to lez/programs/token/Cargo.toml diff --git a/programs/token/core/Cargo.toml b/lez/programs/token/core/Cargo.toml similarity index 100% rename from programs/token/core/Cargo.toml rename to lez/programs/token/core/Cargo.toml diff --git a/programs/token/core/src/lib.rs b/lez/programs/token/core/src/lib.rs similarity index 100% rename from programs/token/core/src/lib.rs rename to lez/programs/token/core/src/lib.rs diff --git a/programs/token/src/burn.rs b/lez/programs/token/src/burn.rs similarity index 100% rename from programs/token/src/burn.rs rename to lez/programs/token/src/burn.rs diff --git a/programs/token/src/initialize.rs b/lez/programs/token/src/initialize.rs similarity index 100% rename from programs/token/src/initialize.rs rename to lez/programs/token/src/initialize.rs diff --git a/programs/token/src/lib.rs b/lez/programs/token/src/lib.rs similarity index 100% rename from programs/token/src/lib.rs rename to lez/programs/token/src/lib.rs diff --git a/program_methods/guest/src/bin/token.rs b/lez/programs/token/src/main.rs similarity index 100% rename from program_methods/guest/src/bin/token.rs rename to lez/programs/token/src/main.rs diff --git a/programs/token/src/mint.rs b/lez/programs/token/src/mint.rs similarity index 100% rename from programs/token/src/mint.rs rename to lez/programs/token/src/mint.rs diff --git a/programs/token/src/new_definition.rs b/lez/programs/token/src/new_definition.rs similarity index 100% rename from programs/token/src/new_definition.rs rename to lez/programs/token/src/new_definition.rs diff --git a/programs/token/src/print_nft.rs b/lez/programs/token/src/print_nft.rs similarity index 100% rename from programs/token/src/print_nft.rs rename to lez/programs/token/src/print_nft.rs diff --git a/programs/token/src/tests.rs b/lez/programs/token/src/tests.rs similarity index 100% rename from programs/token/src/tests.rs rename to lez/programs/token/src/tests.rs diff --git a/programs/token/src/transfer.rs b/lez/programs/token/src/transfer.rs similarity index 100% rename from programs/token/src/transfer.rs rename to lez/programs/token/src/transfer.rs diff --git a/lez/programs/vault/Cargo.toml b/lez/programs/vault/Cargo.toml new file mode 100644 index 00000000..4faed055 --- /dev/null +++ b/lez/programs/vault/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "vault_program" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[dependencies] +lee_core.workspace = true +vault_core.workspace = true +authenticated_transfer_core.workspace = true diff --git a/programs/vault/core/Cargo.toml b/lez/programs/vault/core/Cargo.toml similarity index 100% rename from programs/vault/core/Cargo.toml rename to lez/programs/vault/core/Cargo.toml diff --git a/programs/vault/core/src/lib.rs b/lez/programs/vault/core/src/lib.rs similarity index 100% rename from programs/vault/core/src/lib.rs rename to lez/programs/vault/core/src/lib.rs diff --git a/program_methods/guest/src/bin/vault.rs b/lez/programs/vault/src/main.rs similarity index 100% rename from program_methods/guest/src/bin/vault.rs rename to lez/programs/vault/src/main.rs diff --git a/lez/sequencer/core/Cargo.toml b/lez/sequencer/core/Cargo.toml index 9201d8b3..2931c833 100644 --- a/lez/sequencer/core/Cargo.toml +++ b/lez/sequencer/core/Cargo.toml @@ -18,6 +18,8 @@ testnet_initial_state.workspace = true faucet_core.workspace = true bridge_core.workspace = true vault_core.workspace = true +programs.workspace = true +system_accounts.workspace = true logos-blockchain-key-management-system-service.workspace = true logos-blockchain-core.workspace = true @@ -46,6 +48,7 @@ mock = [] [dev-dependencies] futures.workspace = true -test_program_methods.workspace = true +test_programs.workspace = true lee = { workspace = true, features = ["test-utils"] } key_protocol.workspace = true +token_core.workspace = true diff --git a/lez/sequencer/core/src/block_publisher.rs b/lez/sequencer/core/src/block_publisher.rs index ab2cbf86..21551131 100644 --- a/lez/sequencer/core/src/block_publisher.rs +++ b/lez/sequencer/core/src/block_publisher.rs @@ -4,7 +4,7 @@ use anyhow::{Context as _, Result, anyhow}; use common::block::Block; use log::{info, warn}; pub use logos_blockchain_core::mantle::ops::channel::MsgId; -use logos_blockchain_core::mantle::ops::channel::inscribe::Inscription; +use logos_blockchain_core::mantle::ops::channel::{ChannelId, inscribe::Inscription}; pub use logos_blockchain_key_management_system_service::keys::{Ed25519Key, ZkKey}; pub use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint; use logos_blockchain_zone_sdk::{ @@ -61,11 +61,14 @@ pub trait BlockPublisherTrait: Clone { /// Fire-and-forget publish. Zone-sdk drives the actual submission and /// retries internally; this just hands the payload off. async fn publish_block(&self, block: &Block, withdrawals: Vec) -> Result<()>; + + fn channel_id(&self) -> ChannelId; } /// Real block publisher backed by zone-sdk's `ZoneSequencer`. #[derive(Clone)] pub struct ZoneSdkPublisher { + channel_id: ChannelId, publish_tx: mpsc::Sender<(Inscription, Vec)>, // Aborts the drive task when the last clone is dropped. _drive_task: Arc, @@ -192,6 +195,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher { .context("Zone-sdk readiness channel closed before becoming ready")?; Ok(Self { + channel_id: config.channel_id, publish_tx, _drive_task: Arc::new(DriveTaskGuard(drive_task)), }) @@ -210,6 +214,10 @@ impl BlockPublisherTrait for ZoneSdkPublisher { Ok(()) } + + fn channel_id(&self) -> ChannelId { + self.channel_id + } } /// Deserialize inscription payload as a `Block` and return it's`block_id`. diff --git a/lez/sequencer/core/src/block_store.rs b/lez/sequencer/core/src/block_store.rs index 4e5489ff..0db05d74 100644 --- a/lez/sequencer/core/src/block_store.rs +++ b/lez/sequencer/core/src/block_store.rs @@ -219,7 +219,7 @@ mod tests { let retrieved_tx = node_store.get_transaction_by_hash(tx.hash()); assert_eq!(None, retrieved_tx); // Add the block with the transaction - let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0); + let dummy_state = V03State::new(); node_store .update(&block, &[], vec![], &dummy_state) .unwrap(); @@ -286,7 +286,7 @@ mod tests { let block = common::test_utils::produce_dummy_block(1, None, vec![tx]); let block_hash = block.header.hash; - let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0); + let dummy_state = V03State::new(); node_store .update(&block, &[], vec![], &dummy_state) .unwrap(); @@ -324,7 +324,7 @@ mod tests { let block = common::test_utils::produce_dummy_block(1, None, vec![tx]); let block_id = block.header.block_id; - let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0); + let dummy_state = V03State::new(); node_store .update(&block, &[], vec![], &dummy_state) .unwrap(); @@ -376,12 +376,7 @@ mod tests { // Add a new block let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]); node_store - .update( - &block, - &[], - vec![], - &V03State::new_with_genesis_accounts(&[], vec![], 0), - ) + .update(&block, &[], vec![], &V03State::new()) .unwrap(); } diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index e185b05f..75dcc1a9 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -8,7 +8,7 @@ use common::{ transaction::{LeeTransaction, clock_invocation}, }; use config::{GenesisAction, SequencerConfig}; -use lee::{AccountId, PublicTransaction, program::Program, public_transaction::Message}; +use lee::{AccountId, PublicTransaction, public_transaction::Message}; use lee_core::GENESIS_BLOCK_ID; use log::{error, info, warn}; use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key}; @@ -36,6 +36,7 @@ pub mod config; pub mod mock; /// The origin of a transaction. +#[derive(Clone, Copy)] pub enum TransactionOrigin { /// Basic transactions submitted by users via RPC. User, @@ -69,24 +70,17 @@ impl SequencerCore { /// assumed to represent the correct latest state consistent with Bedrock-finalized data. /// If no database is found, the sequencer performs a fresh start from genesis, /// initializing its state with the accounts defined in the configuration file. - pub async fn start_from_config( - config: SequencerConfig, - ) -> (Self, MemPoolHandle<(TransactionOrigin, LeeTransaction)>) { + fn open_or_create_store(config: &SequencerConfig) -> (SequencerStore, lee::V03State, Block) { let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); - - let bedrock_signing_key = - load_or_create_signing_key(&config.home.join("bedrock_signing_key")) - .expect("Failed to load or create bedrock signing key"); - let db_path = config.home.join("rocksdb"); - let (store, state, genesis_block) = if db_path.exists() { - let store = - SequencerStore::open_db(&db_path, signing_key.clone()).unwrap_or_else(|err| { - panic!( - "Failed to open database at {} with error: {err}", - db_path.display() - ) - }); + + if db_path.exists() { + let store = SequencerStore::open_db(&db_path, signing_key).unwrap_or_else(|err| { + panic!( + "Failed to open database at {} with error: {err}", + db_path.display() + ) + }); let state = store .get_lee_state() .expect("Failed to read state from store"); @@ -101,7 +95,7 @@ impl SequencerCore { db_path.display() ); - let (genesis_state, genesis_txs) = build_genesis_state(&config); + let (genesis_state, genesis_txs) = build_genesis_state(config); let hashable_data = HashableBlockData { block_id: GENESIS_BLOCK_ID, @@ -120,7 +114,17 @@ impl SequencerCore { .expect("Failed to create database with genesis block"); (store, genesis_state, genesis_block) - }; + } + } + + pub async fn start_from_config( + config: SequencerConfig, + ) -> (Self, MemPoolHandle<(TransactionOrigin, LeeTransaction)>) { + let bedrock_signing_key = + load_or_create_signing_key(&config.home.join("bedrock_signing_key")) + .expect("Failed to load or create bedrock signing key"); + + let (store, state, genesis_block) = Self::open_or_create_store(&config); let latest_block_meta = store .latest_block_meta() @@ -331,8 +335,60 @@ impl SequencerCore { Ok(self.chain_height) } - /// Builds a new block from transactions in the mempool. - /// Does NOT publish or store the block โ€” the caller is responsible for that. + /// Validates and applies a single mempool transaction to the current state. + /// Returns `Ok(true)` if the transaction was valid and applied, `Ok(false)` if + /// it was skipped due to validation failure. + fn apply_mempool_transaction( + &mut self, + origin: TransactionOrigin, + tx: &LeeTransaction, + block_height: u64, + timestamp: u64, + deposit_event_ids: &mut Vec, + withdrawals: &mut Vec, + ) -> Result { + let tx_hash = tx.hash(); + match origin { + TransactionOrigin::User => { + let validated_diff = match tx.validate_on_state( + &self.state, + block_height, + timestamp, + ) { + Ok(diff) => diff, + Err(err) => { + error!( + "Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it", + ); + return Ok(false); + } + }; + + if let Some(withdraw_data) = extract_bridge_withdraw_data(tx) { + withdrawals.push(withdraw_data); + } + + self.state.apply_state_diff(validated_diff); + } + TransactionOrigin::Sequencer => { + let LeeTransaction::Public(public_tx) = tx else { + panic!("Sequencer may only generate Public transactions, found {tx:#?}"); + }; + + if let Some(deposit_op_id) = extract_bridge_deposit_id(tx) { + deposit_event_ids.push(deposit_op_id); + } + + self.state + .transition_from_public_transaction(public_tx, block_height, timestamp) + .context("Failed to execute sequencer-generated transaction")?; + } + } + + info!("Validated transaction with hash {tx_hash}, including it in block"); + Ok(true) + } + fn build_block_from_mempool(&mut self) -> Result { let now = Instant::now(); @@ -353,14 +409,12 @@ impl SequencerCore { let new_block_timestamp = u64::try_from(chrono::Utc::now().timestamp_millis()) .expect("Timestamp must be positive"); - // Pre-create the mandatory clock tx so its size is included in the block size check. let clock_tx = clock_invocation(new_block_timestamp); let clock_lee_tx = LeeTransaction::Public(clock_tx.clone()); while let Some((origin, tx)) = self.mempool.pop() { let tx_hash = tx.hash(); - // Check if block size exceeds limit (including the mandatory clock tx). let temp_valid_transactions = [ valid_transactions.as_slice(), std::slice::from_ref(&tx), @@ -379,66 +433,30 @@ impl SequencerCore { .len(); if block_size > max_block_size { - // Block would exceed size limit, remove last transaction and push back warn!( "Transaction with hash {tx_hash} deferred to next block: \ block size {block_size} bytes would exceed limit of {max_block_size} bytes", ); - self.mempool.push_front((origin, tx)); break; } - match origin { - TransactionOrigin::User => { - let validated_diff = match tx.validate_on_state( - &self.state, - new_block_height, - new_block_timestamp, - ) { - Ok(diff) => diff, - Err(err) => { - error!( - "Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it", - ); - continue; - } - }; - - if let Some(withdraw_data) = extract_bridge_withdraw_data(&tx) { - withdrawals.push(withdraw_data); - } - - self.state.apply_state_diff(validated_diff); - } - TransactionOrigin::Sequencer => { - let LeeTransaction::Public(public_tx) = &tx else { - panic!("Sequencer may only generate Public transactions, found {tx:#?}"); - }; - - if let Some(deposit_op_id) = extract_bridge_deposit_id(&tx) { - deposit_event_ids.push(deposit_op_id); - } - - self.state - .transition_from_public_transaction( - public_tx, - new_block_height, - new_block_timestamp, - ) - .context("Failed to execute sequencer-generated transaction")?; - } + if self.apply_mempool_transaction( + origin, + &tx, + new_block_height, + new_block_timestamp, + &mut deposit_event_ids, + &mut withdrawals, + )? { + valid_transactions.push(tx); } - valid_transactions.push(tx); - - info!("Validated transaction with hash {tx_hash}, including it in block"); if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block { break; } } - // Append the Clock Program invocation as the mandatory last transaction. self.state .transition_from_public_transaction(&clock_tx, new_block_height, new_block_timestamp) .context("Clock transaction failed. Aborting block production.")?; @@ -616,13 +634,13 @@ fn build_supply_account_genesis_transaction( account_id: &AccountId, balance: u128, ) -> PublicTransaction { - let faucet_program_id = Program::faucet().id(); - let vault_program_id = Program::vault().id(); + let faucet_program_id = programs::faucet().id(); + let vault_program_id = programs::vault().id(); let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, *account_id); let message = Message::try_new( faucet_program_id, - vec![lee::system_faucet_account_id(), recipient_vault_id], + vec![system_accounts::faucet_account_id(), recipient_vault_id], vec![], faucet_core::Instruction::GenesisTransferVault { vault_program_id, @@ -637,12 +655,12 @@ fn build_supply_account_genesis_transaction( } fn build_supply_bridge_account_genesis_transaction(balance: u128) -> PublicTransaction { - let faucet_program_id = Program::faucet().id(); - let bridge_account_id = lee::system_bridge_account_id(); + let faucet_program_id = programs::faucet().id(); + let bridge_account_id = system_accounts::bridge_account_id(); let message = Message::try_new( faucet_program_id, - vec![lee::system_faucet_account_id(), bridge_account_id], + vec![system_accounts::faucet_account_id(), bridge_account_id], vec![], faucet_core::Instruction::GenesisTransferDirect { amount: balance }, ) @@ -666,14 +684,14 @@ fn build_bridge_deposit_tx_from_event(event: &PendingDepositEventRecord) -> Resu let metadata = DepositMetadata::decode(&event.metadata) .context("Failed to decode finalized Bedrock deposit metadata")?; - let bridge_program_id = Program::bridge().id(); - let vault_program_id = Program::vault().id(); + let bridge_program_id = programs::bridge().id(); + let vault_program_id = programs::vault().id(); let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, metadata.recipient_id); let message = Message::try_new( bridge_program_id, - vec![lee::system_bridge_account_id(), recipient_vault_id], + vec![system_accounts::bridge_account_id(), recipient_vault_id], vec![], bridge_core::Instruction::Deposit { l1_deposit_op_id: event.deposit_op_id.0, @@ -698,7 +716,7 @@ fn extract_bridge_deposit_id(tx: &LeeTransaction) -> Option { }; let message = tx.message(); - if message.program_id != lee::program::Program::bridge().id() { + if message.program_id != programs::bridge().id() { return None; } @@ -721,7 +739,7 @@ fn extract_bridge_withdraw_data(tx: &LeeTransaction) -> Option { }; let message = tx.message(); - if message.program_id != lee::program::Program::bridge().id() { + if message.program_id != programs::bridge().id() { return None; } @@ -729,26 +747,23 @@ fn extract_bridge_withdraw_data(tx: &LeeTransaction) -> Option { risc0_zkvm::serde::from_slice::(&message.instruction_data) .ok()?; - match instruction { - bridge_core::Instruction::Withdraw { - amount, - bedrock_account_pk, - } => { - let recipient_pk = - logos_blockchain_key_management_system_service::keys::ZkPublicKey::from( - BigUint::from_bytes_le(&bedrock_account_pk), - ); + let bridge_core::Instruction::Withdraw { + amount, + bedrock_account_pk, + } = instruction + else { + return None; + }; - Some(WithdrawArg { - outputs: logos_blockchain_core::mantle::ledger::Outputs::new( - logos_blockchain_core::mantle::Note::new(amount, recipient_pk), - ), - }) - } - bridge_core::Instruction::Deposit { .. } => unreachable!( - "Deposit instructions from users should never pass validation, and thus should never be seen here" + let recipient_pk = logos_blockchain_key_management_system_service::keys::ZkPublicKey::from( + BigUint::from_bytes_le(&bedrock_account_pk), + ); + + Some(WithdrawArg { + outputs: logos_blockchain_core::mantle::ledger::Outputs::new( + logos_blockchain_core::mantle::Note::new(amount, recipient_pk), ), - } + }) } fn withdraw_event_reconciliation_key( @@ -806,890 +821,4 @@ fn load_or_create_signing_key(path: &Path) -> Result { #[cfg(test)] #[cfg(feature = "mock")] -mod tests { - #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] - - use std::{pin::pin, time::Duration}; - - use common::{ - HashType, - block::HashableBlockData, - test_utils::sequencer_sign_key_for_testing, - transaction::{LeeTransaction, clock_invocation}, - }; - use key_protocol::key_management::KeyChain; - use lee::{ - Account, AccountId, Data, PrivacyPreservingTransaction, V03State, - error::LeeError, - execute_and_prove, - privacy_preserving_transaction::{Message, circuit::ProgramWithDependencies}, - program::Program, - system_bridge_account_id, - }; - use lee_core::{ - Commitment, InputAccountIdentity, Nullifier, - account::{AccountWithMetadata, Nonce}, - }; - use logos_blockchain_core::mantle::ops::channel::ChannelId; - use mempool::MemPoolHandle; - use storage::sequencer::sequencer_cells::PendingDepositEventRecord; - use tempfile::tempdir; - use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys}; - - use crate::{ - TransactionOrigin, - block_store::SequencerStore, - build_genesis_state, - config::{BedrockConfig, SequencerConfig}, - mock::SequencerCoreWithMockClients, - }; - - #[derive(borsh::BorshSerialize)] - struct DepositMetadataForEncoding { - recipient_id: lee::AccountId, - } - - fn setup_sequencer_config() -> SequencerConfig { - let tempdir = tempfile::tempdir().unwrap(); - let home = tempdir.path().to_path_buf(); - - SequencerConfig { - home, - max_num_tx_in_block: 10, - max_block_size: bytesize::ByteSize::mib(1), - mempool_max_size: 10000, - block_create_timeout: Duration::from_secs(1), - signing_key: *sequencer_sign_key_for_testing().value(), - bedrock_config: BedrockConfig { - channel_id: ChannelId::from([0; 32]), - node_url: "http://not-used-in-unit-tests".parse().unwrap(), - auth: None, - }, - retry_pending_blocks_timeout: Duration::from_mins(4), - genesis: vec![], - } - } - - fn create_signing_key_for_account1() -> lee::PrivateKey { - initial_pub_accounts_private_keys()[0].pub_sign_key.clone() - } - - fn create_signing_key_for_account2() -> lee::PrivateKey { - initial_pub_accounts_private_keys()[1].pub_sign_key.clone() - } - - async fn common_setup() -> ( - SequencerCoreWithMockClients, - MemPoolHandle<(TransactionOrigin, LeeTransaction)>, - ) { - let config = setup_sequencer_config(); - common_setup_with_config(config).await - } - - async fn common_setup_with_config( - config: SequencerConfig, - ) -> ( - SequencerCoreWithMockClients, - MemPoolHandle<(TransactionOrigin, LeeTransaction)>, - ) { - let (mut sequencer, mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - - let tx = common::test_utils::produce_dummy_empty_transaction(); - mempool_handle - .push((TransactionOrigin::User, tx)) - .await - .unwrap(); - - sequencer.produce_new_block().await.unwrap(); - - (sequencer, mempool_handle) - } - - fn tx_is_bridge_deposit( - tx: &LeeTransaction, - deposit_op_id: [u8; 32], - expected_amount: u64, - ) -> bool { - let LeeTransaction::Public(public_tx) = tx else { - return false; - }; - - if public_tx.message.program_id != lee::program::Program::bridge().id() { - return false; - } - - let instruction: bridge_core::Instruction = - match risc0_zkvm::serde::from_slice(&public_tx.message.instruction_data) { - Ok(instruction) => instruction, - Err(_err) => return false, - }; - - matches!( - instruction, - bridge_core::Instruction::Deposit { - l1_deposit_op_id, - amount, - .. - } if l1_deposit_op_id == deposit_op_id && amount == expected_amount - ) - } - - #[tokio::test] - async fn start_from_config() { - let config = setup_sequencer_config(); - let (sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - - assert_eq!(sequencer.chain_height, 1); - assert_eq!(sequencer.sequencer_config.max_num_tx_in_block, 10); - - let acc1_account_id = initial_accounts()[0].account_id; - let acc2_account_id = initial_accounts()[1].account_id; - - let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance; - let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance; - - assert_eq!(10000, balance_acc_1); - assert_eq!(20000, balance_acc_2); - } - - #[tokio::test] - async fn start_from_config_opens_existing_db_if_it_exists() { - let config = setup_sequencer_config(); - let temp_dir = tempdir().unwrap(); - let mut config = config; - config.home = temp_dir.path().to_path_buf(); - - let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); - let (genesis_state, genesis_txs) = build_genesis_state(&config); - let genesis_hashable_data = HashableBlockData { - block_id: 1, - transactions: genesis_txs, - prev_block_hash: HashType([0; 32]), - timestamp: 0, - }; - let genesis_block = genesis_hashable_data.into_pending_block(&signing_key); - - SequencerStore::create_db_with_genesis( - &config.home.join("rocksdb"), - &genesis_block, - &genesis_state, - signing_key, - ) - .unwrap(); - - let (sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - assert_eq!(sequencer.chain_height, 1); - assert!(sequencer.store.latest_block_meta().is_ok()); - } - - #[should_panic(expected = "Failed to open database")] - #[tokio::test] - async fn start_from_config_panics_when_db_open_returns_non_not_found_error() { - let mut config = setup_sequencer_config(); - let temp_dir = tempdir().unwrap(); - config.home = temp_dir.path().to_path_buf(); - - let db_path = config.home.join("rocksdb"); - - std::fs::create_dir_all(&config.home).unwrap(); - // Force RocksDB open to fail with an IO error by placing a file at DB path. - std::fs::write(&db_path, b"not-a-directory").unwrap(); - - let _ = SequencerCoreWithMockClients::start_from_config(config).await; - } - - #[tokio::test] - async fn start_from_config_replays_unfulfilled_deposit_events_from_db() { - let config = setup_sequencer_config(); - let deposit_op_id = [13_u8; 32]; - let expected_amount = 1_u64; - let recipient_id = initial_accounts()[0].account_id; - - { - let (_sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - } - - let pending_event = PendingDepositEventRecord { - deposit_op_id: HashType(deposit_op_id), - source_tx_hash: HashType([7_u8; 32]), - amount: expected_amount, - metadata: borsh::to_vec(&DepositMetadataForEncoding { recipient_id }).unwrap(), - submitted_in_block_id: None, - }; - - { - let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); - let store = SequencerStore::open_db(&config.home.join("rocksdb"), signing_key).unwrap(); - - let inserted = store - .dbio() - .add_pending_deposit_event(pending_event) - .unwrap(); - assert!(inserted); - } - - let (mut sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - - let (origin, tx) = tokio::time::timeout(Duration::from_secs(5), async { - loop { - if let Some((origin, tx)) = sequencer.mempool.pop() { - return (origin, tx); - } - - tokio::time::sleep(Duration::from_millis(100)).await; - } - }) - .await - .expect("Timed out waiting for pending deposit event to be replayed into mempool"); - - match origin { - TransactionOrigin::Sequencer => {} - TransactionOrigin::User => { - panic!("Unexpected user transaction in empty mempool replay test") - } - } - - assert!(tx_is_bridge_deposit(&tx, deposit_op_id, expected_amount)); - - let pending_events = sequencer.store.get_unfulfilled_deposit_events().unwrap(); - let replayed_event = pending_events - .into_iter() - .find(|event| event.deposit_op_id == HashType(deposit_op_id)) - .expect("Pending deposit event should remain in DB until included in a block"); - assert!(replayed_event.submitted_in_block_id.is_none()); - } - - #[test] - fn transaction_pre_check_pass() { - let tx = common::test_utils::produce_dummy_empty_transaction(); - let result = tx.transaction_stateless_check(); - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn transaction_pre_check_native_transfer_valid() { - let (_sequencer, _mempool_handle) = common_setup().await; - - let acc1 = initial_accounts()[0].account_id; - let acc2 = initial_accounts()[1].account_id; - - let sign_key1 = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 10, &sign_key1, - ); - let result = tx.transaction_stateless_check(); - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn transaction_pre_check_native_transfer_other_signature() { - let (mut sequencer, _mempool_handle) = common_setup().await; - - let acc1 = initial_accounts()[0].account_id; - let acc2 = initial_accounts()[1].account_id; - - let sign_key2 = create_signing_key_for_account2(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 10, &sign_key2, - ); - - // Signature is valid, stateless check pass - let tx = tx.transaction_stateless_check().unwrap(); - - // Signature is not from sender. Execution fails - let result = tx.execute_check_on_state(&mut sequencer.state, 0, 0); - - assert!(matches!( - result, - Err(lee::error::LeeError::ProgramExecutionFailed(_)) - )); - } - - #[tokio::test] - async fn transaction_pre_check_native_transfer_sent_too_much() { - let (mut sequencer, _mempool_handle) = common_setup().await; - - let acc1 = initial_accounts()[0].account_id; - let acc2 = initial_accounts()[1].account_id; - - let sign_key1 = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 10_000_000, &sign_key1, - ); - - let result = tx.transaction_stateless_check(); - - // Passed pre-check - assert!(result.is_ok()); - - let result = result - .unwrap() - .execute_check_on_state(&mut sequencer.state, 0, 0); - let is_failed_at_balance_mismatch = matches!( - result.err().unwrap(), - lee::error::LeeError::ProgramExecutionFailed(_) - ); - - assert!(is_failed_at_balance_mismatch); - } - - #[tokio::test] - async fn transaction_execute_native_transfer() { - let (mut sequencer, _mempool_handle) = common_setup().await; - - let acc1 = initial_accounts()[0].account_id; - let acc2 = initial_accounts()[1].account_id; - - let sign_key1 = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 100, &sign_key1, - ); - - tx.execute_check_on_state(&mut sequencer.state, 0, 0) - .unwrap(); - - let bal_from = sequencer.state.get_account_by_id(acc1).balance; - let bal_to = sequencer.state.get_account_by_id(acc2).balance; - - assert_eq!(bal_from, 9900); - assert_eq!(bal_to, 20100); - } - - #[tokio::test] - async fn push_tx_into_mempool_blocks_until_mempool_is_full() { - let config = SequencerConfig { - mempool_max_size: 1, - ..setup_sequencer_config() - }; - let (mut sequencer, mempool_handle) = common_setup_with_config(config).await; - - let tx = common::test_utils::produce_dummy_empty_transaction(); - - // Fill the mempool - mempool_handle - .push((TransactionOrigin::User, tx.clone())) - .await - .unwrap(); - - // Check that pushing another transaction will block - let mut push_fut = pin!(mempool_handle.push((TransactionOrigin::User, tx.clone()))); - let poll = futures::poll!(push_fut.as_mut()); - assert!(poll.is_pending()); - - // Empty the mempool by producing a block - sequencer.produce_new_block().await.unwrap(); - - // Resolve the pending push - assert!(push_fut.await.is_ok()); - } - - #[tokio::test] - async fn build_block_from_mempool() { - let (mut sequencer, mempool_handle) = common_setup().await; - let genesis_height = sequencer.chain_height; - - let tx = common::test_utils::produce_dummy_empty_transaction(); - mempool_handle - .push((TransactionOrigin::User, tx)) - .await - .unwrap(); - - let result = sequencer.build_block_from_mempool(); - assert!(result.is_ok()); - assert_eq!(sequencer.chain_height, genesis_height + 1); - } - - #[tokio::test] - async fn replay_transactions_are_rejected_in_the_same_block() { - let (mut sequencer, mempool_handle) = common_setup().await; - - let acc1 = initial_accounts()[0].account_id; - let acc2 = initial_accounts()[1].account_id; - - let sign_key1 = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 100, &sign_key1, - ); - - let tx_original = tx.clone(); - let tx_replay = tx.clone(); - // Pushing two copies of the same tx to the mempool - mempool_handle - .push((TransactionOrigin::User, tx_original)) - .await - .unwrap(); - mempool_handle - .push((TransactionOrigin::User, tx_replay)) - .await - .unwrap(); - - // Create block - sequencer.produce_new_block().await.unwrap(); - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - - // Only one user tx should be included; the clock tx is always appended last. - assert_eq!( - block.body.transactions, - vec![ - tx.clone(), - LeeTransaction::Public(clock_invocation(block.header.timestamp)) - ] - ); - } - - #[tokio::test] - async fn replay_transactions_are_rejected_in_different_blocks() { - let (mut sequencer, mempool_handle) = common_setup().await; - - let acc1 = initial_accounts()[0].account_id; - let acc2 = initial_accounts()[1].account_id; - - let sign_key1 = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1, 0, acc2, 100, &sign_key1, - ); - - // The transaction should be included the first time - mempool_handle - .push((TransactionOrigin::User, tx.clone())) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - assert_eq!( - block.body.transactions, - vec![ - tx.clone(), - LeeTransaction::Public(clock_invocation(block.header.timestamp)) - ] - ); - - // Add same transaction should fail - mempool_handle - .push((TransactionOrigin::User, tx.clone())) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - // The replay is rejected, so only the clock tx is in the block. - assert_eq!( - block.body.transactions, - vec![LeeTransaction::Public(clock_invocation( - block.header.timestamp - ))] - ); - } - - #[tokio::test] - async fn restart_from_storage() { - let config = setup_sequencer_config(); - let acc1_account_id = initial_accounts()[0].account_id; - let acc2_account_id = initial_accounts()[1].account_id; - let balance_to_move = 13; - - // In the following code block a transaction will be processed that moves `balance_to_move` - // from `acc_1` to `acc_2`. The block created with that transaction will be kept stored in - // the temporary directory for the block storage of this test. - { - let (mut sequencer, mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - let signing_key = create_signing_key_for_account1(); - - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1_account_id, - 0, - acc2_account_id, - balance_to_move, - &signing_key, - ); - - mempool_handle - .push((TransactionOrigin::User, tx.clone())) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - assert_eq!( - block.body.transactions, - vec![ - tx.clone(), - LeeTransaction::Public(clock_invocation(block.header.timestamp)) - ] - ); - } - - // Instantiating a new sequencer from the same config. This should load the existing block - // with the above transaction and update the state to reflect that. - let (sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance; - let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance; - - // Balances should be consistent with the stored block - assert_eq!( - balance_acc_1, - initial_accounts()[0].balance - balance_to_move - ); - assert_eq!( - balance_acc_2, - initial_accounts()[1].balance + balance_to_move - ); - } - - #[tokio::test] - async fn get_pending_blocks() { - let config = setup_sequencer_config(); - let (mut sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - sequencer.produce_new_block().await.unwrap(); - sequencer.produce_new_block().await.unwrap(); - sequencer.produce_new_block().await.unwrap(); - assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 4); - } - - #[tokio::test] - async fn delete_blocks() { - let config = setup_sequencer_config(); - let (mut sequencer, _mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config).await; - sequencer.produce_new_block().await.unwrap(); - sequencer.produce_new_block().await.unwrap(); - sequencer.produce_new_block().await.unwrap(); - - let last_finalized_block = 3; - sequencer - .clean_finalized_blocks_from_db(last_finalized_block) - .unwrap(); - - assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 1); - } - - #[tokio::test] - async fn produce_block_with_correct_prev_meta_after_restart() { - let config = setup_sequencer_config(); - let acc1_account_id = initial_accounts()[0].account_id; - let acc2_account_id = initial_accounts()[1].account_id; - - // Step 1: Create initial database with some block metadata - let expected_prev_meta = { - let (mut sequencer, mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - - let signing_key = create_signing_key_for_account1(); - - // Add a transaction and produce a block to set up block metadata - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1_account_id, - 0, - acc2_account_id, - 100, - &signing_key, - ); - - mempool_handle - .push((TransactionOrigin::User, tx)) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - - // Get the metadata of the last block produced - sequencer.store.latest_block_meta().unwrap() - }; - - // Step 2: Restart sequencer from the same storage - let (mut sequencer, mempool_handle) = - SequencerCoreWithMockClients::start_from_config(config.clone()).await; - - // Step 3: Submit a new transaction - let signing_key = create_signing_key_for_account1(); - let tx = common::test_utils::create_transaction_native_token_transfer( - acc1_account_id, - 1, // Next nonce - acc2_account_id, - 50, - &signing_key, - ); - - mempool_handle - .push((TransactionOrigin::User, tx.clone())) - .await - .unwrap(); - - // Step 4: Produce new block - sequencer.produce_new_block().await.unwrap(); - - // Step 5: Verify the new block has correct previous block metadata - let new_block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - - assert_eq!( - new_block.header.prev_block_hash, expected_prev_meta.hash, - "New block's prev_block_hash should match the stored metadata hash" - ); - assert_eq!( - new_block.body.transactions, - vec![ - tx, - LeeTransaction::Public(clock_invocation(new_block.header.timestamp)) - ], - "New block should contain the submitted transaction and the clock invocation" - ); - } - - #[tokio::test] - async fn transactions_touching_clock_account_are_dropped_from_block() { - let (mut sequencer, mempool_handle) = common_setup().await; - - // Canonical clock invocation and a crafted variant with a different timestamp โ€” both must - // be dropped because their diffs touch the clock accounts. - let crafted_clock_tx = { - let message = lee::public_transaction::Message::try_new( - lee::program::Program::clock().id(), - lee::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(), - vec![], - 42_u64, - ) - .unwrap(); - LeeTransaction::Public(lee::PublicTransaction::new( - message, - lee::public_transaction::WitnessSet::from_raw_parts(vec![]), - )) - }; - mempool_handle - .push(( - TransactionOrigin::User, - LeeTransaction::Public(clock_invocation(0)), - )) - .await - .unwrap(); - mempool_handle - .push((TransactionOrigin::User, crafted_clock_tx)) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - - // Both transactions were dropped. Only the system-appended clock tx remains. - assert_eq!( - block.body.transactions, - vec![LeeTransaction::Public(clock_invocation( - block.header.timestamp - ))] - ); - } - - #[tokio::test] - async fn user_tx_that_chain_calls_clock_is_dropped() { - let (mut sequencer, mempool_handle) = common_setup().await; - - // Deploy the clock_chain_caller test program. - let deploy_tx = LeeTransaction::ProgramDeployment(lee::ProgramDeploymentTransaction::new( - lee::program_deployment_transaction::Message::new( - test_program_methods::CLOCK_CHAIN_CALLER_ELF.to_vec(), - ), - )); - mempool_handle - .push((TransactionOrigin::User, deploy_tx)) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - - // Build a user transaction that invokes clock_chain_caller, which in turn chain-calls the - // clock program with the clock accounts. The sequencer should detect that the resulting - // state diff modifies clock accounts and drop the transaction. - let clock_chain_caller_id = - lee::program::Program::new(test_program_methods::CLOCK_CHAIN_CALLER_ELF.to_vec()) - .unwrap() - .id(); - let clock_program_id = lee::program::Program::clock().id(); - let timestamp: u64 = 0; - - let message = lee::public_transaction::Message::try_new( - clock_chain_caller_id, - lee::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(), - vec![], // no signers - (clock_program_id, timestamp), - ) - .unwrap(); - let user_tx = LeeTransaction::Public(lee::PublicTransaction::new( - message, - lee::public_transaction::WitnessSet::from_raw_parts(vec![]), - )); - - mempool_handle - .push((TransactionOrigin::User, user_tx)) - .await - .unwrap(); - sequencer.produce_new_block().await.unwrap(); - - let block = sequencer - .store - .get_block_at_id(sequencer.chain_height) - .unwrap() - .unwrap(); - - // The user tx must have been dropped; only the mandatory clock invocation remains. - assert_eq!( - block.body.transactions, - vec![LeeTransaction::Public(clock_invocation( - block.header.timestamp - ))] - ); - } - - #[tokio::test] - async fn block_production_aborts_when_clock_account_data_is_corrupted() { - let (mut sequencer, mempool_handle) = common_setup().await; - - // Corrupt the clock 01 account data so the clock program panics on deserialization. - let clock_account_id = lee::CLOCK_01_PROGRAM_ACCOUNT_ID; - let mut corrupted = sequencer.state.get_account_by_id(clock_account_id); - corrupted.data = vec![0xff; 3].try_into().unwrap(); - sequencer - .state - .force_insert_account(clock_account_id, corrupted); - - // Push a dummy transaction so the mempool is non-empty. - let tx = common::test_utils::produce_dummy_empty_transaction(); - mempool_handle - .push((TransactionOrigin::User, tx)) - .await - .unwrap(); - - // Block production must fail because the appended clock tx cannot execute. - let result = sequencer.produce_new_block().await; - assert!( - result.is_err(), - "Block production should abort when clock account data is corrupted" - ); - } - - #[test] - fn private_bridge_withdraw_invocation_is_dropped() { - let sender_keys = KeyChain::new_os_random(); - let sender_account_id = AccountId::for_regular_private_account( - &sender_keys.nullifier_public_key, - &sender_keys.viewing_public_key, - 0, - ); - let sender_private_account = Account { - program_owner: Program::authenticated_transfer_program().id(), - balance: 100, - nonce: Nonce(0xdead_beef), - data: Data::default(), - }; - - let mut state = V03State::new_with_genesis_accounts( - &[], - vec![( - Commitment::new(&sender_account_id, &sender_private_account), - Nullifier::for_account_initialization(&sender_account_id), - )], - 0, - ); - - let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account); - let bridge_account_id = system_bridge_account_id(); - - let sender_pre = AccountWithMetadata::new( - sender_private_account, - true, - ( - &sender_keys.nullifier_public_key, - &sender_keys.viewing_public_key, - 0, - ), - ); - let bridge_pre = AccountWithMetadata::new( - state.get_account_by_id(bridge_account_id), - false, - bridge_account_id, - ); - - let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw { - amount: 1, - bedrock_account_pk: [0; 32], - }) - .unwrap(); - - let program_with_deps = ProgramWithDependencies::new( - Program::bridge(), - [( - Program::authenticated_transfer_program().id(), - Program::authenticated_transfer_program(), - )] - .into(), - ); - - let (output, proof) = execute_and_prove( - vec![sender_pre, bridge_pre], - instruction, - vec![ - InputAccountIdentity::PrivateAuthorizedUpdate { - vpk: sender_keys.viewing_public_key, - random_seed: [0; 32], - nsk: sender_keys.private_key_holder.nullifier_secret_key, - membership_proof: state - .get_proof_for_commitment(&sender_commitment) - .expect("sender commitment must be in state"), - identifier: 0, - }, - InputAccountIdentity::Public, - ], - &program_with_deps, - ) - .expect("Execution should succeed"); - - let message = Message::try_from_circuit_output(vec![bridge_account_id], vec![], output) - .expect("Message construction should succeed"); - let witness_set = - lee::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]); - let tx = LeeTransaction::PrivacyPreserving(PrivacyPreservingTransaction::new( - message, - witness_set, - )); - let res = tx.execute_check_on_state(&mut state, 1, 0); - - assert!( - matches!(res, Err(LeeError::InvalidInput(_))), - "Bridge withdraw invocation should be rejected in private execution" - ); - } -} +mod tests; diff --git a/lez/sequencer/core/src/mock.rs b/lez/sequencer/core/src/mock.rs index 5f2ab8cf..39f635f9 100644 --- a/lez/sequencer/core/src/mock.rs +++ b/lez/sequencer/core/src/mock.rs @@ -2,6 +2,7 @@ use std::time::Duration; use anyhow::Result; use common::block::Block; +use logos_blockchain_core::mantle::ops::channel::ChannelId; use logos_blockchain_key_management_system_service::keys::Ed25519Key; use logos_blockchain_zone_sdk::sequencer::WithdrawArg; @@ -16,11 +17,13 @@ use crate::{ pub type SequencerCoreWithMockClients = crate::SequencerCore; #[derive(Clone)] -pub struct MockBlockPublisher; +pub struct MockBlockPublisher { + channel_id: ChannelId, +} impl BlockPublisherTrait for MockBlockPublisher { async fn new( - _config: &BedrockConfig, + config: &BedrockConfig, _bedrock_signing_key: Ed25519Key, _resubmit_interval: Duration, _initial_checkpoint: Option, @@ -29,7 +32,9 @@ impl BlockPublisherTrait for MockBlockPublisher { _on_deposit_event: OnDepositEventSink, _on_withdraw_event: OnWithdrawEventSink, ) -> Result { - Ok(Self) + Ok(Self { + channel_id: config.channel_id, + }) } async fn publish_block( @@ -39,4 +44,8 @@ impl BlockPublisherTrait for MockBlockPublisher { ) -> Result<()> { Ok(()) } + + fn channel_id(&self) -> ChannelId { + self.channel_id + } } diff --git a/lez/sequencer/core/src/tests.rs b/lez/sequencer/core/src/tests.rs new file mode 100644 index 00000000..4b5ebfbc --- /dev/null +++ b/lez/sequencer/core/src/tests.rs @@ -0,0 +1,1234 @@ +#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] + +use std::{pin::pin, time::Duration}; + +use common::{ + HashType, + block::HashableBlockData, + test_utils::sequencer_sign_key_for_testing, + transaction::{LeeTransaction, clock_invocation}, +}; +use key_protocol::key_management::KeyChain; +use lee::{ + Account, AccountId, Data, PrivacyPreservingTransaction, PrivateKey, PublicKey, + PublicTransaction, V03State, + error::LeeError, + execute_and_prove, + privacy_preserving_transaction::{Message, circuit::ProgramWithDependencies}, + program::Program, +}; +use lee_core::{ + Commitment, InputAccountIdentity, Nullifier, + account::{AccountWithMetadata, Nonce}, + program::PdaSeed, +}; +use logos_blockchain_core::mantle::ops::channel::ChannelId; +use mempool::MemPoolHandle; +use storage::sequencer::sequencer_cells::PendingDepositEventRecord; +use tempfile::tempdir; +use testnet_initial_state::{initial_pub_accounts_private_keys, initial_public_user_accounts}; + +use crate::{ + TransactionOrigin, + block_store::SequencerStore, + build_genesis_state, + config::{BedrockConfig, SequencerConfig}, + mock::SequencerCoreWithMockClients, +}; + +#[derive(borsh::BorshSerialize)] +struct DepositMetadataForEncoding { + recipient_id: lee::AccountId, +} + +fn setup_sequencer_config() -> SequencerConfig { + let tempdir = tempfile::tempdir().unwrap(); + let home = tempdir.path().to_path_buf(); + + SequencerConfig { + home, + max_num_tx_in_block: 10, + max_block_size: bytesize::ByteSize::mib(1), + mempool_max_size: 10000, + block_create_timeout: Duration::from_secs(1), + signing_key: *sequencer_sign_key_for_testing().value(), + bedrock_config: BedrockConfig { + channel_id: ChannelId::from([0; 32]), + node_url: "http://not-used-in-unit-tests".parse().unwrap(), + auth: None, + }, + retry_pending_blocks_timeout: Duration::from_mins(4), + genesis: vec![], + } +} + +fn create_signing_key_for_account1() -> lee::PrivateKey { + initial_pub_accounts_private_keys()[0].pub_sign_key.clone() +} + +fn create_signing_key_for_account2() -> lee::PrivateKey { + initial_pub_accounts_private_keys()[1].pub_sign_key.clone() +} + +async fn common_setup() -> ( + SequencerCoreWithMockClients, + MemPoolHandle<(TransactionOrigin, LeeTransaction)>, +) { + let config = setup_sequencer_config(); + common_setup_with_config(config).await +} + +async fn common_setup_with_config( + config: SequencerConfig, +) -> ( + SequencerCoreWithMockClients, + MemPoolHandle<(TransactionOrigin, LeeTransaction)>, +) { + let (mut sequencer, mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + + let tx = common::test_utils::produce_dummy_empty_transaction(); + mempool_handle + .push((TransactionOrigin::User, tx)) + .await + .unwrap(); + + sequencer.produce_new_block().await.unwrap(); + + (sequencer, mempool_handle) +} + +fn tx_is_bridge_deposit( + tx: &LeeTransaction, + deposit_op_id: [u8; 32], + expected_amount: u64, +) -> bool { + let LeeTransaction::Public(public_tx) = tx else { + return false; + }; + + if public_tx.message.program_id != programs::bridge().id() { + return false; + } + + let instruction: bridge_core::Instruction = + match risc0_zkvm::serde::from_slice(&public_tx.message.instruction_data) { + Ok(instruction) => instruction, + Err(_err) => return false, + }; + + matches!( + instruction, + bridge_core::Instruction::Deposit { + l1_deposit_op_id, + amount, + .. + } if l1_deposit_op_id == deposit_op_id && amount == expected_amount + ) +} + +#[tokio::test] +async fn start_from_config() { + let config = setup_sequencer_config(); + let (sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + + assert_eq!(sequencer.chain_height, 1); + assert_eq!(sequencer.sequencer_config.max_num_tx_in_block, 10); + + let acc1_account_id = initial_public_user_accounts()[0].account_id; + let acc2_account_id = initial_public_user_accounts()[1].account_id; + + let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance; + let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance; + + assert_eq!(10000, balance_acc_1); + assert_eq!(20000, balance_acc_2); +} + +#[tokio::test] +async fn start_from_config_opens_existing_db_if_it_exists() { + let config = setup_sequencer_config(); + let temp_dir = tempdir().unwrap(); + let mut config = config; + config.home = temp_dir.path().to_path_buf(); + + let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); + let (genesis_state, genesis_txs) = build_genesis_state(&config); + let genesis_hashable_data = HashableBlockData { + block_id: 1, + transactions: genesis_txs, + prev_block_hash: HashType([0; 32]), + timestamp: 0, + }; + let genesis_block = genesis_hashable_data.into_pending_block(&signing_key); + + SequencerStore::create_db_with_genesis( + &config.home.join("rocksdb"), + &genesis_block, + &genesis_state, + signing_key, + ) + .unwrap(); + + let (sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + assert_eq!(sequencer.chain_height, 1); + assert!(sequencer.store.latest_block_meta().is_ok()); +} + +#[should_panic(expected = "Failed to open database")] +#[tokio::test] +async fn start_from_config_panics_when_db_open_returns_non_not_found_error() { + let mut config = setup_sequencer_config(); + let temp_dir = tempdir().unwrap(); + config.home = temp_dir.path().to_path_buf(); + + let db_path = config.home.join("rocksdb"); + + std::fs::create_dir_all(&config.home).unwrap(); + // Force RocksDB open to fail with an IO error by placing a file at DB path. + std::fs::write(&db_path, b"not-a-directory").unwrap(); + + let _ = SequencerCoreWithMockClients::start_from_config(config).await; +} + +#[tokio::test] +async fn start_from_config_replays_unfulfilled_deposit_events_from_db() { + let config = setup_sequencer_config(); + let deposit_op_id = [13_u8; 32]; + let expected_amount = 1_u64; + let recipient_id = initial_public_user_accounts()[0].account_id; + + { + let (_sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + } + + let pending_event = PendingDepositEventRecord { + deposit_op_id: HashType(deposit_op_id), + source_tx_hash: HashType([7_u8; 32]), + amount: expected_amount, + metadata: borsh::to_vec(&DepositMetadataForEncoding { recipient_id }).unwrap(), + submitted_in_block_id: None, + }; + + { + let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); + let store = SequencerStore::open_db(&config.home.join("rocksdb"), signing_key).unwrap(); + + let inserted = store + .dbio() + .add_pending_deposit_event(pending_event) + .unwrap(); + assert!(inserted); + } + + let (mut sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + + let (origin, tx) = tokio::time::timeout(Duration::from_secs(5), async { + loop { + if let Some((origin, tx)) = sequencer.mempool.pop() { + return (origin, tx); + } + + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await + .expect("Timed out waiting for pending deposit event to be replayed into mempool"); + + match origin { + TransactionOrigin::Sequencer => {} + TransactionOrigin::User => { + panic!("Unexpected user transaction in empty mempool replay test") + } + } + + assert!(tx_is_bridge_deposit(&tx, deposit_op_id, expected_amount)); + + let pending_events = sequencer.store.get_unfulfilled_deposit_events().unwrap(); + let replayed_event = pending_events + .into_iter() + .find(|event| event.deposit_op_id == HashType(deposit_op_id)) + .expect("Pending deposit event should remain in DB until included in a block"); + assert!(replayed_event.submitted_in_block_id.is_none()); +} + +#[test] +fn transaction_pre_check_pass() { + let tx = common::test_utils::produce_dummy_empty_transaction(); + let result = tx.transaction_stateless_check(); + + assert!(result.is_ok()); +} + +#[tokio::test] +async fn transaction_pre_check_native_transfer_valid() { + let (_sequencer, _mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key1 = create_signing_key_for_account1(); + + let tx = + common::test_utils::create_transaction_native_token_transfer(acc1, 0, acc2, 10, &sign_key1); + let result = tx.transaction_stateless_check(); + + assert!(result.is_ok()); +} + +#[tokio::test] +async fn transaction_pre_check_native_transfer_other_signature() { + let (mut sequencer, _mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key2 = create_signing_key_for_account2(); + + let tx = + common::test_utils::create_transaction_native_token_transfer(acc1, 0, acc2, 10, &sign_key2); + + // Signature is valid, stateless check pass + let tx = tx.transaction_stateless_check().unwrap(); + + // Signature is not from sender. Execution fails + let result = tx.execute_check_on_state(&mut sequencer.state, 0, 0); + + assert!(matches!( + result, + Err(lee::error::LeeError::ProgramExecutionFailed(_)) + )); +} + +#[tokio::test] +async fn transaction_pre_check_native_transfer_sent_too_much() { + let (mut sequencer, _mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key1 = create_signing_key_for_account1(); + + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1, 0, acc2, 10_000_000, &sign_key1, + ); + + let result = tx.transaction_stateless_check(); + + // Passed pre-check + assert!(result.is_ok()); + + let result = result + .unwrap() + .execute_check_on_state(&mut sequencer.state, 0, 0); + let is_failed_at_balance_mismatch = matches!( + result.err().unwrap(), + lee::error::LeeError::ProgramExecutionFailed(_) + ); + + assert!(is_failed_at_balance_mismatch); +} + +#[tokio::test] +async fn transaction_execute_native_transfer() { + let (mut sequencer, _mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key1 = create_signing_key_for_account1(); + + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1, 0, acc2, 100, &sign_key1, + ); + + tx.execute_check_on_state(&mut sequencer.state, 0, 0) + .unwrap(); + + let bal_from = sequencer.state.get_account_by_id(acc1).balance; + let bal_to = sequencer.state.get_account_by_id(acc2).balance; + + assert_eq!(bal_from, 9900); + assert_eq!(bal_to, 20100); +} + +#[tokio::test] +async fn push_tx_into_mempool_blocks_until_mempool_is_full() { + let config = SequencerConfig { + mempool_max_size: 1, + ..setup_sequencer_config() + }; + let (mut sequencer, mempool_handle) = common_setup_with_config(config).await; + + let tx = common::test_utils::produce_dummy_empty_transaction(); + + // Fill the mempool + mempool_handle + .push((TransactionOrigin::User, tx.clone())) + .await + .unwrap(); + + // Check that pushing another transaction will block + let mut push_fut = pin!(mempool_handle.push((TransactionOrigin::User, tx.clone()))); + let poll = futures::poll!(push_fut.as_mut()); + assert!(poll.is_pending()); + + // Empty the mempool by producing a block + sequencer.produce_new_block().await.unwrap(); + + // Resolve the pending push + assert!(push_fut.await.is_ok()); +} + +#[tokio::test] +async fn build_block_from_mempool() { + let (mut sequencer, mempool_handle) = common_setup().await; + let genesis_height = sequencer.chain_height; + + let tx = common::test_utils::produce_dummy_empty_transaction(); + mempool_handle + .push((TransactionOrigin::User, tx)) + .await + .unwrap(); + + let result = sequencer.build_block_from_mempool(); + assert!(result.is_ok()); + assert_eq!(sequencer.chain_height, genesis_height + 1); +} + +#[tokio::test] +async fn replay_transactions_are_rejected_in_the_same_block() { + let (mut sequencer, mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key1 = create_signing_key_for_account1(); + + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1, 0, acc2, 100, &sign_key1, + ); + + let tx_original = tx.clone(); + let tx_replay = tx.clone(); + // Pushing two copies of the same tx to the mempool + mempool_handle + .push((TransactionOrigin::User, tx_original)) + .await + .unwrap(); + mempool_handle + .push((TransactionOrigin::User, tx_replay)) + .await + .unwrap(); + + // Create block + sequencer.produce_new_block().await.unwrap(); + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + + // Only one user tx should be included; the clock tx is always appended last. + assert_eq!( + block.body.transactions, + vec![ + tx.clone(), + LeeTransaction::Public(clock_invocation(block.header.timestamp)) + ] + ); +} + +#[tokio::test] +async fn replay_transactions_are_rejected_in_different_blocks() { + let (mut sequencer, mempool_handle) = common_setup().await; + + let acc1 = initial_public_user_accounts()[0].account_id; + let acc2 = initial_public_user_accounts()[1].account_id; + + let sign_key1 = create_signing_key_for_account1(); + + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1, 0, acc2, 100, &sign_key1, + ); + + // The transaction should be included the first time + mempool_handle + .push((TransactionOrigin::User, tx.clone())) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + assert_eq!( + block.body.transactions, + vec![ + tx.clone(), + LeeTransaction::Public(clock_invocation(block.header.timestamp)) + ] + ); + + // Add same transaction should fail + mempool_handle + .push((TransactionOrigin::User, tx.clone())) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + // The replay is rejected, so only the clock tx is in the block. + assert_eq!( + block.body.transactions, + vec![LeeTransaction::Public(clock_invocation( + block.header.timestamp + ))] + ); +} + +#[tokio::test] +async fn restart_from_storage() { + let config = setup_sequencer_config(); + let acc1_account_id = initial_public_user_accounts()[0].account_id; + let acc2_account_id = initial_public_user_accounts()[1].account_id; + let balance_to_move = 13; + + // In the following code block a transaction will be processed that moves `balance_to_move` + // from `acc_1` to `acc_2`. The block created with that transaction will be kept stored in + // the temporary directory for the block storage of this test. + { + let (mut sequencer, mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + let signing_key = create_signing_key_for_account1(); + + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1_account_id, + 0, + acc2_account_id, + balance_to_move, + &signing_key, + ); + + mempool_handle + .push((TransactionOrigin::User, tx.clone())) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + assert_eq!( + block.body.transactions, + vec![ + tx.clone(), + LeeTransaction::Public(clock_invocation(block.header.timestamp)) + ] + ); + } + + // Instantiating a new sequencer from the same config. This should load the existing block + // with the above transaction and update the state to reflect that. + let (sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + let balance_acc_1 = sequencer.state.get_account_by_id(acc1_account_id).balance; + let balance_acc_2 = sequencer.state.get_account_by_id(acc2_account_id).balance; + + // Balances should be consistent with the stored block + assert_eq!( + balance_acc_1, + initial_public_user_accounts()[0].balance - balance_to_move + ); + assert_eq!( + balance_acc_2, + initial_public_user_accounts()[1].balance + balance_to_move + ); +} + +#[tokio::test] +async fn get_pending_blocks() { + let config = setup_sequencer_config(); + let (mut sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); + assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 4); +} + +#[tokio::test] +async fn delete_blocks() { + let config = setup_sequencer_config(); + let (mut sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); + + let last_finalized_block = 3; + sequencer + .clean_finalized_blocks_from_db(last_finalized_block) + .unwrap(); + + assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 1); +} + +#[tokio::test] +async fn produce_block_with_correct_prev_meta_after_restart() { + let config = setup_sequencer_config(); + let acc1_account_id = initial_public_user_accounts()[0].account_id; + let acc2_account_id = initial_public_user_accounts()[1].account_id; + + // Step 1: Create initial database with some block metadata + let expected_prev_meta = { + let (mut sequencer, mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + + let signing_key = create_signing_key_for_account1(); + + // Add a transaction and produce a block to set up block metadata + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1_account_id, + 0, + acc2_account_id, + 100, + &signing_key, + ); + + mempool_handle + .push((TransactionOrigin::User, tx)) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + + // Get the metadata of the last block produced + sequencer.store.latest_block_meta().unwrap() + }; + + // Step 2: Restart sequencer from the same storage + let (mut sequencer, mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + + // Step 3: Submit a new transaction + let signing_key = create_signing_key_for_account1(); + let tx = common::test_utils::create_transaction_native_token_transfer( + acc1_account_id, + 1, // Next nonce + acc2_account_id, + 50, + &signing_key, + ); + + mempool_handle + .push((TransactionOrigin::User, tx.clone())) + .await + .unwrap(); + + // Step 4: Produce new block + sequencer.produce_new_block().await.unwrap(); + + // Step 5: Verify the new block has correct previous block metadata + let new_block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + + assert_eq!( + new_block.header.prev_block_hash, expected_prev_meta.hash, + "New block's prev_block_hash should match the stored metadata hash" + ); + assert_eq!( + new_block.body.transactions, + vec![ + tx, + LeeTransaction::Public(clock_invocation(new_block.header.timestamp)) + ], + "New block should contain the submitted transaction and the clock invocation" + ); +} + +#[tokio::test] +async fn transactions_touching_clock_account_are_dropped_from_block() { + let (mut sequencer, mempool_handle) = common_setup().await; + + // Canonical clock invocation and a crafted variant with a different timestamp โ€” both must + // be dropped because their diffs touch the clock accounts. + let crafted_clock_tx = { + let message = lee::public_transaction::Message::try_new( + programs::clock().id(), + system_accounts::clock_account_ids().to_vec(), + vec![], + 42_u64, + ) + .unwrap(); + LeeTransaction::Public(lee::PublicTransaction::new( + message, + lee::public_transaction::WitnessSet::from_raw_parts(vec![]), + )) + }; + mempool_handle + .push(( + TransactionOrigin::User, + LeeTransaction::Public(clock_invocation(0)), + )) + .await + .unwrap(); + mempool_handle + .push((TransactionOrigin::User, crafted_clock_tx)) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + + // Both transactions were dropped. Only the system-appended clock tx remains. + assert_eq!( + block.body.transactions, + vec![LeeTransaction::Public(clock_invocation( + block.header.timestamp + ))] + ); +} + +#[tokio::test] +async fn user_tx_that_chain_calls_clock_is_dropped() { + let (mut sequencer, mempool_handle) = common_setup().await; + + let clock_chain_caller = test_programs::clock_chain_caller(); + // Deploy the clock_chain_caller test program. + let deploy_tx = LeeTransaction::ProgramDeployment(lee::ProgramDeploymentTransaction::new( + lee::program_deployment_transaction::Message::new(clock_chain_caller.elf().to_owned()), + )); + mempool_handle + .push((TransactionOrigin::User, deploy_tx)) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + + // Build a user transaction that invokes clock_chain_caller, which in turn chain-calls the + // clock program with the clock accounts. The sequencer should detect that the resulting + // state diff modifies clock accounts and drop the transaction. + let clock_chain_caller_id = test_programs::clock_chain_caller().id(); + let clock_program_id = programs::clock().id(); + let timestamp: u64 = 0; + + let message = lee::public_transaction::Message::try_new( + clock_chain_caller_id, + system_accounts::clock_account_ids().to_vec(), + vec![], // no signers + (clock_program_id, timestamp), + ) + .unwrap(); + let user_tx = LeeTransaction::Public(lee::PublicTransaction::new( + message, + lee::public_transaction::WitnessSet::from_raw_parts(vec![]), + )); + + mempool_handle + .push((TransactionOrigin::User, user_tx)) + .await + .unwrap(); + sequencer.produce_new_block().await.unwrap(); + + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + + // The user tx must have been dropped; only the mandatory clock invocation remains. + assert_eq!( + block.body.transactions, + vec![LeeTransaction::Public(clock_invocation( + block.header.timestamp + ))] + ); +} + +#[tokio::test] +async fn block_production_aborts_when_clock_account_data_is_corrupted() { + let (mut sequencer, mempool_handle) = common_setup().await; + + // Corrupt the clock 01 account data so the clock program panics on deserialization. + let clock_account_id = system_accounts::clock_account_ids()[0]; + let mut corrupted = sequencer.state.get_account_by_id(clock_account_id); + corrupted.data = vec![0xff; 3].try_into().unwrap(); + sequencer + .state + .force_insert_account(clock_account_id, corrupted); + + // Push a dummy transaction so the mempool is non-empty. + let tx = common::test_utils::produce_dummy_empty_transaction(); + mempool_handle + .push((TransactionOrigin::User, tx)) + .await + .unwrap(); + + // Block production must fail because the appended clock tx cannot execute. + let result = sequencer.produce_new_block().await; + assert!( + result.is_err(), + "Block production should abort when clock account data is corrupted" + ); +} + +#[test] +fn private_bridge_withdraw_invocation_is_dropped() { + let sender_keys = KeyChain::new_os_random(); + let sender_account_id = AccountId::for_regular_private_account( + &sender_keys.nullifier_public_key, + &sender_keys.viewing_public_key, + 0, + ); + let sender_private_account = Account { + program_owner: programs::authenticated_transfer().id(), + balance: 100, + nonce: Nonce(0xdead_beef), + data: Data::default(), + }; + let bridge_account_id = system_accounts::bridge_account_id(); + + let mut state = V03State::new() + .with_public_accounts([(bridge_account_id, system_accounts::bridge_account())]) + .with_private_accounts([( + Commitment::new(&sender_account_id, &sender_private_account), + Nullifier::for_account_initialization(&sender_account_id), + )]); + + let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account); + + let sender_pre = AccountWithMetadata::new( + sender_private_account, + true, + ( + &sender_keys.nullifier_public_key, + &sender_keys.viewing_public_key, + 0, + ), + ); + let bridge_pre = AccountWithMetadata::new( + state.get_account_by_id(bridge_account_id), + false, + bridge_account_id, + ); + + let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw { + amount: 1, + bedrock_account_pk: [0; 32], + }) + .unwrap(); + + let program_with_deps = ProgramWithDependencies::new( + programs::bridge(), + [( + programs::authenticated_transfer().id(), + programs::authenticated_transfer(), + )] + .into(), + ); + + let (output, proof) = execute_and_prove( + vec![sender_pre, bridge_pre], + instruction, + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + vpk: sender_keys.viewing_public_key.clone(), + random_seed: [0; 32], + nsk: sender_keys.private_key_holder.nullifier_secret_key, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .expect("Execution should succeed"); + + let message = Message::try_from_circuit_output(vec![bridge_account_id], vec![], output) + .expect("Message construction should succeed"); + let witness_set = + lee::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]); + let tx = + LeeTransaction::PrivacyPreserving(PrivacyPreservingTransaction::new(message, witness_set)); + let res = tx.execute_check_on_state(&mut state, 1, 0); + + assert!( + matches!(res, Err(LeeError::InvalidInput(_))), + "Bridge withdraw invocation should be rejected in private execution" + ); +} + +/// Builds a [`V03State`] with the clock program and `program` registered, the three clock +/// accounts initialized, and the clock advanced to `clock_timestamp` so that reads of the +/// `CLOCK_01` account observe it. +fn state_with_clock_and_program(program: Program, clock_timestamp: u64) -> V03State { + let mut state = V03State::new().with_programs([programs::clock(), program]); + for clock_id in system_accounts::clock_account_ids() { + state.force_insert_account(clock_id, system_accounts::clock_account()); + } + state + .transition_from_public_transaction(&clock_invocation(clock_timestamp), 1, clock_timestamp) + .expect("Clock invocation should advance the clock"); + state +} + +fn time_locked_transfer_transaction( + from: AccountId, + from_key: &PrivateKey, + from_nonce: u128, + to: AccountId, + clock_account_id: AccountId, + amount: u128, + deadline: u64, +) -> PublicTransaction { + let program_id = test_programs::time_locked_transfer().id(); + let message = lee::public_transaction::Message::try_new( + program_id, + vec![from, to, clock_account_id], + vec![Nonce(from_nonce)], + (amount, deadline), + ) + .unwrap(); + let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[from_key]); + PublicTransaction::new(message, witness_set) +} + +#[test] +fn time_locked_transfer_succeeds_when_deadline_has_passed() { + let clock_timestamp = 600; + let mut state = + state_with_clock_and_program(test_programs::time_locked_transfer(), clock_timestamp); + + // The recipient must be a non-default account so the program may credit it without + // claiming it. + let recipient_id = AccountId::new([42; 32]); + state.force_insert_account( + recipient_id, + Account { + program_owner: programs::authenticated_transfer().id(), + ..Account::default() + }, + ); + + let key1 = PrivateKey::try_new([1; 32]).unwrap(); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1)); + state.force_insert_account( + sender_id, + Account { + program_owner: test_programs::time_locked_transfer().id(), + balance: 100, + ..Account::default() + }, + ); + + let amount = 100; + // Deadline is in the past relative to the clock, so the transfer is unlocked. + let deadline = 0; + + let tx = time_locked_transfer_transaction( + sender_id, + &key1, + 0, + recipient_id, + system_accounts::clock_account_ids()[0], + amount, + deadline, + ); + + state + .transition_from_public_transaction(&tx, 2, clock_timestamp) + .unwrap(); + + // Balances changed. + assert_eq!(state.get_account_by_id(sender_id).balance, 0); + assert_eq!(state.get_account_by_id(recipient_id).balance, 100); +} + +#[test] +fn time_locked_transfer_fails_when_deadline_is_in_the_future() { + let clock_timestamp = 600; + let mut state = + state_with_clock_and_program(test_programs::time_locked_transfer(), clock_timestamp); + + let recipient_id = AccountId::new([42; 32]); + state.force_insert_account( + recipient_id, + Account { + program_owner: programs::authenticated_transfer().id(), + ..Account::default() + }, + ); + + let key1 = PrivateKey::try_new([1; 32]).unwrap(); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1)); + state.force_insert_account( + sender_id, + Account { + program_owner: test_programs::time_locked_transfer().id(), + balance: 100, + ..Account::default() + }, + ); + + let amount = 100; + // Far-future deadline: the program panics because the clock has not reached it. + let deadline = u64::MAX; + + let tx = time_locked_transfer_transaction( + sender_id, + &key1, + 0, + recipient_id, + system_accounts::clock_account_ids()[0], + amount, + deadline, + ); + + let result = state.transition_from_public_transaction(&tx, 2, clock_timestamp); + + assert!( + result.is_err(), + "Transfer should fail when deadline is in the future" + ); + // Balances unchanged. + assert_eq!(state.get_account_by_id(sender_id).balance, 100); + assert_eq!(state.get_account_by_id(recipient_id).balance, 0); +} + +fn pinata_cooldown_data(prize: u128, cooldown_ms: u64, last_claim_timestamp: u64) -> Vec { + let mut buf = Vec::with_capacity(32); + buf.extend_from_slice(&prize.to_le_bytes()); + buf.extend_from_slice(&cooldown_ms.to_le_bytes()); + buf.extend_from_slice(&last_claim_timestamp.to_le_bytes()); + buf +} + +fn pinata_cooldown_transaction( + pinata_id: AccountId, + winner_id: AccountId, + clock_account_id: AccountId, +) -> PublicTransaction { + let program_id = test_programs::pinata_cooldown().id(); + let message = lee::public_transaction::Message::try_new( + program_id, + vec![pinata_id, winner_id, clock_account_id], + vec![], + (), + ) + .unwrap(); + let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set) +} + +#[test] +fn pinata_cooldown_claim_succeeds_after_cooldown() { + let winner_id = AccountId::new([11; 32]); + let pinata_id = AccountId::new([99; 32]); + + let genesis_timestamp = 1000; + let prize = 50; + let cooldown_ms = 500; + // Last claim was at genesis, so any timestamp >= genesis + cooldown should work. + let last_claim_timestamp = genesis_timestamp; + + // Advance the clock so the cooldown check reads an updated timestamp. + let block_timestamp = genesis_timestamp + cooldown_ms; + let mut state = state_with_clock_and_program(test_programs::pinata_cooldown(), block_timestamp); + + // The winner must be a non-default account so the program may credit it without claiming. + state.force_insert_account( + winner_id, + Account { + program_owner: programs::authenticated_transfer().id(), + ..Account::default() + }, + ); + state.force_insert_account( + pinata_id, + Account { + program_owner: test_programs::pinata_cooldown().id(), + balance: 1000, + data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) + .try_into() + .unwrap(), + ..Account::default() + }, + ); + + let tx = pinata_cooldown_transaction( + pinata_id, + winner_id, + system_accounts::clock_account_ids()[0], + ); + + state + .transition_from_public_transaction(&tx, 2, block_timestamp) + .unwrap(); + + assert_eq!(state.get_account_by_id(pinata_id).balance, 1000 - prize); + assert_eq!(state.get_account_by_id(winner_id).balance, prize); +} + +#[test] +fn pinata_cooldown_claim_fails_during_cooldown() { + let winner_id = AccountId::new([11; 32]); + let pinata_id = AccountId::new([99; 32]); + + let genesis_timestamp = 1000; + let prize = 50; + let cooldown_ms = 500; + let last_claim_timestamp = genesis_timestamp; + + // Timestamp is only 100ms after the last claim, well within the 500ms cooldown. + let block_timestamp = genesis_timestamp + 100; + let mut state = state_with_clock_and_program(test_programs::pinata_cooldown(), block_timestamp); + + state.force_insert_account( + winner_id, + Account { + program_owner: programs::authenticated_transfer().id(), + ..Account::default() + }, + ); + state.force_insert_account( + pinata_id, + Account { + program_owner: test_programs::pinata_cooldown().id(), + balance: 1000, + data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) + .try_into() + .unwrap(), + ..Account::default() + }, + ); + + let tx = pinata_cooldown_transaction( + pinata_id, + winner_id, + system_accounts::clock_account_ids()[0], + ); + + let result = state.transition_from_public_transaction(&tx, 2, block_timestamp); + + assert!(result.is_err(), "Claim should fail during cooldown period"); + assert_eq!(state.get_account_by_id(pinata_id).balance, 1000); + assert_eq!(state.get_account_by_id(winner_id).balance, 0); +} + +#[test] +fn pda_mechanism_with_pinata_token_program() { + let pinata_token = programs::pinata_token(); + let token = programs::token(); + + let pinata_definition_id = AccountId::new([1; 32]); + let pinata_token_definition_id = AccountId::new([2; 32]); + // Total supply of pinata token will be in an account under a PDA. + let pinata_token_holding_id = + AccountId::for_public_pda(&pinata_token.id(), &PdaSeed::new([0; 32])); + let winner_token_holding_id = AccountId::new([3; 32]); + + let expected_winner_account_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: 150, + }; + let expected_winner_token_holding_post = Account { + program_owner: token.id(), + data: Data::from(&expected_winner_account_holding), + ..Account::default() + }; + + // Register the pinata-token and token programs and create the pinata definition account. + // This replaces the removed `add_pinata_token_program` helper. + let mut state = V03State::new().with_programs([pinata_token.clone(), token.clone()]); + state.force_insert_account( + pinata_definition_id, + Account { + program_owner: pinata_token.id(), + // Difficulty: 3 + data: vec![3; 33].try_into().unwrap(), + ..Account::default() + }, + ); + + // Set up the token accounts directly (bypassing public transactions which + // would require signers for Claim::Authorized). The focus of this test is + // the PDA mechanism in the pinata program's chained call, not token creation. + let total_supply: u128 = 10_000_000; + let token_definition = token_core::TokenDefinition::Fungible { + name: String::from("PINATA"), + total_supply, + metadata_id: None, + }; + let token_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: total_supply, + }; + let winner_holding = token_core::TokenHolding::Fungible { + definition_id: pinata_token_definition_id, + balance: 0, + }; + state.force_insert_account( + pinata_token_definition_id, + Account { + program_owner: token.id(), + data: Data::from(&token_definition), + ..Account::default() + }, + ); + state.force_insert_account( + pinata_token_holding_id, + Account { + program_owner: token.id(), + data: Data::from(&token_holding), + ..Account::default() + }, + ); + state.force_insert_account( + winner_token_holding_id, + Account { + program_owner: token.id(), + data: Data::from(&winner_holding), + ..Account::default() + }, + ); + + // Submit a solution to the pinata program to claim the prize + let solution: u128 = 989_106; + let message = lee::public_transaction::Message::try_new( + pinata_token.id(), + vec![ + pinata_definition_id, + pinata_token_holding_id, + winner_token_holding_id, + ], + vec![], + solution, + ) + .unwrap(); + let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + let winner_token_holding_post = state.get_account_by_id(winner_token_holding_id); + assert_eq!( + winner_token_holding_post, + expected_winner_token_holding_post + ); +} diff --git a/lez/sequencer/service/Cargo.toml b/lez/sequencer/service/Cargo.toml index 4390f0d8..3427dc22 100644 --- a/lez/sequencer/service/Cargo.toml +++ b/lez/sequencer/service/Cargo.toml @@ -14,6 +14,7 @@ mempool.workspace = true sequencer_core = { workspace = true, features = ["testnet"] } sequencer_service_protocol.workspace = true sequencer_service_rpc = { workspace = true, features = ["server"] } +programs.workspace = true clap = { workspace = true, features = ["derive", "env"] } anyhow.workspace = true diff --git a/lez/sequencer/service/Dockerfile b/lez/sequencer/service/Dockerfile index 5b5d3686..1919f775 100644 --- a/lez/sequencer/service/Dockerfile +++ b/lez/sequencer/service/Dockerfile @@ -1,41 +1,5 @@ -# Chef stage - uses pre-built cargo-chef image -FROM lukemathwalker/cargo-chef:latest-rust-1.94.0-slim-trixie AS chef - -# Install dependencies -RUN apt-get update && apt-get install -y \ - build-essential \ - pkg-config \ - libssl-dev \ - libclang-dev \ - clang \ - cmake \ - ninja-build \ - curl \ - git \ - && rm -rf /var/lib/apt/lists/* - -# Install r0vm -# Use quick install for x86-64 (risczero provides binaries only for this linux platform) -# Manual build for other platforms (including arm64 Linux) -RUN ARCH=$(uname -m); \ - if [ "$ARCH" = "x86_64" ]; then \ - echo "Using quick install for $ARCH"; \ - curl -L https://risczero.com/install | bash; \ - export PATH="/root/.cargo/bin:/root/.risc0/bin:${PATH}"; \ - rzup install; \ - else \ - echo "Using manual build for $ARCH"; \ - git clone --depth 1 --branch release-3.0 https://github.com/risc0/risc0.git; \ - git clone --depth 1 --branch r0.1.91.0 https://github.com/risc0/rust.git; \ - cd /risc0; \ - cargo install --path rzup; \ - rzup build --path /rust rust --verbose; \ - cargo install --path risc0/cargo-risczero; \ - fi -ENV PATH="/root/.cargo/bin:/root/.risc0/bin:${PATH}" -RUN cp "$(which r0vm)" /usr/local/bin/r0vm -RUN test -x /usr/local/bin/r0vm -RUN r0vm --version +# Chef stage +FROM risc0_base AS chef WORKDIR /sequencer_service @@ -81,16 +45,21 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry/index \ # Runtime stage - minimal image FROM debian:trixie-slim +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + # Create non-root user for security -RUN useradd -m -u 1000 -s /bin/bash sequencer_user && \ +RUN useradd -m -u 1000 -s /bin/bash sequencer_service_user && \ mkdir -p /sequencer_service /etc/sequencer_service /var/lib/sequencer_service && \ - chown -R sequencer_user:sequencer_user /sequencer_service /etc/sequencer_service /var/lib/sequencer_service + chown -R sequencer_service_user:sequencer_service_user /sequencer_service /etc/sequencer_service /var/lib/sequencer_service # Copy binary from builder -COPY --from=builder --chown=sequencer_user:sequencer_user /usr/local/bin/sequencer_service /usr/local/bin/sequencer_service +COPY --from=builder --chown=sequencer_service_user:sequencer_service_user /usr/local/bin/sequencer_service /usr/local/bin/sequencer_service # Copy r0vm binary from builder -COPY --from=builder --chown=sequencer_user:sequencer_user /usr/local/bin/r0vm /usr/local/bin/r0vm +COPY --from=builder --chown=sequencer_service_user:sequencer_service_user /usr/local/bin/r0vm /usr/local/bin/r0vm VOLUME /var/lib/sequencer_service @@ -103,7 +72,7 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ -H "Content-Type: application/json" \ -d "{ \ \"jsonrpc\": \"2.0\", \ - \"method\": \"hello\", \ + \"method\": \"checkHealth\", \ \"params\": {}, \ \"id\": 1 \ }" || exit 1 @@ -114,7 +83,7 @@ ENV RUST_LOG=info # Set explicit location for r0vm binary ENV RISC0_SERVER_PATH=/usr/local/bin/r0vm -USER sequencer_user +USER sequencer_service_user WORKDIR /sequencer_service CMD ["sequencer_service", "/etc/sequencer_service/sequencer_config.json"] diff --git a/lez/sequencer/service/configs/debug/sequencer_config.json b/lez/sequencer/service/configs/debug/sequencer_config.json index 359c84f4..7ea85b48 100644 --- a/lez/sequencer/service/configs/debug/sequencer_config.json +++ b/lez/sequencer/service/configs/debug/sequencer_config.json @@ -11,9 +11,8 @@ "max_retries": 5 }, "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", - "node_url": "http://localhost:8080" + "node_url": "http://localhost:18080" }, - "indexer_rpc_url": "ws://localhost:8779", "genesis": [ { "supply_bridge_account": { diff --git a/lez/sequencer/service/configs/docker/sequencer_config.json b/lez/sequencer/service/configs/docker/sequencer_config.json index 69238fe7..24184ea8 100644 --- a/lez/sequencer/service/configs/docker/sequencer_config.json +++ b/lez/sequencer/service/configs/docker/sequencer_config.json @@ -11,9 +11,8 @@ "max_retries": 5 }, "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", - "node_url": "http://localhost:18080" + "node_url": "http://host.docker.internal:18080" }, - "indexer_rpc_url": "ws://localhost:8779", "genesis": [ { "supply_bridge_account": { diff --git a/lez/sequencer/service/docker-compose.yml b/lez/sequencer/service/docker-compose.yml index d9c7c2be..477072ad 100644 --- a/lez/sequencer/service/docker-compose.yml +++ b/lez/sequencer/service/docker-compose.yml @@ -1,9 +1,20 @@ services: + # Build-only: shared base image (toolchain + r0vm) referenced as the + # `risc0_base` named context below. It has no long-running command, so it + # only gets built โ€” it exits immediately if started. + risc0_base: + image: lez/risc0_base + build: + context: ../../.. + dockerfile: lez/docker/risc0-base.Dockerfile + sequencer_service: image: lez/sequencer_service build: - context: ../.. + context: ../../.. dockerfile: lez/sequencer/service/Dockerfile + additional_contexts: + risc0_base: "service:risc0_base" container_name: sequencer_service ports: - "3040:3040" diff --git a/lez/sequencer/service/protocol/Cargo.toml b/lez/sequencer/service/protocol/Cargo.toml index 06d51c88..ced19e75 100644 --- a/lez/sequencer/service/protocol/Cargo.toml +++ b/lez/sequencer/service/protocol/Cargo.toml @@ -11,3 +11,6 @@ workspace = true common.workspace = true lee.workspace = true lee_core.workspace = true + +hex.workspace = true +serde_with.workspace = true diff --git a/lez/sequencer/service/protocol/src/lib.rs b/lez/sequencer/service/protocol/src/lib.rs index a8863997..58e300f6 100644 --- a/lez/sequencer/service/protocol/src/lib.rs +++ b/lez/sequencer/service/protocol/src/lib.rs @@ -1,5 +1,28 @@ //! Reexports of types used by sequencer rpc specification. +use std::{fmt::Display, str::FromStr}; + pub use common::{HashType, block::Block, transaction::LeeTransaction}; pub use lee::{Account, AccountId, ProgramId}; pub use lee_core::{BlockId, Commitment, MembershipProof, account::Nonce}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)] +pub struct ChannelId(pub [u8; 32]); + +impl Display for ChannelId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let hex_string = hex::encode(self.0); + write!(f, "{hex_string}") + } +} + +impl FromStr for ChannelId { + type Err = hex::FromHexError; + + fn from_str(s: &str) -> Result { + let mut bytes = [0_u8; 32]; + hex::decode_to_slice(s, &mut bytes)?; + Ok(Self(bytes)) + } +} diff --git a/lez/sequencer/service/rpc/src/lib.rs b/lez/sequencer/service/rpc/src/lib.rs index 2cc85d7d..914232c0 100644 --- a/lez/sequencer/service/rpc/src/lib.rs +++ b/lez/sequencer/service/rpc/src/lib.rs @@ -6,8 +6,8 @@ use jsonrpsee::types::ErrorObjectOwned; #[cfg(feature = "client")] pub use jsonrpsee::{core::ClientError, http_client::HttpClientBuilder as SequencerClientBuilder}; use sequencer_service_protocol::{ - Account, AccountId, Block, BlockId, Commitment, HashType, LeeTransaction, MembershipProof, - Nonce, ProgramId, + Account, AccountId, Block, BlockId, ChannelId, Commitment, HashType, LeeTransaction, + MembershipProof, Nonce, ProgramId, }; #[cfg(all(not(feature = "server"), not(feature = "client")))] @@ -88,5 +88,8 @@ pub trait Rpc { #[method(name = "getProgramIds")] async fn get_program_ids(&self) -> Result, ErrorObjectOwned>; + #[method(name = "getChannelId")] + async fn get_channel_id(&self) -> Result; + // ============================================================================================= } diff --git a/lez/sequencer/service/src/service.rs b/lez/sequencer/service/src/service.rs index 1a781024..3a48e7cc 100644 --- a/lez/sequencer/service/src/service.rs +++ b/lez/sequencer/service/src/service.rs @@ -5,14 +5,15 @@ use jsonrpsee::{ core::async_trait, types::{ErrorCode, ErrorObjectOwned}, }; -use lee::{self, program::Program}; +use lee; use log::warn; use mempool::MemPoolHandle; use sequencer_core::{ DbError, SequencerCore, TransactionOrigin, block_publisher::BlockPublisherTrait, }; use sequencer_service_protocol::{ - Account, AccountId, Block, BlockId, Commitment, HashType, MembershipProof, Nonce, ProgramId, + Account, AccountId, Block, BlockId, ChannelId, Commitment, HashType, MembershipProof, Nonce, + ProgramId, }; use tokio::sync::Mutex; @@ -161,20 +162,26 @@ impl sequencer_service_rpc::RpcServer } async fn get_program_ids(&self) -> Result, ErrorObjectOwned> { + // TODO: Get programs from state let mut program_ids = BTreeMap::new(); program_ids.insert( "authenticated_transfer".to_owned(), - Program::authenticated_transfer_program().id(), + programs::authenticated_transfer().id(), ); - program_ids.insert("token".to_owned(), Program::token().id()); - program_ids.insert("pinata".to_owned(), Program::pinata().id()); - program_ids.insert("amm".to_owned(), Program::amm().id()); + program_ids.insert("token".to_owned(), programs::token().id()); + program_ids.insert("pinata".to_owned(), programs::pinata().id()); + program_ids.insert("amm".to_owned(), programs::amm().id()); program_ids.insert( "privacy_preserving_circuit".to_owned(), lee::PRIVACY_PRESERVING_CIRCUIT_ID, ); Ok(program_ids) } + + async fn get_channel_id(&self) -> Result { + let channel_id = self.sequencer.lock().await.block_publisher().channel_id(); + Ok(ChannelId(*channel_id.as_ref())) + } } fn internal_error(err: &DbError) -> ErrorObjectOwned { diff --git a/lez/storage/Cargo.toml b/lez/storage/Cargo.toml index 4588aab3..8767b525 100644 --- a/lez/storage/Cargo.toml +++ b/lez/storage/Cargo.toml @@ -15,3 +15,7 @@ thiserror.workspace = true borsh.workspace = true rocksdb.workspace = true tempfile.workspace = true + +[dev-dependencies] +programs.workspace = true +system_accounts.workspace = true diff --git a/lez/storage/src/indexer/mod.rs b/lez/storage/src/indexer/mod.rs index b682a4d7..87d586fb 100644 --- a/lez/storage/src/indexer/mod.rs +++ b/lez/storage/src/indexer/mod.rs @@ -153,97 +153,19 @@ impl RocksDBIO { } let br_id = closest_breakpoint_id(block_id); - let mut breakpoint = self.get_breakpoint(br_id)?; + let mut state = self.get_breakpoint(br_id)?; let start = u64::from(BREAKPOINT_INTERVAL) .checked_mul(br_id) .expect("Reached maximum breakpoint id"); - for mut block in self.get_block_batch_seq( + for block in self.get_block_batch_seq( start.checked_add(1).expect("Will be lesser that u64::MAX")..=block_id, )? { - let expected_clock = LeeTransaction::Public(clock_invocation(block.header.timestamp)); - - let clock_tx = block.body.transactions.pop().ok_or_else(|| { - DbError::db_interaction_error( - "Block must contain clock transaction at the end".to_owned(), - ) - })?; - let user_txs = block.body.transactions; - - if clock_tx != expected_clock { - return Err(DbError::db_interaction_error( - "Last transaction in block must be the clock invocation for the block timestamp" - .to_owned(), - )); - } - for transaction in user_txs { - let is_genesis = block.header.block_id == GENESIS_BLOCK_ID; - if is_genesis { - let genesis_tx = match transaction { - LeeTransaction::Public(public_tx) => public_tx, - LeeTransaction::PrivacyPreserving(_) - | LeeTransaction::ProgramDeployment(_) => { - return Err(DbError::db_interaction_error( - "Genesis block should contain only public transactions".to_owned(), - )); - } - }; - breakpoint - .transition_from_public_transaction( - &genesis_tx, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| { - DbError::db_interaction_error(format!( - "genesis transaction execution failed with err {err:?}" - )) - })?; - } else { - transaction - .transaction_stateless_check() - .map_err(|err| { - DbError::db_interaction_error(format!( - "transaction pre check failed with err {err:?}" - )) - })? - // FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to - // sequencer-generated deposit tx'es; - // CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions - .execute_without_system_accounts_check_on_state( - &mut breakpoint, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| { - DbError::db_interaction_error(format!( - "transaction execution failed with err {err:?}" - )) - })?; - } - } - - let LeeTransaction::Public(clock_public_tx) = clock_tx else { - return Err(DbError::db_interaction_error( - "Clock invocation must be a public transaction".to_owned(), - )); - }; - - breakpoint - .transition_from_public_transaction( - &clock_public_tx, - block.header.block_id, - block.header.timestamp, - ) - .map_err(|err| { - DbError::db_interaction_error(format!( - "clock transaction execution failed with err {err:?}" - )) - })?; + apply_block_transactions(block, &mut state)?; } - Ok(breakpoint) + Ok(state) } pub fn final_state(&self) -> DbResult { @@ -252,6 +174,86 @@ impl RocksDBIO { } } +fn apply_block_transactions(mut block: Block, state: &mut V03State) -> DbResult<()> { + let expected_clock = LeeTransaction::Public(clock_invocation(block.header.timestamp)); + + let clock_tx = block.body.transactions.pop().ok_or_else(|| { + DbError::db_interaction_error("Block must contain clock transaction at the end".to_owned()) + })?; + + if clock_tx != expected_clock { + return Err(DbError::db_interaction_error( + "Last transaction in block must be the clock invocation for the block timestamp" + .to_owned(), + )); + } + + for transaction in block.body.transactions { + if block.header.block_id == GENESIS_BLOCK_ID { + let genesis_tx = match transaction { + LeeTransaction::Public(public_tx) => public_tx, + LeeTransaction::PrivacyPreserving(_) | LeeTransaction::ProgramDeployment(_) => { + return Err(DbError::db_interaction_error( + "Genesis block should contain only public transactions".to_owned(), + )); + } + }; + state + .transition_from_public_transaction( + &genesis_tx, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "genesis transaction execution failed with err {err:?}" + )) + })?; + } else { + transaction + .transaction_stateless_check() + .map_err(|err| { + DbError::db_interaction_error(format!( + "transaction pre check failed with err {err:?}" + )) + })? + // FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to + // sequencer-generated deposit tx'es; + // CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions + .execute_without_system_accounts_check_on_state( + state, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "transaction execution failed with err {err:?}" + )) + })?; + } + } + + let LeeTransaction::Public(clock_public_tx) = clock_tx else { + return Err(DbError::db_interaction_error( + "Clock invocation must be a public transaction".to_owned(), + )); + }; + + state + .transition_from_public_transaction( + &clock_public_tx, + block.header.block_id, + block.header.timestamp, + ) + .map_err(|err| { + DbError::db_interaction_error(format!( + "clock transaction execution failed with err {err:?}" + )) + })?; + + Ok(()) +} + fn closest_breakpoint_id(block_id: u64) -> u64 { block_id .saturating_sub(1) @@ -261,464 +263,4 @@ fn closest_breakpoint_id(block_id: u64) -> u64 { #[expect(clippy::shadow_unrelated, reason = "Fine for tests")] #[cfg(test)] -mod tests { - use common::test_utils::produce_dummy_block; - use lee::{AccountId, PublicKey}; - use tempfile::tempdir; - - use super::*; - - fn genesis_block() -> Block { - produce_dummy_block(1, None, vec![]) - } - - fn acc1_sign_key() -> lee::PrivateKey { - lee::PrivateKey::try_new([1; 32]).unwrap() - } - - fn acc2_sign_key() -> lee::PrivateKey { - lee::PrivateKey::try_new([2; 32]).unwrap() - } - - fn acc1() -> AccountId { - AccountId::from(&PublicKey::new_from_private_key(&acc1_sign_key())) - } - - fn acc2() -> AccountId { - AccountId::from(&PublicKey::new_from_private_key(&acc2_sign_key())) - } - - #[test] - fn start_db() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = RocksDBIO::open_or_create( - temdir_path, - &lee::V03State::new_with_genesis_accounts( - &[(acc1(), 10000), (acc2(), 20000)], - vec![], - 0, - ), - ) - .unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap(); - let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); - let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); - let last_observed_l1_header = dbio.get_meta_last_observed_l1_lib_header_in_db().unwrap(); - let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); - let last_block = dbio.get_block(1).unwrap(); - let breakpoint = dbio.get_breakpoint(0).unwrap(); - let final_state = dbio.final_state().unwrap(); - - assert_eq!(last_id, None); - assert_eq!(first_id, None); - assert_eq!(last_observed_l1_header, None); - assert!(!is_first_set); - assert_eq!(last_br_id, Some(0)); // TODO: Will be None after we remove hardcoded testnet state - assert!(last_block.is_none()); - assert_eq!( - breakpoint.get_account_by_id(acc1()), - final_state.get_account_by_id(acc1()) - ); - assert_eq!( - breakpoint.get_account_by_id(acc2()), - final_state.get_account_by_id(acc2()) - ); - } - - #[test] - fn one_block_insertion() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = RocksDBIO::open_or_create( - temdir_path, - &lee::V03State::new_with_genesis_accounts( - &[(acc1(), 10000), (acc2(), 20000)], - vec![], - 0, - ), - ) - .unwrap(); - - let genesis_block = genesis_block(); - dbio.put_block(&genesis_block, [0; 32]).unwrap(); - - let prev_hash = genesis_block.header.hash; - let from = acc1(); - let to = acc2(); - let sign_key = acc1_sign_key(); - - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); - - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); - let last_observed_l1_header = dbio - .get_meta_last_observed_l1_lib_header_in_db() - .unwrap() - .unwrap(); - let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); - let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - let breakpoint = dbio.get_breakpoint(0).unwrap(); - let final_state = dbio.final_state().unwrap(); - - assert_eq!(last_id, 2); - assert_eq!(first_id, Some(1)); - assert_eq!(last_observed_l1_header, [1; 32]); - assert!(is_first_set); - assert_eq!(last_br_id, Some(0)); - assert_eq!(last_block.header.hash, block.header.hash); - assert_eq!( - breakpoint.get_account_by_id(acc1()).balance - - final_state.get_account_by_id(acc1()).balance, - 1 - ); - assert_eq!( - final_state.get_account_by_id(acc2()).balance - - breakpoint.get_account_by_id(acc2()).balance, - 1 - ); - } - - #[test] - fn new_breakpoint() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = RocksDBIO::open_or_create( - temdir_path, - &lee::V03State::new_with_genesis_accounts( - &[(acc1(), 10000), (acc2(), 20000)], - vec![], - 0, - ), - ) - .unwrap(); - - let from = acc1(); - let to = acc2(); - let sign_key = acc1_sign_key(); - - for i in 1..=BREAKPOINT_INTERVAL + 1 { - let prev_hash = dbio.get_meta_last_block_id_in_db().unwrap().map(|last_id| { - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - last_block.header.hash - }); - - let transfer_tx = common::test_utils::create_transaction_native_token_transfer( - from, - (i - 1).into(), - to, - 1, - &sign_key, - ); - let block = produce_dummy_block(i.into(), prev_hash, vec![transfer_tx]); - dbio.put_block(&block, [i; 32]).unwrap(); - } - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); - let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); - let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - let prev_breakpoint = dbio.get_breakpoint(0).unwrap(); - let breakpoint = dbio.get_breakpoint(1).unwrap(); - let final_state = dbio.final_state().unwrap(); - - assert_eq!(last_id, 101); - assert_eq!(first_id, Some(1)); - assert!(is_first_set); - assert_eq!(last_br_id, Some(1)); - assert_ne!(last_block.header.hash, genesis_block().header.hash); - assert_eq!( - prev_breakpoint.get_account_by_id(acc1()).balance - - final_state.get_account_by_id(acc1()).balance, - 101 - ); - assert_eq!( - final_state.get_account_by_id(acc2()).balance - - prev_breakpoint.get_account_by_id(acc2()).balance, - 101 - ); - assert_eq!( - breakpoint.get_account_by_id(acc1()).balance - - final_state.get_account_by_id(acc1()).balance, - 1 - ); - assert_eq!( - final_state.get_account_by_id(acc2()).balance - - breakpoint.get_account_by_id(acc2()).balance, - 1 - ); - } - - #[test] - fn simple_maps() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = RocksDBIO::open_or_create( - temdir_path, - &lee::V03State::new_with_genesis_accounts( - &[(acc1(), 10000), (acc2(), 20000)], - vec![], - 0, - ), - ) - .unwrap(); - - let from = acc1(); - let to = acc2(); - let sign_key = acc1_sign_key(); - - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let block = produce_dummy_block(1, None, vec![transfer_tx]); - - let control_hash1 = block.header.hash; - - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); - let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); - - let control_hash2 = block.header.hash; - - dbio.put_block(&block, [2; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); - - let control_tx_hash1 = transfer_tx.hash(); - - let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); - dbio.put_block(&block, [3; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); - - let control_tx_hash2 = transfer_tx.hash(); - - let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); - dbio.put_block(&block, [4; 32]).unwrap(); - - let control_block_id1 = dbio.get_block_id_by_hash(control_hash1.0).unwrap().unwrap(); - let control_block_id2 = dbio.get_block_id_by_hash(control_hash2.0).unwrap().unwrap(); - let control_block_id3 = dbio - .get_block_id_by_tx_hash(control_tx_hash1.0) - .unwrap() - .unwrap(); - let control_block_id4 = dbio - .get_block_id_by_tx_hash(control_tx_hash2.0) - .unwrap() - .unwrap(); - - assert_eq!(control_block_id1, 1); - assert_eq!(control_block_id2, 2); - assert_eq!(control_block_id3, 3); - assert_eq!(control_block_id4, 4); - } - - #[test] - fn block_batch() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let mut block_res = vec![]; - - let dbio = RocksDBIO::open_or_create( - temdir_path, - &lee::V03State::new_with_genesis_accounts( - &[(acc1(), 10000), (acc2(), 20000)], - vec![], - 0, - ), - ) - .unwrap(); - - let from = acc1(); - let to = acc2(); - let sign_key = acc1_sign_key(); - - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let block = produce_dummy_block(1, None, vec![transfer_tx]); - - block_res.push(block.clone()); - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); - let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); - - block_res.push(block.clone()); - dbio.put_block(&block, [2; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); - - let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); - block_res.push(block.clone()); - dbio.put_block(&block, [3; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); - - let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); - block_res.push(block.clone()); - dbio.put_block(&block, [4; 32]).unwrap(); - - let block_hashes_mem: Vec<[u8; 32]> = - block_res.into_iter().map(|bl| bl.header.hash.0).collect(); - - // Get blocks before ID 5 (i.e., starting from 4 going backwards), limit 4 - // This should return blocks 4, 3, 2, 1 in descending order - let mut batch_res = dbio.get_block_batch(Some(5), 4).unwrap(); - batch_res.reverse(); // Reverse to match ascending order for comparison - - let block_hashes_db: Vec<[u8; 32]> = - batch_res.into_iter().map(|bl| bl.header.hash.0).collect(); - - assert_eq!(block_hashes_mem, block_hashes_db); - - let block_hashes_mem_limited = &block_hashes_mem[1..]; - - // Get blocks before ID 5, limit 3 - // This should return blocks 4, 3, 2 in descending order - let mut batch_res_limited = dbio.get_block_batch(Some(5), 3).unwrap(); - batch_res_limited.reverse(); // Reverse to match ascending order for comparison - - let block_hashes_db_limited: Vec<[u8; 32]> = batch_res_limited - .into_iter() - .map(|bl| bl.header.hash.0) - .collect(); - - assert_eq!(block_hashes_mem_limited, block_hashes_db_limited.as_slice()); - - let block_batch_seq = dbio.get_block_batch_seq(1..=5).unwrap(); - let block_batch_ids = block_batch_seq - .into_iter() - .map(|block| block.header.block_id) - .collect::>(); - - assert_eq!(block_batch_ids, vec![1, 2, 3, 4]); - } - - #[test] - fn account_map() { - let temp_dir = tempdir().unwrap(); - let temdir_path = temp_dir.path(); - - let dbio = RocksDBIO::open_or_create( - temdir_path, - &lee::V03State::new_with_genesis_accounts( - &[(acc1(), 10000), (acc2(), 20000)], - vec![], - 0, - ), - ) - .unwrap(); - - let from = acc1(); - let to = acc2(); - let sign_key = acc1_sign_key(); - - let mut tx_hash_res = vec![]; - - let transfer_tx1 = - common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); - let transfer_tx2 = - common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); - tx_hash_res.push(transfer_tx1.hash().0); - tx_hash_res.push(transfer_tx2.hash().0); - - let block = produce_dummy_block(1, None, vec![transfer_tx1, transfer_tx2]); - - dbio.put_block(&block, [1; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx1 = - common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); - let transfer_tx2 = - common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); - tx_hash_res.push(transfer_tx1.hash().0); - tx_hash_res.push(transfer_tx2.hash().0); - - let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); - - dbio.put_block(&block, [2; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx1 = - common::test_utils::create_transaction_native_token_transfer(from, 4, to, 1, &sign_key); - let transfer_tx2 = - common::test_utils::create_transaction_native_token_transfer(from, 5, to, 1, &sign_key); - tx_hash_res.push(transfer_tx1.hash().0); - tx_hash_res.push(transfer_tx2.hash().0); - - let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); - - dbio.put_block(&block, [3; 32]).unwrap(); - - let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); - let last_block = dbio.get_block(last_id).unwrap().unwrap(); - - let prev_hash = last_block.header.hash; - let transfer_tx = - common::test_utils::create_transaction_native_token_transfer(from, 6, to, 1, &sign_key); - tx_hash_res.push(transfer_tx.hash().0); - - let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); - - dbio.put_block(&block, [4; 32]).unwrap(); - - let acc1_tx = dbio.get_acc_transactions(*acc1().value(), 0, 7).unwrap(); - let acc1_tx_hashes: Vec<[u8; 32]> = acc1_tx.into_iter().map(|tx| tx.hash().0).collect(); - - assert_eq!(acc1_tx_hashes, tx_hash_res); - - let acc1_tx_limited = dbio.get_acc_transactions(*acc1().value(), 1, 4).unwrap(); - let acc1_tx_limited_hashes: Vec<[u8; 32]> = - acc1_tx_limited.into_iter().map(|tx| tx.hash().0).collect(); - - assert_eq!(acc1_tx_limited_hashes.as_slice(), &tx_hash_res[1..5]); - } -} +mod tests; diff --git a/lez/storage/src/indexer/tests.rs b/lez/storage/src/indexer/tests.rs new file mode 100644 index 00000000..44df23d5 --- /dev/null +++ b/lez/storage/src/indexer/tests.rs @@ -0,0 +1,433 @@ +use common::test_utils::produce_dummy_block; +use lee::{Account, AccountId, PublicKey}; +use tempfile::tempdir; + +use super::*; + +fn genesis_block() -> Block { + produce_dummy_block(1, None, vec![]) +} + +fn acc1_sign_key() -> lee::PrivateKey { + lee::PrivateKey::try_new([1; 32]).unwrap() +} + +fn acc2_sign_key() -> lee::PrivateKey { + lee::PrivateKey::try_new([2; 32]).unwrap() +} + +fn acc1() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&acc1_sign_key())) +} + +fn acc2() -> AccountId { + AccountId::from(&PublicKey::new_from_private_key(&acc2_sign_key())) +} + +fn initial_state() -> lee::V03State { + let mut public_accounts = [(acc1(), 10000), (acc2(), 20000)] + .into_iter() + .map(|(id, balance)| { + ( + id, + Account { + program_owner: programs::authenticated_transfer().id(), + balance, + ..Account::default() + }, + ) + }) + .collect::>(); + for clock_id in system_accounts::clock_account_ids() { + public_accounts.push((clock_id, system_accounts::clock_account())); + } + + lee::V03State::new() + .with_public_accounts(public_accounts) + .with_programs([programs::authenticated_transfer(), programs::clock()]) +} + +#[test] +fn start_db() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap(); + let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); + let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); + let last_observed_l1_header = dbio.get_meta_last_observed_l1_lib_header_in_db().unwrap(); + let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); + let last_block = dbio.get_block(1).unwrap(); + let breakpoint = dbio.get_breakpoint(0).unwrap(); + let final_state = dbio.final_state().unwrap(); + + assert_eq!(last_id, None); + assert_eq!(first_id, None); + assert_eq!(last_observed_l1_header, None); + assert!(!is_first_set); + assert_eq!(last_br_id, Some(0)); // TODO: Will be None after we remove hardcoded testnet state + assert!(last_block.is_none()); + assert_eq!( + breakpoint.get_account_by_id(acc1()), + final_state.get_account_by_id(acc1()) + ); + assert_eq!( + breakpoint.get_account_by_id(acc2()), + final_state.get_account_by_id(acc2()) + ); +} + +#[test] +fn one_block_insertion() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let genesis_block = genesis_block(); + dbio.put_block(&genesis_block, [0; 32]).unwrap(); + + let prev_hash = genesis_block.header.hash; + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); + let last_observed_l1_header = dbio + .get_meta_last_observed_l1_lib_header_in_db() + .unwrap() + .unwrap(); + let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); + let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + let breakpoint = dbio.get_breakpoint(0).unwrap(); + let final_state = dbio.final_state().unwrap(); + + assert_eq!(last_id, 2); + assert_eq!(first_id, Some(1)); + assert_eq!(last_observed_l1_header, [1; 32]); + assert!(is_first_set); + assert_eq!(last_br_id, Some(0)); + assert_eq!(last_block.header.hash, block.header.hash); + assert_eq!( + breakpoint.get_account_by_id(acc1()).balance + - final_state.get_account_by_id(acc1()).balance, + 1 + ); + assert_eq!( + final_state.get_account_by_id(acc2()).balance + - breakpoint.get_account_by_id(acc2()).balance, + 1 + ); +} + +#[test] +fn new_breakpoint() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + for i in 1..=BREAKPOINT_INTERVAL + 1 { + let prev_hash = dbio.get_meta_last_block_id_in_db().unwrap().map(|last_id| { + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + last_block.header.hash + }); + + let transfer_tx = common::test_utils::create_transaction_native_token_transfer( + from, + (i - 1).into(), + to, + 1, + &sign_key, + ); + let block = produce_dummy_block(i.into(), prev_hash, vec![transfer_tx]); + dbio.put_block(&block, [i; 32]).unwrap(); + } + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let first_id = dbio.get_meta_first_block_id_in_db().unwrap(); + let is_first_set = dbio.get_meta_is_first_block_set().unwrap(); + let last_br_id = dbio.get_meta_last_breakpoint_id().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + let prev_breakpoint = dbio.get_breakpoint(0).unwrap(); + let breakpoint = dbio.get_breakpoint(1).unwrap(); + let final_state = dbio.final_state().unwrap(); + + assert_eq!(last_id, 101); + assert_eq!(first_id, Some(1)); + assert!(is_first_set); + assert_eq!(last_br_id, Some(1)); + assert_ne!(last_block.header.hash, genesis_block().header.hash); + assert_eq!( + prev_breakpoint.get_account_by_id(acc1()).balance + - final_state.get_account_by_id(acc1()).balance, + 101 + ); + assert_eq!( + final_state.get_account_by_id(acc2()).balance + - prev_breakpoint.get_account_by_id(acc2()).balance, + 101 + ); + assert_eq!( + breakpoint.get_account_by_id(acc1()).balance + - final_state.get_account_by_id(acc1()).balance, + 1 + ); + assert_eq!( + final_state.get_account_by_id(acc2()).balance + - breakpoint.get_account_by_id(acc2()).balance, + 1 + ); +} + +#[test] +fn simple_maps() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let block = produce_dummy_block(1, None, vec![transfer_tx]); + + let control_hash1 = block.header.hash; + + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + + let control_hash2 = block.header.hash; + + dbio.put_block(&block, [2; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); + + let control_tx_hash1 = transfer_tx.hash(); + + let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); + dbio.put_block(&block, [3; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); + + let control_tx_hash2 = transfer_tx.hash(); + + let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + dbio.put_block(&block, [4; 32]).unwrap(); + + let control_block_id1 = dbio.get_block_id_by_hash(control_hash1.0).unwrap().unwrap(); + let control_block_id2 = dbio.get_block_id_by_hash(control_hash2.0).unwrap().unwrap(); + let control_block_id3 = dbio + .get_block_id_by_tx_hash(control_tx_hash1.0) + .unwrap() + .unwrap(); + let control_block_id4 = dbio + .get_block_id_by_tx_hash(control_tx_hash2.0) + .unwrap() + .unwrap(); + + assert_eq!(control_block_id1, 1); + assert_eq!(control_block_id2, 2); + assert_eq!(control_block_id3, 3); + assert_eq!(control_block_id4, 4); +} + +#[test] +fn block_batch() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let mut block_res = vec![]; + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let block = produce_dummy_block(1, None, vec![transfer_tx]); + + block_res.push(block.clone()); + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx]); + + block_res.push(block.clone()); + dbio.put_block(&block, [2; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); + + let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx]); + block_res.push(block.clone()); + dbio.put_block(&block, [3; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); + + let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + block_res.push(block.clone()); + dbio.put_block(&block, [4; 32]).unwrap(); + + let block_hashes_mem: Vec<[u8; 32]> = + block_res.into_iter().map(|bl| bl.header.hash.0).collect(); + + // Get blocks before ID 5 (i.e., starting from 4 going backwards), limit 4 + // This should return blocks 4, 3, 2, 1 in descending order + let mut batch_res = dbio.get_block_batch(Some(5), 4).unwrap(); + batch_res.reverse(); // Reverse to match ascending order for comparison + + let block_hashes_db: Vec<[u8; 32]> = batch_res.into_iter().map(|bl| bl.header.hash.0).collect(); + + assert_eq!(block_hashes_mem, block_hashes_db); + + let block_hashes_mem_limited = &block_hashes_mem[1..]; + + // Get blocks before ID 5, limit 3 + // This should return blocks 4, 3, 2 in descending order + let mut batch_res_limited = dbio.get_block_batch(Some(5), 3).unwrap(); + batch_res_limited.reverse(); // Reverse to match ascending order for comparison + + let block_hashes_db_limited: Vec<[u8; 32]> = batch_res_limited + .into_iter() + .map(|bl| bl.header.hash.0) + .collect(); + + assert_eq!(block_hashes_mem_limited, block_hashes_db_limited.as_slice()); + + let block_batch_seq = dbio.get_block_batch_seq(1..=5).unwrap(); + let block_batch_ids = block_batch_seq + .into_iter() + .map(|block| block.header.block_id) + .collect::>(); + + assert_eq!(block_batch_ids, vec![1, 2, 3, 4]); +} + +#[test] +fn account_map() { + let temp_dir = tempdir().unwrap(); + let temdir_path = temp_dir.path(); + + let dbio = RocksDBIO::open_or_create(temdir_path, &initial_state()).unwrap(); + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + let mut tx_hash_res = vec![]; + + let transfer_tx1 = + common::test_utils::create_transaction_native_token_transfer(from, 0, to, 1, &sign_key); + let transfer_tx2 = + common::test_utils::create_transaction_native_token_transfer(from, 1, to, 1, &sign_key); + tx_hash_res.push(transfer_tx1.hash().0); + tx_hash_res.push(transfer_tx2.hash().0); + + let block = produce_dummy_block(1, None, vec![transfer_tx1, transfer_tx2]); + + dbio.put_block(&block, [1; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx1 = + common::test_utils::create_transaction_native_token_transfer(from, 2, to, 1, &sign_key); + let transfer_tx2 = + common::test_utils::create_transaction_native_token_transfer(from, 3, to, 1, &sign_key); + tx_hash_res.push(transfer_tx1.hash().0); + tx_hash_res.push(transfer_tx2.hash().0); + + let block = produce_dummy_block(2, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); + + dbio.put_block(&block, [2; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx1 = + common::test_utils::create_transaction_native_token_transfer(from, 4, to, 1, &sign_key); + let transfer_tx2 = + common::test_utils::create_transaction_native_token_transfer(from, 5, to, 1, &sign_key); + tx_hash_res.push(transfer_tx1.hash().0); + tx_hash_res.push(transfer_tx2.hash().0); + + let block = produce_dummy_block(3, Some(prev_hash), vec![transfer_tx1, transfer_tx2]); + + dbio.put_block(&block, [3; 32]).unwrap(); + + let last_id = dbio.get_meta_last_block_id_in_db().unwrap().unwrap(); + let last_block = dbio.get_block(last_id).unwrap().unwrap(); + + let prev_hash = last_block.header.hash; + let transfer_tx = + common::test_utils::create_transaction_native_token_transfer(from, 6, to, 1, &sign_key); + tx_hash_res.push(transfer_tx.hash().0); + + let block = produce_dummy_block(4, Some(prev_hash), vec![transfer_tx]); + + dbio.put_block(&block, [4; 32]).unwrap(); + + let acc1_tx = dbio.get_acc_transactions(*acc1().value(), 0, 7).unwrap(); + let acc1_tx_hashes: Vec<[u8; 32]> = acc1_tx.into_iter().map(|tx| tx.hash().0).collect(); + + assert_eq!(acc1_tx_hashes, tx_hash_res); + + let acc1_tx_limited = dbio.get_acc_transactions(*acc1().value(), 1, 4).unwrap(); + let acc1_tx_limited_hashes: Vec<[u8; 32]> = + acc1_tx_limited.into_iter().map(|tx| tx.hash().0).collect(); + + assert_eq!(acc1_tx_limited_hashes.as_slice(), &tx_hash_res[1..5]); +} diff --git a/lez/storage/src/indexer/write_atomic.rs b/lez/storage/src/indexer/write_atomic.rs index a88e46ef..ec28f8ec 100644 --- a/lez/storage/src/indexer/write_atomic.rs +++ b/lez/storage/src/indexer/write_atomic.rs @@ -52,44 +52,8 @@ impl RocksDBIO { acc_id: [u8; 32], tx_hashes: &[[u8; 32]], ) -> DbResult<()> { - let acc_num_tx = self.get_acc_meta_num_tx(acc_id)?.unwrap_or(0); - let cf_att = self.account_id_to_tx_hash_column(); let mut write_batch = WriteBatch::new(); - - for (tx_id, tx_hash) in tx_hashes.iter().enumerate() { - let put_id = acc_num_tx - .checked_add(tx_id.try_into().expect("Must fit into u64")) - .expect("Tx count should be lesser that u64::MAX"); - - let mut prefix = borsh::to_vec(&acc_id).map_err(|berr| { - DbError::borsh_cast_message(berr, Some("Failed to serialize account id".to_owned())) - })?; - let suffix = borsh::to_vec(&put_id).map_err(|berr| { - DbError::borsh_cast_message(berr, Some("Failed to serialize tx id".to_owned())) - })?; - - prefix.extend_from_slice(&suffix); - - write_batch.put_cf( - &cf_att, - prefix, - borsh::to_vec(tx_hash).map_err(|berr| { - DbError::borsh_cast_message( - berr, - Some("Failed to serialize tx hash".to_owned()), - ) - })?, - ); - } - - self.update_acc_meta_batch( - acc_id, - acc_num_tx - .checked_add(tx_hashes.len().try_into().expect("Must fit into u64")) - .expect("Tx count should be lesser that u64::MAX"), - &mut write_batch, - )?; - + self.put_account_transactions_dependant(acc_id, tx_hashes, &mut write_batch)?; self.db.write(write_batch).map_err(|rerr| { DbError::rocksdb_cast_message(rerr, Some("Failed to write batch".to_owned())) }) diff --git a/lez/system_accounts/Cargo.toml b/lez/system_accounts/Cargo.toml new file mode 100644 index 00000000..093e6455 --- /dev/null +++ b/lez/system_accounts/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "system_accounts" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[lints] +workspace = true + +[dependencies] +lee_core.workspace = true +faucet_core.workspace = true +bridge_core.workspace = true +clock_core.workspace = true +programs.workspace = true diff --git a/lez/system_accounts/src/lib.rs b/lez/system_accounts/src/lib.rs new file mode 100644 index 00000000..3b6dd5af --- /dev/null +++ b/lez/system_accounts/src/lib.rs @@ -0,0 +1,71 @@ +//! This crate provides system accounts used by LEZ. + +use std::str::FromStr as _; + +use clock_core::ClockAccountData; +use lee_core::account::{Account, AccountId, Nonce}; + +#[must_use] +pub fn pinata_account_id() -> AccountId { + // TODO: Use derivation from a public key? + AccountId::from_str("EfQhKQAkX2FJiwNii2WFQsGndjvF1Mzd7RuVe7QdPLw7") + .expect("Pinata program id should be valid") +} + +#[must_use] +pub fn pinata_account() -> Account { + Account { + program_owner: programs::pinata().id(), + balance: 1_500_000, + // Difficulty: 3 + data: vec![3; 33].try_into().expect("Should fit"), + nonce: Nonce::default(), + } +} + +#[must_use] +pub fn faucet_account_id() -> AccountId { + faucet_core::compute_faucet_account_id(programs::faucet().id()) +} + +#[must_use] +pub fn faucet_account() -> Account { + Account { + program_owner: programs::authenticated_transfer().id(), + balance: u128::MAX, + ..Account::default() + } +} + +#[must_use] +pub fn bridge_account_id() -> AccountId { + bridge_core::compute_bridge_account_id(programs::bridge().id()) +} + +#[must_use] +pub fn bridge_account() -> Account { + Account { + program_owner: programs::authenticated_transfer().id(), + ..Account::default() + } +} + +#[must_use] +pub const fn clock_account_ids() -> [AccountId; 3] { + clock_core::CLOCK_PROGRAM_ACCOUNT_IDS +} + +#[must_use] +pub fn clock_account() -> Account { + Account { + program_owner: programs::clock().id(), + data: ClockAccountData { + block_id: 0, + timestamp: 0, + } + .to_bytes() + .try_into() + .expect("Clock account data should fit"), + ..Account::default() + } +} diff --git a/lez/testnet_initial_state/Cargo.toml b/lez/testnet_initial_state/Cargo.toml index c2e694b4..f06d5f51 100644 --- a/lez/testnet_initial_state/Cargo.toml +++ b/lez/testnet_initial_state/Cargo.toml @@ -8,7 +8,8 @@ license.workspace = true key_protocol.workspace = true lee.workspace = true lee_core.workspace = true -common.workspace = true +system_accounts.workspace = true +programs.workspace = true serde.workspace = true diff --git a/lez/testnet_initial_state/src/lib.rs b/lez/testnet_initial_state/src/lib.rs index 663b89ff..3296b953 100644 --- a/lez/testnet_initial_state/src/lib.rs +++ b/lez/testnet_initial_state/src/lib.rs @@ -1,10 +1,11 @@ -use common::PINATA_BASE58; +use std::collections::HashMap; + use key_protocol::key_management::{ KeyChain, key_tree::chain_index::ChainIndex, secret_holders::{PrivateKeyHolder, SecretSpendingKey, ViewingSecretKey}, }; -use lee::{Account, AccountId, Data, PrivateKey, PublicKey, V03State}; +use lee::{Account, AccountId, Data, PrivateKey, PublicKey, V03State, program::Program}; use lee_core::{NullifierPublicKey, encryption::ViewingPublicKey}; use serde::{Deserialize, Serialize}; @@ -132,8 +133,7 @@ pub fn initial_pub_accounts_private_keys() -> Vec Vec { +fn initial_priv_accounts_private_keys() -> Vec { let key_chain_1 = KeyChain { secret_spending_key: SecretSpendingKey(SSK_PRIV_ACC_A), private_key_holder: PrivateKeyHolder { @@ -180,8 +180,7 @@ pub fn initial_priv_accounts_private_keys() -> Vec Vec { +fn initial_commitments() -> Vec { initial_priv_accounts_private_keys() .into_iter() .map(|data| PrivateAccountPublicInitialData { @@ -192,8 +191,28 @@ pub fn initial_commitments() -> Vec { .collect() } +fn initial_private_accounts() -> Vec<(lee_core::Commitment, lee_core::Nullifier)> { + initial_commitments() + .iter() + .map(|init_comm_data| { + let npk = &init_comm_data.npk; + let account_id = + lee::AccountId::for_regular_private_account(npk, &init_comm_data.vpk, 0); + + let mut acc = init_comm_data.account.clone(); + + acc.program_owner = programs::authenticated_transfer().id(); + + ( + lee_core::Commitment::new(&account_id, &acc), + lee_core::Nullifier::for_account_initialization(&account_id), + ) + }) + .collect() +} + #[must_use] -pub fn initial_accounts() -> Vec { +pub fn initial_public_user_accounts() -> Vec { let initial_account_ids = initial_pub_accounts_private_keys() .into_iter() .map(|data| data.account_id) @@ -211,42 +230,73 @@ pub fn initial_accounts() -> Vec { ] } +fn initial_public_accounts() -> HashMap { + initial_public_user_accounts() + .iter() + .map(|acc_data| { + ( + acc_data.account_id, + Account { + program_owner: programs::authenticated_transfer().id(), + balance: acc_data.balance, + ..Default::default() + }, + ) + }) + .chain([ + ( + system_accounts::faucet_account_id(), + system_accounts::faucet_account(), + ), + ( + system_accounts::bridge_account_id(), + system_accounts::bridge_account(), + ), + ]) + .chain( + system_accounts::clock_account_ids() + .into_iter() + .map(|clock_id| (clock_id, system_accounts::clock_account())), + ) + .collect() +} + +fn initial_programs() -> Vec { + vec![ + programs::authenticated_transfer(), + programs::token(), + programs::amm(), + programs::clock(), + programs::ata(), + programs::vault(), + programs::faucet(), + programs::bridge(), + ] +} + #[must_use] pub fn initial_state() -> V03State { - let initial_private_accounts: Vec<(lee_core::Commitment, lee_core::Nullifier)> = - initial_commitments() - .iter() - .map(|init_comm_data| { - let npk = &init_comm_data.npk; - let account_id = - lee::AccountId::for_regular_private_account(npk, &init_comm_data.vpk, 0); - - let mut acc = init_comm_data.account.clone(); - - acc.program_owner = lee::program::Program::authenticated_transfer_program().id(); - - ( - lee_core::Commitment::new(&account_id, &acc), - lee_core::Nullifier::for_account_initialization(&account_id), - ) - }) - .collect(); - - let init_accs: Vec<(lee::AccountId, u128)> = initial_accounts() - .iter() - .map(|acc_data| (acc_data.account_id, acc_data.balance)) - .collect(); - - lee::V03State::new_with_genesis_accounts(&init_accs, initial_private_accounts, 0) + lee::V03State::new() + .with_public_accounts(initial_public_accounts()) + .with_private_accounts(initial_private_accounts()) + .with_programs(initial_programs()) } #[must_use] pub fn initial_state_testnet() -> V03State { - let mut state = initial_state(); + let mut initial_public_accounts = initial_public_accounts(); + initial_public_accounts.insert( + system_accounts::pinata_account_id(), + system_accounts::pinata_account(), + ); - state.add_pinata_program(PINATA_BASE58.parse().unwrap()); + let mut programs = initial_programs(); + programs.push(programs::pinata()); - state + V03State::new() + .with_public_accounts(initial_public_accounts) + .with_private_accounts(initial_private_accounts()) + .with_programs(programs) } #[cfg(test)] @@ -264,7 +314,7 @@ mod tests { #[test] fn pub_state_consistency() { let init_accs_private_data = initial_pub_accounts_private_keys(); - let init_accs_pub_data = initial_accounts(); + let init_accs_pub_data = initial_public_user_accounts(); assert_eq!( init_accs_private_data[0].account_id, @@ -424,4 +474,33 @@ mod tests { } ); } + + #[test] + fn genesis_system_accounts_have_expected_contents() { + // System-account IDs must be distinct and non-default, and the genesis + // faucet/bridge accounts must carry their expected field values. Catches + // mutations that replace `system_faucet_account`/`system_bridge_account` + // with `Default::default()`, delete their `balance`/`program_owner` + // fields, or replace `system_bridge_account_id` with `Default::default()`. + let faucet_id = system_accounts::faucet_account_id(); + let bridge_id = system_accounts::bridge_account_id(); + assert_ne!(bridge_id, AccountId::default()); + assert_ne!(faucet_id, bridge_id); + + let state = initial_state(); + let default_owner = Account::default().program_owner; + + let faucet = state.get_account_by_id(faucet_id); + assert_eq!(faucet.balance, u128::MAX, "faucet must hold u128::MAX"); + assert_ne!( + faucet.program_owner, default_owner, + "faucet must have a non-default program_owner" + ); + + let bridge = state.get_account_by_id(bridge_id); + assert_ne!( + bridge.program_owner, default_owner, + "bridge must have a non-default program_owner" + ); + } } diff --git a/lez/wallet-ffi/Cargo.toml b/lez/wallet-ffi/Cargo.toml index 1e8b6395..47d18c53 100644 --- a/lez/wallet-ffi/Cargo.toml +++ b/lez/wallet-ffi/Cargo.toml @@ -16,11 +16,14 @@ lee.workspace = true lee_core.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } common.workspace = true +programs.workspace = true tokio.workspace = true key_protocol.workspace = true serde_json.workspace = true risc0-zkvm.workspace = true +bip39.workspace = true +vault_core.workspace = true [build-dependencies] cbindgen = "0.29" diff --git a/lez/wallet-ffi/src/bridge.rs b/lez/wallet-ffi/src/bridge.rs new file mode 100644 index 00000000..fca71b1c --- /dev/null +++ b/lez/wallet-ffi/src/bridge.rs @@ -0,0 +1,91 @@ +//! Bridge program functions (deposit/withdraw between L1 Bedrock and L2). + +use std::{ffi::CString, ptr}; + +use lee::AccountId; +use wallet::program_facades::bridge::Bridge; + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + map_execution_error, + types::{FfiBytes32, FfiTransferResult, WalletHandle}, + wallet::get_wallet, +}; + +/// Withdraw native tokens from a public account to Bedrock (L1) through the bridge. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `from`: Source public account ID (must be owned by this wallet). Bridge withdrawals only +/// support public sender accounts. +/// - `amount`: Amount of native tokens to withdraw +/// - `bedrock_account_pk`: Recipient's Bedrock (L1) public key, 32 bytes +/// - `out_result`: Output pointer for the withdraw result +/// +/// # Returns +/// - `Success` if the withdraw transaction was submitted successfully +/// - `InsufficientFunds` if the source account doesn't have enough balance +/// - `KeyNotFound` if the source account's signing key is not in this wallet +/// - Error code on other failures +/// +/// # Memory +/// The result must be freed with `wallet_ffi_free_transfer_result()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `from` must be a valid pointer to a `FfiBytes32` struct +/// - `bedrock_account_pk` must be a valid pointer to a `FfiBytes32` struct +/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_bridge_withdraw( + handle: *mut WalletHandle, + from: *const FfiBytes32, + amount: u64, + bedrock_account_pk: *const FfiBytes32, + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if from.is_null() || bedrock_account_pk.is_null() || out_result.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return WalletFfiError::InternalError; + } + }; + + let from_id = AccountId::new(unsafe { (*from).data }); + let bedrock_account_pk = unsafe { (*bedrock_account_pk).data }; + + let bridge = Bridge(&wallet); + + match block_on(bridge.send_withdraw(from_id, amount, bedrock_account_pk)) { + Ok(tx_hash) => { + let tx_hash = CString::new(tx_hash.to_string()) + .map_or(ptr::null_mut(), std::ffi::CString::into_raw); + + unsafe { + (*out_result).tx_hash = tx_hash; + (*out_result).success = true; + } + WalletFfiError::Success + } + Err(e) => { + print_error(format!("Bridge withdraw failed: {e:?}")); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + } +} diff --git a/lez/wallet-ffi/src/generic_transaction.rs b/lez/wallet-ffi/src/generic_transaction.rs index f8f344ad..7be6ddaf 100644 --- a/lez/wallet-ffi/src/generic_transaction.rs +++ b/lez/wallet-ffi/src/generic_transaction.rs @@ -10,7 +10,7 @@ use crate::{ error::{print_error, WalletFfiError}, map_execution_error, wallet::get_wallet, - FfiAccountIdentity, FfiBytes32, WalletHandle, + FfiAccountIdentity, FfiBytes32, FfiProgramId, WalletHandle, }; #[repr(C)] @@ -48,7 +48,7 @@ impl TryFrom<&FfiProgram> for Program { elf.push(unsafe { *value.elf_data.add(i) }); } - Self::new(elf).map_err(|err| { + Self::new(elf.into()).map_err(|err| { print_error(format!("Invalid program bytecode, err: {err}")); WalletFfiError::InvalidBytecode }) @@ -214,7 +214,7 @@ pub unsafe extern "C" fn wallet_ffi_send_generic_public_transaction( account_identities_size: usize, instruction_words: *const u32, instruction_words_size: usize, - program_with_dependencies: *const FfiProgramWithDependencies, + program_id: FfiProgramId, out_result: *mut FfiTransactionResult, ) -> WalletFfiError { let wrapper = match get_wallet(handle) { @@ -260,12 +260,7 @@ pub unsafe extern "C" fn wallet_ffi_send_generic_public_transaction( } } - let program = match unsafe { &*program_with_dependencies }.try_into() { - Ok(v) => v, - Err(err) => return err, - }; - - match block_on(wallet.send_pub_tx(accounts, instruction_data.to_vec(), &program)) { + match block_on(wallet.send_pub_tx(accounts, instruction_data.to_vec(), program_id.into())) { Ok(tx_hash) => { let tx_hash = CString::new(tx_hash.to_string()) .map_or(std::ptr::null_mut(), std::ffi::CString::into_raw); @@ -445,13 +440,11 @@ pub unsafe extern "C" fn wallet_ffi_free_instruction_words(words: *mut FfiInstru #[cfg(test)] mod tests { - use lee::program::Program; - use crate::generic_transaction::FfiProgram; #[test] fn program_cast_consistency() { - let prog = Program::amm(); + let prog = programs::amm(); let first_5_bytes = prog.elf()[..5].to_vec(); diff --git a/lez/wallet-ffi/src/label.rs b/lez/wallet-ffi/src/label.rs new file mode 100644 index 00000000..1f6f1236 --- /dev/null +++ b/lez/wallet-ffi/src/label.rs @@ -0,0 +1,316 @@ +use std::{ + ffi::{c_char, CString}, + str::FromStr as _, +}; + +use crate::{ + c_str_to_string, + error::{print_error, WalletFfiError}, + wallet::get_wallet, + FfiAccountIdWithPrivacy, WalletHandle, +}; + +#[repr(C)] +pub struct LabelAvailability { + pub is_available: bool, + pub error: WalletFfiError, +} + +impl LabelAvailability { + #[must_use] + pub const fn availability(is_available: bool) -> Self { + Self { + is_available, + error: WalletFfiError::Success, + } + } + + #[must_use] + pub const fn error(error: WalletFfiError) -> Self { + Self { + is_available: false, + error, + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct AccountIdResolvedFromLabel { + pub account_id: FfiAccountIdWithPrivacy, + pub error: WalletFfiError, +} + +impl AccountIdResolvedFromLabel { + #[must_use] + pub const fn account_id(account_id: FfiAccountIdWithPrivacy) -> Self { + Self { + account_id, + error: WalletFfiError::Success, + } + } + + #[must_use] + pub fn error(error: WalletFfiError) -> Self { + Self { + account_id: FfiAccountIdWithPrivacy::default(), + error, + } + } +} + +#[repr(C)] +pub struct LabelList { + pub labels_data: *mut *const c_char, + pub labels_size: usize, + pub error: WalletFfiError, +} + +impl LabelList { + #[must_use] + pub fn from_labels(labels: Vec<*const c_char>) -> Self { + let labels_size = labels.len(); + let boxed_slice = labels.into_boxed_slice(); + let labels_data = Box::into_raw(boxed_slice).cast::<*const c_char>(); + + Self { + labels_data, + labels_size, + error: WalletFfiError::Success, + } + } + + #[must_use] + pub const fn error(error: WalletFfiError) -> Self { + Self { + labels_data: std::ptr::null_mut(), + labels_size: 0, + error, + } + } +} + +/// Check if label is available. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `label`: Input null terminated C string for a label +/// +/// # Returns +/// - `LabelAvailability` struct +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `label` must be a valid pointer to a null-terminated C string +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_check_label_available( + handle: *mut WalletHandle, + label: *const c_char, +) -> LabelAvailability { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return LabelAvailability::error(e), + }; + + let label = match c_str_to_string(label, "label") { + Ok(value) => value, + Err(e) => return LabelAvailability::error(e), + }; + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return LabelAvailability::error(WalletFfiError::InternalError); + } + }; + + let is_available = wallet + .storage() + .check_label_availability(&label.into()) + .is_ok(); + + LabelAvailability::availability(is_available) +} + +/// Add new label. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `label`: Input null terminated C string for a label +/// - `account_id_with_privacy`: The account ID (32 bytes) and its privacy. +/// +/// # Returns +/// - `Success` on successful query +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `label` must be a valid pointer to a null-terminated C string +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_add_label( + handle: *mut WalletHandle, + label: *const c_char, + account_id_with_privacy: FfiAccountIdWithPrivacy, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + let label = match c_str_to_string(label, "label") { + Ok(value) => value, + Err(e) => return e, + }; + + let mut wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return WalletFfiError::InternalError; + } + }; + + match wallet + .storage_mut() + .add_label(label.into(), account_id_with_privacy.into()) + { + Ok(()) => WalletFfiError::Success, + Err(err) => { + print_error(format!("Failed to add label : {err}")); + WalletFfiError::InternalError + } + } +} + +/// Resolve a label. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `label`: Input null terminated C string for a label +/// +/// # Returns +/// - `AccountIdResolvedFromLabel` struct +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `label` must be a valid pointer to a null-terminated C string +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_resolve_label( + handle: *mut WalletHandle, + label: *const c_char, +) -> AccountIdResolvedFromLabel { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return AccountIdResolvedFromLabel::error(e), + }; + + let label = match c_str_to_string(label, "label") { + Ok(value) => value, + Err(e) => return AccountIdResolvedFromLabel::error(e), + }; + + let mut wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return AccountIdResolvedFromLabel::error(WalletFfiError::InternalError); + } + }; + + wallet + .storage_mut() + .resolve_label(&label.into()) + .map_or_else( + || { + print_error("Failed to resolve label"); + AccountIdResolvedFromLabel::error(WalletFfiError::InternalError) + }, + |acc_id| AccountIdResolvedFromLabel::account_id(acc_id.into()), + ) +} + +/// Get all labels for account. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `account_id_with_privacy`: The account ID (32 bytes) and its privacy. +/// +/// # Returns +/// - `LabelList` struct +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_all_labels_for_account( + handle: *mut WalletHandle, + account_id_with_privacy: FfiAccountIdWithPrivacy, +) -> LabelList { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return LabelList::error(e), + }; + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return LabelList::error(WalletFfiError::InternalError); + } + }; + + let mut labels = vec![]; + + for label in wallet + .storage() + .labels_for_account(account_id_with_privacy.into()) + { + let Ok(label_c) = CString::from_str(label.as_ref()) else { + print_error(format!("Failed to cast label into C string: {label}")); + return LabelList::error(WalletFfiError::InternalError); + }; + + let label_raw = label_c.into_raw().cast_const(); + + labels.push(label_raw); + } + + LabelList::from_labels(labels) +} + +/// Free label list. +/// +/// # Parameters +/// - `label_list`: Input list of labels +/// +/// # Returns +/// - `Success` on successful query +/// - Error code on failure +/// +/// # Safety +/// - `label_list` must be a valid pointer to `LabelList`, received from +/// `wallet_ffi_get_all_labels_for_account` +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_label_list(label_list: *mut LabelList) -> WalletFfiError { + if label_list.is_null() { + return WalletFfiError::NullPointer; + } + + let labels_raw = unsafe { &*label_list }; + + if !labels_raw.labels_data.is_null() && labels_raw.labels_size > 0 { + let labels_slice = + std::slice::from_raw_parts_mut(labels_raw.labels_data, labels_raw.labels_size); + + for label_ptr in labels_slice.iter() { + if !(*label_ptr).is_null() { + drop(CString::from_raw((*label_ptr).cast_mut())); + } + } + + let boxed_slice = Box::from_raw(std::ptr::from_mut::<[*const c_char]>(labels_slice)); + drop(boxed_slice); + } + + WalletFfiError::Success +} diff --git a/lez/wallet-ffi/src/lib.rs b/lez/wallet-ffi/src/lib.rs index 6f4c1808..361ae672 100644 --- a/lez/wallet-ffi/src/lib.rs +++ b/lez/wallet-ffi/src/lib.rs @@ -42,14 +42,17 @@ pub use types::*; use crate::error::print_error; pub mod account; +pub mod bridge; pub mod error; pub mod generic_transaction; pub mod keys; +pub mod label; pub mod pinata; pub mod program_deployment; pub mod sync; pub mod transfer; pub mod types; +pub mod vault; pub mod wallet; static TOKIO_RUNTIME: OnceLock = OnceLock::new(); diff --git a/lez/wallet-ffi/src/program_deployment.rs b/lez/wallet-ffi/src/program_deployment.rs index 8820a935..2086fa5c 100644 --- a/lez/wallet-ffi/src/program_deployment.rs +++ b/lez/wallet-ffi/src/program_deployment.rs @@ -1,7 +1,7 @@ use std::{ffi::CString, ptr, slice}; use common::transaction::LeeTransaction; -use lee::{program::Program, ProgramDeploymentTransaction}; +use lee::ProgramDeploymentTransaction; use sequencer_service_rpc::RpcClient as _; use crate::{ @@ -112,7 +112,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_elf(ffi_program: *mut FfiProgram) - return WalletFfiError::NullPointer; } - let elf = Program::authenticated_transfer_program().elf().to_vec(); + let elf = programs::authenticated_transfer().elf().to_vec(); let (raw_elf_data, raw_elf_size, _) = elf.into_raw_parts(); @@ -147,7 +147,7 @@ pub unsafe extern "C" fn wallet_ffi_token_elf(ffi_program: *mut FfiProgram) -> W return WalletFfiError::NullPointer; } - let elf = Program::token().elf().to_vec(); + let elf = programs::token().elf().to_vec(); let (raw_elf_data, raw_elf_size, _) = elf.into_raw_parts(); @@ -182,7 +182,7 @@ pub unsafe extern "C" fn wallet_ffi_amm_elf(ffi_program: *mut FfiProgram) -> Wal return WalletFfiError::NullPointer; } - let elf = Program::amm().elf().to_vec(); + let elf = programs::amm().elf().to_vec(); let (raw_elf_data, raw_elf_size, _) = elf.into_raw_parts(); @@ -217,7 +217,7 @@ pub unsafe extern "C" fn wallet_ffi_ata_elf(ffi_program: *mut FfiProgram) -> Wal return WalletFfiError::NullPointer; } - let elf = Program::ata().elf().to_vec(); + let elf = programs::ata().elf().to_vec(); let (raw_elf_data, raw_elf_size, _) = elf.into_raw_parts(); diff --git a/lez/wallet-ffi/src/types.rs b/lez/wallet-ffi/src/types.rs index f9d818ce..4f041c55 100644 --- a/lez/wallet-ffi/src/types.rs +++ b/lez/wallet-ffi/src/types.rs @@ -7,9 +7,9 @@ use std::{ str::FromStr as _, }; -use lee::{Data, SharedSecretKey}; +use lee::{Data, ProgramId, SharedSecretKey}; use lee_core::{encryption::MlKem768EncapsulationKey, NullifierPublicKey}; -use wallet::AccountIdentity; +use wallet::{account::AccountIdWithPrivacy, AccountIdentity}; use crate::error::WalletFfiError; @@ -24,7 +24,7 @@ pub struct WalletHandle { /// 32-byte array type for `AccountId`, keys, hashes, etc. #[repr(C)] -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] pub struct FfiBytes32 { pub data: [u8; 32], } @@ -581,6 +581,50 @@ impl TryFrom<&FfiAccountIdentity> for AccountIdentity { } } +impl From for FfiProgramId { + fn from(value: ProgramId) -> Self { + Self { data: value } + } +} + +impl From for ProgramId { + fn from(value: FfiProgramId) -> Self { + value.data + } +} + +#[repr(C)] +#[derive(Default, PartialEq, Eq, Debug, Clone, Copy)] +pub struct FfiAccountIdWithPrivacy { + pub account_id: FfiBytes32, + pub is_private: bool, +} + +impl From for FfiAccountIdWithPrivacy { + fn from(value: AccountIdWithPrivacy) -> Self { + match value { + AccountIdWithPrivacy::Public(acc) => Self { + account_id: acc.into(), + is_private: false, + }, + AccountIdWithPrivacy::Private(acc) => Self { + account_id: acc.into(), + is_private: true, + }, + } + } +} + +impl From for AccountIdWithPrivacy { + fn from(value: FfiAccountIdWithPrivacy) -> Self { + if value.is_private { + Self::Private(value.account_id.into()) + } else { + Self::Public(value.account_id.into()) + } + } +} + #[cfg(test)] mod tests { use lee::{AccountId, PrivateKey, PublicKey}; diff --git a/lez/wallet-ffi/src/vault.rs b/lez/wallet-ffi/src/vault.rs new file mode 100644 index 00000000..6db1fa66 --- /dev/null +++ b/lez/wallet-ffi/src/vault.rs @@ -0,0 +1,219 @@ +//! Bridge vault claim functions. +//! +//! L1 Bedrock deposits are minted into a per-owner vault PDA account by the bridge program, not +//! directly into the owner's account (see `bridge.rs`). The owner must separately claim the +//! deposited funds from their vault into their own account with a signed transaction. + +use std::{ffi::CString, ptr}; + +use lee::AccountId; +use wallet::program_facades::vault::Vault; + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + map_execution_error, + types::{FfiBytes32, FfiTransferResult, WalletHandle}, + wallet::get_wallet, +}; + +/// Get the claimable balance held in an account's bridge vault. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `owner`: The account ID whose vault balance to query +/// - `out_balance`: Output for balance as little-endian [u8; 16] +/// +/// # Returns +/// - `Success` on successful query +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `owner` must be a valid pointer to a `FfiBytes32` struct +/// - `out_balance` must be a valid pointer to a `[u8; 16]` array +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_get_vault_balance( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + out_balance: *mut [u8; 16], +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if owner.is_null() || out_balance.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return WalletFfiError::InternalError; + } + }; + + let owner_id = AccountId::new(unsafe { (*owner).data }); + let vault_id = vault_core::compute_vault_account_id(programs::vault().id(), owner_id); + + let balance = match block_on(wallet.get_account_balance(vault_id)) { + Ok(b) => b, + Err(e) => { + print_error(format!("Failed to get vault balance: {e}")); + return WalletFfiError::NetworkError; + } + }; + + unsafe { + *out_balance = balance.to_le_bytes(); + } + + WalletFfiError::Success +} + +/// Claim native tokens from a public owner's vault into their account. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `owner`: Owner account ID (must be owned by this wallet, public) +/// - `amount`: Amount to claim as little-endian [u8; 16] +/// - `out_result`: Output pointer for the claim result +/// +/// # Returns +/// - `Success` if the claim was submitted successfully +/// - `InsufficientFunds` if the vault doesn't have enough balance +/// - `KeyNotFound` if the owner's signing key is not in this wallet +/// - Error code on other failures +/// +/// # Memory +/// The result must be freed with `wallet_ffi_free_transfer_result()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `owner` must be a valid pointer to a `FfiBytes32` struct +/// - `amount` must be a valid pointer to a `[u8; 16]` array +/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_vault_claim( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if owner.is_null() || amount.is_null() || out_result.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return WalletFfiError::InternalError; + } + }; + + let owner_id = AccountId::new(unsafe { (*owner).data }); + let amount = u128::from_le_bytes(unsafe { *amount }); + + match block_on(Vault(&wallet).send_claim(owner_id, amount)) { + Ok(tx_hash) => { + let tx_hash = CString::new(tx_hash.to_string()) + .map_or(ptr::null_mut(), std::ffi::CString::into_raw); + + unsafe { + (*out_result).tx_hash = tx_hash; + (*out_result).success = true; + } + WalletFfiError::Success + } + Err(e) => { + print_error(format!("Vault claim failed: {e:?}")); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + } +} + +/// Claim native tokens from a private owner's vault into their account. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `owner`: Owner account ID (must be owned by this wallet, private) +/// - `amount`: Amount to claim as little-endian [u8; 16] +/// - `out_result`: Output pointer for the claim result +/// +/// # Returns +/// - `Success` if the claim was submitted successfully +/// - `InsufficientFunds` if the vault doesn't have enough balance +/// - `KeyNotFound` if the owner's signing key is not in this wallet +/// - Error code on other failures +/// +/// # Memory +/// The result must be freed with `wallet_ffi_free_transfer_result()`. +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `owner` must be a valid pointer to a `FfiBytes32` struct +/// - `amount` must be a valid pointer to a `[u8; 16]` array +/// - `out_result` must be a valid pointer to a `FfiTransferResult` struct +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_vault_claim_private( + handle: *mut WalletHandle, + owner: *const FfiBytes32, + amount: *const [u8; 16], + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if owner.is_null() || amount.is_null() || out_result.is_null() { + print_error("Null pointer argument"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return WalletFfiError::InternalError; + } + }; + + let owner_id = AccountId::new(unsafe { (*owner).data }); + let amount = u128::from_le_bytes(unsafe { *amount }); + + match block_on(Vault(&wallet).send_claim_private_owner(owner_id, amount)) { + Ok((tx_hash, _shared_key)) => { + let tx_hash = CString::new(tx_hash.to_string()) + .map_or(ptr::null_mut(), std::ffi::CString::into_raw); + + unsafe { + (*out_result).tx_hash = tx_hash; + (*out_result).success = true; + } + WalletFfiError::Success + } + Err(e) => { + print_error(format!("Vault claim failed: {e:?}")); + unsafe { + (*out_result).tx_hash = ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + } +} diff --git a/lez/wallet-ffi/src/wallet.rs b/lez/wallet-ffi/src/wallet.rs index 7aabaa2d..b19aaae5 100644 --- a/lez/wallet-ffi/src/wallet.rs +++ b/lez/wallet-ffi/src/wallet.rs @@ -1,16 +1,18 @@ //! Wallet lifecycle management functions. use std::{ - ffi::{c_char, CStr}, + ffi::{c_char, CStr, CString}, path::PathBuf, ptr, + str::FromStr as _, sync::Mutex, }; -use wallet::WalletCore; +use bip39::Mnemonic; +use wallet::{cli::execute_keys_restoration, WalletCore}; use crate::{ - c_str_to_string, + block_on, c_str_to_string, error::{print_error, WalletFfiError}, types::WalletHandle, }; @@ -20,6 +22,22 @@ pub(crate) struct WalletWrapper { pub core: Mutex, } +#[repr(C)] +pub struct FfiCreateWalletOutput { + pub wallet: *mut WalletHandle, + /// C compatible(null terminated) string. + pub mnemonic: *mut c_char, +} + +impl Default for FfiCreateWalletOutput { + fn default() -> Self { + Self { + wallet: std::ptr::null_mut(), + mnemonic: std::ptr::null_mut(), + } + } +} + /// Helper to get the wallet wrapper from an opaque handle. pub(crate) fn get_wallet( handle: *mut WalletHandle, @@ -71,8 +89,8 @@ fn c_str_to_path(ptr: *const c_char, name: &str) -> Result *mut WalletHandle { +) -> FfiCreateWalletOutput { let Ok(config_path) = c_str_to_path(config_path, "config_path") else { - return ptr::null_mut(); + return FfiCreateWalletOutput::default(); }; let Ok(storage_path) = c_str_to_path(storage_path, "storage_path") else { - return ptr::null_mut(); + return FfiCreateWalletOutput::default(); }; let Ok(password) = c_str_to_string(password, "password") else { - return ptr::null_mut(); + return FfiCreateWalletOutput::default(); }; match WalletCore::new_init_storage(config_path, storage_path, None, &password) { - Ok((core, _mnemonic)) => { + Ok((core, mnemonic)) => { let wrapper = Box::new(WalletWrapper { core: Mutex::new(core), }); - Box::into_raw(wrapper).cast::() + let handle = Box::into_raw(wrapper).cast::(); + + let Ok(c_mnemonic_string) = CString::new(mnemonic.to_string()) else { + return FfiCreateWalletOutput::default(); + }; + + let raw_pointer = CString::into_raw(c_mnemonic_string); + + FfiCreateWalletOutput { + wallet: handle, + mnemonic: raw_pointer, + } } Err(e) => { print_error(format!("Failed to create wallet: {e}")); - ptr::null_mut() + FfiCreateWalletOutput::default() } } } @@ -204,6 +233,81 @@ pub unsafe extern "C" fn wallet_ffi_save(handle: *mut WalletHandle) -> WalletFfi } } +/// Restore wallet data from mnemonic and password. +/// +/// # Parameters +/// - `handle`: Valid wallet handle +/// - `mnemonic`: Valid pointer to instance of `* char`, provided by `wallet_ffi_create_new` +/// - `password`: Valid pointer to C string. +/// - `depth`: Depth of a reconstructed tree +/// +/// # Returns +/// - `Success` on successful restoration +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` +/// - `mnemonic` must be a valid pointer to instance of `* char`, provided by +/// `wallet_ffi_create_new` +/// - `password` must be a valid pointer to C string. +/// - `depth` parameter induces exponential growth in execution time, be aware of it. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_restore_data( + handle: *mut WalletHandle, + mnemonic: *const c_char, + password: *const c_char, + depth: u32, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + let mut wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return WalletFfiError::InternalError; + } + }; + + let Ok(password) = c_str_to_string(password, "password") else { + return WalletFfiError::NullPointer; + }; + + let Ok(mnemonic) = c_str_to_string(mnemonic, "mnemonic") else { + return WalletFfiError::NullPointer; + }; + + let mnemonic = match Mnemonic::from_str(&mnemonic) { + Ok(mn) => mn, + Err(e) => { + print_error(format!("Failed to parse mnemonic: {e}")); + return WalletFfiError::SerializationError; + } + }; + + let res = match wallet.restore_storage(&mnemonic, &password) { + Ok(()) => WalletFfiError::Success, + Err(e) => { + print_error(format!("Failed to restore wallet data: {e}")); + WalletFfiError::StorageError + } + }; + + if res == WalletFfiError::Success { + match block_on(execute_keys_restoration(&mut wallet, depth)) { + Ok(()) => WalletFfiError::Success, + Err(err) => { + print_error(format!("Failed to restore wallet data: {err}")); + WalletFfiError::StorageError + } + } + } else { + res + } +} + /// Get the sequencer address from the wallet configuration. /// /// # Parameters diff --git a/lez/wallet-ffi/wallet_ffi.h b/lez/wallet-ffi/wallet_ffi.h index 58a39b47..8f15a888 100644 --- a/lez/wallet-ffi/wallet_ffi.h +++ b/lez/wallet-ffi/wallet_ffi.h @@ -219,6 +219,20 @@ typedef struct FfiAccount { struct FfiU128 nonce; } FfiAccount; +/** + * Result of a transfer operation. + */ +typedef struct FfiTransferResult { + /** + * Transaction hash (null-terminated string, or null on failure). + */ + char *tx_hash; + /** + * Whether the transfer succeeded. + */ + bool success; +} FfiTransferResult; + typedef struct FfiInstructionWords { uint32_t *instruction_words; uintptr_t instruction_words_size; @@ -242,23 +256,6 @@ typedef struct FfiAccountIdentity { struct FfiU128 identifier; } FfiAccountIdentity; -/** - * Intended to be created manually. - */ -typedef struct FfiProgram { - const uint8_t *elf_data; - uintptr_t elf_size; -} FfiProgram; - -/** - * Intended to be created manually. - */ -typedef struct FfiProgramWithDependencies { - struct FfiProgram program; - const struct FfiProgram *deps; - uintptr_t deps_size; -} FfiProgramWithDependencies; - /** * Result of a generic transaction operation. */ @@ -278,6 +275,23 @@ typedef struct FfiTransactionResult { uintptr_t secrets_size; } FfiTransactionResult; +/** + * Intended to be created manually. + */ +typedef struct FfiProgram { + const uint8_t *elf_data; + uintptr_t elf_size; +} FfiProgram; + +/** + * Intended to be created manually. + */ +typedef struct FfiProgramWithDependencies { + struct FfiProgram program; + const struct FfiProgram *deps; + uintptr_t deps_size; +} FfiProgramWithDependencies; + /** * Public key info for a public account. */ @@ -285,19 +299,34 @@ typedef struct FfiPublicAccountKey { struct FfiBytes32 public_key; } FfiPublicAccountKey; -/** - * Result of a transfer operation. - */ -typedef struct FfiTransferResult { +typedef struct LabelAvailability { + bool is_available; + enum WalletFfiError error; +} LabelAvailability; + +typedef struct FfiAccountIdWithPrivacy { + struct FfiBytes32 account_id; + bool is_private; +} FfiAccountIdWithPrivacy; + +typedef struct AccountIdResolvedFromLabel { + struct FfiAccountIdWithPrivacy account_id; + enum WalletFfiError error; +} AccountIdResolvedFromLabel; + +typedef struct LabelList { + const char **labels_data; + uintptr_t labels_size; + enum WalletFfiError error; +} LabelList; + +typedef struct FfiCreateWalletOutput { + struct WalletHandle *wallet; /** - * Transaction hash (null-terminated string, or null on failure). + * C compatible(null terminated) string. */ - char *tx_hash; - /** - * Whether the transfer succeeded. - */ - bool success; -} FfiTransferResult; + char *mnemonic; +} FfiCreateWalletOutput; /** * Create a new public account. @@ -532,6 +561,38 @@ enum WalletFfiError wallet_ffi_import_private_account(struct WalletHandle *handl const struct FfiU128 *identifier, const char *account_state_json); +/** + * Withdraw native tokens from a public account to Bedrock (L1) through the bridge. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `from`: Source public account ID (must be owned by this wallet). Bridge withdrawals only + * support public sender accounts. + * - `amount`: Amount of native tokens to withdraw + * - `bedrock_account_pk`: Recipient's Bedrock (L1) public key, 32 bytes + * - `out_result`: Output pointer for the withdraw result + * + * # Returns + * - `Success` if the withdraw transaction was submitted successfully + * - `InsufficientFunds` if the source account doesn't have enough balance + * - `KeyNotFound` if the source account's signing key is not in this wallet + * - Error code on other failures + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `from` must be a valid pointer to a `FfiBytes32` struct + * - `bedrock_account_pk` must be a valid pointer to a `FfiBytes32` struct + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_bridge_withdraw(struct WalletHandle *handle, + const struct FfiBytes32 *from, + uint64_t amount, + const struct FfiBytes32 *bedrock_account_pk, + struct FfiTransferResult *out_result); + /** * Serialize sequence of bytes into RISC0 readable words. * @@ -573,7 +634,7 @@ enum WalletFfiError wallet_ffi_send_generic_public_transaction(struct WalletHand uintptr_t account_identities_size, const uint32_t *instruction_words, uintptr_t instruction_words_size, - const struct FfiProgramWithDependencies *program_with_dependencies, + struct FfiProgramId program_id, struct FfiTransactionResult *out_result); /** @@ -767,6 +828,92 @@ enum WalletFfiError wallet_ffi_resolve_private_account(struct WalletHandle *hand */ void wallet_ffi_free_account_identity(struct FfiAccountIdentity *account_identity); +/** + * Check if label is available. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `label`: Input null terminated C string for a label + * + * # Returns + * - `LabelAvailability` struct + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `label` must be a valid pointer to a null-terminated C string + */ +struct LabelAvailability wallet_ffi_check_label_available(struct WalletHandle *handle, + const char *label); + +/** + * Add new label. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `label`: Input null terminated C string for a label + * - `account_id_with_privacy`: The account ID (32 bytes) and its privacy. + * + * # Returns + * - `Success` on successful query + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `label` must be a valid pointer to a null-terminated C string + */ +enum WalletFfiError wallet_ffi_add_label(struct WalletHandle *handle, + const char *label, + struct FfiAccountIdWithPrivacy account_id_with_privacy); + +/** + * Resolve a label. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `label`: Input null terminated C string for a label + * + * # Returns + * - `AccountIdResolvedFromLabel` struct + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `label` must be a valid pointer to a null-terminated C string + */ +struct AccountIdResolvedFromLabel wallet_ffi_resolve_label(struct WalletHandle *handle, + const char *label); + +/** + * Get all labels for account. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `account_id_with_privacy`: The account ID (32 bytes) and its privacy. + * + * # Returns + * - `LabelList` struct + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + */ +struct LabelList wallet_ffi_get_all_labels_for_account(struct WalletHandle *handle, + struct FfiAccountIdWithPrivacy account_id_with_privacy); + +/** + * Free label list. + * + * # Parameters + * - `label_list`: Input list of labels + * + * # Returns + * - `Success` on successful query + * - Error code on failure + * + * # Safety + * - `label_list` must be a valid pointer to `LabelList`, received from + * `wallet_ffi_get_all_labels_for_account` + */ +enum WalletFfiError wallet_ffi_free_label_list(struct LabelList *label_list); + /** * Claim a pinata reward using a public transaction. * @@ -1329,6 +1476,85 @@ enum WalletFfiError wallet_ffi_register_private_account(struct WalletHandle *han */ void wallet_ffi_free_transfer_result(struct FfiTransferResult *result); +/** + * Get the claimable balance held in an account's bridge vault. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `owner`: The account ID whose vault balance to query + * - `out_balance`: Output for balance as little-endian [u8; 16] + * + * # Returns + * - `Success` on successful query + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `owner` must be a valid pointer to a `FfiBytes32` struct + * - `out_balance` must be a valid pointer to a `[u8; 16]` array + */ +enum WalletFfiError wallet_ffi_get_vault_balance(struct WalletHandle *handle, + const struct FfiBytes32 *owner, + uint8_t (*out_balance)[16]); + +/** + * Claim native tokens from a public owner's vault into their account. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `owner`: Owner account ID (must be owned by this wallet, public) + * - `amount`: Amount to claim as little-endian [u8; 16] + * - `out_result`: Output pointer for the claim result + * + * # Returns + * - `Success` if the claim was submitted successfully + * - `InsufficientFunds` if the vault doesn't have enough balance + * - `KeyNotFound` if the owner's signing key is not in this wallet + * - Error code on other failures + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `owner` must be a valid pointer to a `FfiBytes32` struct + * - `amount` must be a valid pointer to a `[u8; 16]` array + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_vault_claim(struct WalletHandle *handle, + const struct FfiBytes32 *owner, + const uint8_t (*amount)[16], + struct FfiTransferResult *out_result); + +/** + * Claim native tokens from a private owner's vault into their account. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `owner`: Owner account ID (must be owned by this wallet, private) + * - `amount`: Amount to claim as little-endian [u8; 16] + * - `out_result`: Output pointer for the claim result + * + * # Returns + * - `Success` if the claim was submitted successfully + * - `InsufficientFunds` if the vault doesn't have enough balance + * - `KeyNotFound` if the owner's signing key is not in this wallet + * - Error code on other failures + * + * # Memory + * The result must be freed with `wallet_ffi_free_transfer_result()`. + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `owner` must be a valid pointer to a `FfiBytes32` struct + * - `amount` must be a valid pointer to a `[u8; 16]` array + * - `out_result` must be a valid pointer to a `FfiTransferResult` struct + */ +enum WalletFfiError wallet_ffi_vault_claim_private(struct WalletHandle *handle, + const struct FfiBytes32 *owner, + const uint8_t (*amount)[16], + struct FfiTransferResult *out_result); + /** * Create a new wallet with fresh storage. * @@ -1341,15 +1567,15 @@ void wallet_ffi_free_transfer_result(struct FfiTransferResult *result); * - `password`: Password for encrypting the wallet seed * * # Returns - * - Opaque wallet handle on success - * - Null pointer on error (call `wallet_ffi_get_last_error()` for details) + * - Result, which contains opaque wallet handle and mnemonic words on success + * - Result with null pointers on error (call `wallet_ffi_get_last_error()` for details) * * # Safety * All string parameters must be valid null-terminated UTF-8 strings. */ -struct WalletHandle *wallet_ffi_create_new(const char *config_path, - const char *storage_path, - const char *password); +struct FfiCreateWalletOutput wallet_ffi_create_new(const char *config_path, + const char *storage_path, + const char *password); /** * Open an existing wallet from storage. @@ -1399,6 +1625,31 @@ void wallet_ffi_destroy(struct WalletHandle *handle); */ enum WalletFfiError wallet_ffi_save(struct WalletHandle *handle); +/** + * Restore wallet data from mnemonic and password. + * + * # Parameters + * - `handle`: Valid wallet handle + * - `mnemonic`: Valid pointer to instance of `* char`, provided by `wallet_ffi_create_new` + * - `password`: Valid pointer to C string. + * - `depth`: Depth of a reconstructed tree + * + * # Returns + * - `Success` on successful restoration + * - Error code on failure + * + * # Safety + * - `handle` must be a valid wallet handle from `wallet_ffi_create_new` or `wallet_ffi_open` + * - `mnemonic` must be a valid pointer to instance of `* char`, provided by + * `wallet_ffi_create_new` + * - `password` must be a valid pointer to C string. + * - `depth` parameter induces exponential growth in execution time, be aware of it. + */ +enum WalletFfiError wallet_ffi_restore_data(struct WalletHandle *handle, + const char *mnemonic, + const char *password, + uint32_t depth); + /** * Get the sequencer address from the wallet configuration. * diff --git a/lez/wallet/Cargo.toml b/lez/wallet/Cargo.toml index e04b0d91..974d6a71 100644 --- a/lez/wallet/Cargo.toml +++ b/lez/wallet/Cargo.toml @@ -17,10 +17,12 @@ sequencer_service_rpc = { workspace = true, features = ["client"] } amm_core.workspace = true testnet_initial_state.workspace = true token_core.workspace = true -ata_core.workspace = true vault_core.workspace = true bridge_core.workspace = true keycard_wallet.workspace = true +programs.workspace = true +system_accounts.workspace = true +associated_token_account_core.workspace = true bip39.workspace = true pyo3.workspace = true diff --git a/lez/wallet/src/account.rs b/lez/wallet/src/account.rs index 64eee575..8caa7366 100644 --- a/lez/wallet/src/account.rs +++ b/lez/wallet/src/account.rs @@ -21,6 +21,12 @@ impl Label { } } +impl AsRef for Label { + fn as_ref(&self) -> &str { + &self.0 + } +} + impl FromStr for Label { type Err = std::convert::Infallible; diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index f77d1062..a5f900d6 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -4,8 +4,8 @@ use anyhow::Result; use keycard_wallet::{KeycardWallet, python_path}; use lee::{AccountId, PrivateKey, PublicKey, Signature}; use lee_core::{ - Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, - SharedSecretKey, + CommitmentSetDigest, DUMMY_COMMITMENT_HASH, Identifier, InputAccountIdentity, MembershipProof, + NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{AccountWithMetadata, Nonce}, encryption::ViewingPublicKey, }; @@ -187,6 +187,7 @@ enum State { pub struct AccountManager { states: Vec, pin: Option, + dummy_commitment_root: CommitmentSetDigest, } impl AccountManager { @@ -336,7 +337,24 @@ impl AccountManager { states.push(state); } - Ok(Self { states, pin }) + let has_init_account = states + .iter() + .any(|s| matches!(s, State::Private(pre) if pre.proof.is_none())); + let dummy_commitment_root = if has_init_account { + wallet + .get_commitment_root() + .await + .map_err(ExecutionFailureKind::SequencerError)? + .unwrap_or(DUMMY_COMMITMENT_HASH) + } else { + DUMMY_COMMITMENT_HASH + }; + + Ok(Self { + states, + pin, + dummy_commitment_root, + }) } pub fn pre_states(&self) -> Vec { @@ -415,6 +433,7 @@ impl AccountManager { random_seed: pre.random_seed, npk: pre.npk, identifier: pre.identifier, + commitment_root: self.dummy_commitment_root, seed: None, }, }, @@ -433,12 +452,14 @@ impl AccountManager { random_seed: pre.random_seed, nsk, identifier: pre.identifier, + commitment_root: self.dummy_commitment_root, }, (None, _) => InputAccountIdentity::PrivateUnauthorized { vpk: pre.vpk.clone(), random_seed: pre.random_seed, npk: pre.npk, identifier: pre.identifier, + commitment_root: self.dummy_commitment_root, }, }, }) diff --git a/lez/wallet/src/cli/account.rs b/lez/wallet/src/cli/account.rs index 2f3dfa9f..275e13fc 100644 --- a/lez/wallet/src/cli/account.rs +++ b/lez/wallet/src/cli/account.rs @@ -2,7 +2,7 @@ use anyhow::{Context as _, Result}; use clap::Subcommand; use itertools::Itertools as _; use key_protocol::key_management::{KeyChain, key_tree::chain_index::ChainIndex}; -use lee::{Account, PublicKey, program::Program}; +use lee::{Account, PublicKey}; use lee_core::Identifier; use token_core::{TokenDefinition, TokenHolding}; @@ -125,75 +125,169 @@ pub enum NewSubcommand { }, } +impl NewSubcommand { + fn handle_public( + cci: Option, + label: Option