mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-03 11:33:11 +00:00
Merge pull request #661 from logos-blockchain/dev
This commit is contained in:
commit
15144ddb84
@ -60,7 +60,9 @@ allow-git = [
|
||||
"https://github.com/logos-blockchain/logos-blockchain.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/keycard-tech/keycard-rs",
|
||||
]
|
||||
unknown-git = "deny"
|
||||
unknown-registry = "deny"
|
||||
|
||||
10
.github/actions/install-risc0/action.yml
vendored
10
.github/actions/install-risc0/action.yml
vendored
@ -1,10 +0,0 @@
|
||||
name: Install risc0
|
||||
description: Installs risc0 in the environment
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install risc0
|
||||
run: |
|
||||
curl -L https://risczero.com/install | bash
|
||||
/home/runner/.risc0/bin/rzup install
|
||||
shell: bash
|
||||
10
.github/actions/install-system-deps/action.yml
vendored
10
.github/actions/install-system-deps/action.yml
vendored
@ -1,10 +0,0 @@
|
||||
name: Install system dependencies
|
||||
description: Installs system dependencies in the environment
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential clang libclang-dev libssl-dev pkg-config
|
||||
shell: bash
|
||||
54
.github/actions/run-in-ci-image/action.yml
vendored
Normal file
54
.github/actions/run-in-ci-image/action.yml
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
name: Run in CI image
|
||||
description: >
|
||||
Runs a command inside the CI image on a plain runner, for jobs that need the
|
||||
host's Docker daemon. `container:` cannot work for those: it puts the
|
||||
workspace on a path the host daemon cannot resolve, and it forbids
|
||||
`--network host`.
|
||||
|
||||
inputs:
|
||||
image:
|
||||
description: CI image reference, from the ci-image workflow's output.
|
||||
required: true
|
||||
run:
|
||||
description: Command to run inside the image.
|
||||
required: true
|
||||
env:
|
||||
description: Newline-separated NAME=VALUE pairs to pass into the container.
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Run in CI image
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_IMAGE: ${{ inputs.image }}
|
||||
INPUT_RUN: ${{ inputs.run }}
|
||||
INPUT_ENV: ${{ inputs.env }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
env_args=()
|
||||
while IFS= read -r pair; do
|
||||
[[ -z "$pair" ]] && continue
|
||||
env_args+=(--env "$pair")
|
||||
done <<< "$INPUT_ENV"
|
||||
|
||||
# Cargo's registry is the only part of CARGO_HOME worth sharing with the
|
||||
# host: mounting all of it would shadow the tools baked into the image.
|
||||
mkdir -p "$HOME/.cargo/registry"
|
||||
|
||||
# Mounting the workspace on its own path is what makes this work. The
|
||||
# daemon resolves compose bind mounts against the host, and the test
|
||||
# binaries bake CARGO_MANIFEST_DIR in at compile time, so the path has
|
||||
# to mean the same thing inside and outside the container.
|
||||
docker run --rm \
|
||||
--network host \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$PWD:$PWD" \
|
||||
--volume "$HOME/.cargo/registry:/usr/local/cargo/registry" \
|
||||
--workdir "$PWD" \
|
||||
"${env_args[@]}" \
|
||||
"$INPUT_IMAGE" \
|
||||
bash -e -o pipefail -c "$INPUT_RUN"
|
||||
98
.github/docker/ci.Dockerfile
vendored
Normal file
98
.github/docker/ci.Dockerfile
vendored
Normal file
@ -0,0 +1,98 @@
|
||||
# Image behind the `container:` of the CI jobs. Everything the jobs used to
|
||||
# install per-run is baked in here instead, so a CI run no longer asks the risc0
|
||||
# install servers for anything.
|
||||
#
|
||||
# The image tag is a hash of this file plus rust-toolchain.toml, so editing
|
||||
# either rebuilds the image on the PR that changes it. See ci-image.yml.
|
||||
#
|
||||
# Keep the base tag in sync with rust-toolchain.toml.
|
||||
FROM rust:1.94.0-trixie
|
||||
|
||||
# GitHub sets HOME=/github/home inside container jobs, which hides anything we
|
||||
# bake into the image's ~. rzup defaults to $HOME/.risc0, so pin it to a fixed
|
||||
# path; RUSTUP_HOME and CARGO_HOME already point at /usr/local from the base
|
||||
# image, and rzup honours all three.
|
||||
ENV RISC0_HOME=/usr/local/risc0
|
||||
ENV PATH=/usr/local/risc0/bin:$PATH
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
clang \
|
||||
libclang-dev \
|
||||
libssl-dev \
|
||||
pkg-config \
|
||||
libpcsclite-dev \
|
||||
curl \
|
||||
git \
|
||||
jq \
|
||||
python3-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# The runner pre-creates the workspace as another uid, so git in a container job
|
||||
# calls it dubious. checkout's own fix is --global, which only holds while HOME
|
||||
# still points at the gitconfig it wrote; --system holds regardless.
|
||||
RUN git config --system --add safe.directory '*'
|
||||
|
||||
# The base image already has this toolchain at the minimal profile, so the
|
||||
# `profile = "default"` in rust-toolchain.toml is a no-op here: rustup sees the
|
||||
# toolchain as installed and never applies it. Add what the jobs need by hand.
|
||||
COPY rust-toolchain.toml /tmp/toolchain/rust-toolchain.toml
|
||||
RUN cd /tmp/toolchain \
|
||||
&& rustup toolchain install \
|
||||
&& rustup component add clippy rustfmt \
|
||||
&& rm -rf /tmp/toolchain
|
||||
|
||||
# For `cargo +nightly fmt` in the fmt-rs job.
|
||||
RUN rustup toolchain install nightly --profile minimal --component rustfmt
|
||||
|
||||
# The bootstrap script only ever drops rzup in $HOME/.risc0/bin, so run it
|
||||
# against the build-time HOME and keep just the binary. rzup itself then honours
|
||||
# RISC0_HOME and installs each component under /usr/local.
|
||||
#
|
||||
# Versions pinned to the set currently validated in local dev (`rzup show`);
|
||||
# bare `rzup install` would float all four default components to latest. r0vm
|
||||
# and cargo-risczero track each other and the risc0-zkvm crate version.
|
||||
RUN curl -L https://risczero.com/install | bash \
|
||||
&& mv /root/.risc0/bin/rzup /usr/local/bin/rzup \
|
||||
&& rm -rf /root/.risc0 \
|
||||
&& rzup install rust 1.94.1 \
|
||||
&& rzup install cpp 2024.1.5 \
|
||||
&& rzup install r0vm 3.0.5 \
|
||||
&& rzup install cargo-risczero 3.0.5
|
||||
|
||||
# Copying docker. Static Go binaries, so the alpine-built ones run fine here.
|
||||
COPY --from=docker:29.6.1-cli /usr/local/bin/docker /usr/local/bin/docker
|
||||
COPY --from=docker:29.6.1-cli /usr/local/libexec/docker/cli-plugins/docker-compose \
|
||||
/usr/local/libexec/docker/cli-plugins/docker-compose
|
||||
COPY --from=docker:29.6.1-cli /usr/local/libexec/docker/cli-plugins/docker-buildx \
|
||||
/usr/local/libexec/docker/cli-plugins/docker-buildx
|
||||
|
||||
# Prebuilt binaries; compiling these from source would dominate the image build.
|
||||
# Versions pinned so a rebuild of the same image ships the same tools.
|
||||
ENV BINSTALL_VERSION=1.21.0
|
||||
RUN curl -L --proto '=https' --tlsv1.2 -sSf \
|
||||
https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash \
|
||||
&& cargo binstall --no-confirm \
|
||||
cargo-nextest@0.9.140 \
|
||||
taplo-cli@0.10.0 \
|
||||
cargo-machete@0.9.2 \
|
||||
cargo-deny@0.20.2 \
|
||||
just@1.56.0
|
||||
|
||||
# Smoke-test every tool the jobs use. A bad install should fail here, not later
|
||||
# in some CI job that reuses this cached image. pyo3's `auto-initialize` links
|
||||
# libpython, and the base image ships python3 without it, so check the .so too.
|
||||
RUN cargo --version \
|
||||
&& cargo +nightly fmt --version \
|
||||
&& cargo clippy --version \
|
||||
&& ls /usr/lib/*/libpython3*.so \
|
||||
&& r0vm --version \
|
||||
&& cargo risczero --version \
|
||||
&& cargo nextest --version \
|
||||
&& taplo --version \
|
||||
&& cargo machete --version \
|
||||
&& cargo deny --version \
|
||||
&& just --version \
|
||||
&& docker --version \
|
||||
&& docker compose version \
|
||||
&& docker buildx version
|
||||
24
.github/workflows/bench-regression.yml
vendored
24
.github/workflows/bench-regression.yml
vendored
@ -9,12 +9,25 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
packages: read
|
||||
|
||||
name: bench-regression
|
||||
|
||||
jobs:
|
||||
ci-image:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
uses: ./.github/workflows/ci-image.yml
|
||||
|
||||
crypto-primitives:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@ -24,17 +37,6 @@ jobs:
|
||||
# working tree, so we need the full history.
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- uses: ./.github/actions/install-logos-blockchain-circuits
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Run criterion-compare against base branch
|
||||
uses: boa-dev/criterion-compare-action@v3
|
||||
with:
|
||||
|
||||
75
.github/workflows/ci-image.yml
vendored
Normal file
75
.github/workflows/ci-image.yml
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
name: CI image
|
||||
|
||||
concurrency:
|
||||
group: ci-image-${{ github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
# Resolves the CI image the calling workflow's jobs run in, building and pushing
|
||||
# it only when it isn't published yet.
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
image:
|
||||
description: Image reference to pass to a job's `container:`.
|
||||
value: ${{ jobs.ci-image.outputs.image }}
|
||||
|
||||
jobs:
|
||||
ci-image:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
outputs:
|
||||
image: ${{ steps.image-ref.outputs.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
# Tagging by content hash means a PR that touches the Dockerfile or the
|
||||
# toolchain builds and tests against its own image, while every other PR
|
||||
# reuses the one already in the registry.
|
||||
- name: Compute image reference
|
||||
id: image-ref
|
||||
run: |
|
||||
repo="$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')"
|
||||
tag="$(cat .github/docker/ci.Dockerfile rust-toolchain.toml | sha256sum | cut -c1-16)"
|
||||
echo "image=ghcr.io/${repo}/ci:${tag}" >> "$GITHUB_OUTPUT"
|
||||
echo "CI image: ghcr.io/${repo}/ci:${tag}"
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check whether the image is already published
|
||||
id: probe
|
||||
run: |
|
||||
if docker manifest inspect "${{ steps.image-ref.outputs.image }}" > /dev/null 2>&1; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Image already published, skipping build."
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Image not published yet, building it."
|
||||
fi
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: steps.probe.outputs.exists == 'false'
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# A pull_request from a fork gets a read-only GITHUB_TOKEN, so this step
|
||||
# fails there. It only runs when the fork changed the image, which is the
|
||||
# case that needs a maintainer's eyes anyway.
|
||||
- name: Build and push CI image
|
||||
if: steps.probe.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./.github/docker/ci.Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.image-ref.outputs.image }}
|
||||
# Links the package to the repo, so it shows up there and inherits its
|
||||
# access rules.
|
||||
labels: org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
cache-from: type=gha,scope=ci-image
|
||||
cache-to: type=gha,mode=max,scope=ci-image
|
||||
247
.github/workflows/ci.yml
vendored
247
.github/workflows/ci.yml
vendored
@ -2,6 +2,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "!.github/workflows/*.yml"
|
||||
@ -14,67 +15,91 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
packages: read
|
||||
|
||||
name: General
|
||||
|
||||
jobs:
|
||||
ci-image:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
uses: ./.github/workflows/ci-image.yml
|
||||
|
||||
fmt-rs:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- name: Install nightly toolchain for rustfmt
|
||||
run: rustup install nightly --profile minimal --component rustfmt
|
||||
|
||||
- name: Check Rust files are formatted
|
||||
run: cargo +nightly fmt --check
|
||||
|
||||
fmt-toml:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- name: Install taplo-cli
|
||||
run: cargo install --locked taplo-cli
|
||||
|
||||
# No path argument: taplo reads a `.` as a literal file, excludes it for
|
||||
# not being a .toml, and checks nothing.
|
||||
- name: Check TOML files are formatted
|
||||
run: taplo fmt --check .
|
||||
run: taplo fmt --check
|
||||
|
||||
machete:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Install cargo-machete
|
||||
run: cargo install cargo-machete
|
||||
|
||||
- name: Check for unused dependencies
|
||||
run: cargo machete
|
||||
|
||||
deny:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- name: Install cargo-deny
|
||||
run: cargo install --locked cargo-deny
|
||||
|
||||
- name: Check licenses and advisories
|
||||
run: cargo deny check
|
||||
|
||||
lint:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout-minutes: 60
|
||||
|
||||
name: lint
|
||||
@ -83,18 +108,11 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
|
||||
- name: Restore Rust cache
|
||||
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:
|
||||
@ -107,6 +125,35 @@ jobs:
|
||||
run: cargo clippy -p "*program" -- -D warnings
|
||||
|
||||
unit-tests:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci-rust-cache
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }}
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
RUST_LOG: "info"
|
||||
run: cargo nextest run --workspace --exclude integration_tests --exclude test_fixtures --all-features
|
||||
|
||||
# Not a `container:` job: the tests drive the host's Docker daemon through
|
||||
# testcontainers, so they run in the CI image via `run-in-ci-image` instead.
|
||||
test-fixtures-tests:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
@ -114,29 +161,33 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Restore Rust cache
|
||||
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
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
RUST_LOG: "info"
|
||||
run: cargo nextest run --workspace --exclude integration_tests --all-features
|
||||
- name: Run test_fixtures tests
|
||||
uses: ./.github/actions/run-in-ci-image
|
||||
with:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
env: |
|
||||
RISC0_DEV_MODE=1
|
||||
RUST_LOG=info
|
||||
run: cargo nextest run -p test_fixtures
|
||||
|
||||
# Not a `container:` job: the test binaries bake CARGO_MANIFEST_DIR in at
|
||||
# compile time, so the archive has to be built on the same path the matrix
|
||||
# jobs later run it from.
|
||||
integration-tests-prebuild:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
targets: ${{ steps.discover-targets.outputs.targets }}
|
||||
@ -145,26 +196,25 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci-rust-cache
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
run: cargo install --locked cargo-nextest
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }}
|
||||
|
||||
- name: Build integration test archive
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
run: cargo nextest archive -p integration_tests --archive-file integration-tests.tar.zst --no-pager
|
||||
uses: ./.github/actions/run-in-ci-image
|
||||
with:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
env: RISC0_DEV_MODE=1
|
||||
run: cargo nextest archive -p integration_tests --archive-file integration-tests.tar.zst --no-pager
|
||||
|
||||
- name: Upload integration test archive
|
||||
uses: actions/upload-artifact@v4
|
||||
@ -172,15 +222,22 @@ jobs:
|
||||
name: integration-tests-archive
|
||||
path: integration-tests.tar.zst
|
||||
|
||||
- name: List integration test binaries
|
||||
uses: ./.github/actions/run-in-ci-image
|
||||
with:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
run: |
|
||||
cargo nextest list \
|
||||
--archive-file integration-tests.tar.zst \
|
||||
--list-type binaries-only \
|
||||
--message-format json \
|
||||
--no-pager > integration-tests-binaries.json
|
||||
|
||||
# $GITHUB_OUTPUT is outside the mounted workspace, so the container cannot
|
||||
# write to it. jq is on the runner, so do this pass here.
|
||||
- name: Discover integration test targets from archive
|
||||
id: discover-targets
|
||||
run: |
|
||||
cargo nextest list \
|
||||
--archive-file integration-tests.tar.zst \
|
||||
--list-type binaries-only \
|
||||
--message-format json \
|
||||
--no-pager > integration-tests-binaries.json
|
||||
|
||||
targets_json="$(jq -c '[."rust-binaries" | to_entries[] | select(.value.kind == "test" and .value."binary-name" != "tps") | .value."binary-name"] | sort | unique' integration-tests-binaries.json)"
|
||||
|
||||
if [[ "$targets_json" == "[]" ]]; then
|
||||
@ -192,7 +249,7 @@ jobs:
|
||||
echo "Discovered integration targets: $targets_json"
|
||||
|
||||
integration-tests:
|
||||
needs: integration-tests-prebuild
|
||||
needs: [ci-image, test-fixtures-tests, integration-tests-prebuild]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
@ -205,54 +262,61 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download integration test archive
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: integration-tests-archive
|
||||
|
||||
- name: Install nextest
|
||||
run: cargo install --locked cargo-nextest
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
RISC0_DEV_MODE: "1"
|
||||
RUST_LOG: "info"
|
||||
run: cargo nextest run --archive-file integration-tests.tar.zst -E "binary(${{ matrix.target }})"
|
||||
uses: ./.github/actions/run-in-ci-image
|
||||
with:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
env: |
|
||||
RISC0_DEV_MODE=1
|
||||
RUST_LOG=info
|
||||
run: cargo nextest run --archive-file integration-tests.tar.zst -E "binary(${{ matrix.target }})"
|
||||
|
||||
valid-proof-test:
|
||||
needs: ci-image
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
timeout-minutes: 150
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-system-deps
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
|
||||
- name: Install active toolchain
|
||||
run: rustup install
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Restore Rust cache
|
||||
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:
|
||||
RUST_LOG: "info"
|
||||
run: cargo test -p integration_tests -- --exact private::private_transfer_to_owned_account
|
||||
uses: ./.github/actions/run-in-ci-image
|
||||
with:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
env: RUST_LOG=info
|
||||
run: cargo test -p integration_tests -- --exact private::private_transfer_to_owned_account
|
||||
|
||||
# `just build-artifacts` drives the host's Docker daemon via `cargo risczero
|
||||
# build`, so it goes through the image rather than `container:`.
|
||||
artifacts:
|
||||
needs: ci-image
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
@ -262,19 +326,24 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.head_ref }}
|
||||
|
||||
- uses: ./.github/actions/install-risc0
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Restore Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: ci-rust-cache
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install just
|
||||
run: cargo install --locked just
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' }}
|
||||
|
||||
- name: Build artifacts
|
||||
run: just build-artifacts
|
||||
uses: ./.github/actions/run-in-ci-image
|
||||
with:
|
||||
image: ${{ needs.ci-image.outputs.image }}
|
||||
run: just build-artifacts
|
||||
|
||||
- name: Check if artifacts match repository
|
||||
run: |
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -17,9 +17,5 @@ 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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
1170
Cargo.lock
generated
1170
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
43
Cargo.toml
43
Cargo.toml
@ -16,6 +16,7 @@ members = [
|
||||
|
||||
"lez",
|
||||
"lez/system_accounts",
|
||||
"lez/chain_state",
|
||||
"lez/sequencer/core",
|
||||
"lez/sequencer/service",
|
||||
"lez/sequencer/service/protocol",
|
||||
@ -43,6 +44,13 @@ members = [
|
||||
"lez/programs/pinata_token",
|
||||
"lez/programs/token",
|
||||
"lez/programs/vault",
|
||||
"lez/programs/cross_zone_inbox",
|
||||
"lez/programs/cross_zone_outbox",
|
||||
"lez/programs/bridge_lock",
|
||||
"lez/programs/wrapped_token",
|
||||
"lez/programs/ping_sender",
|
||||
"lez/programs/ping_receiver",
|
||||
"lez/cross_zone",
|
||||
|
||||
"test_programs",
|
||||
"test_programs/guest",
|
||||
@ -58,12 +66,14 @@ members = [
|
||||
"tools/cycle_bench",
|
||||
"tools/crypto_primitives_bench",
|
||||
"tools/integration_bench",
|
||||
"tools/cross_zone_chat",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
lee = { path = "lee/state_machine" }
|
||||
lee_core = { path = "lee/state_machine/core" }
|
||||
common = { path = "lez/common" }
|
||||
chain_state = { path = "lez/chain_state" }
|
||||
mempool = { path = "lez/mempool" }
|
||||
storage = { path = "lez/storage" }
|
||||
key_protocol = { path = "lee/key_protocol" }
|
||||
@ -94,6 +104,12 @@ 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" }
|
||||
cross_zone_inbox_core = { path = "lez/programs/cross_zone_inbox/core" }
|
||||
cross_zone_outbox_core = { path = "lez/programs/cross_zone_outbox/core" }
|
||||
bridge_lock_core = { path = "lez/programs/bridge_lock/core" }
|
||||
wrapped_token_core = { path = "lez/programs/wrapped_token/core" }
|
||||
ping_core = { path = "lez/programs/ping_core" }
|
||||
cross_zone = { path = "lez/cross_zone" }
|
||||
build_utils = { path = "build_utils" }
|
||||
test_programs = { path = "test_programs" }
|
||||
testnet_initial_state = { path = "lez/testnet_initial_state" }
|
||||
@ -116,6 +132,7 @@ openssl = { version = "0.10", features = ["vendored"] }
|
||||
openssl-probe = { version = "0.1.2" }
|
||||
serde = { version = "1.0.60", default-features = false, features = ["derive"] }
|
||||
serde_json = "1.0.81"
|
||||
serde_yaml = "0.9.34"
|
||||
serde_with = "3.16.1"
|
||||
actix = "0.13.0"
|
||||
actix-cors = "0.7.1"
|
||||
@ -146,7 +163,9 @@ base64 = "0.22.1"
|
||||
bip39 = "2.2.0"
|
||||
hmac-sha512 = "1.1.7"
|
||||
chrono = "0.4.41"
|
||||
time = "0.3"
|
||||
borsh = "1.5.7"
|
||||
zstd = "0.13"
|
||||
base58 = "0.2.0"
|
||||
itertools = "0.14.0"
|
||||
num-bigint = "0.4.6"
|
||||
@ -155,19 +174,23 @@ tokio-retry = "0.3.0"
|
||||
schemars = "1.2"
|
||||
async-stream = "0.3.6"
|
||||
|
||||
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" }
|
||||
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-codec = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "0dc34e2c5a6e2ff772140378659489a602ee06bc" }
|
||||
|
||||
keycard-rs = { git = "https://github.com/keycard-tech/keycard-rs", rev = "9535a657ba04b1e6916de51777e22b4837c1a84d" }
|
||||
|
||||
rocksdb = { version = "0.24.0", default-features = false, features = [
|
||||
"snappy",
|
||||
"bindgen-runtime",
|
||||
] }
|
||||
rand = { version = "0.8.5", features = ["std", "std_rng", "getrandom"] }
|
||||
pcsc = "2"
|
||||
k256 = { version = "0.13.3", features = [
|
||||
"ecdsa-core",
|
||||
"arithmetic",
|
||||
@ -181,9 +204,9 @@ elliptic-curve = { version = "0.13.8", features = ["arithmetic"] }
|
||||
actix-web = { version = "4.13.0", default-features = false, features = [
|
||||
"macros",
|
||||
] }
|
||||
axum = "0.8.4"
|
||||
clap = { version = "4.5.42", features = ["derive", "env"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] }
|
||||
pyo3 = { version = "0.29", features = ["auto-initialize"] }
|
||||
zeroize = "1"
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
|
||||
@ -194,6 +217,10 @@ opt-level = 'z'
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
||||
# Keccak speedup for in-guest ML KEM
|
||||
[patch.crates-io]
|
||||
keccak = { git = "https://github.com/logos-blockchain/sponges", rev = "3a56e99771beedf04946eab21a4a62adc2951377" }
|
||||
|
||||
# Keep backtraces but drop full DWARF type info to avoid LLD OOM/SIGBUS when
|
||||
# linking large integration-test binaries on resource-constrained CI runners.
|
||||
[profile.dev]
|
||||
|
||||
53
Justfile
53
Justfile
@ -6,20 +6,33 @@ default:
|
||||
# ---- Configuration ----
|
||||
ARTIFACTS := "artifacts"
|
||||
|
||||
# Build risc0 program artifacts.
|
||||
# On macOS the integration-test binary links pyo3 against the CommandLineTools
|
||||
# Python framework with no embedded rpath, so it needs this to launch. Empty on
|
||||
# Linux/CI, which is unaffected.
|
||||
DEMO_ENV := if os() == "macos" { "DYLD_FALLBACK_FRAMEWORK_PATH=/Library/Developer/CommandLineTools/Library/Frameworks" } else { "" }
|
||||
|
||||
# Build risc0 program artifacts and test fixture.
|
||||
build-artifacts:
|
||||
@echo "🔨 Building artifacts"
|
||||
@rm -rf {{ARTIFACTS}}
|
||||
@just build-artifact lee/privacy_preserving_circuit
|
||||
@just build-artifact lez/programs programs
|
||||
|
||||
@if [ "${GITHUB_ACTIONS:-}" = "true" ]; then \
|
||||
echo "Skipping test fixture regeneration because CI doesn't need it"; \
|
||||
else \
|
||||
just regenerate-test-fixture; \
|
||||
fi
|
||||
|
||||
RISC0_DOCKER_CONTAINER_TAG := "r0.1.91.1"
|
||||
|
||||
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; \
|
||||
RISC0_DOCKER_CONTAINER_TAG={{RISC0_DOCKER_CONTAINER_TAG}} 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; \
|
||||
RISC0_DOCKER_CONTAINER_TAG={{RISC0_DOCKER_CONTAINER_TAG}} 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}}
|
||||
@ -35,6 +48,11 @@ test:
|
||||
@echo "🧪 Running tests"
|
||||
RISC0_DEV_MODE=1 cargo nextest run --no-fail-fast
|
||||
|
||||
# Regenerate the prebuilt sequencer db dump for fast TestContext::new() (needs Docker; commit the dump).
|
||||
regenerate-test-fixture:
|
||||
@echo "🧪 Regenerating test fixture"
|
||||
RISC0_DEV_MODE=1 cargo run -p test_fixtures --bin regenerate_test_fixture
|
||||
|
||||
# Run criterion benches: fast crypto primitives, then the slow PPE verify (real proving setup).
|
||||
bench:
|
||||
@echo "📊 Running criterion benches"
|
||||
@ -48,15 +66,17 @@ run-bedrock:
|
||||
docker compose up
|
||||
|
||||
# Run Sequencer. Run with RISC0_DEV_MODE=1 to disable proof verification for faster iteration.
|
||||
# Optional home/port let a second instance run off the same config, e.g.
|
||||
# `just run-sequencer "" "$TMPDIR/lez-sequencer2" 3041` for the multi-sequencer demo.
|
||||
[working-directory: 'lez/sequencer/service']
|
||||
run-sequencer standalone="":
|
||||
run-sequencer standalone="" home="" port="3040":
|
||||
@echo "🧠 Running sequencer"
|
||||
@if [ "{{standalone}}" = "standalone" ]; then \
|
||||
echo "🧪 Running in standalone mode"; \
|
||||
RUST_LOG=info cargo run --features standalone --release -p sequencer_service configs/debug/sequencer_config.json; \
|
||||
RUST_LOG=info cargo run --features standalone --release -p sequencer_service -- configs/debug/sequencer_config.json --port {{port}} {{ if home != "" { "--home " + quote(home) } else { "" } }}; \
|
||||
else \
|
||||
echo "🚀 Running in normal mode"; \
|
||||
RUST_LOG=info cargo run --release -p sequencer_service configs/debug/sequencer_config.json; \
|
||||
RUST_LOG=info cargo run --release -p sequencer_service -- configs/debug/sequencer_config.json --port {{port}} {{ if home != "" { "--home " + quote(home) } else { "" } }}; \
|
||||
fi
|
||||
|
||||
# Run Indexer. Run with RISC0_DEV_MODE=1 to disable proof verification for faster iteration.
|
||||
@ -94,6 +114,26 @@ wallet-import-test-accounts:
|
||||
|
||||
just run-wallet account list
|
||||
|
||||
# Demo: cross-zone ping. Boots two zones on one Bedrock and sends a message from
|
||||
# zone A to zone B, where the indexer re-derives and verifies it (Option B)
|
||||
# before ping_receiver records it. Dev mode, no proving.
|
||||
demo-cross-zone-ping:
|
||||
@echo "📡 Cross-zone ping demo (message A → B, indexer-verified)"
|
||||
{{DEMO_ENV}} RISC0_DEV_MODE=1 cargo test -p integration_tests --release --test cross_zone_verified -- --nocapture
|
||||
|
||||
# Demo: cross-zone wrapped-token bridge. Locks a balance on zone A and mints the
|
||||
# wrapped token to a recipient on zone B over the same verified spine.
|
||||
demo-cross-zone-bridge:
|
||||
@echo "🌉 Cross-zone bridge demo (lock on A, mint on B)"
|
||||
{{DEMO_ENV}} RISC0_DEV_MODE=1 cargo test -p integration_tests --release --test cross_zone_bridge -- --nocapture
|
||||
|
||||
# Demo: interactive cross-zone chat. Boots two zones on one Bedrock and serves a
|
||||
# local two-column web UI; type in one zone and watch the message cross into the
|
||||
# other. Two people can chat across the zones. Dev mode, no proving.
|
||||
cross-zone-chat:
|
||||
@echo "💬 Cross-zone chat demo — open the printed localhost URL"
|
||||
{{DEMO_ENV}} RISC0_DEV_MODE=1 cargo run -p cross_zone_chat --release
|
||||
|
||||
# Clean runtime data
|
||||
clean:
|
||||
@echo "🧹 Cleaning run artifacts"
|
||||
@ -101,5 +141,6 @@ clean:
|
||||
rm -rf lez/sequencer/service/rocksdb
|
||||
rm -rf lez/indexer/service/rocksdb*
|
||||
rm -rf lez/wallet/configs/debug/storage.json
|
||||
rm -rf lez/wallet/configs/debug/statistics.json
|
||||
rm -rf rocksdb*
|
||||
cd bedrock && docker compose down -v
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/lez/programs/bridge_lock.bin
Normal file
BIN
artifacts/lez/programs/bridge_lock.bin
Normal file
Binary file not shown.
Binary file not shown.
BIN
artifacts/lez/programs/cross_zone_inbox.bin
Normal file
BIN
artifacts/lez/programs/cross_zone_inbox.bin
Normal file
Binary file not shown.
BIN
artifacts/lez/programs/cross_zone_outbox.bin
Normal file
BIN
artifacts/lez/programs/cross_zone_outbox.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/lez/programs/ping_receiver.bin
Normal file
BIN
artifacts/lez/programs/ping_receiver.bin
Normal file
Binary file not shown.
BIN
artifacts/lez/programs/ping_sender.bin
Normal file
BIN
artifacts/lez/programs/ping_sender.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
artifacts/lez/programs/wrapped_token.bin
Normal file
BIN
artifacts/lez/programs/wrapped_token.bin
Normal file
Binary file not shown.
@ -1,48 +1,47 @@
|
||||
blend:
|
||||
common:
|
||||
num_blend_layers: 3
|
||||
num_blend_layers: 1
|
||||
minimum_network_size: 30
|
||||
protocol_name: /blend/integration-tests
|
||||
data_replication_factor: 0
|
||||
protocol_name: /logos-blockchain-LEZ-DEV/blend/1.0.0
|
||||
data_replication_factor: 1
|
||||
core:
|
||||
scheduler:
|
||||
cover:
|
||||
message_frequency_per_round: 1.0
|
||||
delayer:
|
||||
maximum_release_delay_in_rounds: 3
|
||||
maximum_release_delay_in_rounds: 1
|
||||
minimum_messages_coefficient: 1
|
||||
normalization_constant: 1.03
|
||||
activity_threshold_sensitivity: 1
|
||||
network:
|
||||
kademlia_protocol_name: /integration/logos-blockchain/kad/1.0.0
|
||||
identify_protocol_name: /integration/logos-blockchain/identify/1.0.0
|
||||
chain_sync_protocol_name: /integration/logos-blockchain/chainsync/1.0.0
|
||||
kademlia_protocol_name: /logos-blockchain-LEZ-DEV/kad/1.0.0
|
||||
identify_protocol_name: /logos-blockchain-LEZ-DEV/identify/1.0.0
|
||||
chain_sync_protocol_name: /logos-blockchain-LEZ-DEV/chainsync/1.0.0
|
||||
cryptarchia:
|
||||
epoch_config:
|
||||
epoch_stake_distribution_stabilization: 3
|
||||
epoch_period_nonce_buffer: 3
|
||||
epoch_period_nonce_stabilization: 4
|
||||
security_param: 10
|
||||
security_param: 5
|
||||
slot_activation_coeff:
|
||||
numerator: 1
|
||||
denominator: 2
|
||||
learning_rate: 0.1
|
||||
learning_rate: 0.5
|
||||
sdp_config:
|
||||
service_params:
|
||||
BN:
|
||||
inactivity_period: 1
|
||||
retention_period: 1
|
||||
inactivity_period: 2
|
||||
epoch: 0
|
||||
min_stake:
|
||||
threshold: 1
|
||||
timestamp: 0
|
||||
gossipsub_protocol: /integration/logos-blockchain/cryptarchia/proto/1.0.0
|
||||
gossipsub_protocol: /logos-blockchain-LEZ-DEV/cryptarchia/1.0.0
|
||||
genesis_block:
|
||||
header:
|
||||
version: Bedrock
|
||||
parent_block: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
slot: 0
|
||||
block_root: b5f8787ac23674822414c70eea15d842da38f2e806ede1a73cf7b5cf0277da07
|
||||
block_root: cb5951ac1ffa1aa5d0e585fb54e784bd9c025b28d752324e98b3837f34648692
|
||||
proof_of_leadership:
|
||||
proof: '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
entropy_contribution: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
@ -56,24 +55,134 @@ cryptarchia:
|
||||
payload:
|
||||
inputs: []
|
||||
outputs:
|
||||
- value: 1
|
||||
pk: d204000000000000000000000000000000000000000000000000000000000000
|
||||
- value: 100
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1
|
||||
pk: ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 1000000
|
||||
pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26'
|
||||
- value: 100000
|
||||
pk: '6b2bcd3029fba573cff0c332dc4de7430faf5e261383d693d8dbb5b97665660a'
|
||||
- value: 18446744073709551615
|
||||
pk: c2a6a4a0981d5bdcf8ddeb8d7934fd8c5510efeb1053f613b45871670b6f7b19
|
||||
- opcode: 17
|
||||
payload:
|
||||
channel_id: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
# chain_id_len=12 (u64_le), chain_id=logos-devnet (utf-8),
|
||||
# genesis_time=2026-01-10T07:47:56Z (u64_le), epoch_nonce=[0u8; 32]
|
||||
inscription: '0c000000000000006c6f676f732d6465766e65742c046269000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
inscription: '05302e322e3123766c6a2d2ddf918544bca603c5a291c7dd1b902d6769ff4b00021506780e075c06051a'
|
||||
parent: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
signer: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
- opcode: 32
|
||||
payload:
|
||||
service_type: BN
|
||||
locators:
|
||||
- /ip4/65.109.51.37/udp/3400/quic-v1
|
||||
provider_id: '59c662860b737f4e2515599adb3434856db8070b373a449ff66955ad3da6b473'
|
||||
zk_id: '6b2bcd3029fba573cff0c332dc4de7430faf5e261383d693d8dbb5b97665660a'
|
||||
locked_note_id: '7e449a14172fc90679f6fca7b49a2d58c305ebf7ac42ef20202e533c31115222'
|
||||
ops_proofs:
|
||||
- !ZkSig
|
||||
pi_a: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
pi_b: '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
pi_c: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
- !Ed25519Sig '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
- !Ed25519Sig '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
- !ZkAndEd25519Sigs
|
||||
zk_sig:
|
||||
pi_a: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
pi_b: '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
pi_c: '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
ed25519_sig: '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
|
||||
faucet_pk: c2a6a4a0981d5bdcf8ddeb8d7934fd8c5510efeb1053f613b45871670b6f7b19
|
||||
time:
|
||||
slot_duration: '1.0'
|
||||
slot_duration: '1.000000000'
|
||||
mempool:
|
||||
pubsub_topic: mantle_e2e_tests
|
||||
pubsub_topic: /logos-blockchain-LEZ-DEV/mempool/1.0.0
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
services:
|
||||
|
||||
logos-blockchain-node-0:
|
||||
image: ghcr.io/logos-blockchain/logos-blockchain@sha256:91d6c5bf07e07fcfba5e7cf07d21ee686a6bc4b9f6210f2d28bffbcad9a3729f
|
||||
image: ghcr.io/logos-blockchain/logos-blockchain:0.2.1-lssa
|
||||
ports:
|
||||
- "${PORT:-18080}:18080/tcp"
|
||||
volumes:
|
||||
|
||||
@ -7,14 +7,6 @@ export POL_PROOF_DEV_MODE=true
|
||||
# Use static configs mounted from host. Both node-config.yaml and
|
||||
# deployment-settings.yaml have matching validator keys so the node
|
||||
# can produce blocks as a single-validator network.
|
||||
# Copy deployment-settings to a writable path because sed -i can't
|
||||
# rename on a bind-mounted file.
|
||||
cp /etc/logos-blockchain/deployment-settings.yaml /deployment-settings.yaml
|
||||
|
||||
# Set chain_start_time to "now" so the chain starts immediately.
|
||||
sed -i "s/PLACEHOLDER_CHAIN_START_TIME/$(date -u '+%Y-%m-%d %H:%M:%S.000000 +00:00:00')/" \
|
||||
/deployment-settings.yaml
|
||||
|
||||
exec /usr/bin/logos-blockchain-node \
|
||||
/etc/logos-blockchain/node-config.yaml \
|
||||
--deployment /deployment-settings.yaml
|
||||
--deployment /etc/logos-blockchain/deployment-settings.yaml
|
||||
|
||||
@ -6,46 +6,45 @@ This tutorial walks you through using Keycard with Wallet CLI. Keycard is option
|
||||
### Required hardware
|
||||
- Keycard (Blank) - a Keycard, directly, from Keycard.tech cannot (currently) be updated to support LEE.
|
||||
- Smartcard reader
|
||||
- Applets (`math.cap` and `LEE_keycard.cap`). Eventually, both of these applets will be available in separate repos.
|
||||
- `math.cap` is an applet to speed up computations on Keycard; developed by Bitgamma (Keycard-tech team).
|
||||
- `LEE_keycard.cap` is an applet that contains LEE keycard protocol; developed by Bitgamma (Keycard-tech team)
|
||||
|
||||
### Firmware installation
|
||||
Installation:
|
||||
|
||||
1. Install math applet on your keycard; this process only needs to be done once. In the root of repo:
|
||||
```
|
||||
sudo apt-get install -y default-jdk
|
||||
wget https://github.com/martinpaljak/GlobalPlatformPro/releases/download/v25.10.20/gp.jar -P lez/keycard_wallet/keycard_applets
|
||||
cd lez/keycard_wallet/keycard_applets
|
||||
java -jar gp.jar --key c212e073ff8b4bbfaff4de8ab655221f --load math.cap
|
||||
```
|
||||
2. Install `keycard-desktop` from [github](https://github.com/choppu/keycard-desktop)
|
||||
- Keycard Desktop is used to install the LEE key protocol to a blank keycard.
|
||||
- Select (Re)Install Applet and upload the key binary (`lez/keycard_wallet/keycard_applets/LEE_keycard.cap`).
|
||||

|
||||
- **Important:** keycard can only connect with one application at a time; if Keycard-Desktop is using keycard then Wallet CLI cannot access the same keycard, and vice-versa.
|
||||
|
||||
## Wallet with Keycard
|
||||
Keycard functionality is available to Wallet CLI by setting up the following Python virtual environment. The steps below can also be run via `lez/keycard_wallet/wallet_with_keycard.sh`.
|
||||
LEE key protocol support (on top of standard Status Keycard commands) is built from source, from [`keycard-tech/status-keycard`](https://github.com/keycard-tech/status-keycard)'s default branch:
|
||||
|
||||
```bash
|
||||
# Install appropriate version of `keycard-py`.
|
||||
git clone --branch lee-schnorr --single-branch https://github.com/bitgamma/keycard-py.git lez/keycard_wallet/python/keycard-py
|
||||
|
||||
# Set up virtual environment.
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install pyscard mnemonic ecdsa pyaes
|
||||
pip install -e lez/keycard_wallet/python/keycard-py
|
||||
git clone --recurse-submodules https://github.com/keycard-tech/status-keycard.git
|
||||
cd status-keycard
|
||||
```
|
||||
|
||||
**Important**: Keycard wallet commands only work within the virtual environment.
|
||||
The build requires **OpenJDK 11 specifically** (newer JDKs aren't compatible with its Gradle/plugin versions):
|
||||
|
||||
```bash
|
||||
# In the root of LEE repo:
|
||||
source venv/bin/activate
|
||||
sudo apt-get install -y openjdk-11-jdk
|
||||
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
|
||||
```
|
||||
|
||||
Gradle's default heap is too small for this build and will OOM during `buildSrc` compilation; bump it once:
|
||||
|
||||
```bash
|
||||
echo "org.gradle.jvmargs=-Xmx2g" >> gradle.properties
|
||||
```
|
||||
|
||||
Build and install onto a connected, blank card (disconnect all other card readers first):
|
||||
|
||||
```bash
|
||||
./gradlew install
|
||||
```
|
||||
|
||||
This uses the GlobalPlatform default keys (`404142434445464748494a4b4c4d4e4f`) or the Keycard development-card key (`c212e073ff8b4bbfaff4de8ab655221f`) to load it onto the card.
|
||||
|
||||
**Warning: `./gradlew install` uninstalls and reinstalls the applet, which erases any existing personalization.** If you run this against a card that's already personalized (identity certificate, PIN, PUK, and any loaded keys), all of that is wiped, regardless of whether the firmware source changed at all — reinstalling the exact same build twice has the same effect.
|
||||
|
||||
### Personalizing your card
|
||||
|
||||
**Personalization is mandatory, not optional — every card requires it before any command will work, immediately after installing the firmware.** A freshly installed (or freshly reinstalled) card has no identity certificate, and refuses every command.
|
||||
|
||||
**Important:** keycard can only connect with one application at a time; if another tool is using the keycard then Wallet CLI cannot access the same keycard, and vice-versa.
|
||||
|
||||
## PIN entry
|
||||
|
||||
Each Keycard command prompts for a PIN interactively. To avoid re-entering it across multiple commands, export it as an environment variable:
|
||||
@ -60,43 +59,36 @@ Unset it when done:
|
||||
unset KEYCARD_PIN
|
||||
```
|
||||
|
||||
## Pairing password
|
||||
## Default CA public key
|
||||
|
||||
The pairing password is used to establish a secure channel between the wallet and the card. It is set permanently on the card during `wallet keycard init` and must match on every subsequent re-pair.
|
||||
`keycard-rs` verifies every card's identity certificate against a trusted CA public key before anything else happens — no match, no commands, regardless of whether the firmware or PIN is correct. The baked-in default is:
|
||||
|
||||
The default password (`KeycardDefaultPairing`) is [recommended](https://docs.keycard.tech/en/developers/core) for most users. Wallet CLI allows advance users the flexibility to set their own pairing password.
|
||||
|
||||
To use a custom pairing password, set it before `init`:
|
||||
|
||||
```bash
|
||||
# Note: Keep the leading space before this command.
|
||||
# Leading space prevents this command from being stored in shell history
|
||||
# (when HISTCONTROL=ignorespace is enabled).
|
||||
export KEYCARD_PAIRING_PASSWORD=my-custom-password
|
||||
wallet keycard init
|
||||
```
|
||||
029ab99ee1e7a71bdf45b3f9c58c99866ff1294d2c1e304e228a86e10c3343501c
|
||||
```
|
||||
|
||||
After a successful initializaation, subsequent commands (`connect`, transfers) use the cached pairing index and key — the pairing password is not needed again until the pairing is cleared.
|
||||
|
||||
**Important:** if you initialized with a custom password, `KEYCARD_PAIRING_PASSWORD` must be set in every session where re-pairing can occur (after `disconnect`, or on a new machine). If the env var is missing then wallet CLI will attempt to use the default password. As a result, pairing will fail.
|
||||
|
||||
Unset the pairing password variable when done:
|
||||
Cards personalized for development/testing (see "Personalizing the card" above) are signed by a different, throwaway CA instead, so the wallet needs to be told to trust it explicitly:
|
||||
|
||||
```bash
|
||||
unset KEYCARD_PAIRING_PASSWORD
|
||||
export KEYCARD_CA_PUBLIC_KEY=025877220aaae6e54a6f974602d5995c0fe24a3ea7ddabd8644bec795b9da00743
|
||||
# unset KEYCARD_CA_PUBLIC_KEY when done testing against a dev card
|
||||
```
|
||||
|
||||
If the card's certificate doesn't match whichever CA is in effect, every command reports the card as simply "not available."
|
||||
|
||||
## Keycard Commands
|
||||
|
||||
Keycard uses Secure Channel V2 (applet version >= 4.0) — the wallet authenticates the card via its identity certificate and opens a fresh ECDHE-derived channel every session. There's no pairing step and nothing cached between commands; you'll enter your PIN each time you connect.
|
||||
|
||||
### Keycard
|
||||
|
||||
| Command | Description |
|
||||
|----------------------------------|-----------------------------------------------------------------------|
|
||||
| `wallet keycard available` | Checks whether a Keycard reader and card are accessible |
|
||||
| `wallet keycard init` | Initializes a blank Keycard with a PIN and a generated PUK |
|
||||
| `wallet keycard connect` | Establishes and saves a pairing with the Keycard |
|
||||
| `wallet keycard disconnect` | Unpairs the Keycard and clears the saved pairing |
|
||||
| `wallet keycard connect` | Opens a secure channel with the Keycard and verifies the PIN |
|
||||
| `wallet keycard load` | Loads a mnemonic phrase onto the Keycard |
|
||||
| `wallet keycard factory-reset` | Wipes PIN/PUK/keys back to uninitialized, for re-`init` — **debug builds only** (see below) |
|
||||
| `wallet keycard get-private-keys`| Prints NSK and VSK for a BIP-32 path — **debug builds only** (see below) |
|
||||
|
||||
1. Check keycard availability
|
||||
@ -118,13 +110,13 @@ Record this PUK and store it somewhere safe. It cannot be recovered.
|
||||
✅ Keycard initialized successfully.
|
||||
```
|
||||
|
||||
3. Connect (pair and save pairing for subsequent commands)
|
||||
3. Connect (open a secure channel and verify the PIN)
|
||||
```bash
|
||||
wallet keycard connect
|
||||
|
||||
# Output:
|
||||
Keycard PIN:
|
||||
✅ Keycard paired and ready.
|
||||
✅ Keycard connected and PIN verified.
|
||||
```
|
||||
|
||||
4. Load a mnemonic phrase
|
||||
@ -140,25 +132,24 @@ Keycard PIN:
|
||||
✅ Mnemonic phrase loaded successfully.
|
||||
```
|
||||
|
||||
5. Disconnect (unpair and clear saved pairing)
|
||||
5. `factory-reset`
|
||||
|
||||
Wipes the card's PIN, PUK, and loaded keys back to an uninitialized state, so it can be re-`init`ialized — the counterpart to `init`. It does **not** remove the identity certificate, so the card doesn't need re-personalizing afterward. Irreversibly destroys any keys currently on the card, so it requires `--confirm`:
|
||||
```bash
|
||||
wallet keycard disconnect
|
||||
wallet keycard factory-reset --confirm
|
||||
|
||||
# Output:
|
||||
Keycard PIN:
|
||||
✅ Keycard unpaired and pairing cleared.
|
||||
✅ Keycard factory-reset. Run `wallet keycard init` to reinitialize it.
|
||||
```
|
||||
|
||||
6. Get private keys for a BIP-32 path (**debug builds only**)
|
||||
6. `get-private-keys` (**debug builds only**)
|
||||
|
||||
`get-private-keys` exports the raw NSK and VSK for a derivation path. NSK gates nullifier creation and VSK gates note decryption — either key is sufficient to fully compromise that account's privacy. The command is only available in debug builds and requires `--reveal` to confirm intent.
|
||||
|
||||
First install the wallet with the `keycard-debug` feature:
|
||||
Requires building the wallet with the `keycard-debug` feature:
|
||||
```bash
|
||||
cargo install --path lez/wallet --force --features keycard-debug
|
||||
```
|
||||
|
||||
Then run the command:
|
||||
Exports the raw NSK and VSK for a derivation path. NSK gates nullifier creation and VSK gates note decryption — either key is sufficient to fully compromise that account's privacy. Requires `--reveal` to confirm intent:
|
||||
```bash
|
||||
wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/0" --reveal
|
||||
|
||||
@ -515,20 +506,4 @@ bash lez/keycard_wallet/tests/keycard_tests.sh
|
||||
bash lez/keycard_wallet/tests/keycard_tests_2.sh
|
||||
bash lez/keycard_wallet/tests/keycard_test_3.sh
|
||||
bash lez/keycard_wallet/tests/keycard_power_recovery_tests.sh
|
||||
```
|
||||
|
||||
## SigningGroup
|
||||
|
||||
`SigningGroup` (`lez/wallet/src/signing.rs`) partitions a transaction's signers into two buckets — local accounts and Keycard accounts. This ensures that Python GIL is only used at most once per transaction, regardless of how many Keycard accounts are involved.
|
||||
|
||||
Local signers are resolved and signed in pure Rust. Keycard signers store only their BIP32 key path; all of them are signed inside a single Python session (`connect` / `close_session`) when `sign_all` is called. The command calls `needs_pin` to decide whether to prompt for a PIN before signing.
|
||||
|
||||
Foreign recipient accounts — those with no local key and no Keycard path — are silently skipped and require neither a signature nor a nonce.
|
||||
|
||||
```
|
||||
SigningGroup {
|
||||
local: [(AccountId, PrivateKey)], // signed in pure Rust
|
||||
keycard: [(AccountId, BIP32Path)], // signed via a single Python/Keycard session
|
||||
}
|
||||
```
|
||||
```
|
||||
@ -27,7 +27,7 @@ use wallet::WalletCore;
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize wallet
|
||||
let wallet_core = WalletCore::from_env().unwrap();
|
||||
let wallet_core = WalletCore::from_env().await.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the program binary
|
||||
@ -59,7 +59,7 @@ async fn main() {
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.helm_owned()
|
||||
.send_transaction(LeeTransaction::Public(tx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -23,7 +23,7 @@ use wallet::{AccountIdentity, WalletCore};
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize wallet
|
||||
let wallet_core = WalletCore::from_env().unwrap();
|
||||
let wallet_core = WalletCore::from_env().await.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the program binary
|
||||
|
||||
@ -27,7 +27,7 @@ use wallet::WalletCore;
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize wallet
|
||||
let wallet_core = WalletCore::from_env().unwrap();
|
||||
let wallet_core = WalletCore::from_env().await.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the program binary
|
||||
@ -55,7 +55,7 @@ async fn main() {
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.helm_owned()
|
||||
.send_transaction(LeeTransaction::Public(tx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -26,7 +26,7 @@ use wallet::{AccountIdentity, WalletCore};
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize wallet
|
||||
let wallet_core = WalletCore::from_env().unwrap();
|
||||
let wallet_core = WalletCore::from_env().await.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the simple_tail_call program binary
|
||||
|
||||
@ -29,7 +29,7 @@ use wallet::WalletCore;
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize wallet
|
||||
let wallet_core = WalletCore::from_env().unwrap();
|
||||
let wallet_core = WalletCore::from_env().await.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the program binary
|
||||
@ -52,7 +52,8 @@ async fn main() {
|
||||
.storage()
|
||||
.key_chain()
|
||||
.pub_account_signing_key(account_id)
|
||||
.expect("Input account should be a self owned public account");
|
||||
.expect("Input account should be a self owned public account")
|
||||
.clone();
|
||||
|
||||
// Define the desired greeting in ASCII
|
||||
let greeting: Vec<u8> = vec![72, 111, 108, 97, 32, 109, 117, 110, 100, 111, 33];
|
||||
@ -60,10 +61,10 @@ async fn main() {
|
||||
// Construct the public transaction
|
||||
// Query the current nonce from the node
|
||||
let nonces = wallet_core
|
||||
.get_accounts_nonces(vec![account_id])
|
||||
.get_accounts_nonces(&[account_id])
|
||||
.await
|
||||
.expect("Node should be reachable to query account data");
|
||||
let signing_keys = [signing_key];
|
||||
let signing_keys = [&signing_key];
|
||||
let message = Message::try_new(program.id(), vec![account_id], nonces, greeting).unwrap();
|
||||
// Pass the signing key to sign the message. This will be used by the node
|
||||
// to flag the pre_state as `is_authorized` when executing the program
|
||||
@ -72,7 +73,7 @@ async fn main() {
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.helm_owned()
|
||||
.send_transaction(LeeTransaction::Public(tx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -35,7 +35,7 @@ const PDA_SEED: PdaSeed = PdaSeed::new([37; 32]);
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize wallet
|
||||
let wallet_core = WalletCore::from_env().unwrap();
|
||||
let wallet_core = WalletCore::from_env().await.unwrap();
|
||||
|
||||
// Parse arguments
|
||||
// First argument is the path to the program binary
|
||||
@ -57,7 +57,7 @@ async fn main() {
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.helm_owned()
|
||||
.send_transaction(LeeTransaction::Public(tx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -66,7 +66,7 @@ async fn main() {
|
||||
let program = Program::new(bytecode.into()).unwrap();
|
||||
|
||||
// Initialize wallet
|
||||
let wallet_core = WalletCore::from_env().unwrap();
|
||||
let wallet_core = WalletCore::from_env().await.unwrap();
|
||||
|
||||
match cli.command {
|
||||
Command::WritePublic {
|
||||
@ -88,7 +88,7 @@ async fn main() {
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.helm_owned()
|
||||
.send_transaction(LeeTransaction::Public(tx))
|
||||
.await
|
||||
.unwrap();
|
||||
@ -127,7 +127,7 @@ async fn main() {
|
||||
|
||||
// Submit the transaction
|
||||
let _response = wallet_core
|
||||
.sequencer_client
|
||||
.helm_owned()
|
||||
.send_transaction(LeeTransaction::Public(tx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@ -117,13 +117,14 @@
|
||||
done
|
||||
if [ -n "$tool" ]; then
|
||||
unset DEVELOPER_DIR SDKROOT
|
||||
export xcrun_nocache=1
|
||||
fi
|
||||
exec /usr/bin/xcrun "$@"
|
||||
'';
|
||||
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
buildInputs = [ pkgs.openssl ];
|
||||
buildInputs = [ pkgs.openssl pkgs.pcsclite ];
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
pkgs.clang
|
||||
|
||||
@ -22,6 +22,12 @@ associated_token_account_core.workspace = true
|
||||
vault_core.workspace = true
|
||||
faucet_core.workspace = true
|
||||
bridge_core.workspace = true
|
||||
ping_core.workspace = true
|
||||
cross_zone_outbox_core.workspace = true
|
||||
cross_zone_inbox_core.workspace = true
|
||||
bridge_lock_core.workspace = true
|
||||
wrapped_token_core.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
indexer_service_rpc = { workspace = true, features = ["client"] }
|
||||
sequencer_service_rpc = { workspace = true, features = ["client"] }
|
||||
wallet-ffi.workspace = true
|
||||
@ -30,18 +36,13 @@ indexer_service_protocol.workspace = true
|
||||
system_accounts.workspace = true
|
||||
programs.workspace = true
|
||||
test_programs.workspace = true
|
||||
testnet_initial_state.workspace = true
|
||||
|
||||
logos-blockchain-http-api-common.workspace = true
|
||||
logos-blockchain-core.workspace = true
|
||||
logos-blockchain-zone-sdk.workspace = true
|
||||
logos-blockchain-key-management-system-service.workspace = true
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
futures.workspace = true
|
||||
hex.workspace = true
|
||||
tempfile.workspace = true
|
||||
bytesize.workspace = true
|
||||
reqwest.workspace = true
|
||||
borsh.workspace = true
|
||||
num-bigint.workspace = true
|
||||
|
||||
@ -6,11 +6,218 @@
|
||||
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::{
|
||||
AccountIdentity,
|
||||
cli::{
|
||||
CliAccountMention, Command, SubcommandReturnValue,
|
||||
account::{AccountSubcommand, NewSubcommand},
|
||||
programs::{
|
||||
native_token_transfer::AuthTransferSubcommand, token::TokenProgramAgnosticSubcommand,
|
||||
},
|
||||
},
|
||||
program_facades::{native_token_transfer::NativeTokenTransfer, token::Token},
|
||||
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(7);
|
||||
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<ChainIndex>,
|
||||
) -> Result<AccountId> {
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Like [`send`], but for a `to` that is still a fresh, unclaimed account.
|
||||
///
|
||||
/// The wallet CLI's `AuthTransfer::Send` never signs with the recipient's key (by design: the
|
||||
/// sender's wallet must not sign on behalf of an account it doesn't own). But claiming a fresh
|
||||
/// account is only possible if that account's own key signs the transaction, so this bypasses
|
||||
/// the CLI and calls the program facade directly with an explicit `AccountIdentity::Public` for
|
||||
/// the recipient, using the key the test wallet holds for the account it just created.
|
||||
///
|
||||
/// Unlike `send`, this doesn't go through the CLI's own poll-until-included step, so it waits
|
||||
/// for block creation itself before returning.
|
||||
pub async fn send_claiming_new_account(
|
||||
ctx: &mut TestContext,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<()> {
|
||||
NativeTokenTransfer(ctx.wallet())
|
||||
.send_public_transfer(
|
||||
AccountIdentity::Public(from),
|
||||
AccountIdentity::Public(to),
|
||||
amount,
|
||||
)
|
||||
.await?;
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).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<String>,
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Like [`token_send`], but for a `to` that is still a fresh, unclaimed holding account. See
|
||||
/// [`send_claiming_new_account`] for why the CLI can't be used here.
|
||||
pub async fn token_send_claiming_new_account(
|
||||
ctx: &mut TestContext,
|
||||
from: AccountId,
|
||||
to: AccountId,
|
||||
amount: u128,
|
||||
) -> Result<()> {
|
||||
Token(ctx.wallet())
|
||||
.send_transfer_transaction(
|
||||
AccountIdentity::Public(from),
|
||||
AccountIdentity::Public(to),
|
||||
amount,
|
||||
)
|
||||
.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<u128> {
|
||||
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<lee::Account> {
|
||||
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.
|
||||
|
||||
@ -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;
|
||||
use lee_core::account::Nonce;
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
account::{AccountIdWithPrivacy, HumanReadableAccount, Label},
|
||||
@ -24,10 +23,7 @@ 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,
|
||||
@ -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!(
|
||||
@ -156,7 +141,11 @@ async fn import_private_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let key_chain = KeyChain::new_os_random();
|
||||
let account_id = lee::AccountId::from((&key_chain.nullifier_public_key, 0));
|
||||
let account_id = lee::AccountId::from((
|
||||
&key_chain.nullifier_public_key,
|
||||
&key_chain.viewing_public_key,
|
||||
0,
|
||||
));
|
||||
let account = lee::Account {
|
||||
program_owner: programs::authenticated_transfer().id(),
|
||||
balance: 777,
|
||||
@ -213,7 +202,11 @@ async fn import_private_account_second_time_overrides_account_data() -> Result<(
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let key_chain = KeyChain::new_os_random();
|
||||
let account_id = lee::AccountId::from((&key_chain.nullifier_public_key, 0));
|
||||
let account_id = lee::AccountId::from((
|
||||
&key_chain.nullifier_public_key,
|
||||
&key_chain.viewing_public_key,
|
||||
0,
|
||||
));
|
||||
let key_chain_json =
|
||||
serde_json::to_string(&key_chain).context("Failed to serialize key chain")?;
|
||||
|
||||
|
||||
@ -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_claiming_new_account,
|
||||
};
|
||||
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,53 @@ 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,
|
||||
})),
|
||||
let recipient_account_id_2 = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
// Create new token
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id_1),
|
||||
public_mention(supply_account_id_1),
|
||||
"A NAM1".to_owned(),
|
||||
37,
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
anyhow::bail!("Expected RegisterAccount return value");
|
||||
};
|
||||
.await?;
|
||||
|
||||
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1`.
|
||||
// `recipient_account_id_1` is still unclaimed, so this bypasses the wallet CLI (which never
|
||||
// signs with the recipient's key) and signs with the recipient's own key directly.
|
||||
token_send_claiming_new_account(&mut ctx, supply_account_id_1, recipient_account_id_1, 7)
|
||||
.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(),
|
||||
create_token(
|
||||
&mut ctx,
|
||||
public_mention(definition_account_id_2),
|
||||
public_mention(supply_account_id_2),
|
||||
"A NAM2".to_owned(),
|
||||
37,
|
||||
)
|
||||
.await?;
|
||||
|
||||
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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
// Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_2`.
|
||||
// `recipient_account_id_2` is still unclaimed, so this bypasses the wallet CLI the same way.
|
||||
token_send_claiming_new_account(&mut ctx, supply_account_id_2, recipient_account_id_2, 7)
|
||||
.await?;
|
||||
|
||||
info!("=================== SETUP FINISHED ===============");
|
||||
|
||||
@ -174,19 +81,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 +96,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 +133,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 +170,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 +208,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 +246,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 +277,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 +298,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 +335,31 @@ 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;
|
||||
// `holding_a_id` is still unclaimed, so bypass the wallet CLI (see
|
||||
// `token_send_claiming_new_account`'s docs for why).
|
||||
token_send_claiming_new_account(&mut ctx, supply_account_id_1, 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;
|
||||
// `holding_b_id` is still unclaimed, so bypass the wallet CLI the same way.
|
||||
token_send_claiming_new_account(&mut ctx, supply_account_id_2, holding_b_id, 5).await?;
|
||||
|
||||
// Create AMM pool using account labels instead of IDs
|
||||
let subcommand = AmmProgramAgnosticSubcommand::New {
|
||||
@ -572,7 +372,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!(
|
||||
|
||||
@ -9,75 +9,34 @@ use std::time::Duration;
|
||||
use anyhow::{Context as _, Result};
|
||||
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 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<lee::AccountId> {
|
||||
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<lee::AccountId> {
|
||||
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(),
|
||||
@ -121,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(),
|
||||
@ -196,28 +150,23 @@ 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 = programs::ata().id();
|
||||
let sender_ata_id = get_associated_token_account_id(
|
||||
@ -252,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(
|
||||
@ -286,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,
|
||||
@ -297,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,
|
||||
@ -323,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,
|
||||
@ -334,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,
|
||||
@ -355,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(),
|
||||
@ -424,28 +356,23 @@ 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 = programs::ata().id();
|
||||
let sender_ata_id = get_associated_token_account_id(
|
||||
@ -480,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(
|
||||
@ -514,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,
|
||||
@ -525,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,
|
||||
@ -549,27 +467,22 @@ 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 = programs::ata().id();
|
||||
let holder_ata_id = get_associated_token_account_id(
|
||||
@ -592,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(
|
||||
@ -625,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,
|
||||
@ -636,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,
|
||||
|
||||
@ -3,17 +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, SharedSecretKey, execute_and_prove,
|
||||
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
|
||||
AccountId, execute_and_prove, privacy_preserving_transaction::circuit::ProgramWithDependencies,
|
||||
program::Program,
|
||||
};
|
||||
use lee_core::{
|
||||
EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
|
||||
account::AccountWithMetadata,
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
DUMMY_COMMITMENT_HASH, InputAccountIdentity, Nullifier, NullifierPublicKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
@ -34,32 +35,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");
|
||||
|
||||
@ -86,8 +68,8 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
|
||||
});
|
||||
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = result else {
|
||||
anyhow::bail!("Expected PrivacyPreservingTransfer return value");
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = result else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
@ -99,9 +81,8 @@ async fn private_transfer_to_foreign_account() -> Result<()> {
|
||||
.context("Failed to get private account commitment for sender")?;
|
||||
|
||||
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
||||
assert_eq!(tx.message.new_commitments[0], new_commitment1);
|
||||
assert!(tx.message.new_commitments.contains(&new_commitment1));
|
||||
|
||||
assert_eq!(tx.message.new_commitments.len(), 2);
|
||||
for commitment in tx.message.new_commitments {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
@ -125,6 +106,38 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
|
||||
.context("Failed to get sender's private account")?;
|
||||
assert_eq!(from_acc.balance, 10000);
|
||||
|
||||
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;
|
||||
|
||||
let from_acc = ctx
|
||||
.wallet()
|
||||
.get_account_private(from)
|
||||
.context("Failed to get sender's private account")?;
|
||||
assert_private_commitment_in_state(&ctx, from, "sender").await?;
|
||||
|
||||
let acc_2_balance = account_balance(&ctx, to).await?;
|
||||
|
||||
assert_eq!(from_acc.balance, 9900);
|
||||
assert_eq!(acc_2_balance, 20100);
|
||||
|
||||
info!("Successfully deshielded transfer to public account");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A deshielded transfer's public recipient must not be asked to sign the transaction: the
|
||||
/// sender's private-side proof is the only authorization the protocol requires, and signing
|
||||
/// with the recipient's key (when the wallet happens to hold it) would leak a link between
|
||||
/// the two accounts.
|
||||
#[test]
|
||||
async fn deshielded_transfer_does_not_sign_with_recipient_key() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let from: AccountId = ctx.existing_private_accounts()[0];
|
||||
let to: AccountId = ctx.existing_public_accounts()[1];
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: private_mention(from),
|
||||
to: Some(public_mention(to)),
|
||||
@ -135,27 +148,22 @@ async fn deshielded_transfer_to_public_account() -> Result<()> {
|
||||
amount: 100,
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = result else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
let from_acc = ctx
|
||||
.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);
|
||||
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
||||
|
||||
let acc_2_balance = ctx.sequencer_client().get_account_balance(to).await?;
|
||||
assert!(
|
||||
tx.witness_set().signatures_and_public_keys().is_empty(),
|
||||
"deshielded transfer must not carry any signature, in particular not the recipient's"
|
||||
);
|
||||
|
||||
assert_eq!(from_acc.balance, 9900);
|
||||
assert_eq!(acc_2_balance, 20100);
|
||||
|
||||
info!("Successfully deshielded transfer to public account");
|
||||
info!("Deshielded transfer correctly did not sign with the recipient's key");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -167,18 +175,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
|
||||
@ -200,23 +197,21 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> {
|
||||
});
|
||||
|
||||
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else {
|
||||
anyhow::bail!("Expected PrivacyPreservingTransfer return value");
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = sub_ret else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
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!(tx.message.new_commitments.contains(&sender_commitment));
|
||||
|
||||
assert_eq!(tx.message.new_commitments.len(), 2);
|
||||
for commitment in tx.message.new_commitments {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
@ -239,17 +234,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;
|
||||
@ -258,13 +243,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);
|
||||
@ -294,8 +275,8 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> {
|
||||
});
|
||||
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = result else {
|
||||
anyhow::bail!("Expected PrivacyPreservingTransfer return value");
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = result else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
@ -303,15 +284,11 @@ 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(
|
||||
tx.message.new_commitments[0].clone(),
|
||||
ctx.sequencer_client()
|
||||
)
|
||||
.await
|
||||
);
|
||||
for commitment in tx.message.new_commitments {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
|
||||
assert_eq!(acc_1_balance, 9900);
|
||||
|
||||
@ -332,18 +309,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
|
||||
@ -365,7 +331,7 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
|
||||
});
|
||||
|
||||
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else {
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = sub_ret else {
|
||||
anyhow::bail!("Failed to send transaction");
|
||||
};
|
||||
|
||||
@ -376,7 +342,6 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
// Verify commitments are in state
|
||||
assert_eq!(tx.message.new_commitments.len(), 2);
|
||||
for commitment in tx.message.new_commitments {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
@ -396,14 +361,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),
|
||||
@ -413,14 +371,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()
|
||||
@ -455,32 +408,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");
|
||||
|
||||
@ -510,14 +450,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()
|
||||
@ -593,21 +528,17 @@ 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, identifier_1);
|
||||
let account_id_1 = AccountId::for_regular_private_account(&npk, &vpk, identifier_1);
|
||||
let acc_1 = ctx
|
||||
.wallet()
|
||||
.get_account_private(account_id_1)
|
||||
.context("account for identifier 1 not found after sync")?;
|
||||
assert_eq!(acc_1.balance, 100);
|
||||
|
||||
let account_id_2 = AccountId::for_regular_private_account(&npk, identifier_2);
|
||||
let account_id_2 = AccountId::for_regular_private_account(&npk, &vpk, identifier_2);
|
||||
let acc_2 = ctx
|
||||
.wallet()
|
||||
.get_account_private(account_id_2)
|
||||
@ -663,25 +594,19 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
let nsk: lee_core::NullifierSecretKey = [3; 32];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
|
||||
let ssk = SharedSecretKey([55_u8; 32]);
|
||||
let epk = EphemeralPublicKey(vec![55_u8; 1088]);
|
||||
let attacker_vault_id = {
|
||||
let seed = vault_core::compute_vault_seed(attacker_id);
|
||||
AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337)
|
||||
AccountId::for_private_pda(&vault_program_id, &seed, &npk, &vpk, 1337)
|
||||
};
|
||||
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,
|
||||
);
|
||||
@ -705,11 +630,11 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
|
||||
vpk,
|
||||
random_seed: [0; 32],
|
||||
npk,
|
||||
ssk,
|
||||
identifier: 1337,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
},
|
||||
],
|
||||
@ -720,3 +645,89 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prove_init_with_commitment_root(
|
||||
ctx: &TestContext,
|
||||
commitment_root: lee_core::CommitmentSetDigest,
|
||||
) -> Result<lee_core::PrivacyPreservingCircuitOutput> {
|
||||
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(), true, 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::PrivateForeignInit {
|
||||
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 (_, expected_digest) = ctx.sequencer_client().get_proofs_and_root(vec![]).await?;
|
||||
|
||||
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 (_, expected_digest) = ctx.sequencer_client().get_proofs_and_root(vec![]).await?;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, public_mention};
|
||||
use lee::public_transaction;
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, account_balance, get_account, new_account,
|
||||
public_mention, send, send_claiming_new_account,
|
||||
};
|
||||
use lee::{PublicKey, 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, SubcommandReturnValue, account::AccountSubcommand,
|
||||
programs::native_token_transfer::AuthTransferSubcommand,
|
||||
},
|
||||
};
|
||||
@ -20,30 +22,29 @@ use wallet::{
|
||||
async fn successful_transfer_to_existing_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let sender = ctx.existing_public_accounts()[0];
|
||||
let receiver = ctx.existing_public_accounts()[1];
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Send {
|
||||
from: public_mention(ctx.existing_public_accounts()[0]),
|
||||
to: Some(public_mention(ctx.existing_public_accounts()[1])),
|
||||
from: public_mention(sender),
|
||||
to: Some(public_mention(receiver)),
|
||||
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 result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = result else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
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:#?}");
|
||||
@ -51,6 +52,34 @@ async fn successful_transfer_to_existing_account() -> Result<()> {
|
||||
assert_eq!(acc_1_balance, 9900);
|
||||
assert_eq!(acc_2_balance, 20100);
|
||||
|
||||
// The recipient already exists, so the protocol doesn't require its signature, and the
|
||||
// wallet must never sign with a key it doesn't need to use. Assert the transfer's witness
|
||||
// set contains exactly the sender's signature, not the recipient's.
|
||||
let (tx, _block_id) = ctx
|
||||
.sequencer_client()
|
||||
.get_transaction(tx_hash)
|
||||
.await?
|
||||
.context("transfer transaction should be included in a block")?;
|
||||
let LeeTransaction::Public(tx) = tx else {
|
||||
anyhow::bail!("Expected a public transaction");
|
||||
};
|
||||
let sender_public_key = PublicKey::new_from_private_key(
|
||||
ctx.wallet()
|
||||
.get_account_public_signing_key(sender)
|
||||
.context("sender should have a signing key")?,
|
||||
);
|
||||
let signers: Vec<_> = tx
|
||||
.witness_set()
|
||||
.signatures_and_public_keys()
|
||||
.iter()
|
||||
.map(|(_, public_key)| public_key)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
signers,
|
||||
vec![&sender_public_key],
|
||||
"only the sender should sign a transfer to an existing account"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -58,51 +87,16 @@ 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?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
let sender = ctx.existing_public_accounts()[0];
|
||||
// The wallet CLI never signs with the recipient's key, but claiming this fresh account
|
||||
// requires it, so bypass the CLI for this one send.
|
||||
send_claiming_new_account(&mut ctx, sender, new_persistent_account_id, 100).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 +128,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 +144,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 +172,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 +202,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,7 +210,7 @@ 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,
|
||||
@ -274,30 +238,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 +276,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);
|
||||
@ -359,14 +307,8 @@ async fn cannot_transfer_funds_from_system_faucet_account() -> Result<()> {
|
||||
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(
|
||||
@ -387,14 +329,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);
|
||||
@ -413,14 +349,8 @@ async fn cannot_execute_faucet_program() -> Result<()> {
|
||||
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(
|
||||
@ -445,14 +375,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);
|
||||
@ -493,28 +417,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);
|
||||
|
||||
@ -1,42 +1,23 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
clippy::arithmetic_side_effects,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use std::{ops::Deref as _, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
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,
|
||||
};
|
||||
use lee::{
|
||||
AccountId, execute_and_prove, privacy_preserving_transaction, program::Program,
|
||||
public_transaction,
|
||||
execute_and_prove, privacy_preserving_transaction, program::Program, public_transaction,
|
||||
};
|
||||
use lee_core::{InputAccountIdentity, account::AccountWithMetadata};
|
||||
use log::info;
|
||||
use logos_blockchain_core::mantle::{ledger::Inputs, ops::channel::deposit::DepositOp};
|
||||
use logos_blockchain_http_api_common::bodies::{
|
||||
channel::ChannelDepositRequestBody,
|
||||
wallet::{
|
||||
balance::WalletBalanceResponseBody,
|
||||
transfer_funds::{WalletTransferFundsRequestBody, WalletTransferFundsResponseBody},
|
||||
},
|
||||
};
|
||||
use logos_blockchain_zone_sdk::{
|
||||
CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer,
|
||||
};
|
||||
use num_bigint::BigUint;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use test_fixtures::public_mention;
|
||||
use tokio::test;
|
||||
use wallet::cli::{Command, execute_subcommand, programs::bridge::BridgeSubcommand};
|
||||
|
||||
const TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK: Duration = Duration::from_mins(2);
|
||||
// const TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK: Duration = Duration::from_mins(2);
|
||||
|
||||
#[test]
|
||||
async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
@ -46,10 +27,11 @@ async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
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 receipt_id = bridge_core::deposit_receipt_account_id(programs::bridge().id(), [0_u8; 32]);
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
programs::bridge().id(),
|
||||
vec![bridge_account_id, recipient_vault_id],
|
||||
vec![bridge_account_id, recipient_vault_id, receipt_id],
|
||||
vec![],
|
||||
bridge_core::Instruction::Deposit {
|
||||
l1_deposit_op_id: [0_u8; 32],
|
||||
@ -65,27 +47,15 @@ async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
));
|
||||
|
||||
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);
|
||||
@ -106,10 +76,11 @@ async fn public_bridge_deposit_with_zero_amount_is_rejected() -> anyhow::Result<
|
||||
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 receipt_id = bridge_core::deposit_receipt_account_id(programs::bridge().id(), [0_u8; 32]);
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
programs::bridge().id(),
|
||||
vec![bridge_account_id, recipient_vault_id],
|
||||
vec![bridge_account_id, recipient_vault_id, receipt_id],
|
||||
vec![],
|
||||
bridge_core::Instruction::Deposit {
|
||||
l1_deposit_op_id: [0_u8; 32],
|
||||
@ -166,22 +137,22 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
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 receipt_id = bridge_core::deposit_receipt_account_id(programs::bridge().id(), [0_u8; 32]);
|
||||
|
||||
// Get pre-state of bridge and vault accounts
|
||||
// Get pre-state of bridge and vault accounts; the receipt is unminted (a
|
||||
// default account), so the program would create it on a first mint.
|
||||
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,
|
||||
);
|
||||
let receipt_pre =
|
||||
AccountWithMetadata::new(lee_core::account::Account::default(), false, receipt_id);
|
||||
|
||||
// Create program with dependencies
|
||||
let program_with_deps =
|
||||
@ -208,17 +179,25 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
|
||||
// Execute and prove the bridge deposit
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![bridge_pre.clone(), vault_pre.clone()],
|
||||
vec![bridge_pre.clone(), vault_pre.clone(), receipt_pre.clone()],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::Public, InputAccountIdentity::Public],
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.context("Failed to execute/prove bridge deposit")?;
|
||||
|
||||
// Create privacy-preserving transaction from circuit output
|
||||
let message = privacy_preserving_transaction::Message::try_from_circuit_output(
|
||||
vec![bridge_account_id, recipient_vault_id],
|
||||
vec![bridge_pre.account.nonce, vault_pre.account.nonce],
|
||||
vec![bridge_account_id, recipient_vault_id, receipt_id],
|
||||
vec![
|
||||
bridge_pre.account.nonce,
|
||||
vault_pre.account.nonce,
|
||||
receipt_pre.account.nonce,
|
||||
],
|
||||
output,
|
||||
)
|
||||
.context("Failed to build privacy-preserving bridge deposit message")?;
|
||||
@ -229,27 +208,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);
|
||||
@ -262,389 +229,386 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn submit_bedrock_deposit(
|
||||
bedrock_addr: std::net::SocketAddr,
|
||||
bedrock_account_pk: &str,
|
||||
recipient_id: AccountId,
|
||||
amount: u64,
|
||||
) -> anyhow::Result<()> {
|
||||
#[derive(BorshSerialize)]
|
||||
struct DepositMetadata {
|
||||
recipient_id: AccountId,
|
||||
}
|
||||
// async fn submit_bedrock_deposit(
|
||||
// bedrock_addr: std::net::SocketAddr,
|
||||
// bedrock_account_pk: &str,
|
||||
// recipient_id: AccountId,
|
||||
// amount: u64,
|
||||
// ) -> anyhow::Result<()> {
|
||||
// #[derive(BorshSerialize)]
|
||||
// struct DepositMetadata {
|
||||
// recipient_id: AccountId,
|
||||
// }
|
||||
|
||||
// Encode deposit metadata
|
||||
let metadata = borsh::to_vec(&DepositMetadata { recipient_id })
|
||||
.context("Failed to encode deposit metadata")?
|
||||
.try_into()
|
||||
.context("Encoded metadata is too big")?;
|
||||
// // Encode deposit metadata
|
||||
// let metadata = borsh::to_vec(&DepositMetadata { recipient_id })
|
||||
// .context("Failed to encode deposit metadata")?
|
||||
// .try_into()
|
||||
// .context("Encoded metadata is too big")?;
|
||||
|
||||
let channel_id = integration_tests::config::bedrock_channel_id();
|
||||
let client = reqwest::Client::new();
|
||||
// let channel_id = integration_tests::config::bedrock_channel_id();
|
||||
// let client = reqwest::Client::new();
|
||||
|
||||
let query_balance = || async {
|
||||
let balance_response = client
|
||||
.get(format!(
|
||||
"http://{bedrock_addr}/wallet/{bedrock_account_pk}/balance"
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to query Bedrock wallet balance")?;
|
||||
// let mut balance = bedrock_wallet_balance(bedrock_addr, bedrock_account_pk).await?;
|
||||
|
||||
let balance_response = check_response_success(balance_response).await?;
|
||||
// info!(
|
||||
// "Queried Bedrock balance for key {bedrock_account_pk}: {:?}",
|
||||
// balance.balance
|
||||
// );
|
||||
|
||||
balance_response
|
||||
.json::<WalletBalanceResponseBody>()
|
||||
.await
|
||||
.context("Failed to decode Bedrock balance response")
|
||||
};
|
||||
// if balance.balance < amount {
|
||||
// anyhow::bail!(
|
||||
// "Bedrock wallet with key {bedrock_account_pk} has insufficient balance {:?} for
|
||||
// deposit amount {:?}", balance.balance,
|
||||
// amount
|
||||
// );
|
||||
// }
|
||||
|
||||
let mut balance = query_balance().await?;
|
||||
// let mut selected_note_id = balance
|
||||
// .notes
|
||||
// .iter()
|
||||
// .find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
|
||||
|
||||
info!(
|
||||
"Queried Bedrock balance for key {bedrock_account_pk}: {:?}",
|
||||
balance.balance
|
||||
);
|
||||
// if selected_note_id.is_none() {
|
||||
// let transfer_body = WalletTransferFundsRequestBody {
|
||||
// tip: None,
|
||||
// change_public_key: balance.address,
|
||||
// funding_public_keys: vec![balance.address],
|
||||
// recipient_public_key: balance.address,
|
||||
// amount,
|
||||
// };
|
||||
|
||||
if balance.balance < amount {
|
||||
anyhow::bail!(
|
||||
"Bedrock wallet with key {bedrock_account_pk} has insufficient balance {:?} for deposit amount {:?}",
|
||||
balance.balance,
|
||||
amount
|
||||
);
|
||||
}
|
||||
// let transfer_response = client
|
||||
// .post(format!(
|
||||
// "http://{bedrock_addr}/wallet/transactions/transfer-funds"
|
||||
// ))
|
||||
// .json(&transfer_body)
|
||||
// .send()
|
||||
// .await
|
||||
// .context("Failed to submit Bedrock transfer-funds request")?;
|
||||
// let transfer_response = check_response_success(transfer_response).await?;
|
||||
|
||||
let mut selected_note_id = balance
|
||||
.notes
|
||||
.iter()
|
||||
.find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
|
||||
// let transfer: WalletTransferFundsResponseBody = transfer_response
|
||||
// .json()
|
||||
// .await
|
||||
// .context("Failed to decode Bedrock transfer-funds response")?;
|
||||
|
||||
if selected_note_id.is_none() {
|
||||
let transfer_body = WalletTransferFundsRequestBody {
|
||||
tip: None,
|
||||
change_public_key: balance.address,
|
||||
funding_public_keys: vec![balance.address],
|
||||
recipient_public_key: balance.address,
|
||||
amount,
|
||||
};
|
||||
// info!(
|
||||
// "Submitted transfer-funds to create exact deposit note, tx hash {:?}",
|
||||
// transfer.hash
|
||||
// );
|
||||
|
||||
let transfer_response = client
|
||||
.post(format!(
|
||||
"http://{bedrock_addr}/wallet/transactions/transfer-funds"
|
||||
))
|
||||
.json(&transfer_body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to submit Bedrock transfer-funds request")?;
|
||||
let transfer_response = check_response_success(transfer_response).await?;
|
||||
// let mut found_note = None;
|
||||
// for _ in 0..20 {
|
||||
// tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
// balance = bedrock_wallet_balance(bedrock_addr, bedrock_account_pk).await?;
|
||||
// found_note = balance
|
||||
// .notes
|
||||
// .iter()
|
||||
// .find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
|
||||
// if found_note.is_some() {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
let transfer: WalletTransferFundsResponseBody = transfer_response
|
||||
.json()
|
||||
.await
|
||||
.context("Failed to decode Bedrock transfer-funds response")?;
|
||||
// selected_note_id = found_note;
|
||||
// }
|
||||
|
||||
info!(
|
||||
"Submitted transfer-funds to create exact deposit note, tx hash {:?}",
|
||||
transfer.hash
|
||||
);
|
||||
// let Some(selected_note_id) = selected_note_id else {
|
||||
// anyhow::bail!(
|
||||
// "Failed to locate exact-value note {amount:?} for Bedrock deposit; available notes:
|
||||
// {:?}", balance.notes,
|
||||
// );
|
||||
// };
|
||||
|
||||
let mut found_note = None;
|
||||
for _ in 0..20 {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
balance = query_balance().await?;
|
||||
found_note = balance
|
||||
.notes
|
||||
.iter()
|
||||
.find_map(|(note_id, value)| (*value == amount).then_some(*note_id));
|
||||
if found_note.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// let body = ChannelDepositRequestBody {
|
||||
// tip: None,
|
||||
// deposit: DepositOp {
|
||||
// channel_id,
|
||||
// inputs: Inputs::new(selected_note_id),
|
||||
// metadata,
|
||||
// },
|
||||
// change_public_key: balance.address,
|
||||
// funding_public_keys: vec![balance.address],
|
||||
// max_tx_fee: u64::MAX.into(),
|
||||
// };
|
||||
|
||||
selected_note_id = found_note;
|
||||
}
|
||||
// let response = client
|
||||
// .post(format!("http://{bedrock_addr}/channel/deposit"))
|
||||
// .json(&body)
|
||||
// .send()
|
||||
// .await
|
||||
// .context("Failed to submit Bedrock deposit request")?;
|
||||
// let response = check_response_success(response).await?;
|
||||
|
||||
let Some(selected_note_id) = selected_note_id else {
|
||||
anyhow::bail!(
|
||||
"Failed to locate exact-value note {amount:?} for Bedrock deposit; available notes: {:?}",
|
||||
balance.notes,
|
||||
);
|
||||
};
|
||||
// let body_text = response
|
||||
// .text()
|
||||
// .await
|
||||
// .unwrap_or_else(|_| "<failed to decode>".to_owned());
|
||||
// info!(
|
||||
// "Successfully submitted Bedrock deposit request for recipient {recipient_id} and amount
|
||||
// {amount}, response body: {body_text}", );
|
||||
|
||||
let body = ChannelDepositRequestBody {
|
||||
tip: None,
|
||||
deposit: DepositOp {
|
||||
channel_id,
|
||||
inputs: Inputs::new(selected_note_id),
|
||||
metadata,
|
||||
},
|
||||
change_public_key: balance.address,
|
||||
funding_public_keys: vec![balance.address],
|
||||
max_tx_fee: 1_000_u64.into(),
|
||||
};
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
let response = client
|
||||
.post(format!("http://{bedrock_addr}/channel/deposit"))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to submit Bedrock deposit request")?;
|
||||
let response = check_response_success(response).await?;
|
||||
// /// The Bedrock wallet state of `bedrock_account_pk`: its total balance and the
|
||||
// /// notes it owns, keyed by note id.
|
||||
// async fn bedrock_wallet_balance(
|
||||
// bedrock_addr: std::net::SocketAddr,
|
||||
// bedrock_account_pk: &str,
|
||||
// ) -> anyhow::Result<WalletBalanceResponseBody> {
|
||||
// let response = reqwest::Client::new()
|
||||
// .get(format!(
|
||||
// "http://{bedrock_addr}/wallet/{bedrock_account_pk}/balance"
|
||||
// ))
|
||||
// .send()
|
||||
// .await
|
||||
// .context("Failed to query Bedrock wallet balance")?;
|
||||
|
||||
let body_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "<failed to decode>".to_owned());
|
||||
info!(
|
||||
"Successfully submitted Bedrock deposit request for recipient {recipient_id} and amount {amount}, response body: {body_text}",
|
||||
);
|
||||
// check_response_success(response)
|
||||
// .await?
|
||||
// .json::<WalletBalanceResponseBody>()
|
||||
// .await
|
||||
// .context("Failed to decode Bedrock balance response")
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// async fn check_response_success(response: reqwest::Response) -> anyhow::Result<reqwest::Response>
|
||||
// { if response.status().is_success() {
|
||||
// Ok(response)
|
||||
// } else {
|
||||
// let status = response.status();
|
||||
// let body_text = response.text().await.unwrap_or_default();
|
||||
// anyhow::bail!("Request failed with status {status} and body {body_text}");
|
||||
// }
|
||||
// }
|
||||
|
||||
async fn check_response_success(response: reqwest::Response) -> anyhow::Result<reqwest::Response> {
|
||||
if response.status().is_success() {
|
||||
Ok(response)
|
||||
} else {
|
||||
let status = response.status();
|
||||
let body_text = response.text().await.unwrap_or_default();
|
||||
anyhow::bail!("Request failed with status {status} and body {body_text}");
|
||||
}
|
||||
}
|
||||
// async fn wait_for_vault_balance(
|
||||
// ctx: &TestContext,
|
||||
// vault_id: AccountId,
|
||||
// expected_balance: u128,
|
||||
// ) -> anyhow::Result<()> {
|
||||
// let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
|
||||
// + Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
|
||||
// tokio::time::timeout(timeout, async {
|
||||
// loop {
|
||||
// let balance = account_balance(ctx, vault_id).await?;
|
||||
// if balance == expected_balance {
|
||||
// return Ok(());
|
||||
// }
|
||||
// tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
// }
|
||||
// })
|
||||
// .await
|
||||
// .with_context(|| {
|
||||
// format!("Timed out waiting for vault {vault_id} balance to reach {expected_balance}")
|
||||
// })?
|
||||
// }
|
||||
|
||||
async fn wait_for_vault_balance(
|
||||
ctx: &TestContext,
|
||||
vault_id: AccountId,
|
||||
expected_balance: u128,
|
||||
) -> anyhow::Result<()> {
|
||||
let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
|
||||
+ 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?;
|
||||
if balance == expected_balance {
|
||||
return Ok(());
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Timed out waiting for vault {vault_id} balance to reach {expected_balance}")
|
||||
})?
|
||||
}
|
||||
// /// Test deposit and withdraw round trip.
|
||||
// ///
|
||||
// /// Implemented as one test instead of two separate tests for deposit and withdraw, because the
|
||||
// /// withdraw test depends on the deposit to set up the necessary state (funds in vault) for
|
||||
// testing /// withdraw functionality.
|
||||
// #[test]
|
||||
// async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Result<()> {
|
||||
// let mut ctx = TestContext::new().await?;
|
||||
|
||||
/// Test deposit and withdraw round trip.
|
||||
///
|
||||
/// Implemented as one test instead of two separate tests for deposit and withdraw, because the
|
||||
/// withdraw test depends on the deposit to set up the necessary state (funds in vault) for testing
|
||||
/// withdraw functionality.
|
||||
#[test]
|
||||
async fn bedrock_deposit_claim_and_withdraw_round_trip_succeeds() -> anyhow::Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
// let bedrock_account_pk = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
|
||||
// let recipient_id = ctx.existing_public_accounts()[0];
|
||||
// let amount = 1_u64;
|
||||
// let vault_program_id = programs::vault().id();
|
||||
// let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id,
|
||||
// recipient_id);
|
||||
|
||||
let bedrock_account_pk = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26";
|
||||
let recipient_id = ctx.existing_public_accounts()[0];
|
||||
let amount = 1_u64;
|
||||
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 = account_balance(&ctx, recipient_vault_id).await?;
|
||||
// let recipient_balance_before = account_balance(&ctx, recipient_id).await?;
|
||||
|
||||
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?;
|
||||
// // Submit deposit to Bedrock
|
||||
// submit_bedrock_deposit(ctx.bedrock_addr(), bedrock_account_pk, recipient_id, amount)
|
||||
// .await
|
||||
// .context("Failed to submit Bedrock deposit for round-trip setup")?;
|
||||
|
||||
// Submit deposit to Bedrock
|
||||
submit_bedrock_deposit(ctx.bedrock_addr(), bedrock_account_pk, recipient_id, amount)
|
||||
.await
|
||||
.context("Failed to submit Bedrock deposit for round-trip setup")?;
|
||||
// // Wait for vault to receive the deposit (minted from bridge to vault)
|
||||
// wait_for_vault_balance(
|
||||
// &ctx,
|
||||
// recipient_vault_id,
|
||||
// vault_balance_before + u128::from(amount),
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
// Wait for vault to receive the deposit (minted from bridge to vault)
|
||||
wait_for_vault_balance(
|
||||
&ctx,
|
||||
recipient_vault_id,
|
||||
vault_balance_before + u128::from(amount),
|
||||
)
|
||||
.await?;
|
||||
// // Now claim funds from vault back to recipient
|
||||
// let nonces = ctx
|
||||
// .wallet()
|
||||
// .get_accounts_nonces(&[recipient_id])
|
||||
// .await
|
||||
// .context("Failed to get nonce for vault claim")?;
|
||||
|
||||
// Now claim funds from vault back to recipient
|
||||
let nonces = ctx
|
||||
.wallet()
|
||||
.get_accounts_nonces(vec![recipient_id])
|
||||
.await
|
||||
.context("Failed to get nonce for vault claim")?;
|
||||
// let signing_key = ctx
|
||||
// .wallet()
|
||||
// .storage()
|
||||
// .key_chain()
|
||||
// .pub_account_signing_key(recipient_id)
|
||||
// .with_context(|| format!("Missing signing key for account {recipient_id}"))?;
|
||||
|
||||
let signing_key = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.key_chain()
|
||||
.pub_account_signing_key(recipient_id)
|
||||
.with_context(|| format!("Missing signing key for account {recipient_id}"))?;
|
||||
// let claim_message = public_transaction::Message::try_new(
|
||||
// vault_program_id,
|
||||
// vec![recipient_id, recipient_vault_id],
|
||||
// nonces,
|
||||
// vault_core::Instruction::Claim {
|
||||
// amount: u128::from(amount),
|
||||
// },
|
||||
// )
|
||||
// .context("Failed to build vault claim message")?;
|
||||
|
||||
let claim_message = public_transaction::Message::try_new(
|
||||
vault_program_id,
|
||||
vec![recipient_id, recipient_vault_id],
|
||||
nonces,
|
||||
vault_core::Instruction::Claim {
|
||||
amount: u128::from(amount),
|
||||
},
|
||||
)
|
||||
.context("Failed to build vault claim message")?;
|
||||
// let claim_witness_set =
|
||||
// public_transaction::WitnessSet::for_message(&claim_message, &[signing_key]);
|
||||
// let claim_tx = LeeTransaction::Public(lee::PublicTransaction::new(
|
||||
// claim_message,
|
||||
// claim_witness_set,
|
||||
// ));
|
||||
|
||||
let claim_witness_set =
|
||||
public_transaction::WitnessSet::for_message(&claim_message, &[signing_key]);
|
||||
let claim_tx = LeeTransaction::Public(lee::PublicTransaction::new(
|
||||
claim_message,
|
||||
claim_witness_set,
|
||||
));
|
||||
// let claim_hash = ctx.sequencer_client().send_transaction(claim_tx).await?;
|
||||
|
||||
let claim_hash = ctx.sequencer_client().send_transaction(claim_tx).await?;
|
||||
// tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
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 = account_balance(&ctx, recipient_vault_id).await?;
|
||||
// let recipient_balance_after_claim = account_balance(&ctx, recipient_id).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?;
|
||||
// assert!(
|
||||
// claim_on_chain.is_some(),
|
||||
// "Vault claim transaction must be included on-chain"
|
||||
// );
|
||||
// assert_eq!(
|
||||
// vault_balance_after_claim, vault_balance_before,
|
||||
// "Vault balance should return to initial state after claim"
|
||||
// );
|
||||
// assert_eq!(
|
||||
// recipient_balance_after_claim,
|
||||
// recipient_balance_before + u128::from(amount),
|
||||
// "Recipient balance should increase by claimed amount"
|
||||
// );
|
||||
|
||||
assert!(
|
||||
claim_on_chain.is_some(),
|
||||
"Vault claim transaction must be included on-chain"
|
||||
);
|
||||
assert_eq!(
|
||||
vault_balance_after_claim, vault_balance_before,
|
||||
"Vault balance should return to initial state after claim"
|
||||
);
|
||||
assert_eq!(
|
||||
recipient_balance_after_claim,
|
||||
recipient_balance_before + u128::from(amount),
|
||||
"Recipient balance should increase by claimed amount"
|
||||
);
|
||||
// // The indexer must replay the deposit and claim blocks and reach the same
|
||||
// // 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 = 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
|
||||
// // of the async `get_account` method on `RpcClient`
|
||||
// ctx.indexer_client().deref(),
|
||||
// account_id.into(),
|
||||
// )
|
||||
// .await?;
|
||||
// let sequencer_account = get_account(&ctx, account_id).await?;
|
||||
// assert_eq!(
|
||||
// indexer_account,
|
||||
// sequencer_account.into(),
|
||||
// "Indexer and sequencer diverged for account {account_id} after deposit"
|
||||
// );
|
||||
// }
|
||||
|
||||
// The indexer must replay the deposit and claim blocks and reach the same
|
||||
// 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 = 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
|
||||
// of the async `get_account` method on `RpcClient`
|
||||
ctx.indexer_client().deref(),
|
||||
account_id.into(),
|
||||
)
|
||||
.await?;
|
||||
let sequencer_account = ctx.sequencer_client().get_account(account_id).await?;
|
||||
assert_eq!(
|
||||
indexer_account,
|
||||
sequencer_account.into(),
|
||||
"Indexer and sequencer diverged for account {account_id} after deposit"
|
||||
);
|
||||
}
|
||||
// // Withdraw back to Bedrock and wait for finalized withdraw event.
|
||||
// let sender_id = recipient_id;
|
||||
|
||||
// Withdraw back to Bedrock and wait for finalized withdraw event.
|
||||
let sender_id = recipient_id;
|
||||
// let observer = create_zone_indexer_observer(ctx.bedrock_addr())?;
|
||||
// let observe_fut =
|
||||
// wait_for_finalized_withdraw_op(&observer, ctx.bedrock_addr(), amount,
|
||||
// bedrock_account_pk);
|
||||
|
||||
let observer = create_zone_indexer_observer(ctx.bedrock_addr())?;
|
||||
let observe_fut = wait_for_finalized_withdraw_op(&observer, amount, bedrock_account_pk);
|
||||
// let withdraw_fut = execute_subcommand(
|
||||
// ctx.wallet_mut(),
|
||||
// Command::Bridge(BridgeSubcommand::Withdraw {
|
||||
// from: public_mention(sender_id),
|
||||
// amount,
|
||||
// bedrock_account_pk: bedrock_account_pk.to_owned(),
|
||||
// }),
|
||||
// );
|
||||
|
||||
let withdraw_fut = execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Bridge(BridgeSubcommand::Withdraw {
|
||||
from: public_mention(sender_id),
|
||||
amount,
|
||||
bedrock_account_pk: bedrock_account_pk.to_owned(),
|
||||
}),
|
||||
);
|
||||
// let (observe_result, withdraw_result) = tokio::join!(observe_fut, withdraw_fut);
|
||||
|
||||
let (observe_result, withdraw_result) = tokio::join!(observe_fut, withdraw_fut);
|
||||
// withdraw_result.context("Failed to execute wallet bridge withdraw command")?;
|
||||
|
||||
withdraw_result.context("Failed to execute wallet bridge withdraw command")?;
|
||||
// observe_result
|
||||
// .context("Failed while waiting for finalized withdraw event from zone indexer")?;
|
||||
|
||||
observe_result
|
||||
.context("Failed while waiting for finalized withdraw event from zone indexer")?;
|
||||
// // Sleep to observe sequencer log about validated withdraw event
|
||||
// tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// Sleep to observe sequencer log about validated withdraw event
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// fn create_zone_indexer_observer(
|
||||
// bedrock_addr: std::net::SocketAddr,
|
||||
// ) -> anyhow::Result<ZoneIndexer<NodeHttpClient>> {
|
||||
// let bedrock_url = integration_tests::config::addr_to_url(
|
||||
// integration_tests::config::UrlProtocol::Http,
|
||||
// bedrock_addr,
|
||||
// )
|
||||
// .context("Failed to convert Bedrock addr to URL for zone indexer observer")?;
|
||||
|
||||
fn create_zone_indexer_observer(
|
||||
bedrock_addr: std::net::SocketAddr,
|
||||
) -> anyhow::Result<ZoneIndexer<NodeHttpClient>> {
|
||||
let bedrock_url = integration_tests::config::addr_to_url(
|
||||
integration_tests::config::UrlProtocol::Http,
|
||||
bedrock_addr,
|
||||
)
|
||||
.context("Failed to convert Bedrock addr to URL for zone indexer observer")?;
|
||||
// let node = NodeHttpClient::new(CommonHttpClient::new(None), bedrock_url);
|
||||
|
||||
let node = NodeHttpClient::new(CommonHttpClient::new(None), bedrock_url);
|
||||
// Ok(ZoneIndexer::new(
|
||||
// integration_tests::config::bedrock_channel_id(),
|
||||
// node,
|
||||
// ))
|
||||
// }
|
||||
|
||||
Ok(ZoneIndexer::new(
|
||||
integration_tests::config::bedrock_channel_id(),
|
||||
node,
|
||||
))
|
||||
}
|
||||
// /// Waits for a finalized withdraw that pays `expected_amount` to `receiver_pk`.
|
||||
// ///
|
||||
// /// A withdraw op releases channel-owned notes and carries nothing but their
|
||||
// /// ids — the value and recipient live in the note itself. A released note keeps
|
||||
// /// its id, value and public key, so the pairing is checked on the receiver's
|
||||
// /// Bedrock wallet: one of the released notes must land there with the expected
|
||||
// /// value.
|
||||
// async fn wait_for_finalized_withdraw_op(
|
||||
// observer: &ZoneIndexer<NodeHttpClient>,
|
||||
// bedrock_addr: std::net::SocketAddr,
|
||||
// expected_amount: u64,
|
||||
// receiver_pk: &str,
|
||||
// ) -> anyhow::Result<()> {
|
||||
// let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
|
||||
// + Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
|
||||
|
||||
async fn wait_for_finalized_withdraw_op(
|
||||
observer: &ZoneIndexer<NodeHttpClient>,
|
||||
expected_amount: u64,
|
||||
receiver_pk: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let timeout = TIME_TO_FINALIZE_DEPOSIT_EVENT_ON_BEDROCK
|
||||
+ Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS);
|
||||
// tokio::time::timeout(timeout, async {
|
||||
// // The wallet can trail the channel event, so released notes accumulate
|
||||
// // across polls instead of being checked once when first observed.
|
||||
// let mut released_notes = HashSet::new();
|
||||
|
||||
let bedrock_account_pk_bytes = hex::decode(receiver_pk)
|
||||
.context("Failed to decode expected receiver public key from hex")?;
|
||||
let expected_receiver_pk =
|
||||
logos_blockchain_key_management_system_service::keys::ZkPublicKey::from(
|
||||
BigUint::from_bytes_le(&bedrock_account_pk_bytes),
|
||||
);
|
||||
// loop {
|
||||
// let stream = observer
|
||||
// .follow()
|
||||
// .await
|
||||
// .context("Failed to read zone indexer message batch")?;
|
||||
// let mut stream = std::pin::pin!(stream);
|
||||
|
||||
tokio::time::timeout(timeout, async {
|
||||
loop {
|
||||
let stream = observer
|
||||
.follow()
|
||||
.await
|
||||
.context("Failed to read zone indexer message batch")?;
|
||||
let mut stream = std::pin::pin!(stream);
|
||||
// while let Some(message) = stream.next().await {
|
||||
// info!("Observed zone message {message:?}");
|
||||
|
||||
while let Some(message) = stream.next().await {
|
||||
info!("Observed zone message {message:?}");
|
||||
// if let ZoneMessage::Withdraw(withdraw) = message {
|
||||
// released_notes.extend(withdraw.inputs.iter().copied());
|
||||
// }
|
||||
// }
|
||||
|
||||
let ZoneMessage::Withdraw(withdraw) = message else {
|
||||
continue;
|
||||
};
|
||||
// if !released_notes.is_empty() {
|
||||
// let balance = bedrock_wallet_balance(bedrock_addr, receiver_pk).await?;
|
||||
// if released_notes
|
||||
// .iter()
|
||||
// .any(|note_id| balance.notes.get(note_id) == Some(&expected_amount))
|
||||
// {
|
||||
// return Ok(());
|
||||
// }
|
||||
// }
|
||||
|
||||
let mut iter = withdraw.outputs.iter();
|
||||
let Some(note) = iter.next() else {
|
||||
continue;
|
||||
};
|
||||
if iter.next().is_some() {
|
||||
// Withdraw op should only have one output
|
||||
continue;
|
||||
}
|
||||
|
||||
if note.value == expected_amount && note.pk == expected_receiver_pk {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Timed out waiting for finalized withdraw message with amount {expected_amount}")
|
||||
})?
|
||||
}
|
||||
// tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
// }
|
||||
// })
|
||||
// .await
|
||||
// .with_context(|| {
|
||||
// format!("Timed out waiting for finalized withdraw message with amount {expected_amount}")
|
||||
// })?
|
||||
// }
|
||||
|
||||
191
integration_tests/tests/cross_zone_bridge.rs
Normal file
191
integration_tests/tests/cross_zone_bridge.rs
Normal file
@ -0,0 +1,191 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! Demo 2: a wrapped-token bridge over the cross-zone spine. A holder locks part
|
||||
//! of their bridgeable balance on zone A; the watcher carries the emitted mint to
|
||||
//! zone B, where the indexer re-derives and verifies it (Option B) before the
|
||||
//! wrapped token is minted to the recipient. Reuses the M3/M4 spine unchanged;
|
||||
//! only the source caller (`bridge_lock`) and target (`wrapped_token`) are new.
|
||||
//!
|
||||
//! Not production-safe. The inbox allowlist gates the target program, not the
|
||||
//! source emitter, and `extract_emission` recognizes any known emitter, so in a
|
||||
//! zone that allows `wrapped_token` as a target a permissionless `ping_sender`
|
||||
//! send can carry a `wrapped_token::Mint` and mint with no lock. Making this safe
|
||||
//! needs source verification, where a value-bearing target checks the message
|
||||
//! originated from `bridge_lock`; that is out of scope for the demo.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use cross_zone_outbox_core::outbox_pda;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
indexer_client::IndexerClient,
|
||||
setup::{SequencerSetup, indexer_client, sequencer_client, setup_bedrock_node, setup_indexer},
|
||||
};
|
||||
use lee::{
|
||||
AccountId, PrivateKey, PublicKey, PublicTransaction,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer, GenesisAction};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
|
||||
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(600);
|
||||
const INITIAL_BALANCE: u128 = 100;
|
||||
const LOCK_AMOUNT: u128 = 30;
|
||||
const RECIPIENT: [u8; 32] = [9; 32];
|
||||
|
||||
#[test]
|
||||
async fn lock_on_zone_a_mints_wrapped_token_on_zone_b() -> Result<()> {
|
||||
// Declared first so it outlives both zones (drops run in reverse order).
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up shared Bedrock node")?;
|
||||
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel_a = config::bedrock_channel_id();
|
||||
let channel_b = config::bedrock_channel_id_b();
|
||||
let zone_b: [u8; 32] = *channel_b.as_ref();
|
||||
|
||||
let holder_key = PrivateKey::try_new([7; 32]).expect("valid key");
|
||||
let holder_id = AccountId::from(&PublicKey::new_from_private_key(&holder_key));
|
||||
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: *channel_a.as_ref(),
|
||||
allowed_targets: vec![wrapped_token_id],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
// Zone A seeds the holder's bridgeable balance. Zone B runs the watcher on its
|
||||
// sequencer and the verifier on its indexer.
|
||||
let genesis_a = vec![GenesisAction::SupplyBridgeLockHolding {
|
||||
holder: holder_id,
|
||||
amount: INITIAL_BALANCE,
|
||||
}];
|
||||
let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_a)
|
||||
.with_genesis(genesis_a)
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (_seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_b)
|
||||
.with_genesis(vec![])
|
||||
.with_cross_zone(cross_zone.clone())
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, Some(cross_zone))
|
||||
.await
|
||||
.context("Failed to set up zone B indexer")?;
|
||||
|
||||
// Lock LOCK_AMOUNT on zone A, addressed to the recipient on zone B.
|
||||
let lock = build_lock_tx(&holder_key, holder_id, zone_b);
|
||||
sequencer_client(seq_a.addr())?
|
||||
.send_transaction(lock)
|
||||
.await
|
||||
.context("Failed to submit lock on zone A")?;
|
||||
|
||||
// Wait until zone B's indexer reflects the verified mint.
|
||||
let holding_id = wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT);
|
||||
let indexer = indexer_client(idx_b.addr())
|
||||
.await
|
||||
.context("Failed to build indexer client")?;
|
||||
|
||||
let minted = wait_for_mint(&indexer, holding_id).await?;
|
||||
assert_eq!(
|
||||
minted, LOCK_AMOUNT,
|
||||
"zone B must mint exactly the locked amount"
|
||||
);
|
||||
|
||||
// Conservation: the mint on B must be backed by an equal lock on A. The lock
|
||||
// has already landed (it preceded delivery), so zone A reflects the debit and
|
||||
// escrow now.
|
||||
let seq_a_client = sequencer_client(seq_a.addr())?;
|
||||
let escrow_id = bridge_lock_core::escrow_account_id(programs::bridge_lock().id());
|
||||
let escrowed = seq_a_client.get_account(escrow_id).await?.balance;
|
||||
assert_eq!(
|
||||
escrowed, LOCK_AMOUNT,
|
||||
"zone A escrow must hold the locked amount"
|
||||
);
|
||||
let remaining = seq_a_client.get_account(holder_id).await?.balance;
|
||||
assert_eq!(
|
||||
remaining,
|
||||
INITIAL_BALANCE - LOCK_AMOUNT,
|
||||
"zone A holder must be debited by the locked amount"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a signed `bridge_lock` Lock that forwards a wrapped-token Mint of the
|
||||
/// locked amount to the recipient on the target zone.
|
||||
fn build_lock_tx(
|
||||
holder_key: &PrivateKey,
|
||||
holder_id: AccountId,
|
||||
target_zone: [u8; 32],
|
||||
) -> LeeTransaction {
|
||||
let bridge_lock_id = programs::bridge_lock().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
let outbox_id = programs::cross_zone_outbox().id();
|
||||
let ordinal = 0;
|
||||
|
||||
let mint = wrapped_token_core::Instruction::Mint {
|
||||
recipient: RECIPIENT,
|
||||
amount: LOCK_AMOUNT,
|
||||
};
|
||||
let words = risc0_zkvm::serde::to_vec(&mint).expect("serialize mint");
|
||||
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
|
||||
|
||||
let target_accounts = vec![
|
||||
wrapped_token_core::config_account_id(wrapped_token_id).into_value(),
|
||||
wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT).into_value(),
|
||||
];
|
||||
let lock = bridge_lock_core::Instruction::Lock {
|
||||
amount: LOCK_AMOUNT,
|
||||
target_zone,
|
||||
target_program_id: wrapped_token_id,
|
||||
target_accounts,
|
||||
payload,
|
||||
outbox_program_id: outbox_id,
|
||||
ordinal,
|
||||
};
|
||||
|
||||
let accounts = vec![
|
||||
holder_id,
|
||||
bridge_lock_core::escrow_account_id(bridge_lock_id),
|
||||
outbox_pda(outbox_id, &target_zone, ordinal),
|
||||
];
|
||||
// One nonce per signature: the holder signs, at its genesis nonce 0.
|
||||
let message = Message::try_new(bridge_lock_id, accounts, vec![0_u128.into()], lock)
|
||||
.expect("build lock message");
|
||||
let witness = WitnessSet::for_message(&message, &[holder_key]);
|
||||
LeeTransaction::Public(PublicTransaction::new(message, witness))
|
||||
}
|
||||
|
||||
/// Polls zone B's indexer until the recipient's wrapped holding is non-zero.
|
||||
async fn wait_for_mint(indexer: &IndexerClient, holding_id: AccountId) -> Result<u128> {
|
||||
let account_id = indexer_service_protocol::AccountId {
|
||||
value: holding_id.into_value(),
|
||||
};
|
||||
let wait = async {
|
||||
loop {
|
||||
let account =
|
||||
indexer_service_rpc::RpcClient::get_account(&**indexer, account_id).await?;
|
||||
let balance = wrapped_token_core::read_balance(&account.data.0);
|
||||
if balance != 0 {
|
||||
return Ok::<u128, anyhow::Error>(balance);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(DELIVERY_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Zone B's indexer did not mint the wrapped token in time")?
|
||||
}
|
||||
73
integration_tests/tests/cross_zone_ingress_guard.rs
Normal file
73
integration_tests/tests/cross_zone_ingress_guard.rs
Normal file
@ -0,0 +1,73 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
//! M6 ingress guard: the cross-zone inbox is sequencer-only. Only the watcher
|
||||
//! injects inbox dispatches; a user must not be able to invoke the inbox through
|
||||
//! the public RPC, or anyone could forge an inbound cross-zone delivery. The
|
||||
//! inbox guest's caller-is-none assertion passes for a top-level user tx, so the
|
||||
//! sequencer ingress guard is the only thing that stops this.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use cross_zone_inbox_core::{
|
||||
CrossZoneMessage, Instruction, inbox_config_account_id, inbox_seen_shard_account_id,
|
||||
};
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
setup::{SequencerSetup, sequencer_client, setup_bedrock_node},
|
||||
};
|
||||
use lee::{
|
||||
PublicTransaction,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
|
||||
#[test]
|
||||
async fn user_origin_inbox_call_rejected() -> Result<()> {
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up Bedrock node")?;
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel = config::bedrock_channel_id();
|
||||
let (seq, _seq_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel)
|
||||
.with_genesis(vec![])
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up sequencer")?;
|
||||
|
||||
// A user hand-builds a top-level inbox Dispatch and submits it via RPC.
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone: [2; 32],
|
||||
src_block_id: 1,
|
||||
src_tx_index: 0,
|
||||
src_program_id: [9; 8],
|
||||
target_program_id: programs::ping_receiver().id(),
|
||||
payload: vec![],
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &msg.src_zone, msg.src_block_id);
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![inbox_config_account_id(inbox_id), seen_id],
|
||||
vec![],
|
||||
Instruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = LeeTransaction::Public(PublicTransaction::new(
|
||||
message,
|
||||
WitnessSet::from_raw_parts(vec![]),
|
||||
));
|
||||
|
||||
let result = sequencer_client(seq.addr())?.send_transaction(tx).await;
|
||||
let err = result.expect_err("the sequencer must reject a user-origin inbox call");
|
||||
assert!(
|
||||
err.to_string().contains("sequencer-only"),
|
||||
"rejection should cite the sequencer-only guard, got: {err}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
141
integration_tests/tests/cross_zone_ping.rs
Normal file
141
integration_tests/tests/cross_zone_ping.rs
Normal file
@ -0,0 +1,141 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! End-to-end cross-zone round trip: a ping submitted on zone A is delivered by
|
||||
//! zone B's watcher to `ping_receiver` on zone B, which records the payload.
|
||||
//!
|
||||
//! Two sequencers share one Bedrock node (no indexers): zone A publishes the
|
||||
//! ping to Bedrock, zone B's watcher reads zone A's finalized blocks, injects the
|
||||
//! inbox dispatch, and zone B's sequencer delivers it. This is the M3 milestone,
|
||||
//! sequencer-trusted, with no indexer re-derivation (that is M4).
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use cross_zone_outbox_core::outbox_pda;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
setup::{SequencerSetup, sequencer_client, setup_bedrock_node},
|
||||
};
|
||||
use lee::{AccountId, PublicTransaction, public_transaction::Message};
|
||||
use lee_core::program::ProgramId;
|
||||
use ping_core::{ReceiverInstruction, SenderInstruction, ping_record_pda};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
|
||||
use tokio::test;
|
||||
|
||||
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(480);
|
||||
const PING_PAYLOAD: &[u8] = b"hello-cross-zone";
|
||||
|
||||
#[test]
|
||||
async fn ping_crosses_from_zone_a_to_zone_b() -> Result<()> {
|
||||
// Declared first so it outlives both zones (drops run in reverse order).
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up shared Bedrock node")?;
|
||||
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel_a = config::bedrock_channel_id();
|
||||
let channel_b = config::bedrock_channel_id_b();
|
||||
let zone_a: [u8; 32] = *channel_a.as_ref();
|
||||
let zone_b: [u8; 32] = *channel_b.as_ref();
|
||||
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
|
||||
// Zone B watches zone A and allows delivery only to ping_receiver.
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: zone_a,
|
||||
allowed_targets: vec![receiver_id],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_a)
|
||||
.with_genesis(vec![])
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_b)
|
||||
.with_genesis(vec![])
|
||||
.with_cross_zone(cross_zone)
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
|
||||
// Submit the ping on zone A, addressed to ping_receiver on zone B.
|
||||
let ping = build_ping_tx(zone_b, receiver_id);
|
||||
sequencer_client(seq_a.addr())?
|
||||
.send_transaction(ping)
|
||||
.await
|
||||
.context("Failed to submit ping on zone A")?;
|
||||
|
||||
// Wait until zone B's sequencer records the delivered payload.
|
||||
let record_id = ping_record_pda(receiver_id);
|
||||
let delivered = wait_for_delivery(sequencer_client(seq_b.addr())?, record_id).await?;
|
||||
|
||||
assert_eq!(
|
||||
delivered, PING_PAYLOAD,
|
||||
"Zone B must record the payload delivered from zone A"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a top-level `ping_sender` transaction that chains into the outbox to emit
|
||||
/// a message carrying a `ping_receiver::Record` instruction for the target zone.
|
||||
fn build_ping_tx(target_zone: [u8; 32], receiver_id: ProgramId) -> LeeTransaction {
|
||||
let outbox_id = programs::cross_zone_outbox().id();
|
||||
let ordinal = 0;
|
||||
|
||||
// The payload is the ping_receiver instruction, serialized as risc0 words in
|
||||
// little-endian bytes (the contract the inbox reverses when forwarding).
|
||||
let words = risc0_zkvm::serde::to_vec(&ReceiverInstruction::Record {
|
||||
payload: PING_PAYLOAD.to_vec(),
|
||||
})
|
||||
.expect("serialize ping instruction");
|
||||
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
|
||||
|
||||
let send = SenderInstruction::Send {
|
||||
outbox_program_id: outbox_id,
|
||||
target_zone,
|
||||
target_program_id: receiver_id,
|
||||
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
|
||||
payload,
|
||||
ordinal,
|
||||
};
|
||||
|
||||
let outbox_account = outbox_pda(outbox_id, &target_zone, ordinal);
|
||||
let message = Message::try_new(
|
||||
programs::ping_sender().id(),
|
||||
vec![outbox_account],
|
||||
vec![],
|
||||
send,
|
||||
)
|
||||
.expect("build ping message");
|
||||
LeeTransaction::Public(PublicTransaction::new(
|
||||
message,
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
))
|
||||
}
|
||||
|
||||
/// Polls zone B's sequencer until the ping record PDA holds a payload.
|
||||
async fn wait_for_delivery(client: SequencerClient, record_id: AccountId) -> Result<Vec<u8>> {
|
||||
let wait = async {
|
||||
loop {
|
||||
let account = client.get_account(record_id).await?;
|
||||
let data = account.data.into_inner();
|
||||
if !data.is_empty() {
|
||||
return Ok::<Vec<u8>, anyhow::Error>(data);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(DELIVERY_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Zone B did not record the cross-zone payload in time")?
|
||||
}
|
||||
367
integration_tests/tests/cross_zone_state_machine.rs
Normal file
367
integration_tests/tests/cross_zone_state_machine.rs
Normal file
@ -0,0 +1,367 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! Single-zone state-machine tests for cross-zone delivery (ping demo) and the
|
||||
//! wrapped-token bridge (Demo 2). They drive the guests in isolation, no watcher
|
||||
//! or Bedrock: a hand-built `cross_zone_inbox::Dispatch` (as the watcher would
|
||||
//! inject) and the source `bridge_lock::Lock` (which escrows and chains
|
||||
//! `outbox::Emit`). Fast, so they pin guest logic before the e2e exercises the
|
||||
//! plumbing. Run with `RISC0_DEV_MODE=1`.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use cross_zone_inbox_core::{
|
||||
CrossZoneMessage, InboxConfig, Instruction as InboxInstruction, SeenShard,
|
||||
inbox_config_account_id, inbox_seen_shard_account_id, message_key,
|
||||
};
|
||||
use cross_zone_outbox_core::{OutboxRecord, outbox_pda};
|
||||
use lee::{
|
||||
AccountId, PrivateKey, PublicKey, PublicTransaction, V03State, ValidatedStateDiff,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
};
|
||||
use lee_core::account::Account;
|
||||
use ping_core::{ReceiverInstruction, ping_record_pda};
|
||||
|
||||
const INITIAL_BALANCE: u128 = 100;
|
||||
const LOCK_AMOUNT: u128 = 30;
|
||||
const RECIPIENT: [u8; 32] = [9; 32];
|
||||
|
||||
/// State registering the cross-zone builtins these tests exercise.
|
||||
fn base_state() -> V03State {
|
||||
V03State::new().with_programs([
|
||||
programs::cross_zone_inbox(),
|
||||
programs::cross_zone_outbox(),
|
||||
programs::ping_receiver(),
|
||||
programs::bridge_lock(),
|
||||
programs::wrapped_token(),
|
||||
])
|
||||
}
|
||||
|
||||
/// Seeds an inbox config (inbox-owned) allowing `src_zone -> target`.
|
||||
fn seed_inbox_config(
|
||||
state: &mut V03State,
|
||||
self_zone: [u8; 32],
|
||||
src_zone: [u8; 32],
|
||||
target: lee_core::program::ProgramId,
|
||||
) {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let mut allowed_targets = BTreeMap::new();
|
||||
allowed_targets.insert(src_zone, vec![target]);
|
||||
let config = InboxConfig {
|
||||
self_zone,
|
||||
allowed_peers: BTreeMap::new(),
|
||||
allowed_targets,
|
||||
};
|
||||
*state = std::mem::replace(state, V03State::new()).with_public_accounts([(
|
||||
inbox_config_account_id(inbox_id),
|
||||
Account {
|
||||
program_owner: inbox_id,
|
||||
balance: 0,
|
||||
data: config
|
||||
.to_bytes()
|
||||
.try_into()
|
||||
.expect("config fits in account data"),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
)]);
|
||||
}
|
||||
|
||||
/// Seeds the wrapped-token config account pinning the inbox as authorized minter,
|
||||
/// matching what genesis seeds for a real zone.
|
||||
fn seed_wrapped_config(state: &mut V03State) {
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
*state = std::mem::replace(state, V03State::new()).with_public_accounts([(
|
||||
wrapped_token_core::config_account_id(wrapped_token_id),
|
||||
Account {
|
||||
program_owner: wrapped_token_id,
|
||||
data: wrapped_token_core::minter_bytes(programs::cross_zone_inbox().id())
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("minter id fits in account data"),
|
||||
..Default::default()
|
||||
},
|
||||
)]);
|
||||
}
|
||||
|
||||
/// The wrapped-token `Mint` the bridge forwards, serialized as the cross-zone
|
||||
/// payload (risc0 words, little-endian bytes).
|
||||
fn mint_payload() -> Vec<u8> {
|
||||
let mint = wrapped_token_core::Instruction::Mint {
|
||||
recipient: RECIPIENT,
|
||||
amount: LOCK_AMOUNT,
|
||||
};
|
||||
let words = risc0_zkvm::serde::to_vec(&mint).expect("serialize mint");
|
||||
words.iter().flat_map(|word| word.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
/// Drives `cross_zone_inbox::Dispatch` directly through the state machine
|
||||
/// (no watcher) and asserts the message is delivered to `ping_receiver`, which
|
||||
/// records the payload into its own PDA.
|
||||
#[test]
|
||||
fn inbox_dispatch_delivers_payload_to_ping_receiver() {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
let src_block_id = 5;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(&mut state, self_zone, src_zone, receiver_id);
|
||||
|
||||
// The payload is the ping_receiver instruction, serialized as risc0 words in
|
||||
// little-endian bytes (the contract the inbox reverses when forwarding).
|
||||
let inner = b"hello-cross-zone".to_vec();
|
||||
let words = risc0_zkvm::serde::to_vec(&ReceiverInstruction::Record {
|
||||
payload: inner.clone(),
|
||||
})
|
||||
.expect("serialize ping instruction");
|
||||
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_tx_index: 0,
|
||||
src_program_id: [9_u32; 8],
|
||||
target_program_id: receiver_id,
|
||||
payload,
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id);
|
||||
let record_id = ping_record_pda(receiver_id);
|
||||
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![inbox_config_account_id(inbox_id), seen_id, record_id],
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("dispatch must validate and execute");
|
||||
let record = diff
|
||||
.public_diff()
|
||||
.get(&record_id)
|
||||
.expect("ping record account must change")
|
||||
.clone();
|
||||
assert_eq!(
|
||||
record.data.into_inner(),
|
||||
inner,
|
||||
"ping_receiver must record the delivered payload"
|
||||
);
|
||||
}
|
||||
|
||||
/// Drives `bridge_lock::Lock` and asserts it debits the holder, credits the
|
||||
/// escrow, and records the forwarded mint in the outbox PDA.
|
||||
#[test]
|
||||
fn lock_escrows_balance_and_emits_to_outbox() {
|
||||
let bridge_lock_id = programs::bridge_lock().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
let outbox_id = programs::cross_zone_outbox().id();
|
||||
let zone_b = [2_u8; 32];
|
||||
let ordinal = 0;
|
||||
|
||||
let mut state = base_state();
|
||||
|
||||
let holder_key = PrivateKey::try_new([7; 32]).expect("valid key");
|
||||
let holder_id = AccountId::from(&PublicKey::new_from_private_key(&holder_key));
|
||||
state = state.with_public_accounts([(
|
||||
holder_id,
|
||||
Account {
|
||||
program_owner: bridge_lock_id,
|
||||
balance: INITIAL_BALANCE,
|
||||
..Default::default()
|
||||
},
|
||||
)]);
|
||||
|
||||
let payload = mint_payload();
|
||||
let target_accounts = vec![
|
||||
wrapped_token_core::config_account_id(wrapped_token_id).into_value(),
|
||||
wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT).into_value(),
|
||||
];
|
||||
let lock = bridge_lock_core::Instruction::Lock {
|
||||
amount: LOCK_AMOUNT,
|
||||
target_zone: zone_b,
|
||||
target_program_id: wrapped_token_id,
|
||||
target_accounts,
|
||||
payload: payload.clone(),
|
||||
outbox_program_id: outbox_id,
|
||||
ordinal,
|
||||
};
|
||||
|
||||
let escrow_id = bridge_lock_core::escrow_account_id(bridge_lock_id);
|
||||
let outbox_record_id = outbox_pda(outbox_id, &zone_b, ordinal);
|
||||
let message = Message::try_new(
|
||||
bridge_lock_id,
|
||||
vec![holder_id, escrow_id, outbox_record_id],
|
||||
vec![0_u128.into()],
|
||||
lock,
|
||||
)
|
||||
.expect("build lock message");
|
||||
let witness = WitnessSet::for_message(&message, &[&holder_key]);
|
||||
let tx = PublicTransaction::new(message, witness);
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("lock must validate and execute");
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
let holder_after = public_diff[&holder_id].balance;
|
||||
assert_eq!(
|
||||
holder_after,
|
||||
INITIAL_BALANCE - LOCK_AMOUNT,
|
||||
"holder debited"
|
||||
);
|
||||
|
||||
let escrow_after = public_diff[&escrow_id].balance;
|
||||
assert_eq!(escrow_after, LOCK_AMOUNT, "escrow credited");
|
||||
|
||||
let record =
|
||||
OutboxRecord::from_bytes(&public_diff[&outbox_record_id].data.clone().into_inner())
|
||||
.expect("outbox PDA holds an OutboxRecord");
|
||||
assert_eq!(record.target_zone, zone_b);
|
||||
assert_eq!(record.target_program_id, wrapped_token_id);
|
||||
assert_eq!(
|
||||
record.payload, payload,
|
||||
"emitted payload is the wrapped mint"
|
||||
);
|
||||
}
|
||||
|
||||
/// Drives a hand-built `cross_zone_inbox::Dispatch` (as the watcher would inject)
|
||||
/// and asserts it chains into `wrapped_token::Mint`, crediting the recipient.
|
||||
#[test]
|
||||
fn inbox_dispatch_mints_wrapped_token() {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
let src_block_id = 5;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(&mut state, self_zone, src_zone, wrapped_token_id);
|
||||
seed_wrapped_config(&mut state);
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_tx_index: 0,
|
||||
src_program_id: [9_u32; 8],
|
||||
target_program_id: wrapped_token_id,
|
||||
payload: mint_payload(),
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id);
|
||||
let wrapped_config_id = wrapped_token_core::config_account_id(wrapped_token_id);
|
||||
let holding_id = wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT);
|
||||
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![
|
||||
inbox_config_account_id(inbox_id),
|
||||
seen_id,
|
||||
wrapped_config_id,
|
||||
holding_id,
|
||||
],
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("dispatch must validate and execute");
|
||||
let minted = wrapped_token_core::read_balance(
|
||||
&diff.public_diff()[&holding_id].data.clone().into_inner(),
|
||||
);
|
||||
assert_eq!(
|
||||
minted, LOCK_AMOUNT,
|
||||
"recipient holding minted the locked amount"
|
||||
);
|
||||
}
|
||||
|
||||
/// A dispatch whose message key is already in the seen-shard is an idempotent
|
||||
/// no-op: the inbox makes no chained call, so the wrapped token is not minted a
|
||||
/// second time. This is the bridge's replay defense.
|
||||
#[test]
|
||||
fn mint_replay_rejected() {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let wrapped_token_id = programs::wrapped_token().id();
|
||||
|
||||
let self_zone = [1_u8; 32];
|
||||
let src_zone = [2_u8; 32];
|
||||
let src_block_id = 5;
|
||||
let src_tx_index = 0;
|
||||
|
||||
let mut state = base_state();
|
||||
seed_inbox_config(&mut state, self_zone, src_zone, wrapped_token_id);
|
||||
seed_wrapped_config(&mut state);
|
||||
|
||||
// Seed the seen-shard as already containing this message's key, so the inbox
|
||||
// takes the replay no-op branch. The shard is inbox-owned (claimed on a prior
|
||||
// delivery), so the guest leaves it untouched.
|
||||
let seen_id = inbox_seen_shard_account_id(inbox_id, &src_zone, src_block_id);
|
||||
let mut shard = SeenShard::default();
|
||||
shard.insert(message_key(&src_zone, src_block_id, src_tx_index));
|
||||
state = state.with_public_accounts([(
|
||||
seen_id,
|
||||
Account {
|
||||
program_owner: inbox_id,
|
||||
balance: 0,
|
||||
data: shard
|
||||
.to_bytes()
|
||||
.try_into()
|
||||
.expect("shard fits in account data"),
|
||||
nonce: 0_u128.into(),
|
||||
},
|
||||
)]);
|
||||
|
||||
let msg = CrossZoneMessage {
|
||||
src_zone,
|
||||
src_block_id,
|
||||
src_tx_index,
|
||||
src_program_id: [9_u32; 8],
|
||||
target_program_id: wrapped_token_id,
|
||||
payload: mint_payload(),
|
||||
l1_inclusion_witness: None,
|
||||
};
|
||||
|
||||
let wrapped_config_id = wrapped_token_core::config_account_id(wrapped_token_id);
|
||||
let holding_id = wrapped_token_core::holding_account_id(wrapped_token_id, &RECIPIENT);
|
||||
|
||||
let message = Message::try_new(
|
||||
inbox_id,
|
||||
vec![
|
||||
inbox_config_account_id(inbox_id),
|
||||
seen_id,
|
||||
wrapped_config_id,
|
||||
holding_id,
|
||||
],
|
||||
vec![],
|
||||
InboxInstruction::Dispatch(msg),
|
||||
)
|
||||
.expect("build dispatch message");
|
||||
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(vec![]));
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("a replayed dispatch is a valid no-op, not an error");
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
// No mint: the holding is never credited on replay.
|
||||
let minted = public_diff.get(&holding_id).map_or(0, |account| {
|
||||
wrapped_token_core::read_balance(&account.data.clone().into_inner())
|
||||
});
|
||||
assert_eq!(minted, 0, "a replayed message must not mint again");
|
||||
|
||||
// The seen-shard is untouched by the no-op.
|
||||
if let Some(seen) = public_diff.get(&seen_id) {
|
||||
let shard_after =
|
||||
SeenShard::from_bytes(&seen.data.clone().into_inner()).expect("seen shard decodes");
|
||||
assert_eq!(shard_after, shard, "replay must not modify the seen-shard");
|
||||
}
|
||||
}
|
||||
153
integration_tests/tests/cross_zone_verified.rs
Normal file
153
integration_tests/tests/cross_zone_verified.rs
Normal file
@ -0,0 +1,153 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! Cross-zone round trip with the indexer in the loop (Option B). A ping on zone
|
||||
//! A is delivered to zone B, and zone B's indexer independently re-derives the
|
||||
//! injected dispatch from zone A's finalized blocks before applying it. The
|
||||
//! payload landing in the indexer's state proves verification passed; a forgery
|
||||
//! would have halted the indexer instead.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use cross_zone_outbox_core::outbox_pda;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
indexer_client::IndexerClient,
|
||||
setup::{SequencerSetup, indexer_client, sequencer_client, setup_bedrock_node, setup_indexer},
|
||||
};
|
||||
use lee::{AccountId, PublicTransaction, public_transaction::Message};
|
||||
use lee_core::program::ProgramId;
|
||||
use ping_core::{ReceiverInstruction, SenderInstruction, ping_record_pda};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer};
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use tokio::test;
|
||||
|
||||
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(600);
|
||||
const PING_PAYLOAD: &[u8] = b"hello-verified-zone";
|
||||
|
||||
#[test]
|
||||
async fn indexer_verifies_and_delivers_cross_zone_ping() -> Result<()> {
|
||||
// Declared first so it outlives both zones (drops run in reverse order).
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up shared Bedrock node")?;
|
||||
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel_a = config::bedrock_channel_id();
|
||||
let channel_b = config::bedrock_channel_id_b();
|
||||
let zone_a: [u8; 32] = *channel_a.as_ref();
|
||||
let zone_b: [u8; 32] = *channel_b.as_ref();
|
||||
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: zone_a,
|
||||
allowed_targets: vec![receiver_id],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
// Zone A: source. Zone B: destination, with the watcher on its sequencer and
|
||||
// the verifier on its indexer.
|
||||
let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_a)
|
||||
.with_genesis(vec![])
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (_idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a, None)
|
||||
.await
|
||||
.context("Failed to set up zone A indexer")?;
|
||||
let (_seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_b)
|
||||
.with_genesis(vec![])
|
||||
.with_cross_zone(cross_zone.clone())
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, Some(cross_zone))
|
||||
.await
|
||||
.context("Failed to set up zone B indexer")?;
|
||||
|
||||
// Submit the ping on zone A, addressed to ping_receiver on zone B.
|
||||
let ping = build_ping_tx(zone_b, receiver_id);
|
||||
sequencer_client(seq_a.addr())?
|
||||
.send_transaction(ping)
|
||||
.await
|
||||
.context("Failed to submit ping on zone A")?;
|
||||
|
||||
// Wait until zone B's indexer records the delivered payload. The indexer only
|
||||
// applies the dispatch after re-deriving and verifying it.
|
||||
let record_id = ping_record_pda(receiver_id);
|
||||
let indexer = indexer_client(idx_b.addr())
|
||||
.await
|
||||
.context("Failed to build indexer client")?;
|
||||
|
||||
let delivered = wait_for_indexer_delivery(&indexer, record_id).await?;
|
||||
assert_eq!(
|
||||
delivered, PING_PAYLOAD,
|
||||
"Zone B's indexer must record the verified cross-zone payload"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_ping_tx(target_zone: [u8; 32], receiver_id: ProgramId) -> LeeTransaction {
|
||||
let outbox_id = programs::cross_zone_outbox().id();
|
||||
let ordinal = 0;
|
||||
|
||||
let words = risc0_zkvm::serde::to_vec(&ReceiverInstruction::Record {
|
||||
payload: PING_PAYLOAD.to_vec(),
|
||||
})
|
||||
.expect("serialize ping instruction");
|
||||
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
|
||||
|
||||
let send = SenderInstruction::Send {
|
||||
outbox_program_id: outbox_id,
|
||||
target_zone,
|
||||
target_program_id: receiver_id,
|
||||
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
|
||||
payload,
|
||||
ordinal,
|
||||
};
|
||||
|
||||
let outbox_account = outbox_pda(outbox_id, &target_zone, ordinal);
|
||||
let message = Message::try_new(
|
||||
programs::ping_sender().id(),
|
||||
vec![outbox_account],
|
||||
vec![],
|
||||
send,
|
||||
)
|
||||
.expect("build ping message");
|
||||
LeeTransaction::Public(PublicTransaction::new(
|
||||
message,
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
))
|
||||
}
|
||||
|
||||
/// Polls zone B's indexer until the ping record PDA holds a payload.
|
||||
async fn wait_for_indexer_delivery(
|
||||
indexer: &IndexerClient,
|
||||
record_id: AccountId,
|
||||
) -> Result<Vec<u8>> {
|
||||
let account_id = indexer_service_protocol::AccountId {
|
||||
value: record_id.into_value(),
|
||||
};
|
||||
let wait = async {
|
||||
loop {
|
||||
let account =
|
||||
indexer_service_rpc::RpcClient::get_account(&**indexer, account_id).await?;
|
||||
let data = account.data.0;
|
||||
if !data.is_empty() {
|
||||
return Ok::<Vec<u8>, anyhow::Error>(data);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(DELIVERY_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Zone B's indexer did not record the payload in time")?
|
||||
}
|
||||
227
integration_tests/tests/cross_zone_watcher_restart.rs
Normal file
227
integration_tests/tests/cross_zone_watcher_restart.rs
Normal file
@ -0,0 +1,227 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! A sequencer restart must resume its cross-zone watcher from the persisted
|
||||
//! per-peer delivery floor instead of re-reading the peer channel from genesis.
|
||||
//!
|
||||
//! Re-reading is safe (the dispatch key is content-addressed and the inbox
|
||||
//! no-ops a replay) so on-chain state cannot tell the two apart. What does tell
|
||||
//! them apart is the transactions: a watcher that lost its cursor re-injects
|
||||
//! every already-delivered dispatch, which shows up as inbox transactions in
|
||||
//! blocks produced after the restart. This is the only test that covers the
|
||||
//! wiring from `spawn_watchers` through the store, so a silent regression here
|
||||
//! would leave every other test green while the feature does nothing.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use common::transaction::LeeTransaction;
|
||||
use cross_zone_outbox_core::outbox_pda;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
setup::{SequencerSetup, sequencer_client, setup_bedrock_node},
|
||||
};
|
||||
use lee::{AccountId, PublicTransaction, public_transaction::Message};
|
||||
use lee_core::program::ProgramId;
|
||||
use ping_core::{ReceiverInstruction, SenderInstruction, ping_record_pda};
|
||||
use sequencer_core::config::{CrossZoneConfig, CrossZonePeer};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
|
||||
use tokio::test;
|
||||
|
||||
const DELIVERY_TIMEOUT: Duration = Duration::from_secs(480);
|
||||
/// Blocks zone B must produce after the restart before we judge the watcher.
|
||||
/// A watcher that lost its cursor re-reads the peer channel on its first pass,
|
||||
/// so a handful of blocks is ample room for the replay to appear.
|
||||
const BLOCKS_AFTER_RESTART: u64 = 5;
|
||||
const RESTART_TIMEOUT: Duration = Duration::from_secs(240);
|
||||
const PING_PAYLOAD: &[u8] = b"hello-cross-zone";
|
||||
|
||||
#[test]
|
||||
async fn restarted_watcher_resumes_instead_of_replaying_the_peer_channel() -> Result<()> {
|
||||
// Declared first so it outlives both zones (drops run in reverse order).
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up shared Bedrock node")?;
|
||||
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel_a = config::bedrock_channel_id();
|
||||
let channel_b = config::bedrock_channel_id_b();
|
||||
let zone_a: [u8; 32] = *channel_a.as_ref();
|
||||
let zone_b: [u8; 32] = *channel_b.as_ref();
|
||||
let receiver_id = programs::ping_receiver().id();
|
||||
|
||||
let cross_zone = CrossZoneConfig {
|
||||
peers: vec![CrossZonePeer {
|
||||
channel_id: zone_a,
|
||||
allowed_targets: vec![receiver_id],
|
||||
expected_block_signing_pubkey: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_a)
|
||||
.with_genesis(vec![])
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
|
||||
// Zone B keeps an explicit home so it can be restarted on the same store.
|
||||
let home_b = tempfile::tempdir().context("Failed to create zone B home")?;
|
||||
let mut seq_b = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_b)
|
||||
.with_genesis(vec![])
|
||||
.with_cross_zone(cross_zone.clone())
|
||||
.setup_at(home_b.path())
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
|
||||
// Deliver one ping, so the peer channel holds a dispatch worth replaying.
|
||||
sequencer_client(seq_a.addr())?
|
||||
.send_transaction(build_ping_tx(zone_b, receiver_id))
|
||||
.await
|
||||
.context("Failed to submit ping on zone A")?;
|
||||
let record_id = ping_record_pda(receiver_id);
|
||||
let delivered = wait_for_delivery(sequencer_client(seq_b.addr())?, record_id).await?;
|
||||
assert_eq!(
|
||||
delivered, PING_PAYLOAD,
|
||||
"Zone B must record the payload before the restart"
|
||||
);
|
||||
|
||||
let tip_before = sequencer_client(seq_b.addr())?.get_last_block_id().await?;
|
||||
|
||||
// Restart zone B on the same home. Zone A stays quiet from here, so any
|
||||
// inbox transaction after the restart is a replay, not a new delivery.
|
||||
//
|
||||
// `shutdown` rather than `drop`: dropping aborts the main loop without
|
||||
// awaiting it and leaves the watchers and the publisher's drive task holding
|
||||
// the store, so the reopen below would race the `RocksDB` lock.
|
||||
seq_b.shutdown().await;
|
||||
seq_b = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_b)
|
||||
.with_genesis(vec![])
|
||||
.with_cross_zone(cross_zone)
|
||||
.setup_at(home_b.path())
|
||||
.await
|
||||
.context("Failed to restart zone B sequencer")?;
|
||||
let client_b = sequencer_client(seq_b.addr())?;
|
||||
|
||||
let tip_after = wait_for_block_id(
|
||||
&client_b,
|
||||
tip_before.saturating_add(BLOCKS_AFTER_RESTART),
|
||||
RESTART_TIMEOUT,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let replayed = count_inbox_transactions(&client_b, tip_before.saturating_add(1), tip_after)
|
||||
.await
|
||||
.context("Failed to scan zone B blocks after the restart")?;
|
||||
assert_eq!(
|
||||
replayed,
|
||||
0,
|
||||
"a restarted watcher must resume from its persisted delivery floor; found {replayed} inbox transaction(s) in blocks {}..={tip_after}, which means it re-read the peer channel from genesis",
|
||||
tip_before.saturating_add(1)
|
||||
);
|
||||
|
||||
// The delivery itself must survive the restart untouched.
|
||||
let account = client_b.get_account(record_id).await?;
|
||||
assert_eq!(
|
||||
account.data.into_inner(),
|
||||
PING_PAYLOAD,
|
||||
"the delivered payload must survive the restart"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Counts inbox transactions across `from..=to`, the signature of a re-injected
|
||||
/// dispatch.
|
||||
async fn count_inbox_transactions(client: &SequencerClient, from: u64, to: u64) -> Result<usize> {
|
||||
let inbox_id = programs::cross_zone_inbox().id();
|
||||
let mut count = 0_usize;
|
||||
for block_id in from..=to {
|
||||
let Some(block) = client.get_block(block_id).await? else {
|
||||
continue;
|
||||
};
|
||||
for tx in &block.body.transactions {
|
||||
if let LeeTransaction::Public(public_tx) = tx
|
||||
&& public_tx.message().program_id == inbox_id
|
||||
{
|
||||
count = count.saturating_add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Waits until the sequencer's tip reaches `target`, returning the tip.
|
||||
async fn wait_for_block_id(
|
||||
client: &SequencerClient,
|
||||
target: u64,
|
||||
timeout: Duration,
|
||||
) -> Result<u64> {
|
||||
let wait = async {
|
||||
loop {
|
||||
let tip = client.get_last_block_id().await?;
|
||||
if tip >= target {
|
||||
return Ok::<u64, anyhow::Error>(tip);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(timeout, wait)
|
||||
.await
|
||||
.context("Zone B did not produce enough blocks after the restart")?
|
||||
}
|
||||
|
||||
/// Builds a top-level `ping_sender` transaction that chains into the outbox to emit
|
||||
/// a message carrying a `ping_receiver::Record` instruction for the target zone.
|
||||
fn build_ping_tx(target_zone: [u8; 32], receiver_id: ProgramId) -> LeeTransaction {
|
||||
let outbox_id = programs::cross_zone_outbox().id();
|
||||
let ordinal = 0;
|
||||
|
||||
let words = risc0_zkvm::serde::to_vec(&ReceiverInstruction::Record {
|
||||
payload: PING_PAYLOAD.to_vec(),
|
||||
})
|
||||
.expect("serialize ping instruction");
|
||||
let payload: Vec<u8> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
|
||||
|
||||
let send = SenderInstruction::Send {
|
||||
outbox_program_id: outbox_id,
|
||||
target_zone,
|
||||
target_program_id: receiver_id,
|
||||
target_accounts: vec![ping_record_pda(receiver_id).into_value()],
|
||||
payload,
|
||||
ordinal,
|
||||
};
|
||||
|
||||
let outbox_account = outbox_pda(outbox_id, &target_zone, ordinal);
|
||||
let message = Message::try_new(
|
||||
programs::ping_sender().id(),
|
||||
vec![outbox_account],
|
||||
vec![],
|
||||
send,
|
||||
)
|
||||
.expect("build ping message");
|
||||
LeeTransaction::Public(PublicTransaction::new(
|
||||
message,
|
||||
lee::public_transaction::WitnessSet::from_raw_parts(vec![]),
|
||||
))
|
||||
}
|
||||
|
||||
/// Polls zone B's sequencer until the ping record PDA holds a payload.
|
||||
async fn wait_for_delivery(client: SequencerClient, record_id: AccountId) -> Result<Vec<u8>> {
|
||||
let wait = async {
|
||||
loop {
|
||||
let account = client.get_account(record_id).await?;
|
||||
let data = account.data.into_inner();
|
||||
if !data.is_empty() {
|
||||
return Ok::<Vec<u8>, anyhow::Error>(data);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(DELIVERY_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Zone B did not record the cross-zone payload in time")?
|
||||
}
|
||||
@ -50,8 +50,12 @@ pub fn setup_indexer_ffi(bedrock_addr: SocketAddr) -> Result<(IndexerServiceFFI,
|
||||
temp_indexer_dir.path().display()
|
||||
);
|
||||
|
||||
let indexer_config = integration_tests::config::indexer_config(bedrock_addr)
|
||||
.context("Failed to create Indexer config")?;
|
||||
let indexer_config = integration_tests::config::indexer_config(
|
||||
bedrock_addr,
|
||||
integration_tests::config::bedrock_channel_id(),
|
||||
None,
|
||||
)
|
||||
.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");
|
||||
|
||||
54
integration_tests/tests/indexer_stall.rs
Normal file
54
integration_tests/tests/indexer_stall.rs
Normal file
@ -0,0 +1,54 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use indexer_service_protocol::IndexerSyncState;
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
use integration_tests::{TestContext, wait_for_indexer_to_catch_up};
|
||||
use log::info;
|
||||
|
||||
const CAUGHT_UP_STATUS_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Test that the indexer status RPC reports caught-up with no stall after a clean run.
|
||||
///
|
||||
/// The sequencer keeps producing blocks while we assert, so the status is polled until a
|
||||
/// `CaughtUp` snapshot is observed and the indexed tip is checked as a lower bound.
|
||||
///
|
||||
/// TODO: Integration-level park testing (publishing a bad block to force a stall) is a follow-up
|
||||
/// needing fault injection support in the test harness.
|
||||
#[tokio::test]
|
||||
async fn indexer_status_rpc_reports_caught_up_with_no_stall() -> Result<()> {
|
||||
let ctx = TestContext::new().await?;
|
||||
|
||||
let indexer_tip = wait_for_indexer_to_catch_up(&ctx).await?;
|
||||
|
||||
let status = tokio::time::timeout(CAUGHT_UP_STATUS_TIMEOUT, async {
|
||||
loop {
|
||||
let status = ctx.indexer_client().get_status().await?;
|
||||
if status.state == IndexerSyncState::CaughtUp {
|
||||
return anyhow::Ok(status);
|
||||
}
|
||||
info!("Waiting for caught-up indexer status, got {status:?}");
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("Timed out waiting for indexer status to report caught-up")??;
|
||||
|
||||
assert!(
|
||||
status.stall_reason.is_none(),
|
||||
"indexer should have no stall reason after a clean run, got {status:?}"
|
||||
);
|
||||
// test for >= here because the sequencer keeps producing blocks while we assert,
|
||||
// so the indexed tip may be ahead of the tip we observed when we waited for caught-up.
|
||||
assert!(
|
||||
status.indexed_block_id >= Some(indexer_tip),
|
||||
"status indexed_block_id should be at least the caught-up tip {indexer_tip}, got {status:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -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());
|
||||
|
||||
@ -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());
|
||||
|
||||
|
||||
@ -8,8 +8,9 @@ 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, send_claiming_new_account, verify_commitment_is_in_state,
|
||||
};
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use lee::AccountId;
|
||||
@ -17,8 +18,7 @@ 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
|
||||
@ -80,8 +57,8 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
|
||||
});
|
||||
|
||||
let sub_ret = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash } = sub_ret else {
|
||||
anyhow::bail!("Expected PrivacyPreservingTransfer return value");
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = sub_ret else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
||||
@ -94,9 +71,8 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> {
|
||||
.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!(tx.message.new_commitments.contains(&new_commitment1));
|
||||
|
||||
assert_eq!(tx.message.new_commitments.len(), 2);
|
||||
for commitment in tx.message.new_commitments {
|
||||
assert!(verify_commitment_is_in_state(commitment, ctx.sequencer_client()).await);
|
||||
}
|
||||
@ -118,107 +94,36 @@ 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. Both are still unclaimed, so bypass the wallet CLI (which
|
||||
// never signs with the recipient's key) and sign with the recipient's own key directly.
|
||||
send_claiming_new_account(&mut ctx, from, to_account_id3, 102).await?;
|
||||
send_claiming_new_account(&mut ctx, from, to_account_id4, 103).await?;
|
||||
|
||||
info!("Preparation complete, performing keys restoration");
|
||||
|
||||
@ -226,34 +131,12 @@ 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,
|
||||
@ -270,27 +153,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;
|
||||
|
||||
|
||||
237
integration_tests/tests/multi_sequencer.rs
Normal file
237
integration_tests/tests/multi_sequencer.rs
Normal file
@ -0,0 +1,237 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! Two sequencers share one channel: A starts solo as channel admin, live-
|
||||
//! accredits `[A, B]` with round-robin rotation, B joins and syncs, both
|
||||
//! produce on their turns, and A, B and an indexer converge on the same chain.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
indexer_client::IndexerClient,
|
||||
setup::{SequencerSetup, indexer_client, sequencer_client, setup_bedrock_node, setup_indexer},
|
||||
};
|
||||
use logos_blockchain_key_management_system_service::keys::{ED25519_SECRET_KEY_SIZE, Ed25519Key};
|
||||
use sequencer_core::{block_publisher::post_channel_config, config::BedrockConfig};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
|
||||
use testnet_initial_state::{initial_pub_accounts_private_keys, initial_public_user_accounts};
|
||||
use tokio::test;
|
||||
|
||||
/// 1 s bedrock slots: rotate the turn every ~20 s of tenure; steal a stalled
|
||||
/// turn after ~30 s (bounds the stall while B is accredited but not started).
|
||||
const POSTING_TIMEFRAME_SLOTS: u32 = 20;
|
||||
const POSTING_TIMEOUT_SLOTS: u32 = 30;
|
||||
const PHASE_TIMEOUT: Duration = Duration::from_secs(360);
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(2);
|
||||
const TRANSFER_AMOUNT: u128 = 10;
|
||||
/// ≈4 turn windows past B's join (5 s blocks, ~20 s turns → ~4 blocks/window).
|
||||
const ROTATION_BLOCKS: u64 = 8;
|
||||
|
||||
#[test]
|
||||
async fn multi_sequencer_committee_converges() -> Result<()> {
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up Bedrock node")?;
|
||||
|
||||
// Fixed seeds so A can accredit B's public key before B exists.
|
||||
let key_a = [0xA1_u8; ED25519_SECRET_KEY_SIZE];
|
||||
let key_b = [0xB2_u8; ED25519_SECRET_KEY_SIZE];
|
||||
let pub_a = Ed25519Key::from_bytes(&key_a).public_key();
|
||||
let pub_b = Ed25519Key::from_bytes(&key_b).public_key();
|
||||
|
||||
let partial = SequencerPartialConfig {
|
||||
block_create_timeout: Duration::from_secs(5),
|
||||
..SequencerPartialConfig::default()
|
||||
};
|
||||
|
||||
// Phase 1: A solo (its first inscription creates the channel), plus an indexer.
|
||||
let (seq_a, _a_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_genesis(vec![])
|
||||
.with_bedrock_signing_key(key_a)
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up sequencer A")?;
|
||||
let a = sequencer_client(seq_a.addr())?;
|
||||
let (idx, _idx_home) = setup_indexer(bedrock_addr, config::bedrock_channel_id(), None)
|
||||
.await
|
||||
.context("Failed to set up indexer")?;
|
||||
let indexer = indexer_client(idx.addr()).await?;
|
||||
|
||||
wait_for_height(&a, 2, "sequencer A to produce past genesis").await?;
|
||||
|
||||
// Phase 2: live roster change to [A, B] with rotation enabled, posted
|
||||
// straight to bedrock with A's admin key (the operator one-shot path).
|
||||
post_channel_config(
|
||||
&BedrockConfig {
|
||||
channel_id: config::bedrock_channel_id(),
|
||||
node_url: config::addr_to_url(config::UrlProtocol::Http, bedrock_addr)?,
|
||||
funding_key: config::bedrock_funding_key(),
|
||||
auth: None,
|
||||
},
|
||||
&Ed25519Key::from_bytes(&key_a),
|
||||
vec![pub_a, pub_b],
|
||||
POSTING_TIMEFRAME_SLOTS,
|
||||
POSTING_TIMEOUT_SLOTS,
|
||||
1,
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.context("Failed to configure the channel committee")?;
|
||||
|
||||
let height_at_config = a.get_last_block_id().await?;
|
||||
wait_for_height(
|
||||
&a,
|
||||
height_at_config + 1,
|
||||
"A to produce after the roster change",
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Phase 3: B joins live and syncs the existing chain.
|
||||
let (seq_b, _b_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_genesis(vec![])
|
||||
.with_bedrock_signing_key(key_b)
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up sequencer B")?;
|
||||
let b = sequencer_client(seq_b.addr())?;
|
||||
|
||||
let join_height = a.get_last_block_id().await?;
|
||||
wait_for_height(&b, join_height, "B to sync to A's height at join").await?;
|
||||
|
||||
// Phase 4: rotation + convergence over ≈4 turn windows.
|
||||
let rotation_target = join_height + ROTATION_BLOCKS;
|
||||
wait_for_height(
|
||||
&a,
|
||||
rotation_target,
|
||||
"the chain to advance across turn windows",
|
||||
)
|
||||
.await?;
|
||||
wait_for_height(&b, rotation_target, "B to follow across turn windows").await?;
|
||||
assert_same_chain(&a, &b).await?;
|
||||
|
||||
// Phase 5: a tx submitted only to B is included by B and visible on A.
|
||||
let accounts = initial_public_user_accounts();
|
||||
let from = accounts[0].account_id;
|
||||
let to = accounts[1].account_id;
|
||||
let sign_key = initial_pub_accounts_private_keys()[0].pub_sign_key.clone();
|
||||
|
||||
let to_balance_before = a.get_account_balance(to).await?;
|
||||
let nonce = b.get_accounts_nonces(vec![from]).await?[0];
|
||||
let tx = common::test_utils::create_transaction_native_token_transfer(
|
||||
from,
|
||||
nonce.0,
|
||||
to,
|
||||
TRANSFER_AMOUNT,
|
||||
&sign_key,
|
||||
);
|
||||
b.send_transaction(tx)
|
||||
.await
|
||||
.context("Failed to submit the transfer to B")?;
|
||||
|
||||
wait_for_balance(&a, to, to_balance_before + TRANSFER_AMOUNT).await?;
|
||||
|
||||
// Phase 6: the indexer finalizes the same chain, with no stall.
|
||||
wait_for_finalized(&indexer, join_height).await?;
|
||||
let finalized = indexer.get_last_finalized_block_id().await?.unwrap_or(0);
|
||||
for id in 1..=finalized {
|
||||
let block_i = indexer
|
||||
.get_block_by_id(id)
|
||||
.await?
|
||||
.with_context(|| format!("Indexer is missing finalized block {id}"))?;
|
||||
let block_a = a
|
||||
.get_block(id)
|
||||
.await?
|
||||
.with_context(|| format!("A is missing block {id}"))?;
|
||||
ensure!(
|
||||
block_i.header.hash == indexer_service_protocol::HashType::from(block_a.header.hash),
|
||||
"Indexer diverges from A at block {id}"
|
||||
);
|
||||
}
|
||||
let status = indexer.get_status().await?;
|
||||
ensure!(
|
||||
status.stall_reason.is_none(),
|
||||
"Indexer is stalled: {:?}",
|
||||
status.stall_reason
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Polls the sequencer until its chain height reaches `target`.
|
||||
async fn wait_for_height(client: &SequencerClient, target: u64, what: &str) -> Result<()> {
|
||||
let wait = async {
|
||||
loop {
|
||||
if client.get_last_block_id().await? >= target {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(PHASE_TIMEOUT, wait)
|
||||
.await
|
||||
.with_context(|| format!("Timed out waiting for {what} (target height {target})"))?
|
||||
}
|
||||
|
||||
/// Polls the sequencer until `account`'s balance reaches `expected`.
|
||||
async fn wait_for_balance(
|
||||
client: &SequencerClient,
|
||||
account: lee::AccountId,
|
||||
expected: u128,
|
||||
) -> Result<()> {
|
||||
let wait = async {
|
||||
loop {
|
||||
if client.get_account_balance(account).await? == expected {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(PHASE_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Timed out waiting for the cross-sequencer transfer to reach A")?
|
||||
}
|
||||
|
||||
/// Polls the indexer until its finalized height reaches `target`.
|
||||
async fn wait_for_finalized(indexer: &IndexerClient, target: u64) -> Result<()> {
|
||||
let wait = async {
|
||||
loop {
|
||||
if indexer.get_last_finalized_block_id().await?.unwrap_or(0) >= target {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(PHASE_TIMEOUT, wait)
|
||||
.await
|
||||
.context("Timed out waiting for the indexer to finalize")?
|
||||
}
|
||||
|
||||
/// Asserts A and B hold byte-identical block hashes over their common prefix.
|
||||
async fn assert_same_chain(a: &SequencerClient, b: &SequencerClient) -> Result<()> {
|
||||
let common = a
|
||||
.get_last_block_id()
|
||||
.await?
|
||||
.min(b.get_last_block_id().await?);
|
||||
for id in 1..=common {
|
||||
let block_a = a
|
||||
.get_block(id)
|
||||
.await?
|
||||
.with_context(|| format!("A is missing block {id}"))?;
|
||||
let block_b = b
|
||||
.get_block(id)
|
||||
.await?
|
||||
.with_context(|| format!("B is missing block {id}"))?;
|
||||
ensure!(
|
||||
block_a.header.hash == block_b.header.hash,
|
||||
"Chain divergence at block {id}: A {:?} vs B {:?}",
|
||||
block_a.header.hash,
|
||||
block_b.header.hash
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -8,15 +8,13 @@ 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, account_balance, new_account, private_mention,
|
||||
public_mention, sync_private, verify_commitment_is_in_state, wait_for_indexer_to_catch_up,
|
||||
};
|
||||
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,
|
||||
},
|
||||
@ -26,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(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
let claim_result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
@ -64,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(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
assert_eq!(pinata_balance_post, pinata_balance_pre);
|
||||
|
||||
@ -78,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(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
let claim_result = wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
@ -116,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(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
assert_eq!(pinata_balance_post, pinata_balance_pre);
|
||||
|
||||
@ -135,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(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
@ -146,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(system_accounts::pinata_account_id())
|
||||
.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);
|
||||
@ -164,6 +115,41 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn claim_pinata_indexer_keeps_up() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim {
|
||||
to: public_mention(ctx.existing_public_accounts()[0]),
|
||||
});
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
|
||||
info!("Waiting for indexer to parse blocks");
|
||||
wait_for_indexer_to_catch_up(&ctx).await?;
|
||||
|
||||
let winner_ind_state = indexer_service_rpc::RpcClient::get_account(
|
||||
&**ctx.indexer_client(),
|
||||
ctx.existing_public_accounts()[0].into(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let winner_seq_state = sequencer_service_rpc::RpcClient::get_account(
|
||||
ctx.sequencer_client(),
|
||||
ctx.existing_public_accounts()[0],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(winner_ind_state, winner_seq_state.into());
|
||||
|
||||
info!("Indexer correctly indexed the pinata claim");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn claim_pinata_to_existing_private_account() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
@ -173,22 +159,18 @@ 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(system_accounts::pinata_account_id())
|
||||
.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 {
|
||||
anyhow::bail!("Expected PrivacyPreservingTransfer return value");
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash: _ } = result else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
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()
|
||||
@ -196,10 +178,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(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
assert_eq!(pinata_balance_post, pinata_balance_pre - pinata_prize);
|
||||
|
||||
@ -215,20 +194,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 {
|
||||
@ -250,10 +216,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(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_pre = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
@ -266,10 +229,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(system_accounts::pinata_account_id())
|
||||
.await?;
|
||||
let pinata_balance_post = account_balance(&ctx, system_accounts::pinata_account_id()).await?;
|
||||
|
||||
assert_eq!(pinata_balance_post, pinata_balance_pre - pinata_prize);
|
||||
|
||||
|
||||
@ -9,9 +9,8 @@ use anyhow::{Context as _, Result};
|
||||
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{
|
||||
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 key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use lee::{
|
||||
AccountId, PrivacyPreservingTransaction, ProgramId,
|
||||
privacy_preserving_transaction::{
|
||||
@ -22,7 +21,7 @@ use lee::{
|
||||
program::Program,
|
||||
};
|
||||
use lee_core::{
|
||||
EncryptedAccountData, 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(
|
||||
@ -51,7 +47,8 @@ async fn fund_private_pda(
|
||||
amount: u128,
|
||||
auth_transfer: &ProgramWithDependencies,
|
||||
) -> Result<()> {
|
||||
let pda_account_id = AccountId::for_private_pda(&authority_program_id, &seed, &npk, identifier);
|
||||
let pda_account_id =
|
||||
AccountId::for_private_pda(&authority_program_id, &seed, &npk, &vpk, identifier);
|
||||
let sender_account = wallet
|
||||
.get_account_public(sender)
|
||||
.await
|
||||
@ -63,21 +60,17 @@ async fn fund_private_pda(
|
||||
let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_account_id);
|
||||
|
||||
let eph_holder = EphemeralKeyHolder::new(&vpk);
|
||||
let ssk = eph_holder.calculate_shared_secret_sender();
|
||||
let epk = eph_holder.ephemeral_public_key().clone();
|
||||
|
||||
let instruction = Program::serialize_instruction(AuthTransferInstruction::Transfer { amount })
|
||||
.context("failed to serialize auth_transfer instruction")?;
|
||||
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
|
||||
vpk,
|
||||
random_seed: [0; 32],
|
||||
npk,
|
||||
ssk,
|
||||
identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
},
|
||||
];
|
||||
@ -98,7 +91,7 @@ async fn fund_private_pda(
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
wallet
|
||||
.sequencer_client
|
||||
.helm_owned()
|
||||
.send_transaction(LeeTransaction::PrivacyPreserving(tx))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("send transaction failed: {e}"))?;
|
||||
@ -175,8 +168,8 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
let spend_program =
|
||||
ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into());
|
||||
|
||||
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0);
|
||||
let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1);
|
||||
let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_vpk, 0);
|
||||
let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, &alice_vpk, 1);
|
||||
|
||||
// Use two different public senders to avoid nonce conflicts between the back-to-back txs.
|
||||
let senders = ctx.existing_public_accounts();
|
||||
@ -187,7 +180,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
|
||||
info!("Sending to alice_pda_0 (identifier=0)");
|
||||
fund_private_pda(
|
||||
ctx.wallet(),
|
||||
ctx.wallet_mut(),
|
||||
sender_0,
|
||||
alice_npk,
|
||||
alice_vpk.clone(),
|
||||
@ -201,7 +194,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
|
||||
info!("Sending to alice_pda_1 (identifier=1)");
|
||||
fund_private_pda(
|
||||
ctx.wallet(),
|
||||
ctx.wallet_mut(),
|
||||
sender_1,
|
||||
alice_npk,
|
||||
alice_vpk.clone(),
|
||||
@ -217,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
|
||||
@ -242,7 +231,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
.get_private_account_commitment(alice_pda_0_id)
|
||||
.context("commitment for alice_pda_0 missing")?;
|
||||
assert!(
|
||||
verify_commitment_is_in_state(commitment_0.clone(), ctx.sequencer_client()).await,
|
||||
verify_commitment_is_in_state(commitment_0, ctx.sequencer_client()).await,
|
||||
"alice_pda_0 commitment not in state after receive"
|
||||
);
|
||||
|
||||
@ -251,7 +240,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
.get_private_account_commitment(alice_pda_1_id)
|
||||
.context("commitment for alice_pda_1 missing")?;
|
||||
assert!(
|
||||
verify_commitment_is_in_state(commitment_1.clone(), ctx.sequencer_client()).await,
|
||||
verify_commitment_is_in_state(commitment_1, ctx.sequencer_client()).await,
|
||||
"alice_pda_1 commitment not in state after receive"
|
||||
);
|
||||
assert_ne!(
|
||||
@ -273,7 +262,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
|
||||
info!("Alice spending from alice_pda_0");
|
||||
spend_private_pda(
|
||||
ctx.wallet(),
|
||||
ctx.wallet_mut(),
|
||||
alice_pda_0_id,
|
||||
recipient_npk_0,
|
||||
recipient_vpk_0,
|
||||
@ -286,7 +275,7 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> {
|
||||
|
||||
info!("Alice spending from alice_pda_1");
|
||||
spend_private_pda(
|
||||
ctx.wallet(),
|
||||
ctx.wallet_mut(),
|
||||
alice_pda_1_id,
|
||||
recipient_npk_1,
|
||||
recipient_vpk_1,
|
||||
@ -300,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
|
||||
|
||||
34
integration_tests/tests/private_transaction_padding.rs
Normal file
34
integration_tests/tests/private_transaction_padding.rs
Normal file
@ -0,0 +1,34 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use anyhow::Result;
|
||||
use integration_tests::{TestContext, fetch_privacy_preserving_tx, new_account, private_mention};
|
||||
use tokio::test;
|
||||
use wallet::cli::{
|
||||
Command, SubcommandReturnValue, programs::native_token_transfer::AuthTransferSubcommand,
|
||||
};
|
||||
|
||||
#[test]
|
||||
async fn private_transaction_pads_notes_to_max() -> Result<()> {
|
||||
let mut ctx = TestContext::new().await?;
|
||||
|
||||
let account_id = new_account(&mut ctx, true, None).await?;
|
||||
|
||||
let command = Command::AuthTransfer(AuthTransferSubcommand::Init {
|
||||
account_id: private_mention(account_id),
|
||||
});
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
let SubcommandReturnValue::TransactionExecuted { tx_hash } = result else {
|
||||
anyhow::bail!("Expected TransactionExecuted return value");
|
||||
};
|
||||
|
||||
let tx = fetch_privacy_preserving_tx(ctx.sequencer_client(), tx_hash).await;
|
||||
|
||||
assert_eq!(tx.message.new_commitments.len(), 7);
|
||||
assert_eq!(tx.message.new_nullifiers.len(), 7);
|
||||
assert_eq!(tx.message.encrypted_private_post_states.len(), 7);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -7,14 +7,11 @@ use std::{io::Write as _, time::Duration};
|
||||
|
||||
use anyhow::Result;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext};
|
||||
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, config::WalletConfigOverrides};
|
||||
|
||||
#[test]
|
||||
async fn deploy_and_execute_program() -> Result<()> {
|
||||
@ -32,22 +29,9 @@ async fn deploy_and_execute_program() -> Result<()> {
|
||||
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?;
|
||||
|
||||
info!("Waiting for next block creation");
|
||||
tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await;
|
||||
let account_id = new_account(&mut ctx, false, None).await?;
|
||||
|
||||
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 nonces = ctx.wallet().get_accounts_nonces(vec![account_id]).await?;
|
||||
let nonces = ctx.wallet_mut().get_accounts_nonces(&[account_id]).await?;
|
||||
let private_key = ctx
|
||||
.wallet()
|
||||
.get_account_public_signing_key(account_id)
|
||||
@ -66,7 +50,7 @@ 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?;
|
||||
|
||||
let expected_data: &[u8] = &[];
|
||||
assert_eq!(post_state_account.program_owner, claimer.id());
|
||||
@ -78,3 +62,37 @@ async fn deploy_and_execute_program() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn deploy_invalid_program_fails() -> Result<()> {
|
||||
// An invalid program bytecode is rejected by the sequencer during block production, so the
|
||||
// deployment transaction is never included in a block. Shrink the wallet's polling window so
|
||||
// the command gives up quickly instead of waiting for the full default timeout.
|
||||
let mut ctx = TestContext::builder()
|
||||
.with_wallet_config_overrides(WalletConfigOverrides {
|
||||
seq_poll_timeout: Some(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)),
|
||||
seq_tx_poll_max_blocks: Some(5),
|
||||
seq_poll_max_retries: Some(2),
|
||||
..WalletConfigOverrides::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let mut tempfile = tempfile::NamedTempFile::new()?;
|
||||
tempfile.write_all(b"this is not a valid program binary")?;
|
||||
|
||||
let command = Command::DeployProgram {
|
||||
binary_filepath: tempfile.path().to_owned(),
|
||||
};
|
||||
|
||||
let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await;
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Deploying an invalid program should fail, but got: {result:?}"
|
||||
);
|
||||
|
||||
info!("Deploying an invalid program failed as expected");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
541
integration_tests/tests/sequencer_bootstrap.rs
Normal file
541
integration_tests/tests/sequencer_bootstrap.rs
Normal file
@ -0,0 +1,541 @@
|
||||
//! End-to-end tests for the sequencer's startup bootstrap/reconstruction from
|
||||
//! Bedrock (verify-and-reconstruct). Each test drives a real Bedrock node and one
|
||||
//! or more sequencer instances sharing the same channel, exercising how a
|
||||
//! sequencer reconciles its local store against what the channel serves.
|
||||
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "Integration tests live at crate root and don't care about these lints"
|
||||
)]
|
||||
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
use lee::{AccountId, PrivateKey, PublicKey};
|
||||
use logos_blockchain_core::mantle::ops::channel::ChannelId;
|
||||
use sequencer_core::config::GenesisAction;
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClient};
|
||||
use test_fixtures::{
|
||||
config::{SequencerPartialConfig, UrlProtocol, addr_to_url},
|
||||
indexer_client::IndexerClient,
|
||||
setup::{SequencerSetup, sequencer_client, setup_bedrock_node, setup_indexer},
|
||||
};
|
||||
use tokio::test;
|
||||
|
||||
/// Finalization can lag several minutes under CI load; give the bootstrap and
|
||||
/// reconstruction waits generous headroom so runner-speed variance does not flake.
|
||||
const FINALIZE_TIMEOUT: Duration = Duration::from_mins(12);
|
||||
|
||||
/// Block cadence for the tests: short so we don't wait long for local production.
|
||||
fn fast_blocks() -> SequencerPartialConfig {
|
||||
SequencerPartialConfig {
|
||||
block_create_timeout: Duration::from_secs(2),
|
||||
..SequencerPartialConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Block cadence for a sequencer we don't want producing during the inspection
|
||||
/// window, so its post-reconstruction tip stays put while we read it.
|
||||
fn slow_blocks() -> SequencerPartialConfig {
|
||||
SequencerPartialConfig {
|
||||
block_create_timeout: Duration::from_secs(30),
|
||||
..SequencerPartialConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Polls the indexer's last finalized block id until it reaches `target`. The
|
||||
/// indexer reads finalized channel history, so this is our oracle for "block
|
||||
/// `target` is finalized on Bedrock" — exactly what a reconstructing sequencer
|
||||
/// can read back.
|
||||
async fn wait_for_finalized(
|
||||
indexer: &IndexerClient,
|
||||
target: u64,
|
||||
timeout: Duration,
|
||||
) -> Result<u64> {
|
||||
let poll = async {
|
||||
loop {
|
||||
let finalized = indexer
|
||||
.get_last_finalized_block_id()
|
||||
.await
|
||||
.context("Failed to read indexer last finalized block id")?
|
||||
.unwrap_or(0);
|
||||
if finalized >= target {
|
||||
return Ok::<_, anyhow::Error>(finalized);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(timeout, poll)
|
||||
.await
|
||||
.with_context(|| format!("Timed out waiting for indexer to finalize block {target}"))?
|
||||
}
|
||||
|
||||
/// Polls the sequencer's last block id until it reaches `target` or `timeout` elapses.
|
||||
async fn wait_for_block_id(
|
||||
client: &SequencerClient,
|
||||
target: u64,
|
||||
timeout: Duration,
|
||||
) -> Result<u64> {
|
||||
let poll = async {
|
||||
loop {
|
||||
let id = client
|
||||
.get_last_block_id()
|
||||
.await
|
||||
.context("Failed to read sequencer last block id")?;
|
||||
if id >= target {
|
||||
return Ok::<_, anyhow::Error>(id);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(timeout, poll)
|
||||
.await
|
||||
.with_context(|| format!("Timed out waiting for block id {target}"))?
|
||||
}
|
||||
|
||||
/// Best-effort extraction of a panic payload's message (panics carry a `String`
|
||||
/// or `&str`), for asserting a startup aborted for the *expected* reason.
|
||||
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
|
||||
payload
|
||||
.downcast_ref::<String>()
|
||||
.cloned()
|
||||
.or_else(|| payload.downcast_ref::<&str>().map(|s| (*s).to_owned()))
|
||||
.unwrap_or_else(|| "<non-string panic payload>".to_owned())
|
||||
}
|
||||
|
||||
/// A `SupplyAccount` genesis action for a fresh account, returning the vault
|
||||
/// account id the funds land in (genesis supply goes into a claimable vault, not
|
||||
/// the account directly), so tests can assert genesis state is present.
|
||||
fn supplied_account(balance: u128) -> (AccountId, GenesisAction) {
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(
|
||||
&PrivateKey::new_os_random(),
|
||||
));
|
||||
let vault_id = vault_core::compute_vault_account_id(programs::vault().id(), account_id);
|
||||
(
|
||||
vault_id,
|
||||
GenesisAction::SupplyAccount {
|
||||
account_id,
|
||||
balance,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Recursively copies the contents of `src` into `dst` (used to snapshot/restore a
|
||||
/// sequencer's rocksdb directory while it is stopped).
|
||||
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(dst)
|
||||
.with_context(|| format!("Failed to create dir {}", dst.display()))?;
|
||||
for entry in
|
||||
std::fs::read_dir(src).with_context(|| format!("Failed to read dir {}", src.display()))?
|
||||
{
|
||||
let entry = entry.context("Failed to read dir entry")?;
|
||||
let target = dst.join(entry.file_name());
|
||||
if entry
|
||||
.file_type()
|
||||
.context("Failed to read file type")?
|
||||
.is_dir()
|
||||
{
|
||||
copy_dir_recursive(&entry.path(), &target)?;
|
||||
} else {
|
||||
std::fs::copy(entry.path(), &target)
|
||||
.with_context(|| format!("Failed to copy into {}", target.display()))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Case 1: local store is empty and the Bedrock channel is empty.
|
||||
///
|
||||
/// The sequencer bootstraps genesis state, finds nothing to reconstruct, publishes
|
||||
/// its own genesis to open the channel, and starts producing.
|
||||
#[test]
|
||||
async fn empty_local_and_empty_bedrock_bootstraps_from_genesis() -> Result<()> {
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to setup Bedrock")?;
|
||||
let home = tempfile::tempdir().context("Failed to create sequencer home")?;
|
||||
|
||||
let (vault_id, supply) = supplied_account(12_345);
|
||||
let genesis = vec![
|
||||
supply,
|
||||
GenesisAction::SupplyBridgeAccount { balance: 1_000_000 },
|
||||
];
|
||||
|
||||
let handle = SequencerSetup::new(fast_blocks(), bedrock_addr)
|
||||
.with_genesis(genesis)
|
||||
.setup_at(home.path())
|
||||
.await
|
||||
.context("Failed to start sequencer")?;
|
||||
let client = sequencer_client(handle.addr())?;
|
||||
|
||||
// Fresh store + empty channel: startup bootstrapped genesis state directly.
|
||||
assert_eq!(
|
||||
client.get_account_balance(vault_id).await?,
|
||||
12_345,
|
||||
"genesis-supplied vault balance must be present after bootstrap"
|
||||
);
|
||||
|
||||
// The sequencer is live and producing on the freshly opened channel.
|
||||
let last = wait_for_block_id(&client, 3, Duration::from_secs(60)).await?;
|
||||
assert!(
|
||||
last >= 3,
|
||||
"sequencer should keep producing blocks, last={last}"
|
||||
);
|
||||
assert!(handle.is_healthy(), "sequencer must stay healthy");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Case 2: local store is empty, but the Bedrock channel already has blocks.
|
||||
///
|
||||
/// A first sequencer opens the channel and produces blocks; a second sequencer
|
||||
/// starts from an empty store on the same channel (reusing the first's bedrock
|
||||
/// signing key) and reconstructs the finalized history into its state.
|
||||
#[test]
|
||||
async fn empty_local_reconstructs_from_populated_bedrock() -> Result<()> {
|
||||
const PRODUCED_TARGET: u64 = 3;
|
||||
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to setup Bedrock")?;
|
||||
let (indexer_handle, _indexer_dir) = setup_indexer(
|
||||
bedrock_addr,
|
||||
test_fixtures::config::bedrock_channel_id(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to setup indexer")?;
|
||||
let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())
|
||||
.context("Failed to build indexer URL")?;
|
||||
let indexer = IndexerClient::new(&indexer_url)
|
||||
.await
|
||||
.context("Failed to build indexer client")?;
|
||||
|
||||
let (vault_id, supply) = supplied_account(7_777);
|
||||
let genesis = vec![
|
||||
supply,
|
||||
GenesisAction::SupplyBridgeAccount { balance: 1_000_000 },
|
||||
];
|
||||
|
||||
// Sequencer A opens the channel and produces a few blocks.
|
||||
let home_a = tempfile::tempdir().context("Failed to create sequencer A home")?;
|
||||
let handle_a = SequencerSetup::new(fast_blocks(), bedrock_addr)
|
||||
.with_genesis(genesis.clone())
|
||||
.setup_at(home_a.path())
|
||||
.await
|
||||
.context("Failed to start sequencer A")?;
|
||||
let client_a = sequencer_client(handle_a.addr())?;
|
||||
wait_for_block_id(&client_a, PRODUCED_TARGET, Duration::from_secs(60)).await?;
|
||||
|
||||
// Wait until those blocks are finalized on Bedrock — reconstruction only
|
||||
// reads finalized history. A stays alive so its publish task keeps flushing.
|
||||
let finalized = wait_for_finalized(&indexer, PRODUCED_TARGET, FINALIZE_TIMEOUT).await?;
|
||||
|
||||
// Stop A, then wipe just its L2 store (keeping the bedrock signing key) so it
|
||||
// restarts from an empty store on the same channel/identity — a sequencer that
|
||||
// lost its local DB.
|
||||
drop(handle_a);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
std::fs::remove_dir_all(home_a.path().join("rocksdb"))
|
||||
.context("Failed to wipe sequencer L2 store")?;
|
||||
|
||||
// Sequencer B restarts on the same home from that empty store and reconstructs.
|
||||
let handle_b = SequencerSetup::new(slow_blocks(), bedrock_addr)
|
||||
.with_genesis(genesis)
|
||||
.setup_at(home_a.path())
|
||||
.await
|
||||
.context("Failed to start sequencer B")?;
|
||||
let client_b = sequencer_client(handle_b.addr())?;
|
||||
|
||||
// Reconstruction ran synchronously during B's startup: even though its local
|
||||
// store was empty, its tip is past genesis, matching the finalized channel.
|
||||
let tip_b = client_b.get_last_block_id().await?;
|
||||
assert!(
|
||||
tip_b >= finalized,
|
||||
"B should reconstruct at least the finalized blocks; tip_b={tip_b}, finalized={finalized}"
|
||||
);
|
||||
assert!(
|
||||
tip_b > 1,
|
||||
"B should have reconstructed blocks beyond genesis; tip_b={tip_b}"
|
||||
);
|
||||
|
||||
// Genesis state was rebuilt as part of the reconstruction.
|
||||
assert_eq!(
|
||||
client_b.get_account_balance(vault_id).await?,
|
||||
7_777,
|
||||
"reconstructed genesis vault balance must be present"
|
||||
);
|
||||
assert!(handle_b.is_healthy(), "sequencer B must stay healthy");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Case 3: local store is not empty, but the Bedrock channel is empty.
|
||||
///
|
||||
/// A sequencer produces blocks (committing to a channel), is stopped, and is
|
||||
/// restarted against a fresh/empty channel — i.e. the channel it committed to
|
||||
/// was wiped or the node points at a different chain. Startup must fail rather
|
||||
/// than silently resume onto a foreign channel. Crucially this must hold even
|
||||
/// though the sequencer only ever *produced* (so it never recorded a per-block
|
||||
/// anchor): the committed-but-missing-channel invariant catches it.
|
||||
#[test]
|
||||
async fn nonempty_local_against_empty_channel_fails_startup() -> Result<()> {
|
||||
const PRODUCED_TARGET: u64 = 3;
|
||||
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to setup Bedrock")?;
|
||||
|
||||
let (_vault_id, supply) = supplied_account(1);
|
||||
let genesis = vec![
|
||||
supply,
|
||||
GenesisAction::SupplyBridgeAccount { balance: 1_000_000 },
|
||||
];
|
||||
|
||||
// A opens the channel and produces blocks. They land in its local store
|
||||
// immediately, so no need to wait for finalization.
|
||||
let home_a = tempfile::tempdir().context("Failed to create sequencer A home")?;
|
||||
let handle_a = SequencerSetup::new(fast_blocks(), bedrock_addr)
|
||||
.with_genesis(genesis.clone())
|
||||
.setup_at(home_a.path())
|
||||
.await
|
||||
.context("Failed to start sequencer A")?;
|
||||
wait_for_block_id(
|
||||
&sequencer_client(handle_a.addr())?,
|
||||
PRODUCED_TARGET,
|
||||
Duration::from_secs(60),
|
||||
)
|
||||
.await?;
|
||||
drop(handle_a);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
// Restart on the SAME home (A's committed store: blocks + checkpoint) but
|
||||
// pointed at a fresh, never-used channel — the channel it committed to is gone.
|
||||
let empty_channel = ChannelId::from([0x5a_u8; 32]);
|
||||
|
||||
// Startup aborts on the missing-channel invariant (a panic in
|
||||
// `start_from_config`). Run it on a dedicated OS thread with its own runtime
|
||||
// so the panic is isolated to `join()` instead of failing the test thread.
|
||||
// The `timeout` future must be created *inside* `block_on` (it needs a running
|
||||
// reactor), so build it in an `async` block rather than as an eager argument.
|
||||
let home_a_path = home_a.path().to_owned();
|
||||
let outcome = std::thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("Failed to build runtime");
|
||||
runtime.block_on(async {
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(90),
|
||||
SequencerSetup::new(slow_blocks(), bedrock_addr)
|
||||
.with_channel_id(empty_channel)
|
||||
.with_genesis(genesis)
|
||||
.setup_at(&home_a_path),
|
||||
)
|
||||
.await
|
||||
})
|
||||
})
|
||||
.join();
|
||||
|
||||
match outcome {
|
||||
// Expected: `start_from_config` panicked on the missing-channel invariant.
|
||||
// Assert the *reason*, so an unrelated panic fails the test rather than
|
||||
// masquerading as success.
|
||||
Err(panic) => {
|
||||
let message = panic_message(&*panic);
|
||||
assert!(
|
||||
message.contains("Refusing to resume onto a foreign channel"),
|
||||
"startup panicked for an unexpected reason: {message}"
|
||||
);
|
||||
}
|
||||
Ok(Err(_elapsed)) => {
|
||||
bail!("Sequencer startup hung instead of failing against an empty channel")
|
||||
}
|
||||
Ok(Ok(Err(err))) => {
|
||||
bail!("Sequencer expected to panic, but it failed with error: {err:#?}")
|
||||
}
|
||||
Ok(Ok(Ok(_handle))) => {
|
||||
bail!("Sequencer startup unexpectedly succeeded against an empty channel")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Case 4: both non-empty, but the local store is *ahead* of the finalized channel.
|
||||
///
|
||||
/// A sequencer produces blocks faster than Bedrock finalizes them (the normal
|
||||
/// steady state), so on restart its local tip leads the channel's finalized tip.
|
||||
/// Startup must succeed: reconstruction re-verifies the finalized blocks it already
|
||||
/// holds and leaves the extra, not-yet-finalized local blocks untouched.
|
||||
#[test]
|
||||
async fn local_ahead_of_channel_resumes() -> Result<()> {
|
||||
const FINALIZED_TARGET: u64 = 2;
|
||||
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to setup Bedrock")?;
|
||||
let (indexer_handle, _indexer_dir) = setup_indexer(
|
||||
bedrock_addr,
|
||||
test_fixtures::config::bedrock_channel_id(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to setup indexer")?;
|
||||
let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())
|
||||
.context("Failed to build indexer URL")?;
|
||||
let indexer = IndexerClient::new(&indexer_url)
|
||||
.await
|
||||
.context("Failed to build indexer client")?;
|
||||
|
||||
let (vault_id, supply) = supplied_account(4_242);
|
||||
let genesis = vec![
|
||||
supply,
|
||||
GenesisAction::SupplyBridgeAccount { balance: 1_000_000 },
|
||||
];
|
||||
|
||||
// A produces continuously (fast cadence) while Bedrock finalizes slowly, so
|
||||
// its local tip runs well ahead of the channel's finalized tip.
|
||||
let home = tempfile::tempdir().context("Failed to create sequencer home")?;
|
||||
let handle_a = SequencerSetup::new(fast_blocks(), bedrock_addr)
|
||||
.with_genesis(genesis.clone())
|
||||
.setup_at(home.path())
|
||||
.await
|
||||
.context("Failed to start sequencer A")?;
|
||||
let client_a = sequencer_client(handle_a.addr())?;
|
||||
let finalized = wait_for_finalized(&indexer, FINALIZED_TARGET, FINALIZE_TIMEOUT).await?;
|
||||
let tip_before = client_a.get_last_block_id().await?;
|
||||
assert!(
|
||||
tip_before > finalized,
|
||||
"local tip {tip_before} should lead the finalized tip {finalized}"
|
||||
);
|
||||
|
||||
// Restart on the same home; slow cadence so its tip stays put while we inspect.
|
||||
drop(handle_a);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
let handle_b = SequencerSetup::new(slow_blocks(), bedrock_addr)
|
||||
.with_genesis(genesis)
|
||||
.setup_at(home.path())
|
||||
.await
|
||||
.context("Failed to restart sequencer")?;
|
||||
let client_b = sequencer_client(handle_b.addr())?;
|
||||
|
||||
// Reconstruction verified the finalized prefix and preserved the extra blocks.
|
||||
let tip_b = client_b.get_last_block_id().await?;
|
||||
assert!(
|
||||
tip_b >= tip_before,
|
||||
"restart must not lose locally-produced blocks; tip_b={tip_b}, before={tip_before}"
|
||||
);
|
||||
assert_eq!(
|
||||
client_b.get_account_balance(vault_id).await?,
|
||||
4_242,
|
||||
"genesis state must survive the restart"
|
||||
);
|
||||
assert!(handle_b.is_healthy(), "sequencer must stay healthy");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Case 5: both non-empty, but the local store is *behind* the finalized channel.
|
||||
///
|
||||
/// We snapshot a sequencer's store at an early tip, let it keep extending and
|
||||
/// finalizing the channel, then restore the early snapshot and restart. Startup
|
||||
/// must reconstruct forward — replay the finalized blocks the local store is
|
||||
/// missing — catching the local tip up to the channel.
|
||||
///
|
||||
/// The snapshot/restore is essential here (not a gratuitous copy): a live
|
||||
/// sequencer's local tip always leads finalization, so the only way to obtain a
|
||||
/// local store that *lags* the finalized channel is to preserve an earlier state
|
||||
/// while the same channel advances past it.
|
||||
#[test]
|
||||
async fn local_behind_channel_reconstructs_forward() -> Result<()> {
|
||||
const SNAPSHOT_TIP: u64 = 2;
|
||||
const FINALIZED_TARGET: u64 = 4;
|
||||
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to setup Bedrock")?;
|
||||
let (indexer_handle, _indexer_dir) = setup_indexer(
|
||||
bedrock_addr,
|
||||
test_fixtures::config::bedrock_channel_id(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to setup indexer")?;
|
||||
let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())
|
||||
.context("Failed to build indexer URL")?;
|
||||
let indexer = IndexerClient::new(&indexer_url)
|
||||
.await
|
||||
.context("Failed to build indexer client")?;
|
||||
|
||||
let (vault_id, supply) = supplied_account(5_005);
|
||||
let genesis = vec![
|
||||
supply,
|
||||
GenesisAction::SupplyBridgeAccount { balance: 1_000_000 },
|
||||
];
|
||||
|
||||
let home = tempfile::tempdir().context("Failed to create sequencer home")?;
|
||||
let rocksdb = home.path().join("rocksdb");
|
||||
|
||||
// Bring the sequencer up to an early tip, then stop it so its store is at rest.
|
||||
{
|
||||
let handle = SequencerSetup::new(slow_blocks(), bedrock_addr)
|
||||
.with_genesis(genesis.clone())
|
||||
.setup_at(home.path())
|
||||
.await
|
||||
.context("Failed to start sequencer")?;
|
||||
wait_for_block_id(
|
||||
&sequencer_client(handle.addr())?,
|
||||
SNAPSHOT_TIP,
|
||||
Duration::from_secs(120),
|
||||
)
|
||||
.await?;
|
||||
drop(handle);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
|
||||
// Snapshot the early store (tip == SNAPSHOT_TIP), safe because it is at rest.
|
||||
let snapshot = tempfile::tempdir().context("Failed to create snapshot dir")?;
|
||||
copy_dir_recursive(&rocksdb, snapshot.path()).context("Failed to snapshot store")?;
|
||||
|
||||
// Resume the sequencer (fast cadence) so it extends and finalizes the channel
|
||||
// beyond the snapshot.
|
||||
let finalized = {
|
||||
let handle = SequencerSetup::new(fast_blocks(), bedrock_addr)
|
||||
.with_genesis(genesis.clone())
|
||||
.setup_at(home.path())
|
||||
.await
|
||||
.context("Failed to resume sequencer")?;
|
||||
let finalized = wait_for_finalized(&indexer, FINALIZED_TARGET, FINALIZE_TIMEOUT).await?;
|
||||
drop(handle);
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
finalized
|
||||
};
|
||||
|
||||
// Restore the early snapshot: the local store now lags the finalized channel.
|
||||
std::fs::remove_dir_all(&rocksdb).context("Failed to remove store before restore")?;
|
||||
copy_dir_recursive(snapshot.path(), &rocksdb).context("Failed to restore snapshot")?;
|
||||
|
||||
// Restart: reconstruction must catch the lagging store up to the channel.
|
||||
let handle = SequencerSetup::new(slow_blocks(), bedrock_addr)
|
||||
.with_genesis(genesis)
|
||||
.setup_at(home.path())
|
||||
.await
|
||||
.context("Failed to restart sequencer from a lagging store")?;
|
||||
let client = sequencer_client(handle.addr())?;
|
||||
let tip = client.get_last_block_id().await?;
|
||||
assert!(
|
||||
tip >= finalized,
|
||||
"lagging store must reconstruct forward to the finalized tip; tip={tip}, finalized={finalized}"
|
||||
);
|
||||
assert!(
|
||||
tip > SNAPSHOT_TIP,
|
||||
"reconstruction must advance beyond the snapshot tip; tip={tip}"
|
||||
);
|
||||
assert_eq!(
|
||||
client.get_account_balance(vault_id).await?,
|
||||
5_005,
|
||||
"genesis state must be intact after reconstruction"
|
||||
);
|
||||
assert!(handle.is_healthy(), "sequencer must stay healthy");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -8,12 +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, token_send_claiming_new_account, verify_commitment_is_in_state,
|
||||
};
|
||||
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
|
||||
use log::info;
|
||||
use sequencer_service_rpc::RpcClient as _;
|
||||
use token_core::{TokenDefinition, TokenHolding};
|
||||
use tokio::test;
|
||||
use wallet::{
|
||||
@ -30,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();
|
||||
@ -92,10 +52,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
|
||||
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, programs::token().id());
|
||||
@ -109,10 +66,7 @@ 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, programs::token().id());
|
||||
@ -125,28 +79,20 @@ async fn create_and_transfer_public_token() -> Result<()> {
|
||||
}
|
||||
);
|
||||
|
||||
// Transfer 7 tokens from supply_acc to recipient_account_id
|
||||
// Transfer 7 tokens from supply_acc to recipient_account_id. `recipient_account_id` is
|
||||
// still unclaimed, so this bypasses the wallet CLI (which never signs with the recipient's
|
||||
// key) and signs with the recipient's own key directly.
|
||||
let transfer_amount = 7;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::Send {
|
||||
from: public_mention(supply_account_id),
|
||||
to: Some(public_mention(recipient_account_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: transfer_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;
|
||||
token_send_claiming_new_account(
|
||||
&mut ctx,
|
||||
supply_account_id,
|
||||
recipient_account_id,
|
||||
transfer_amount,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Check the status of the supply account after transfer
|
||||
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, programs::token().id());
|
||||
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
||||
assert_eq!(
|
||||
@ -158,10 +104,7 @@ 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?;
|
||||
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!(
|
||||
@ -186,10 +129,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!(
|
||||
@ -202,10 +142,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!(
|
||||
@ -234,10 +171,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!(
|
||||
@ -250,10 +184,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!(
|
||||
@ -274,52 +205,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();
|
||||
@ -337,10 +229,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 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, programs::token().id());
|
||||
@ -402,10 +291,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!(
|
||||
@ -448,36 +334,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();
|
||||
@ -502,10 +362,7 @@ 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, programs::token().id());
|
||||
let token_holding = TokenHolding::try_from(&supply_acc.data)?;
|
||||
@ -518,36 +375,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;
|
||||
@ -583,10 +414,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!(
|
||||
@ -646,36 +474,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();
|
||||
@ -722,20 +524,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;
|
||||
@ -804,52 +593,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();
|
||||
@ -884,10 +634,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,
|
||||
@ -928,52 +675,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();
|
||||
@ -1029,10 +737,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,
|
||||
@ -1052,36 +757,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();
|
||||
@ -1099,20 +778,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
|
||||
@ -1143,8 +809,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
|
||||
@ -1224,10 +889,7 @@ 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, programs::token().id());
|
||||
@ -1240,10 +902,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,
|
||||
@ -1263,20 +922,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");
|
||||
@ -1296,20 +942,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;
|
||||
@ -1324,26 +957,30 @@ 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;
|
||||
|
||||
// Transfer token using from_label instead of from
|
||||
// Confirm the label resolves to the account created for it.
|
||||
let resolved_sender = ctx
|
||||
.wallet()
|
||||
.storage()
|
||||
.resolve_label(&supply_label)
|
||||
.context("supply_label should resolve to an account")?;
|
||||
assert_eq!(
|
||||
resolved_sender,
|
||||
wallet::account::AccountIdWithPrivacy::Public(supply_account_id)
|
||||
);
|
||||
|
||||
// Transfer token from the label-resolved account. `recipient_account_id` is still
|
||||
// unclaimed, so this bypasses the wallet CLI (which never signs with the recipient's key)
|
||||
// and signs with the recipient's own key directly.
|
||||
let transfer_amount = 20;
|
||||
let subcommand = TokenProgramAgnosticSubcommand::Send {
|
||||
from: supply_label.into(),
|
||||
to: Some(public_mention(recipient_account_id)),
|
||||
to_npk: None,
|
||||
to_vpk: None,
|
||||
to_keys: None,
|
||||
to_identifier: Some(0),
|
||||
amount: transfer_amount,
|
||||
};
|
||||
wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?;
|
||||
token_send_claiming_new_account(
|
||||
&mut ctx,
|
||||
supply_account_id,
|
||||
recipient_account_id,
|
||||
transfer_amount,
|
||||
)
|
||||
.await?;
|
||||
|
||||
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,
|
||||
|
||||
@ -15,7 +15,6 @@ use anyhow::{Context as _, Result};
|
||||
use bytesize::ByteSize;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{TestContext, config::SequencerPartialConfig};
|
||||
use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder;
|
||||
use lee::{
|
||||
Account, AccountId, PrivacyPreservingTransaction, PrivateKey, PublicKey, PublicTransaction,
|
||||
privacy_preserving_transaction::{self as pptx, circuit},
|
||||
@ -23,7 +22,7 @@ use lee::{
|
||||
public_transaction as putx,
|
||||
};
|
||||
use lee_core::{
|
||||
EncryptedAccountData, InputAccountIdentity, MembershipProof, NullifierPublicKey,
|
||||
DUMMY_COMMITMENT_HASH, InputAccountIdentity, MembershipProof, NullifierPublicKey,
|
||||
account::{AccountWithMetadata, Nonce, data::Data},
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
@ -266,25 +265,17 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
data: Data::default(),
|
||||
},
|
||||
true,
|
||||
AccountId::for_regular_private_account(&sender_npk, 0),
|
||||
AccountId::for_regular_private_account(&sender_npk, &sender_vpk, 0),
|
||||
);
|
||||
let recipient_nsk = [2; 32];
|
||||
let recipient_vpk = ViewingPublicKey::from_seed(&[101_u8; 32], &[102_u8; 32]);
|
||||
let recipient_npk = NullifierPublicKey::from(&recipient_nsk);
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
AccountId::for_regular_private_account(&recipient_npk, 0),
|
||||
true,
|
||||
AccountId::for_regular_private_account(&recipient_npk, &recipient_vpk, 0),
|
||||
);
|
||||
|
||||
let eph_holder_from = EphemeralKeyHolder::new(&sender_vpk);
|
||||
let sender_ss = eph_holder_from.calculate_shared_secret_sender();
|
||||
let sender_epk = eph_holder_from.ephemeral_public_key().clone();
|
||||
|
||||
let eph_holder_to = EphemeralKeyHolder::new(&recipient_vpk);
|
||||
let recipient_ss = eph_holder_to.calculate_shared_secret_sender();
|
||||
let recipient_epk = eph_holder_to.ephemeral_public_key().clone();
|
||||
|
||||
let balance_to_move: u128 = 1;
|
||||
let proof: MembershipProof = (
|
||||
1,
|
||||
@ -301,19 +292,19 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: sender_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&sender_npk, &sender_vpk),
|
||||
ssk: sender_ss,
|
||||
vpk: sender_vpk,
|
||||
random_seed: [0; 32],
|
||||
view_tag: 0,
|
||||
nsk: sender_nsk,
|
||||
membership_proof: proof,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: recipient_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
|
||||
InputAccountIdentity::PrivateForeignInit {
|
||||
vpk: recipient_vpk,
|
||||
random_seed: [0; 32],
|
||||
npk: recipient_npk,
|
||||
ssk: recipient_ss,
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
|
||||
116
integration_tests/tests/two_zone.rs
Normal file
116
integration_tests/tests/two_zone.rs
Normal file
@ -0,0 +1,116 @@
|
||||
#![expect(
|
||||
clippy::tests_outside_test_module,
|
||||
reason = "top-level test functions are conventional for integration tests"
|
||||
)]
|
||||
|
||||
//! Two zones (sequencer + indexer each, on separate channels) sharing one
|
||||
//! Bedrock node, each producing and finalizing blocks independently.
|
||||
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use indexer_service_rpc::RpcClient as _;
|
||||
use integration_tests::{
|
||||
config::{self, SequencerPartialConfig},
|
||||
indexer_client::IndexerClient,
|
||||
setup::{SequencerSetup, setup_bedrock_node, setup_indexer},
|
||||
};
|
||||
use sequencer_service_rpc::{RpcClient as _, SequencerClientBuilder};
|
||||
use tokio::test;
|
||||
|
||||
const ZONE_LIVE_TIMEOUT: Duration = Duration::from_secs(360);
|
||||
|
||||
// Genesis is block 1, so reaching 2 means a block was produced past it.
|
||||
const MIN_BLOCK_ID: u64 = 2;
|
||||
|
||||
#[test]
|
||||
async fn two_zones_share_one_bedrock_and_both_advance() -> Result<()> {
|
||||
// Declared first so it outlives both zones (drops run in reverse order).
|
||||
let (_bedrock, bedrock_addr) = setup_bedrock_node()
|
||||
.await
|
||||
.context("Failed to set up shared Bedrock node")?;
|
||||
|
||||
let partial = SequencerPartialConfig::default();
|
||||
let channel_a = config::bedrock_channel_id();
|
||||
let channel_b = config::bedrock_channel_id_b();
|
||||
|
||||
// Empty genesis is enough: the clock transaction drives block production.
|
||||
let (seq_a, _seq_a_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_a)
|
||||
.with_genesis(vec![])
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up zone A sequencer")?;
|
||||
let (idx_a, _idx_a_home) = setup_indexer(bedrock_addr, channel_a, None)
|
||||
.await
|
||||
.context("Failed to set up zone A indexer")?;
|
||||
let (seq_b, _seq_b_home) = SequencerSetup::new(partial, bedrock_addr)
|
||||
.with_channel_id(channel_b)
|
||||
.with_genesis(vec![])
|
||||
.setup()
|
||||
.await
|
||||
.context("Failed to set up zone B sequencer")?;
|
||||
let (idx_b, _idx_b_home) = setup_indexer(bedrock_addr, channel_b, None)
|
||||
.await
|
||||
.context("Failed to set up zone B indexer")?;
|
||||
|
||||
let (height_a, height_b) = tokio::try_join!(
|
||||
wait_until_zone_live("A", seq_a.addr(), idx_a.addr()),
|
||||
wait_until_zone_live("B", seq_b.addr(), idx_b.addr()),
|
||||
)?;
|
||||
|
||||
assert!(
|
||||
height_a >= MIN_BLOCK_ID,
|
||||
"Zone A indexer only reached block {height_a}, expected >= {MIN_BLOCK_ID}"
|
||||
);
|
||||
assert!(
|
||||
height_b >= MIN_BLOCK_ID,
|
||||
"Zone B indexer only reached block {height_b}, expected >= {MIN_BLOCK_ID}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wait for the sequencer to produce past genesis and the indexer to finalize up
|
||||
/// to it. Returns the indexer's finalized block id.
|
||||
async fn wait_until_zone_live(
|
||||
label: &str,
|
||||
sequencer_addr: SocketAddr,
|
||||
indexer_addr: SocketAddr,
|
||||
) -> Result<u64> {
|
||||
let sequencer_url = config::addr_to_url(config::UrlProtocol::Http, sequencer_addr)
|
||||
.context("Failed to build sequencer URL")?;
|
||||
let sequencer = SequencerClientBuilder::default()
|
||||
.build(sequencer_url)
|
||||
.context("Failed to build sequencer client")?;
|
||||
|
||||
let indexer_url = config::addr_to_url(config::UrlProtocol::Ws, indexer_addr)
|
||||
.context("Failed to build indexer URL")?;
|
||||
let indexer = IndexerClient::new(&indexer_url)
|
||||
.await
|
||||
.context("Failed to build indexer client")?;
|
||||
|
||||
let wait = async {
|
||||
loop {
|
||||
if sequencer.get_last_block_id().await? >= MIN_BLOCK_ID {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
let target = sequencer.get_last_block_id().await?;
|
||||
loop {
|
||||
let finalized = indexer.get_last_finalized_block_id().await?.unwrap_or(0);
|
||||
if finalized >= target {
|
||||
log::info!(
|
||||
"Zone {label} live: sequencer at {target}, indexer finalized {finalized}"
|
||||
);
|
||||
return Ok::<u64, anyhow::Error>(finalized);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
};
|
||||
|
||||
tokio::time::timeout(ZONE_LIVE_TIMEOUT, wait)
|
||||
.await
|
||||
.with_context(|| format!("Zone {label} did not become live within {ZONE_LIVE_TIMEOUT:?}"))?
|
||||
}
|
||||
@ -30,7 +30,7 @@ async fn public_transfer_and_public_claim() -> Result<()> {
|
||||
.get_account_balance(recipient_vault_id)
|
||||
.await?;
|
||||
|
||||
let transfer_result = wallet::cli::execute_subcommand(
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Vault(VaultSubcommand::Transfer {
|
||||
from: public_mention(sender),
|
||||
@ -39,10 +39,6 @@ async fn public_transfer_and_public_claim() -> Result<()> {
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
matches!(transfer_result, SubcommandReturnValue::Empty),
|
||||
"Expected Empty return value for public vault transfer"
|
||||
);
|
||||
|
||||
let sender_balance_after_transfer = ctx.sequencer_client().get_account_balance(sender).await?;
|
||||
let recipient_balance_after_transfer = ctx
|
||||
@ -64,7 +60,7 @@ async fn public_transfer_and_public_claim() -> Result<()> {
|
||||
recipient_vault_balance_before + amount
|
||||
);
|
||||
|
||||
let claim_result = wallet::cli::execute_subcommand(
|
||||
wallet::cli::execute_subcommand(
|
||||
ctx.wallet_mut(),
|
||||
Command::Vault(VaultSubcommand::Claim {
|
||||
account_id: public_mention(recipient),
|
||||
@ -72,10 +68,6 @@ async fn public_transfer_and_public_claim() -> Result<()> {
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
matches!(claim_result, SubcommandReturnValue::Empty),
|
||||
"Expected Empty return value for public vault claim"
|
||||
);
|
||||
|
||||
let sender_balance_after_claim = ctx.sequencer_client().get_account_balance(sender).await?;
|
||||
let recipient_balance_after_claim = ctx
|
||||
@ -138,9 +130,9 @@ async fn private_transfer_and_private_claim() -> Result<()> {
|
||||
assert!(
|
||||
matches!(
|
||||
transfer_result,
|
||||
SubcommandReturnValue::PrivacyPreservingTransfer { .. }
|
||||
SubcommandReturnValue::TransactionExecuted { .. }
|
||||
),
|
||||
"Expected PrivacyPreservingTransfer return value for private vault transfer"
|
||||
"Expected TransactionExecuted return value for private vault transfer"
|
||||
);
|
||||
|
||||
let sender_balance_after_transfer = ctx
|
||||
@ -179,9 +171,9 @@ async fn private_transfer_and_private_claim() -> Result<()> {
|
||||
assert!(
|
||||
matches!(
|
||||
claim_result,
|
||||
SubcommandReturnValue::PrivacyPreservingTransfer { .. }
|
||||
SubcommandReturnValue::TransactionExecuted { .. }
|
||||
),
|
||||
"Expected PrivacyPreservingTransfer return value for private vault claim"
|
||||
"Expected TransactionExecuted return value for private vault claim"
|
||||
);
|
||||
|
||||
let sender_balance_after_claim = ctx
|
||||
|
||||
@ -16,6 +16,7 @@ use std::{
|
||||
ffi::{CStr, CString, c_char},
|
||||
io::Write as _,
|
||||
path::Path,
|
||||
str::FromStr as _,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@ -27,12 +28,13 @@ use lee::{
|
||||
};
|
||||
use lee_core::program::DEFAULT_PROGRAM_ID;
|
||||
use log::info;
|
||||
use tempfile::tempdir;
|
||||
use wallet::{account::HumanReadableAccount, program_facades::vault::Vault};
|
||||
use wallet_ffi::{
|
||||
FfiAccount, FfiAccountIdentity, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys,
|
||||
FfiProgramId, 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,
|
||||
};
|
||||
|
||||
@ -40,12 +42,14 @@ unsafe extern "C" {
|
||||
fn wallet_ffi_create_new(
|
||||
config_path: *const c_char,
|
||||
storage_path: *const c_char,
|
||||
metrics_path: *const c_char,
|
||||
password: *const c_char,
|
||||
) -> FfiCreateWalletOutput;
|
||||
|
||||
fn wallet_ffi_open(
|
||||
config_path: *const c_char,
|
||||
storage_path: *const c_char,
|
||||
metrics_path: *const c_char,
|
||||
) -> *mut WalletHandle;
|
||||
|
||||
fn wallet_ffi_destroy(handle: *mut WalletHandle);
|
||||
@ -166,13 +170,13 @@ 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_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,
|
||||
@ -257,6 +261,29 @@ 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(
|
||||
@ -265,6 +292,7 @@ fn new_wallet_ffi_with_test_context_config(
|
||||
) -> Result<FfiCreateWalletOutput> {
|
||||
let config_path = home.join("wallet_config.json");
|
||||
let storage_path = home.join("storage.json");
|
||||
let metrics_path = home.join("metrics.json");
|
||||
let mut config = ctx.ctx().wallet().config().to_owned();
|
||||
if let Some(config_overrides) = ctx.ctx().wallet().config_overrides().clone() {
|
||||
config.apply_overrides(config_overrides);
|
||||
@ -281,12 +309,14 @@ fn new_wallet_ffi_with_test_context_config(
|
||||
|
||||
let config_path = CString::new(config_path.to_str().unwrap())?;
|
||||
let storage_path = CString::new(storage_path.to_str().unwrap())?;
|
||||
let metrics_path = CString::new(metrics_path.to_str().unwrap())?;
|
||||
let password = CString::new(ctx.ctx().wallet_password())?;
|
||||
|
||||
let create_wallet_result = unsafe {
|
||||
wallet_ffi_create_new(
|
||||
config_path.as_ptr(),
|
||||
storage_path.as_ptr(),
|
||||
metrics_path.as_ptr(),
|
||||
password.as_ptr(),
|
||||
)
|
||||
};
|
||||
@ -340,47 +370,37 @@ fn new_wallet_ffi_with_test_context_config(
|
||||
Ok(create_wallet_result)
|
||||
}
|
||||
|
||||
fn new_wallet_ffi_with_default_config(password: &str) -> Result<FfiCreateWalletOutput> {
|
||||
let tempdir = tempdir()?;
|
||||
let config_path = tempdir.path().join("wallet_config.json");
|
||||
let storage_path = tempdir.path().join("storage.json");
|
||||
let config_path_c = CString::new(config_path.to_str().unwrap())?;
|
||||
let storage_path_c = CString::new(storage_path.to_str().unwrap())?;
|
||||
let password = CString::new(password)?;
|
||||
|
||||
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> {
|
||||
let config_path = home.join("wallet_config.json");
|
||||
let storage_path = home.join("storage.json");
|
||||
let metrics_path = home.join("metrics.json");
|
||||
let config_path = CString::new(config_path.to_str().unwrap())?;
|
||||
let storage_path = CString::new(storage_path.to_str().unwrap())?;
|
||||
let metrics_path = CString::new(metrics_path.to_str().unwrap())?;
|
||||
|
||||
Ok(unsafe { wallet_ffi_open(config_path.as_ptr(), storage_path.as_ptr()) })
|
||||
Ok(unsafe {
|
||||
wallet_ffi_open(
|
||||
config_path.as_ptr(),
|
||||
storage_path.as_ptr(),
|
||||
metrics_path.as_ptr(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_ffi_create_public_accounts() -> Result<()> {
|
||||
let password = "password_for_tests";
|
||||
let ctx = BlockingTestContext::new()?;
|
||||
let n_accounts = 10;
|
||||
|
||||
// Create `n_accounts` public accounts with wallet FFI
|
||||
let new_public_account_ids_ffi = unsafe {
|
||||
let mut account_ids = Vec::new();
|
||||
|
||||
let home = tempfile::tempdir()?;
|
||||
let FfiCreateWalletOutput {
|
||||
wallet: wallet_ffi_handle,
|
||||
mnemonic: _,
|
||||
} = new_wallet_ffi_with_default_config(password)?;
|
||||
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
|
||||
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();
|
||||
@ -410,16 +430,17 @@ fn wallet_ffi_create_public_accounts() -> Result<()> {
|
||||
|
||||
#[test]
|
||||
fn wallet_ffi_create_private_accounts() -> Result<()> {
|
||||
let password = "password_for_tests";
|
||||
let ctx = BlockingTestContext::new()?;
|
||||
let n_accounts = 10;
|
||||
// Create `n_accounts` receiving keys with wallet FFI
|
||||
let new_npks_ffi = unsafe {
|
||||
let mut npks = Vec::new();
|
||||
|
||||
let home = tempfile::tempdir()?;
|
||||
let FfiCreateWalletOutput {
|
||||
wallet: wallet_ffi_handle,
|
||||
mnemonic: _,
|
||||
} = new_wallet_ffi_with_default_config(password)?;
|
||||
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
|
||||
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();
|
||||
@ -484,14 +505,14 @@ fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> {
|
||||
|
||||
#[test]
|
||||
fn test_wallet_ffi_list_accounts() -> Result<()> {
|
||||
let password = "password_for_tests";
|
||||
|
||||
let ctx = BlockingTestContext::new()?;
|
||||
// Create the wallet FFI and track which account IDs were created as public/private
|
||||
let (wallet_ffi_handle, created_public_ids) = unsafe {
|
||||
let home = tempfile::tempdir()?;
|
||||
let FfiCreateWalletOutput {
|
||||
wallet: handle,
|
||||
mnemonic: _,
|
||||
} = new_wallet_ffi_with_default_config(password)?;
|
||||
} = new_wallet_ffi_with_test_context_config(&ctx, home.path())?;
|
||||
let mut public_ids: Vec<[u8; 32]> = Vec::new();
|
||||
|
||||
// Create 5 public accounts and 5 receiving keys
|
||||
@ -986,7 +1007,11 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
|
||||
let (to, to_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(), 0_u128);
|
||||
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)
|
||||
};
|
||||
@ -1129,7 +1154,11 @@ fn test_wallet_ffi_transfer_private() -> Result<()> {
|
||||
let (to, to_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(), 0_u128);
|
||||
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)
|
||||
};
|
||||
@ -1210,7 +1239,11 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
|
||||
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(), 0_u128);
|
||||
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)
|
||||
};
|
||||
@ -1218,7 +1251,11 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
|
||||
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(), 0_u128);
|
||||
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)
|
||||
};
|
||||
@ -1484,68 +1521,68 @@ fn restore_keys_from_seed_ffi() -> Result<()> {
|
||||
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;
|
||||
// #[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();
|
||||
}
|
||||
// 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));
|
||||
// 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 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)
|
||||
};
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
// unsafe {
|
||||
// wallet_ffi_free_transfer_result(&raw mut transfer_result);
|
||||
// wallet_ffi_destroy(wallet_ffi_handle);
|
||||
// }
|
||||
|
||||
Ok(())
|
||||
}
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_wallet_ffi_transfer_generic_public() -> Result<()> {
|
||||
@ -1926,3 +1963,125 @@ fn test_wallet_ffi_vault_balance_and_claim_private() -> Result<()> {
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _};
|
||||
use lee_core::{
|
||||
SharedSecretKey,
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
Identifier, SharedSecretKey,
|
||||
encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN, ViewingPublicKey},
|
||||
program::{PdaSeed, ProgramId},
|
||||
};
|
||||
use rand::{RngCore as _, rngs::OsRng};
|
||||
@ -146,11 +146,28 @@ impl GroupKeyHolder {
|
||||
SecretSpendingKey(hasher.finalize_fixed().into()).produce_private_key_holder(None)
|
||||
}
|
||||
|
||||
/// Derive keys for a shared regular account from its `identifier`.
|
||||
///
|
||||
/// Computes the derivation seed via the `SharedAccountTag` domain separator, then delegates
|
||||
/// to [`Self::derive_keys_for_shared_account`].
|
||||
#[must_use]
|
||||
pub fn derive_regular_shared_account_keys_from_identifier(
|
||||
&self,
|
||||
identifier: Identifier,
|
||||
) -> PrivateKeyHolder {
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00";
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(PREFIX);
|
||||
hasher.update(identifier.to_le_bytes());
|
||||
let derivation_seed: [u8; 32] = hasher.finalize().into();
|
||||
self.derive_keys_for_shared_account(&derivation_seed)
|
||||
}
|
||||
|
||||
/// Encrypts this holder's GMS under the recipient's [`SealingPublicKey`].
|
||||
///
|
||||
/// 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 +187,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 +203,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<Self, SealError> {
|
||||
// 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());
|
||||
|
||||
@ -334,10 +351,10 @@ mod tests {
|
||||
let program_id: ProgramId = [9; 8];
|
||||
|
||||
let holder = GroupKeyHolder::from_gms(gms);
|
||||
let npk = holder
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed)
|
||||
.generate_nullifier_public_key();
|
||||
let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX);
|
||||
let keys = holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &seed);
|
||||
let npk = keys.generate_nullifier_public_key();
|
||||
let vpk = keys.generate_viewing_public_key();
|
||||
let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, &vpk, u128::MAX);
|
||||
|
||||
let expected_npk = NullifierPublicKey([
|
||||
136, 176, 234, 71, 208, 8, 143, 142, 126, 155, 132, 18, 71, 27, 88, 56, 100, 90, 79,
|
||||
@ -346,7 +363,7 @@ mod tests {
|
||||
// AccountId is derived from (program_id, seed, npk), so it changes when npk changes.
|
||||
// We verify npk is pinned, and AccountId is deterministically derived from it.
|
||||
let expected_account_id =
|
||||
AccountId::for_private_pda(&program_id, &seed, &expected_npk, u128::MAX);
|
||||
AccountId::for_private_pda(&program_id, &seed, &expected_npk, &vpk, u128::MAX);
|
||||
|
||||
assert_eq!(npk, expected_npk);
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
@ -543,13 +560,16 @@ mod tests {
|
||||
let bob_holder =
|
||||
GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS");
|
||||
|
||||
let bob_npk = bob_holder
|
||||
.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed)
|
||||
.generate_nullifier_public_key();
|
||||
let bob_group_keys = bob_holder.derive_keys_for_pda(&TEST_PROGRAM_ID, &pda_seed);
|
||||
let bob_npk = bob_group_keys.generate_nullifier_public_key();
|
||||
assert_eq!(alice_npk, bob_npk);
|
||||
|
||||
let alice_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk, 0);
|
||||
let bob_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk, 0);
|
||||
let alice_vpk = alice_keys.generate_viewing_public_key();
|
||||
let bob_group_vpk = bob_group_keys.generate_viewing_public_key();
|
||||
let alice_account_id =
|
||||
AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk, &alice_vpk, 0);
|
||||
let bob_account_id =
|
||||
AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk, &bob_group_vpk, 0);
|
||||
assert_eq!(alice_account_id, bob_account_id);
|
||||
}
|
||||
|
||||
|
||||
@ -139,7 +139,7 @@ impl ChainIndex {
|
||||
.map(|item| Self(item.into_iter().copied().collect()))
|
||||
}
|
||||
|
||||
pub fn chain_ids_at_depth(depth: usize) -> impl Iterator<Item = Self> {
|
||||
fn collect_chain_ids_at_depth(depth: usize) -> Vec<Self> {
|
||||
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<Item = Self> {
|
||||
Self::collect_chain_ids_at_depth(depth).into_iter().unique()
|
||||
}
|
||||
|
||||
pub fn chain_ids_at_depth_rev(depth: usize) -> impl Iterator<Item = Self> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<u32>) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,10 +94,11 @@ impl KeyTreeNode for ChildKeysPrivate {
|
||||
|
||||
fn account_ids(&self) -> impl Iterator<Item = lee::AccountId> {
|
||||
let npk = self.value.0.nullifier_public_key;
|
||||
let vpk = self.value.0.viewing_public_key.clone();
|
||||
self.value
|
||||
.1
|
||||
.keys()
|
||||
.map(move |kind| lee::AccountId::for_private_account(&npk, kind))
|
||||
.map(move |kind| lee::AccountId::for_private_account(&npk, &vpk, kind))
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,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,
|
||||
@ -179,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,
|
||||
@ -253,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([
|
||||
@ -293,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,
|
||||
|
||||
@ -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<u32>) -> 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);
|
||||
|
||||
|
||||
@ -39,16 +39,7 @@ impl<N: KeyTreeNode> KeyTree<N> {
|
||||
.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<N: KeyTreeNode> KeyTree<N> {
|
||||
}
|
||||
}
|
||||
|
||||
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<ChainIndex> {
|
||||
let parent_keys = self.key_map.get(parent_cci)?;
|
||||
let next_child_id = self
|
||||
@ -71,14 +71,8 @@ impl<N: KeyTreeNode> KeyTree<N> {
|
||||
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<ChainIndex> {
|
||||
@ -86,14 +80,8 @@ impl<N: KeyTreeNode> KeyTree<N> {
|
||||
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<N: KeyTreeNode> KeyTree<N> {
|
||||
}
|
||||
|
||||
impl KeyTree<ChildKeysPublic> {
|
||||
/// 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.
|
||||
@ -277,6 +268,7 @@ impl KeyTree<ChildKeysPrivate> {
|
||||
let node = self.key_map.get(cci)?;
|
||||
let account_id = lee::AccountId::for_regular_private_account(
|
||||
&node.value.0.nullifier_public_key,
|
||||
&node.value.0.viewing_public_key,
|
||||
identifier,
|
||||
);
|
||||
if self.account_id_map.contains_key(&account_id) {
|
||||
@ -322,6 +314,16 @@ impl KeyTree<ChildKeysPrivate> {
|
||||
}
|
||||
}
|
||||
|
||||
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")]
|
||||
@ -339,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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<u8> = 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 = <MlKem768 as Kem>::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();
|
||||
|
||||
@ -15,3 +15,6 @@ workspace = true
|
||||
[dependencies]
|
||||
lee_core.workspace = true
|
||||
risc0-zkvm.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
lee_core = { workspace = true, features = ["host"] }
|
||||
|
||||
@ -6,6 +6,7 @@ use std::{
|
||||
use lee_core::{
|
||||
Identifier, InputAccountIdentity, NullifierPublicKey,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
program::{
|
||||
AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID,
|
||||
MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow,
|
||||
@ -21,7 +22,7 @@ pub struct ExecutionState {
|
||||
block_validity_window: BlockValidityWindow,
|
||||
timestamp_validity_window: TimestampValidityWindow,
|
||||
/// Positions (in `pre_states`) of private-PDA accounts whose supplied npk has been bound to
|
||||
/// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk,
|
||||
/// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk, vpk,
|
||||
/// identifier)` check.
|
||||
/// Two proof paths populate this set: a `Claim::Pda(seed)` in a program's `post_state` on
|
||||
/// that `pre_state`, or a caller's `ChainedCall.pda_seeds` entry matching that `pre_state`
|
||||
@ -43,12 +44,13 @@ pub struct ExecutionState {
|
||||
/// `AccountId` entry or as an equality check against the existing one, making the rule: one
|
||||
/// `(program, seed)` → one account per tx.
|
||||
pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
/// Map from a private-PDA `pre_state`'s position in `account_identities` to the (npk,
|
||||
/// Map from a private-PDA `pre_state`'s position in `account_identities` to the (npk, vpk,
|
||||
/// identifier) supplied for that position. Built once in `derive_from_outputs` by walking
|
||||
/// `account_identities` and consulting `npk_if_private_pda`. Used later by the claim and
|
||||
/// `account_identities` and consulting `npk_vpk_if_private_pda`. Used later by the claim and
|
||||
/// caller-seeds authorization paths to verify
|
||||
/// `AccountId::for_private_pda(program_id, seed, npk, identifier) == pre_state.account_id`.
|
||||
private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)>,
|
||||
/// `AccountId::for_private_pda(program_id, seed, npk, vpk, identifier) ==
|
||||
/// pre_state.account_id`.
|
||||
private_pda_by_position: HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
|
||||
authorized_accounts: HashSet<AccountId>,
|
||||
}
|
||||
|
||||
@ -63,11 +65,13 @@ impl ExecutionState {
|
||||
// in `account_identities`. The vec is documented as 1:1 with the program's pre_state
|
||||
// order, so position here matches `pre_state_position` used downstream in
|
||||
// `validate_and_sync_states`.
|
||||
let mut private_pda_npk_by_position: HashMap<usize, (NullifierPublicKey, Identifier)> =
|
||||
HashMap::new();
|
||||
let mut private_pda_by_position: HashMap<
|
||||
usize,
|
||||
(NullifierPublicKey, ViewingPublicKey, Identifier),
|
||||
> = HashMap::new();
|
||||
for (pos, account_identity) in account_identities.iter().enumerate() {
|
||||
if let Some((npk, identifier)) = account_identity.npk_if_private_pda() {
|
||||
private_pda_npk_by_position.insert(pos, (npk, identifier));
|
||||
if let Some((npk, vpk, identifier)) = account_identity.npk_vpk_if_private_pda() {
|
||||
private_pda_by_position.insert(pos, (npk, vpk, identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +111,7 @@ impl ExecutionState {
|
||||
timestamp_validity_window,
|
||||
private_pda_bound_positions: HashMap::new(),
|
||||
pda_family_binding: HashMap::new(),
|
||||
private_pda_npk_by_position,
|
||||
private_pda_by_position,
|
||||
authorized_accounts: HashSet::new(),
|
||||
};
|
||||
|
||||
@ -289,7 +293,7 @@ impl ExecutionState {
|
||||
let is_authorized = resolve_authorization_and_record_bindings(
|
||||
&mut self.pda_family_binding,
|
||||
&mut self.private_pda_bound_positions,
|
||||
&self.private_pda_npk_by_position,
|
||||
&self.private_pda_by_position,
|
||||
&mut self.authorized_accounts,
|
||||
pre_account_id,
|
||||
pre_state_position,
|
||||
@ -309,6 +313,7 @@ impl ExecutionState {
|
||||
let external_seed = match account_identities.get(pre_state_position) {
|
||||
Some(InputAccountIdentity::PrivatePdaInit {
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
..
|
||||
@ -317,6 +322,7 @@ impl ExecutionState {
|
||||
authority_program_id,
|
||||
seed,
|
||||
npk,
|
||||
vpk,
|
||||
*identifier,
|
||||
);
|
||||
assert_eq!(
|
||||
@ -327,6 +333,7 @@ impl ExecutionState {
|
||||
}
|
||||
Some(InputAccountIdentity::PrivatePdaUpdate {
|
||||
nsk,
|
||||
vpk,
|
||||
identifier,
|
||||
seed: Some((seed, authority_program_id)),
|
||||
..
|
||||
@ -336,6 +343,7 @@ impl ExecutionState {
|
||||
authority_program_id,
|
||||
seed,
|
||||
&npk,
|
||||
vpk,
|
||||
*identifier,
|
||||
);
|
||||
assert_eq!(
|
||||
@ -416,14 +424,19 @@ impl ExecutionState {
|
||||
match claim {
|
||||
Claim::Authorized => {}
|
||||
Claim::Pda(seed) => {
|
||||
let (npk, identifier) = self
|
||||
.private_pda_npk_by_position
|
||||
let (npk, vpk, identifier) = self
|
||||
.private_pda_by_position
|
||||
.get(&pre_state_position)
|
||||
.expect(
|
||||
"private PDA pre_state must have an npk in the position map",
|
||||
);
|
||||
let pda =
|
||||
AccountId::for_private_pda(&program_id, &seed, npk, *identifier);
|
||||
let pda = AccountId::for_private_pda(
|
||||
&program_id,
|
||||
&seed,
|
||||
npk,
|
||||
vpk,
|
||||
*identifier,
|
||||
);
|
||||
assert_eq!(
|
||||
pre_account_id, pda,
|
||||
"Invalid private PDA claim for account {pre_account_id}"
|
||||
@ -548,7 +561,7 @@ fn bind_private_pda_position(
|
||||
fn resolve_authorization_and_record_bindings(
|
||||
pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>,
|
||||
private_pda_bound_positions: &mut HashMap<usize, (ProgramId, PdaSeed)>,
|
||||
private_pda_npk_by_position: &HashMap<usize, (NullifierPublicKey, Identifier)>,
|
||||
private_pda_by_position: &HashMap<usize, (NullifierPublicKey, ViewingPublicKey, Identifier)>,
|
||||
authorized_accounts: &mut HashSet<AccountId>,
|
||||
pre_account_id: AccountId,
|
||||
pre_state_position: usize,
|
||||
@ -562,9 +575,10 @@ fn resolve_authorization_and_record_bindings(
|
||||
if AccountId::for_public_pda(&caller, seed) == pre_account_id {
|
||||
return Some((*seed, false, caller));
|
||||
}
|
||||
if let Some((npk, identifier)) =
|
||||
private_pda_npk_by_position.get(&pre_state_position)
|
||||
&& AccountId::for_private_pda(&caller, seed, npk, *identifier) == pre_account_id
|
||||
if let Some((npk, vpk, identifier)) =
|
||||
private_pda_by_position.get(&pre_state_position)
|
||||
&& AccountId::for_private_pda(&caller, seed, npk, vpk, *identifier)
|
||||
== pre_account_id
|
||||
{
|
||||
return Some((*seed, true, caller));
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ fn main() {
|
||||
program_outputs,
|
||||
account_identities,
|
||||
program_id,
|
||||
dummy_inputs,
|
||||
} = env::read();
|
||||
|
||||
let execution_state = execution_state::ExecutionState::derive_from_outputs(
|
||||
@ -17,7 +18,7 @@ fn main() {
|
||||
program_outputs,
|
||||
);
|
||||
|
||||
let output = output::compute_circuit_output(execution_state, &account_identities);
|
||||
let output = output::compute_circuit_output(execution_state, &account_identities, dummy_inputs);
|
||||
|
||||
env::commit(&output);
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
use lee_core::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralPublicKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
Commitment, CommitmentSetDigest, DummyInput, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralSecretKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
|
||||
account::{Account, AccountId, Nonce},
|
||||
compute_digest_for_path,
|
||||
encryption::{ViewTag, ViewingPublicKey},
|
||||
};
|
||||
|
||||
use crate::execution_state::ExecutionState;
|
||||
@ -11,6 +12,7 @@ use crate::execution_state::ExecutionState;
|
||||
pub fn compute_circuit_output(
|
||||
execution_state: ExecutionState,
|
||||
account_identities: &[InputAccountIdentity],
|
||||
dummy_inputs: Vec<DummyInput>,
|
||||
) -> PrivacyPreservingCircuitOutput {
|
||||
let (block_validity_window, timestamp_validity_window, pda_seed_by_position, states_iter) =
|
||||
execution_state.into_parts();
|
||||
@ -30,7 +32,6 @@ pub fn compute_circuit_output(
|
||||
"Invalid account_identities length"
|
||||
);
|
||||
|
||||
let mut output_index = 0;
|
||||
for (pos, (account_identity, (pre_state, post_state))) in
|
||||
account_identities.iter().zip(states_iter).enumerate()
|
||||
{
|
||||
@ -40,14 +41,14 @@ pub fn compute_circuit_output(
|
||||
output.public_post_states.push(post_state);
|
||||
}
|
||||
InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk,
|
||||
view_tag,
|
||||
ssk,
|
||||
vpk,
|
||||
random_seed,
|
||||
nsk,
|
||||
identifier,
|
||||
commitment_root,
|
||||
} => {
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, *identifier);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, vpk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert!(
|
||||
@ -62,33 +63,33 @@ 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);
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(&npk, vpk);
|
||||
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
view_tag,
|
||||
vpk,
|
||||
random_seed,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk,
|
||||
vpk,
|
||||
random_seed,
|
||||
view_tag,
|
||||
ssk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier,
|
||||
} => {
|
||||
let npk = NullifierPublicKey::from(nsk);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, *identifier);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, vpk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert!(
|
||||
@ -106,25 +107,24 @@ pub fn compute_circuit_output(
|
||||
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
vpk,
|
||||
random_seed,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag,
|
||||
InputAccountIdentity::PrivateForeignInit {
|
||||
vpk,
|
||||
random_seed,
|
||||
npk,
|
||||
ssk,
|
||||
identifier,
|
||||
commitment_root,
|
||||
} => {
|
||||
let account_id = AccountId::for_regular_private_account(npk, *identifier);
|
||||
let account_id = AccountId::for_regular_private_account(npk, vpk, *identifier);
|
||||
|
||||
assert_eq!(account_id, pre_state.account_id, "AccountId mismatch");
|
||||
assert_eq!(
|
||||
@ -133,41 +133,41 @@ pub fn compute_circuit_output(
|
||||
"Found new private account with non default values",
|
||||
);
|
||||
assert!(
|
||||
!pre_state.is_authorized,
|
||||
"Found new private account marked as authorized."
|
||||
pre_state.is_authorized,
|
||||
"Found new private account marked as unauthorized."
|
||||
);
|
||||
|
||||
let new_nullifier = (
|
||||
Nullifier::for_account_initialization(&account_id),
|
||||
DUMMY_COMMITMENT_HASH,
|
||||
*commitment_root,
|
||||
);
|
||||
let new_nonce = Nonce::private_account_nonce_init(&account_id);
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(npk, vpk);
|
||||
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
view_tag,
|
||||
vpk,
|
||||
random_seed,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag,
|
||||
npk: _,
|
||||
ssk,
|
||||
vpk,
|
||||
random_seed,
|
||||
npk,
|
||||
identifier,
|
||||
commitment_root,
|
||||
seed: _,
|
||||
} => {
|
||||
// The npk-to-account_id binding is established upstream in
|
||||
// `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds`
|
||||
// match. Here we only enforce the init pre-conditions. The supplied npk on
|
||||
// the variant has been recorded into `private_pda_npk_by_position` and used
|
||||
// the variant has been recorded into `private_pda_by_position` and used
|
||||
// for the binding check; we use `pre_state.account_id` directly for nullifier
|
||||
// and commitment derivation.
|
||||
assert!(
|
||||
@ -182,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);
|
||||
|
||||
@ -190,9 +190,9 @@ pub fn compute_circuit_output(
|
||||
let (authority_program_id, seed) = pda_seed_by_position
|
||||
.get(&pos)
|
||||
.expect("PrivatePdaInit position must be in pda_seed_by_position");
|
||||
let view_tag = EncryptedAccountData::compute_view_tag(npk, vpk);
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Pda {
|
||||
@ -200,17 +200,17 @@ pub fn compute_circuit_output(
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
view_tag,
|
||||
vpk,
|
||||
random_seed,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk,
|
||||
vpk,
|
||||
random_seed,
|
||||
view_tag,
|
||||
ssk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
identifier,
|
||||
@ -240,7 +240,6 @@ pub fn compute_circuit_output(
|
||||
.expect("PrivatePdaUpdate position must be in pda_seed_by_position");
|
||||
emit_private_output(
|
||||
&mut output,
|
||||
&mut output_index,
|
||||
post_state,
|
||||
&account_id,
|
||||
&PrivateAccountKind::Pda {
|
||||
@ -248,9 +247,9 @@ pub fn compute_circuit_output(
|
||||
seed: *seed,
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
vpk,
|
||||
random_seed,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
@ -258,50 +257,85 @@ pub fn compute_circuit_output(
|
||||
}
|
||||
}
|
||||
|
||||
for dummy in dummy_inputs {
|
||||
emit_dummy_output(&mut output, dummy);
|
||||
}
|
||||
|
||||
obfuscate_output_ordering(&mut output);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn obfuscate_output_ordering(output: &mut PrivacyPreservingCircuitOutput) {
|
||||
output
|
||||
.new_commitments
|
||||
.sort_unstable_by_key(Commitment::to_byte_array);
|
||||
|
||||
let mut notes: Vec<_> = core::mem::take(&mut output.new_nullifiers)
|
||||
.into_iter()
|
||||
.zip(core::mem::take(&mut output.encrypted_private_post_states))
|
||||
.collect();
|
||||
notes.sort_unstable_by_key(|((nullifier, _), _)| nullifier.to_byte_array());
|
||||
(output.new_nullifiers, output.encrypted_private_post_states) = notes.into_iter().unzip();
|
||||
}
|
||||
|
||||
fn emit_dummy_output(output: &mut PrivacyPreservingCircuitOutput, dummy: DummyInput) {
|
||||
// Note: the nullifiers and commitments are generated from seeds.
|
||||
// The prover is responsible for their randomness.
|
||||
let nullifier = Nullifier::for_dummy(&dummy.nullifier_seed);
|
||||
let commitment = Commitment::for_dummy(&nullifier, &dummy.commitment_seed);
|
||||
output
|
||||
.new_nullifiers
|
||||
.push((nullifier, dummy.commitment_root));
|
||||
output.new_commitments.push(commitment);
|
||||
// Note: the encrypted post states are pushed as fed into the circuit.
|
||||
// That means that the prover is responsible for managing the randomness
|
||||
// so as to not reveal the padding.
|
||||
//
|
||||
// In particular, it is recommended to generate the ML KEM ciphertext
|
||||
// explicitly as these are not uniformly random.
|
||||
output.encrypted_private_post_states.push(dummy.note);
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "Inputs are distinct concerns from the variant arms; bundling would be artificial"
|
||||
)]
|
||||
fn emit_private_output(
|
||||
output: &mut PrivacyPreservingCircuitOutput,
|
||||
output_index: &mut u32,
|
||||
post_state: Account,
|
||||
account_id: &AccountId,
|
||||
kind: &PrivateAccountKind,
|
||||
shared_secret: &SharedSecretKey,
|
||||
epk: &EphemeralPublicKey,
|
||||
view_tag: u8,
|
||||
view_tag: ViewTag,
|
||||
vpk: &ViewingPublicKey,
|
||||
random_seed: &[u8; 32],
|
||||
new_nullifier: (Nullifier, CommitmentSetDigest),
|
||||
new_nonce: Nonce,
|
||||
) {
|
||||
output.new_nullifiers.push(new_nullifier);
|
||||
|
||||
let mut post_with_updated_nonce = post_state;
|
||||
post_with_updated_nonce.nonce = new_nonce;
|
||||
|
||||
let commitment_post = Commitment::new(account_id, &post_with_updated_nonce);
|
||||
|
||||
let esk = EphemeralSecretKey::new(account_id, random_seed, &new_nonce);
|
||||
let (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(vpk, &esk);
|
||||
|
||||
let encrypted_account = EncryptionScheme::encrypt(
|
||||
&post_with_updated_nonce,
|
||||
kind,
|
||||
shared_secret,
|
||||
&commitment_post,
|
||||
*output_index,
|
||||
&shared_secret,
|
||||
&new_nullifier.0,
|
||||
);
|
||||
|
||||
output.new_nullifiers.push(new_nullifier);
|
||||
output.new_commitments.push(commitment_post);
|
||||
output
|
||||
.encrypted_private_post_states
|
||||
.push(EncryptedAccountData {
|
||||
ciphertext: encrypted_account,
|
||||
epk: epk.clone(),
|
||||
epk,
|
||||
view_tag,
|
||||
});
|
||||
*output_index = output_index
|
||||
.checked_add(1)
|
||||
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
|
||||
}
|
||||
|
||||
fn compute_update_nullifier_and_set_digest(
|
||||
@ -315,3 +349,83 @@ fn compute_update_nullifier_and_set_digest(
|
||||
let nullifier = Nullifier::for_account_update(&commitment_pre, nsk);
|
||||
(nullifier, set_digest)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lee_core::{DUMMY_COMMITMENT_HASH, EphemeralPublicKey};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn note(tag: u8) -> (Nullifier, Commitment, EncryptedAccountData) {
|
||||
let nullifier = Nullifier::for_dummy(&[tag; 32]);
|
||||
let commitment = Commitment::for_dummy(&nullifier, &[tag; 32]);
|
||||
let ciphertext = EncryptionScheme::encrypt(
|
||||
&Account::default(),
|
||||
&PrivateAccountKind::Regular(0),
|
||||
&SharedSecretKey([0; 32]),
|
||||
&nullifier,
|
||||
);
|
||||
let encrypted = EncryptedAccountData {
|
||||
ciphertext,
|
||||
epk: EphemeralPublicKey(vec![tag]),
|
||||
view_tag: 0,
|
||||
};
|
||||
(nullifier, commitment, encrypted)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn obfuscate_byte_sorts_commitments_and_nullifiers() {
|
||||
let mut output = PrivacyPreservingCircuitOutput::default();
|
||||
for tag in 0..3 {
|
||||
let (nullifier, commitment, encrypted) = note(tag);
|
||||
output
|
||||
.new_nullifiers
|
||||
.push((nullifier, DUMMY_COMMITMENT_HASH));
|
||||
output.new_commitments.push(commitment);
|
||||
output.encrypted_private_post_states.push(encrypted);
|
||||
}
|
||||
|
||||
obfuscate_output_ordering(&mut output);
|
||||
|
||||
assert!(
|
||||
output
|
||||
.new_commitments
|
||||
.is_sorted_by_key(Commitment::to_byte_array)
|
||||
);
|
||||
assert!(
|
||||
output
|
||||
.new_nullifiers
|
||||
.is_sorted_by_key(|(nullifier, _)| nullifier.to_byte_array())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn obfuscate_keeps_each_nullifier_with_its_ciphertext() {
|
||||
let mut output = PrivacyPreservingCircuitOutput::default();
|
||||
for tag in 0..3 {
|
||||
let (nullifier, _, encrypted) = note(tag);
|
||||
output
|
||||
.new_nullifiers
|
||||
.push((nullifier, DUMMY_COMMITMENT_HASH));
|
||||
output.encrypted_private_post_states.push(encrypted);
|
||||
}
|
||||
let paired: HashMap<[u8; 32], EphemeralPublicKey> = output
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.zip(&output.encrypted_private_post_states)
|
||||
.map(|((nullifier, _), note)| (nullifier.to_byte_array(), note.epk.clone()))
|
||||
.collect();
|
||||
|
||||
obfuscate_output_ordering(&mut output);
|
||||
|
||||
for ((nullifier, _), note) in output
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.zip(&output.encrypted_private_post_states)
|
||||
{
|
||||
assert_eq!(paired[&nullifier.to_byte_array()], note.epk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,10 +28,8 @@ build_utils.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
lee_core = { workspace = true, features = ["test_utils"] }
|
||||
token_core.workspace = true
|
||||
test_methods = { path = "test_methods" }
|
||||
|
||||
env_logger.workspace = true
|
||||
hex-literal = "1.0.0"
|
||||
test-case = "3.3.1"
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ thiserror.workspace = true
|
||||
bytemuck.workspace = true
|
||||
bytesize.workspace = true
|
||||
base58.workspace = true
|
||||
ml-kem = { workspace = true, optional = true, features = ["getrandom"] }
|
||||
ml-kem = { workspace = true }
|
||||
chacha20 = { version = "0.10" }
|
||||
|
||||
[dev-dependencies]
|
||||
@ -24,5 +24,5 @@ serde_json.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
host = ["dep:ml-kem"]
|
||||
host = ["ml-kem/getrandom"]
|
||||
test_utils = ["host"]
|
||||
|
||||
@ -2,9 +2,9 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, SharedSecretKey,
|
||||
NullifierSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::{EncryptedAccountData, EphemeralPublicKey, ViewTag},
|
||||
encryption::{EncryptedAccountData, ViewTag, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
@ -14,15 +14,14 @@ pub struct PrivacyPreservingCircuitInput {
|
||||
pub program_outputs: Vec<ProgramOutput>,
|
||||
/// One entry per `pre_state`, in the same order as the program's `pre_states`.
|
||||
/// Length must equal the number of `pre_states` derived from `program_outputs`.
|
||||
/// The guest's `private_pda_npk_by_position` and `private_pda_bound_positions`
|
||||
/// The guest's `private_pda_by_position` and `private_pda_bound_positions`
|
||||
/// rely on this position alignment.
|
||||
pub account_identities: Vec<InputAccountIdentity>,
|
||||
/// Program ID.
|
||||
pub program_id: ProgramId,
|
||||
pub dummy_inputs: Vec<DummyInput>,
|
||||
}
|
||||
|
||||
/// Per-account input to the privacy-preserving circuit. Each variant carries exactly the fields
|
||||
/// the guest needs for that account's code path.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub enum InputAccountIdentity {
|
||||
/// Public account. The guest reads pre/post state from `program_outputs` and emits no
|
||||
@ -30,47 +29,47 @@ pub enum InputAccountIdentity {
|
||||
Public,
|
||||
/// Init of an authorized standalone private account: no membership proof. The `pre_state`
|
||||
/// must be `Account::default()`. The `account_id` is derived as
|
||||
/// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), identifier)` and
|
||||
/// matched against `pre_state.account_id`.
|
||||
/// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), vpk, identifier)`
|
||||
/// and matched against `pre_state.account_id`.
|
||||
PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
vpk: ViewingPublicKey,
|
||||
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.
|
||||
PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
random_seed: [u8; 32],
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
identifier: Identifier,
|
||||
},
|
||||
/// Init of a standalone private account the caller does not own (e.g. a recipient who
|
||||
/// doesn't yet exist on chain). No `nsk`, no membership proof.
|
||||
PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
PrivateForeignInit {
|
||||
vpk: ViewingPublicKey,
|
||||
random_seed: [u8; 32],
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
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
|
||||
/// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it
|
||||
/// as the 4th input.
|
||||
PrivatePdaInit {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
vpk: ViewingPublicKey,
|
||||
random_seed: [u8; 32],
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
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, identifier) ==
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) ==
|
||||
/// pre_state.account_id` rather than requiring a `Claim::Pda` or caller
|
||||
/// `pda_seeds` to establish the binding. The `pre_state` must have `is_authorized
|
||||
/// == false`.
|
||||
@ -80,21 +79,35 @@ pub enum InputAccountIdentity {
|
||||
/// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a
|
||||
/// previously-seen authorization in a chained call.
|
||||
PrivatePdaUpdate {
|
||||
epk: EphemeralPublicKey,
|
||||
vpk: ViewingPublicKey,
|
||||
random_seed: [u8; 32],
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
identifier: Identifier,
|
||||
/// 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, identifier) ==
|
||||
/// `AccountId::for_private_pda(authority_program_id, seed, npk, vpk, identifier) ==
|
||||
/// pre_state.account_id` rather than requiring a caller `pda_seeds` to establish
|
||||
/// the binding. The `pre_state` must have `is_authorized == false`.
|
||||
seed: Option<(PdaSeed, ProgramId)>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A struct containing necessary data for dummy nullifier and
|
||||
/// commitment generation.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DummyInput {
|
||||
/// The seed used for generating the dummy nullifier.
|
||||
pub nullifier_seed: [u8; 32],
|
||||
/// The seed used for generating the dummy commitment.
|
||||
pub commitment_seed: [u8; 32],
|
||||
/// The dummy ciphertext, epk, and view tag.
|
||||
pub note: EncryptedAccountData,
|
||||
/// The dummy root.
|
||||
pub commitment_root: CommitmentSetDigest,
|
||||
}
|
||||
|
||||
impl InputAccountIdentity {
|
||||
#[must_use]
|
||||
pub const fn is_public(&self) -> bool {
|
||||
@ -109,27 +122,33 @@ impl InputAccountIdentity {
|
||||
)
|
||||
}
|
||||
|
||||
/// For private PDA variants, return the `(npk, identifier)` pair. `Init` carries both
|
||||
/// directly; `Update` derives `npk` from `nsk`. For non-PDA variants returns `None`.
|
||||
#[must_use]
|
||||
pub fn npk_if_private_pda(&self) -> Option<(NullifierPublicKey, Identifier)> {
|
||||
pub fn npk_vpk_if_private_pda(
|
||||
&self,
|
||||
) -> Option<(NullifierPublicKey, ViewingPublicKey, Identifier)> {
|
||||
match self {
|
||||
Self::PrivatePdaInit {
|
||||
npk, identifier, ..
|
||||
} => Some((*npk, *identifier)),
|
||||
npk,
|
||||
vpk,
|
||||
identifier,
|
||||
..
|
||||
} => Some((*npk, vpk.clone(), *identifier)),
|
||||
Self::PrivatePdaUpdate {
|
||||
nsk, identifier, ..
|
||||
} => Some((NullifierPublicKey::from(nsk), *identifier)),
|
||||
nsk,
|
||||
vpk,
|
||||
identifier,
|
||||
..
|
||||
} => Some((NullifierPublicKey::from(nsk), vpk.clone(), *identifier)),
|
||||
Self::Public
|
||||
| Self::PrivateAuthorizedInit { .. }
|
||||
| Self::PrivateAuthorizedUpdate { .. }
|
||||
| Self::PrivateUnauthorized { .. } => None,
|
||||
| Self::PrivateForeignInit { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq, Default))]
|
||||
pub struct PrivacyPreservingCircuitOutput {
|
||||
pub public_pre_states: Vec<AccountWithMetadata>,
|
||||
pub public_post_states: Vec<Account>,
|
||||
@ -158,7 +177,7 @@ mod tests {
|
||||
use crate::{
|
||||
Commitment, Nullifier,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce},
|
||||
encryption::Ciphertext,
|
||||
encryption::{Ciphertext, EphemeralPublicKey},
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
||||
@ -2,7 +2,10 @@ use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::account::{Account, AccountId};
|
||||
use crate::{
|
||||
Nullifier,
|
||||
account::{Account, AccountId},
|
||||
};
|
||||
|
||||
/// A commitment to all zero data.
|
||||
/// ```python
|
||||
@ -32,7 +35,7 @@ pub const DUMMY_COMMITMENT_HASH: [u8; 32] = [
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)
|
||||
derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)
|
||||
)]
|
||||
pub struct Commitment(pub(super) [u8; 32]);
|
||||
|
||||
@ -78,6 +81,15 @@ impl Commitment {
|
||||
bytes.extend_from_slice(&account_bytes_with_hashed_data);
|
||||
Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn for_dummy(nullifier: &Nullifier, commitment_seed: &[u8; 32]) -> Self {
|
||||
const DUMMY_PREFIX: &[u8; 32] = b"/LEE/v0.3/Commitment/Dummy/\x00\x00\x00\x00\x00";
|
||||
let mut bytes = DUMMY_PREFIX.to_vec();
|
||||
bytes.extend_from_slice(&nullifier.0);
|
||||
bytes.extend_from_slice(commitment_seed);
|
||||
Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
pub type CommitmentSetDigest = [u8; 32];
|
||||
@ -117,7 +129,7 @@ mod tests {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
use crate::{
|
||||
Commitment, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH,
|
||||
Commitment, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, Nullifier,
|
||||
account::{Account, AccountId},
|
||||
};
|
||||
|
||||
@ -138,4 +150,18 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(DUMMY_COMMITMENT_HASH, expected_dummy_commitment_hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_dummy_matches_pinned_value() {
|
||||
let nullifier = Nullifier::for_dummy(&[0; 32]);
|
||||
let commitment_seed = [1; 32];
|
||||
let expected_commitment = Commitment([
|
||||
106, 88, 233, 248, 28, 251, 254, 48, 62, 53, 61, 248, 25, 148, 223, 133, 108, 213, 184,
|
||||
83, 73, 145, 122, 104, 89, 220, 111, 132, 40, 87, 12, 105,
|
||||
]);
|
||||
assert_eq!(
|
||||
Commitment::for_dummy(&nullifier, &commitment_seed),
|
||||
expected_commitment
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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::{
|
||||
@ -97,11 +97,6 @@ impl NullifierPublicKey {
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
impl Nullifier {
|
||||
#[must_use]
|
||||
pub const fn to_byte_array(&self) -> [u8; 32] {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[must_use]
|
||||
pub const fn from_byte_array(bytes: [u8; 32]) -> Self {
|
||||
@ -168,7 +163,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<Self, LeeCoreError> {
|
||||
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))
|
||||
}
|
||||
|
||||
@ -5,20 +5,46 @@ use chacha20::{
|
||||
};
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "host")]
|
||||
pub use shared_key_derivation::{MlKem768EncapsulationKey, ViewingPublicKey};
|
||||
|
||||
use crate::{Commitment, account::Account, program::PrivateAccountKind};
|
||||
#[cfg(feature = "host")]
|
||||
use crate::{Nullifier, 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)]
|
||||
pub struct EphemeralSecretKey(pub [u8; 32]);
|
||||
|
||||
impl EphemeralSecretKey {
|
||||
/// Derives an ephemeral secret key from OS randomness and account-specific values.
|
||||
///
|
||||
/// For updates, `nonce` carries `nsk`-derived entropy, making `esk` strong even
|
||||
/// with a compromised RNG. For inits, `nonce` is deterministic, so `random_seed`
|
||||
/// is the sole entropy source.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
account_id: &crate::account::AccountId,
|
||||
random_seed: &[u8; 32],
|
||||
nonce: &crate::account::Nonce,
|
||||
) -> Self {
|
||||
const PREFIX: &[u8; 14] = b"/LEE/v0.3/esk/";
|
||||
let mut input = [0_u8; 14 + 32 + 32 + 16];
|
||||
input[0..14].copy_from_slice(PREFIX);
|
||||
input[14..46].copy_from_slice(account_id.value());
|
||||
input[46..78].copy_from_slice(random_seed);
|
||||
input[78..94].copy_from_slice(&nonce.0.to_le_bytes());
|
||||
Self(Impl::hash_bytes(&input).as_bytes().try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
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<u8>);
|
||||
|
||||
@ -52,6 +78,18 @@ pub struct EncryptedAccountData {
|
||||
pub view_tag: ViewTag,
|
||||
}
|
||||
|
||||
impl EncryptedAccountData {
|
||||
#[must_use]
|
||||
pub fn compute_view_tag(npk: &crate::NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag {
|
||||
const PREFIX: &[u8; 18] = b"/LEE/v0.3/ViewTag/";
|
||||
let mut bytes = [0_u8; 18 + 32 + ViewingPublicKey::LEN];
|
||||
bytes[0..18].copy_from_slice(PREFIX);
|
||||
bytes[18..50].copy_from_slice(&npk.to_byte_array());
|
||||
bytes[50..].copy_from_slice(vpk.to_bytes());
|
||||
Impl::hash_bytes(&bytes).as_bytes()[0]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
impl EncryptedAccountData {
|
||||
#[must_use]
|
||||
@ -68,16 +106,6 @@ impl EncryptedAccountData {
|
||||
view_tag,
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the tag as the first byte of SHA256("/LEE/v0.3/ViewTag/" || npk || vpk).
|
||||
#[must_use]
|
||||
pub fn compute_view_tag(npk: &crate::NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag {
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(b"/LEE/v0.3/ViewTag/");
|
||||
bytes.extend_from_slice(&npk.to_byte_array());
|
||||
bytes.extend_from_slice(vpk.to_bytes());
|
||||
Impl::hash_bytes(&bytes).as_bytes()[0]
|
||||
}
|
||||
}
|
||||
|
||||
impl EncryptionScheme {
|
||||
@ -86,39 +114,32 @@ impl EncryptionScheme {
|
||||
account: &Account,
|
||||
kind: &PrivateAccountKind,
|
||||
shared_secret: &SharedSecretKey,
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
nullifier: &Nullifier,
|
||||
) -> Ciphertext {
|
||||
// Plaintext: PrivateAccountKind::HEADER_LEN bytes header || account bytes.
|
||||
// Both variants produce the same header length — see PrivateAccountKind::to_header_bytes.
|
||||
let mut buffer = kind.to_header_bytes().to_vec();
|
||||
buffer.extend_from_slice(&account.to_bytes());
|
||||
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
|
||||
Self::symmetric_transform(&mut buffer, shared_secret, nullifier);
|
||||
Ciphertext(buffer)
|
||||
}
|
||||
|
||||
fn symmetric_transform(
|
||||
buffer: &mut [u8],
|
||||
shared_secret: &SharedSecretKey,
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
nullifier: &Nullifier,
|
||||
) {
|
||||
let key = Self::kdf(shared_secret, commitment, output_index);
|
||||
let key = Self::kdf(shared_secret, nullifier);
|
||||
let mut cipher = ChaCha20::new(&key.into(), &[0; 12].into());
|
||||
cipher.apply_keystream(buffer);
|
||||
}
|
||||
|
||||
fn kdf(
|
||||
shared_secret: &SharedSecretKey,
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
) -> [u8; 32] {
|
||||
let mut bytes = Vec::new();
|
||||
|
||||
bytes.extend_from_slice(b"LEE/v0.2/KDF-SHA256/");
|
||||
bytes.extend_from_slice(&shared_secret.0);
|
||||
bytes.extend_from_slice(&commitment.to_byte_array());
|
||||
bytes.extend_from_slice(&output_index.to_le_bytes());
|
||||
fn kdf(shared_secret: &SharedSecretKey, nullifier: &Nullifier) -> [u8; 32] {
|
||||
const PREFIX: &[u8; 20] = b"LEE/v0.3/KDF-SHA256/";
|
||||
let mut bytes = [0_u8; 20 + 32 + 32];
|
||||
bytes[0..20].copy_from_slice(PREFIX);
|
||||
bytes[20..52].copy_from_slice(&shared_secret.0);
|
||||
bytes[52..84].copy_from_slice(&nullifier.to_byte_array());
|
||||
|
||||
Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap()
|
||||
}
|
||||
@ -132,12 +153,11 @@ impl EncryptionScheme {
|
||||
pub fn decrypt(
|
||||
ciphertext: &Ciphertext,
|
||||
shared_secret: &SharedSecretKey,
|
||||
commitment: &Commitment,
|
||||
output_index: u32,
|
||||
nullifier: &Nullifier,
|
||||
) -> Option<(PrivateAccountKind, Account)> {
|
||||
use std::io::Cursor;
|
||||
let mut buffer = ciphertext.0.clone();
|
||||
Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index);
|
||||
Self::symmetric_transform(&mut buffer, shared_secret, nullifier);
|
||||
|
||||
if buffer.len() < PrivateAccountKind::HEADER_LEN {
|
||||
return None;
|
||||
@ -152,8 +172,7 @@ impl EncryptionScheme {
|
||||
println!(
|
||||
"Failed to decode {ciphertext:?} \n
|
||||
with secret {:?} ,\n
|
||||
commitment {commitment:?} ,\n
|
||||
and output_index {output_index} ,\n
|
||||
nullifier {nullifier:?} ,\n
|
||||
with error {err:?}",
|
||||
shared_secret.0
|
||||
);
|
||||
@ -175,14 +194,13 @@ mod tests {
|
||||
fn encrypt_same_length_for_account_and_pda() {
|
||||
let account = Account::default();
|
||||
let secret = SharedSecretKey([0_u8; 32]);
|
||||
let commitment = crate::Commitment::new(&AccountId::new([0_u8; 32]), &Account::default());
|
||||
let nullifier = Nullifier::for_account_initialization(&AccountId::new([0_u8; 32]));
|
||||
|
||||
let account_ct = EncryptionScheme::encrypt(
|
||||
&account,
|
||||
&PrivateAccountKind::Regular(42),
|
||||
&secret,
|
||||
&commitment,
|
||||
0,
|
||||
&nullifier,
|
||||
);
|
||||
let pda_ct = EncryptionScheme::encrypt(
|
||||
&account,
|
||||
@ -192,8 +210,7 @@ mod tests {
|
||||
identifier: 42,
|
||||
},
|
||||
&secret,
|
||||
&commitment,
|
||||
0,
|
||||
&nullifier,
|
||||
);
|
||||
|
||||
assert_eq!(account_ct.0.len(), pda_ct.0.len());
|
||||
@ -217,11 +234,11 @@ mod tests {
|
||||
..Account::default()
|
||||
};
|
||||
let kind = PrivateAccountKind::Regular(0);
|
||||
let commitment = crate::Commitment::new(&AccountId::new([7_u8; 32]), &account);
|
||||
let nullifier = Nullifier::for_account_initialization(&AccountId::new([7_u8; 32]));
|
||||
|
||||
let ct = EncryptionScheme::encrypt(&account, &kind, &sender_ss, &commitment, 0);
|
||||
let ct = EncryptionScheme::encrypt(&account, &kind, &sender_ss, &nullifier);
|
||||
let (decoded_kind, decoded_account) =
|
||||
EncryptionScheme::decrypt(&ct, &receiver_ss, &commitment, 0)
|
||||
EncryptionScheme::decrypt(&ct, &receiver_ss, &nullifier)
|
||||
.expect("decryption must succeed with correct shared secret");
|
||||
|
||||
assert_eq!(decoded_account, account);
|
||||
@ -229,10 +246,55 @@ mod tests {
|
||||
|
||||
// Wrong shared secret must not decrypt correctly.
|
||||
let wrong_ss = SharedSecretKey([0_u8; 32]);
|
||||
let bad = EncryptionScheme::decrypt(&ct, &wrong_ss, &commitment, 0);
|
||||
let bad_via_ss = EncryptionScheme::decrypt(&ct, &wrong_ss, &nullifier);
|
||||
assert!(
|
||||
bad.is_none() || bad.is_some_and(|(_, a)| a.balance != 999),
|
||||
bad_via_ss.is_none() || bad_via_ss.is_some_and(|(_, a)| a.balance != 999),
|
||||
"wrong shared secret must not produce the correct plaintext"
|
||||
);
|
||||
|
||||
// Wrong nullifier must not decrypt correctly.
|
||||
let wrong_nullifier = Nullifier::for_account_initialization(&AccountId::new([9; 32]));
|
||||
let bad_via_nlf = EncryptionScheme::decrypt(&ct, &receiver_ss, &wrong_nullifier);
|
||||
assert!(
|
||||
bad_via_nlf.is_none() || bad_via_nlf.is_some_and(|(_, a)| a.balance != 999),
|
||||
"wrong nullifier must not produce the correct plaintext"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esk_is_deterministic() {
|
||||
let account_id = AccountId::new([1_u8; 32]);
|
||||
let random_seed = [2_u8; 32];
|
||||
let nonce = crate::account::Nonce(42);
|
||||
let esk1 = EphemeralSecretKey::new(&account_id, &random_seed, &nonce);
|
||||
let esk2 = EphemeralSecretKey::new(&account_id, &random_seed, &nonce);
|
||||
assert_eq!(esk1.0, esk2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esk_differs_for_different_account_id() {
|
||||
let random_seed = [2_u8; 32];
|
||||
let nonce = crate::account::Nonce(42);
|
||||
let esk_a = EphemeralSecretKey::new(&AccountId::new([0_u8; 32]), &random_seed, &nonce);
|
||||
let esk_b = EphemeralSecretKey::new(&AccountId::new([1_u8; 32]), &random_seed, &nonce);
|
||||
assert_ne!(esk_a.0, esk_b.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esk_differs_for_different_random_seed() {
|
||||
let account_id = AccountId::new([1_u8; 32]);
|
||||
let nonce = crate::account::Nonce(42);
|
||||
let esk_a = EphemeralSecretKey::new(&account_id, &[0_u8; 32], &nonce);
|
||||
let esk_b = EphemeralSecretKey::new(&account_id, &[1_u8; 32], &nonce);
|
||||
assert_ne!(esk_a.0, esk_b.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esk_differs_for_different_nonce() {
|
||||
let account_id = AccountId::new([1_u8; 32]);
|
||||
let random_seed = [2_u8; 32];
|
||||
let esk_a = EphemeralSecretKey::new(&account_id, &random_seed, &crate::account::Nonce(0));
|
||||
let esk_b = EphemeralSecretKey::new(&account_id, &random_seed, &crate::account::Nonce(1));
|
||||
assert_ne!(esk_a.0, esk_b.0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed};
|
||||
#[cfg(feature = "host")]
|
||||
use ml_kem::Encapsulate as _;
|
||||
use ml_kem::{Decapsulate as _, KeyExport as _, Seed};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{EphemeralPublicKey, SharedSecretKey};
|
||||
@ -26,6 +28,7 @@ impl MlKem768EncapsulationKey {
|
||||
pub const LEN: usize = 1184;
|
||||
|
||||
/// Construct from raw bytes, returning an error if the length is not [`Self::LEN`].
|
||||
#[cfg(feature = "host")]
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, crate::error::LeeCoreError> {
|
||||
if bytes.len() != Self::LEN {
|
||||
return Err(crate::error::LeeCoreError::DeserializationError(format!(
|
||||
@ -59,12 +62,13 @@ impl SharedSecretKey {
|
||||
/// Returns `(shared_secret, ciphertext)`. The ciphertext must be included in the transaction
|
||||
/// as the `EphemeralPublicKey`; the receiver recovers the same shared secret via
|
||||
/// [`Self::decapsulate`].
|
||||
#[cfg(feature = "host")]
|
||||
#[must_use]
|
||||
pub fn encapsulate(ek: &MlKem768EncapsulationKey) -> (Self, EphemeralPublicKey) {
|
||||
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> =
|
||||
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",
|
||||
);
|
||||
@ -76,35 +80,23 @@ impl SharedSecretKey {
|
||||
(Self(ss_bytes), EphemeralPublicKey(ct.to_vec()))
|
||||
}
|
||||
|
||||
/// Deterministically encapsulate a shared secret toward `ek` for use in tests.
|
||||
/// Deterministically encapsulate a shared secret toward `ek` using a
|
||||
/// pre-derived `esk` as the ML-KEM encapsulation randomness.
|
||||
///
|
||||
/// The shared secret has no secret entropy — it is fully determined by `ek`,
|
||||
/// `message_hash`, and `output_index`, all of which are public. This makes it
|
||||
/// unsuitable for real encryption but useful for producing stable, reproducible
|
||||
/// shared secrets in unit tests. Use a distinct `output_index` per output to
|
||||
/// avoid EPK collisions across multiple outputs in the same test.
|
||||
///
|
||||
/// For production use [`Self::encapsulate`], which draws randomness from the OS.
|
||||
#[cfg(any(test, feature = "test_utils"))]
|
||||
/// The `esk` must be derived via `derive_esk(account_id, random_seed, nonce)`
|
||||
/// which binds it to the account and incorporates OS entropy.
|
||||
#[must_use]
|
||||
pub fn encapsulate_deterministic(
|
||||
ek: &MlKem768EncapsulationKey,
|
||||
message_hash: &[u8; 32],
|
||||
output_index: u32,
|
||||
esk: &crate::encryption::EphemeralSecretKey,
|
||||
) -> (Self, EphemeralPublicKey) {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
|
||||
let mut input = Vec::with_capacity(36);
|
||||
input.extend_from_slice(message_hash);
|
||||
input.extend_from_slice(&output_index.to_le_bytes());
|
||||
let hash = Impl::hash_bytes(&input);
|
||||
let m: ml_kem::B32 =
|
||||
ml_kem::array::Array::try_from(hash.as_bytes()).expect("SHA-256 output is 32 bytes");
|
||||
let m: ml_kem::B32 = ml_kem::array::Array::try_from(esk.0.as_slice())
|
||||
.expect("EphemeralSecretKey is 32 bytes");
|
||||
|
||||
let ek_bytes: ml_kem::kem::Key<ml_kem::EncapsulationKey768> =
|
||||
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",
|
||||
);
|
||||
@ -118,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]
|
||||
@ -146,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() {
|
||||
@ -164,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"
|
||||
);
|
||||
}
|
||||
|
||||
@ -177,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"
|
||||
|
||||
@ -4,14 +4,15 @@
|
||||
)]
|
||||
|
||||
pub use circuit_io::{
|
||||
InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
|
||||
DummyInput, InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
|
||||
};
|
||||
pub use commitment::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof,
|
||||
compute_digest_for_path,
|
||||
};
|
||||
pub use encryption::{
|
||||
EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, SharedSecretKey, ViewTag,
|
||||
EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, EphemeralSecretKey,
|
||||
ML_KEM_768_CIPHERTEXT_LEN, SharedSecretKey, ViewTag,
|
||||
};
|
||||
pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey};
|
||||
pub use program::PrivateAccountKind;
|
||||
|
||||
@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Commitment, account::AccountId};
|
||||
use crate::{Commitment, account::AccountId, encryption::ViewingPublicKey};
|
||||
|
||||
const PRIVATE_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/Private/\x00\x00\x00\x00";
|
||||
|
||||
@ -16,12 +16,16 @@ impl AccountId {
|
||||
/// Derives an [`AccountId`] for a regular (non-PDA) private account from the nullifier public
|
||||
/// key and identifier.
|
||||
#[must_use]
|
||||
pub fn for_regular_private_account(npk: &NullifierPublicKey, identifier: Identifier) -> Self {
|
||||
// 32 bytes prefix || 32 bytes npk || 16 bytes identifier
|
||||
let mut bytes = [0; 80];
|
||||
pub fn for_regular_private_account(
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
) -> Self {
|
||||
let mut bytes = [0_u8; 32 + 32 + ViewingPublicKey::LEN + 16];
|
||||
bytes[0..32].copy_from_slice(PRIVATE_ACCOUNT_ID_PREFIX);
|
||||
bytes[32..64].copy_from_slice(&npk.0);
|
||||
bytes[64..80].copy_from_slice(&identifier.to_le_bytes());
|
||||
bytes[64..64 + ViewingPublicKey::LEN].copy_from_slice(vpk.to_bytes());
|
||||
bytes[64 + ViewingPublicKey::LEN..].copy_from_slice(&identifier.to_le_bytes());
|
||||
|
||||
Self::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
@ -32,9 +36,9 @@ impl AccountId {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&NullifierPublicKey, Identifier)> for AccountId {
|
||||
fn from((npk, identifier): (&NullifierPublicKey, Identifier)) -> Self {
|
||||
Self::for_regular_private_account(npk, identifier)
|
||||
impl From<(&NullifierPublicKey, &ViewingPublicKey, Identifier)> for AccountId {
|
||||
fn from((npk, vpk, identifier): (&NullifierPublicKey, &ViewingPublicKey, Identifier)) -> Self {
|
||||
Self::for_regular_private_account(npk, vpk, identifier)
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,6 +109,19 @@ impl Nullifier {
|
||||
bytes.extend_from_slice(account_id.value());
|
||||
Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn for_dummy(nullifier_seed: &[u8; 32]) -> Self {
|
||||
const DUMMY_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Dummy/\x00\x00\x00\x00\x00\x00";
|
||||
let mut bytes = DUMMY_PREFIX.to_vec();
|
||||
bytes.extend_from_slice(nullifier_seed);
|
||||
Self(Impl::hash_bytes(&bytes).as_bytes().try_into().unwrap())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn to_byte_array(&self) -> [u8; 32] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -158,12 +175,13 @@ mod tests {
|
||||
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
|
||||
];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
let expected_account_id = AccountId::new([
|
||||
165, 52, 40, 32, 231, 171, 113, 10, 65, 241, 156, 72, 154, 207, 122, 192, 15, 46, 50,
|
||||
253, 105, 164, 89, 84, 40, 191, 182, 119, 64, 255, 67, 142,
|
||||
242, 239, 57, 244, 89, 109, 65, 201, 223, 100, 43, 87, 205, 83, 148, 161, 176, 22, 208,
|
||||
220, 68, 135, 10, 171, 182, 80, 54, 74, 228, 244, 236, 7,
|
||||
]);
|
||||
|
||||
let account_id = AccountId::for_regular_private_account(&npk, 0);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, &vpk, 0);
|
||||
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
}
|
||||
@ -175,12 +193,13 @@ mod tests {
|
||||
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
|
||||
];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
let expected_account_id = AccountId::new([
|
||||
203, 201, 109, 245, 40, 54, 195, 12, 55, 33, 0, 86, 245, 65, 70, 156, 24, 249, 26, 95,
|
||||
56, 247, 99, 121, 165, 182, 234, 255, 19, 127, 191, 72,
|
||||
149, 125, 157, 109, 119, 81, 9, 163, 231, 181, 214, 43, 57, 113, 221, 72, 180, 149,
|
||||
189, 170, 32, 181, 255, 231, 19, 92, 235, 59, 153, 185, 172, 206,
|
||||
]);
|
||||
|
||||
let account_id = AccountId::for_regular_private_account(&npk, 1);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, &vpk, 1);
|
||||
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
}
|
||||
@ -193,13 +212,24 @@ mod tests {
|
||||
196, 134, 22, 224, 211, 237, 120, 136, 225, 188, 220, 249, 28,
|
||||
];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let vpk = ViewingPublicKey::from_seed(&[1_u8; 32], &[2_u8; 32]);
|
||||
let expected_account_id = AccountId::new([
|
||||
178, 16, 226, 206, 217, 38, 38, 45, 155, 240, 226, 253, 168, 87, 146, 70, 72, 32, 174,
|
||||
19, 245, 25, 214, 162, 209, 135, 252, 82, 27, 2, 174, 196,
|
||||
30, 232, 222, 201, 233, 125, 124, 194, 58, 39, 121, 96, 185, 84, 168, 109, 80, 111,
|
||||
159, 112, 84, 100, 133, 244, 16, 34, 221, 35, 128, 131, 98, 159,
|
||||
]);
|
||||
|
||||
let account_id = AccountId::for_regular_private_account(&npk, identifier);
|
||||
let account_id = AccountId::for_regular_private_account(&npk, &vpk, identifier);
|
||||
|
||||
assert_eq!(account_id, expected_account_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_dummy_matches_pinned_value() {
|
||||
let nullifier_seed = [0; 32];
|
||||
let expected_nullifier = Nullifier([
|
||||
244, 220, 48, 137, 204, 138, 180, 41, 108, 86, 40, 46, 187, 7, 232, 57, 57, 167, 143,
|
||||
157, 125, 171, 137, 46, 64, 206, 191, 211, 231, 0, 11, 86,
|
||||
]);
|
||||
assert_eq!(Nullifier::for_dummy(&nullifier_seed), expected_nullifier);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
BlockId, Identifier, NullifierPublicKey, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
|
||||
pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
|
||||
@ -154,19 +155,21 @@ impl AccountId {
|
||||
program_id: &ProgramId,
|
||||
seed: &PdaSeed,
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
identifier: Identifier,
|
||||
) -> Self {
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
const PRIVATE_PDA_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/PrivatePDA/\x00";
|
||||
|
||||
let mut bytes = [0_u8; 144];
|
||||
let mut bytes = [0_u8; 32 + 32 + 32 + 32 + ViewingPublicKey::LEN + 16];
|
||||
bytes[0..32].copy_from_slice(PRIVATE_PDA_PREFIX);
|
||||
let program_id_bytes: &[u8] =
|
||||
bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]");
|
||||
bytes[32..64].copy_from_slice(program_id_bytes);
|
||||
bytes[64..96].copy_from_slice(&seed.0);
|
||||
bytes[96..128].copy_from_slice(&npk.to_byte_array());
|
||||
bytes[128..144].copy_from_slice(&identifier.to_le_bytes());
|
||||
bytes[128..128 + ViewingPublicKey::LEN].copy_from_slice(vpk.to_bytes());
|
||||
bytes[128 + ViewingPublicKey::LEN..].copy_from_slice(&identifier.to_le_bytes());
|
||||
Self::new(
|
||||
Impl::hash_bytes(&bytes)
|
||||
.as_bytes()
|
||||
@ -177,16 +180,20 @@ impl AccountId {
|
||||
|
||||
/// Derives the [`AccountId`] for a private account from the nullifier public key and kind.
|
||||
#[must_use]
|
||||
pub fn for_private_account(npk: &NullifierPublicKey, kind: &PrivateAccountKind) -> Self {
|
||||
pub fn for_private_account(
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
kind: &PrivateAccountKind,
|
||||
) -> Self {
|
||||
match kind {
|
||||
PrivateAccountKind::Regular(identifier) => {
|
||||
Self::for_regular_private_account(npk, *identifier)
|
||||
Self::for_regular_private_account(npk, vpk, *identifier)
|
||||
}
|
||||
PrivateAccountKind::Pda {
|
||||
program_id,
|
||||
seed,
|
||||
identifier,
|
||||
} => Self::for_private_pda(program_id, seed, npk, *identifier),
|
||||
} => Self::for_private_pda(program_id, seed, npk, vpk, *identifier),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -230,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))]
|
||||
@ -317,7 +324,7 @@ impl AccountPostState {
|
||||
pub type BlockValidityWindow = ValidityWindow<BlockId>;
|
||||
pub type TimestampValidityWindow = ValidityWindow<Timestamp>;
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(
|
||||
any(feature = "host", test),
|
||||
derive(Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)
|
||||
@ -766,338 +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<u64> = 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<u64> = (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<u64> = (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<u64> = (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::<u64>::try_from((Some(5), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_inverted_bounds_are_invalid() {
|
||||
assert!(ValidityWindow::<u64>::try_from((Some(10), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_getters_match_construction() {
|
||||
let w: ValidityWindow<u64> = (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<u64> = ValidityWindow::new_unbounded();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range() {
|
||||
let w: ValidityWindow<u64> = 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::<u64>::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::<u64>::try_from(from..to).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_from() {
|
||||
let w: ValidityWindow<u64> = (5_u64..).into();
|
||||
assert_eq!(w.start(), Some(5));
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_to() {
|
||||
let w: ValidityWindow<u64> = (..10_u64).into();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_full() {
|
||||
let w: ValidityWindow<u64> = (..).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 identifier: Identifier = u128::MAX;
|
||||
let expected = AccountId::new([
|
||||
59, 239, 182, 97, 14, 220, 96, 115, 238, 133, 143, 33, 234, 82, 237, 255, 148, 110, 54,
|
||||
124, 98, 159, 245, 101, 146, 182, 150, 54, 37, 62, 25, 17,
|
||||
]);
|
||||
assert_eq!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 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]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_a, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_b, 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]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed_a, &npk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed_b, &npk, 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]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id_a, &seed, &npk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id_b, &seed, &npk, 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]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 1),
|
||||
);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 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 private_id = AccountId::for_private_pda(&program_id, &seed, &npk, 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 identifier: Identifier = 77;
|
||||
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier)),
|
||||
AccountId::for_regular_private_account(&npk, identifier),
|
||||
);
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(
|
||||
&npk,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id,
|
||||
seed,
|
||||
identifier
|
||||
}
|
||||
),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 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;
|
||||
341
lee/state_machine/core/src/program/tests.rs
Normal file
341
lee/state_machine/core/src/program/tests.rs
Normal file
@ -0,0 +1,341 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validity_window_unbounded_accepts_any_value() {
|
||||
let w: ValidityWindow<u64> = 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<u64> = (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<u64> = (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<u64> = (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::<u64>::try_from((Some(5), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_inverted_bounds_are_invalid() {
|
||||
assert!(ValidityWindow::<u64>::try_from((Some(10), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_getters_match_construction() {
|
||||
let w: ValidityWindow<u64> = (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<u64> = ValidityWindow::new_unbounded();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range() {
|
||||
let w: ValidityWindow<u64> = 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::<u64>::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::<u64>::try_from(from..to).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_from() {
|
||||
let w: ValidityWindow<u64> = (5_u64..).into();
|
||||
assert_eq!(w.start(), Some(5));
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_to() {
|
||||
let w: ValidityWindow<u64> = (..10_u64).into();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_full() {
|
||||
let w: ValidityWindow<u64> = (..).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());
|
||||
}
|
||||
@ -131,6 +131,11 @@ pub enum InvalidProgramBehaviorError {
|
||||
|
||||
#[error("Called program {program_id:?} which is not listed in dependencies")]
|
||||
UndeclaredProgramDependency { program_id: ProgramId },
|
||||
|
||||
#[error(
|
||||
"Account {account_id} was declared in the transaction but is missing from the program output"
|
||||
)]
|
||||
DeclaredAccountMissingFromOutput { account_id: AccountId },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
pub use lee_core::{
|
||||
GENESIS_BLOCK_ID, SharedSecretKey,
|
||||
account::{Account, AccountId, Data},
|
||||
account::{Account, AccountId, Balance, Data},
|
||||
encryption::EphemeralPublicKey,
|
||||
program::ProgramId,
|
||||
};
|
||||
@ -30,6 +30,8 @@ pub mod program_deployment_transaction;
|
||||
pub mod public_transaction;
|
||||
mod signature;
|
||||
mod state;
|
||||
#[cfg(feature = "test-utils")]
|
||||
pub mod test_utils;
|
||||
mod validated_state_diff;
|
||||
|
||||
mod privacy_preserving_circuit {
|
||||
@ -77,6 +79,14 @@ mod test_methods {
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn dropped_account() -> Program {
|
||||
Program::new_unchecked(
|
||||
test_methods::DROPPED_ACCOUNT_ID,
|
||||
Cow::Borrowed(test_methods::DROPPED_ACCOUNT_ELF),
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn program_owner_changer() -> Program {
|
||||
Program::new_unchecked(
|
||||
|
||||
@ -8,8 +8,8 @@ mod default_values;
|
||||
type Value = [u8; 32];
|
||||
type Node = [u8; 32];
|
||||
|
||||
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
|
||||
#[derive(Clone, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct MerkleTree {
|
||||
nodes: Vec<Node>,
|
||||
capacity: usize,
|
||||
@ -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;
|
||||
|
||||
380
lee/state_machine/src/merkle_tree/tests.rs
Normal file
380
lee/state_machine/src/merkle_tree/tests.rs
Normal file
@ -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);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user