diff --git a/.deny.toml b/.deny.toml index ed628f09..e65cdd34 100644 --- a/.deny.toml +++ b/.deny.toml @@ -13,6 +13,7 @@ ignore = [ { id = "RUSTSEC-2025-0055", reason = "`tracing-subscriber` v0.2.25 pulled in by ark-relations v0.4.0 - will be addressed before mainnet" }, { id = "RUSTSEC-2025-0141", reason = "`bincode` is unmaintained but continuing to use it." }, { id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" }, + { id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" }, ] yanked = "deny" unused-ignored-advisory = "deny" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 858a43c9..02381dfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,10 @@ on: - "**.md" - "!.github/workflows/*.yml" +permissions: + contents: read + pull-requests: read + name: General jobs: @@ -19,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - name: Install nightly toolchain for rustfmt run: rustup install nightly --profile minimal --component rustfmt @@ -32,7 +36,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - name: Install taplo-cli run: cargo install --locked taplo-cli @@ -45,7 +49,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - name: Install active toolchain run: rustup install @@ -61,7 +65,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - name: Install cargo-deny run: cargo install --locked cargo-deny @@ -77,7 +81,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - uses: ./.github/actions/install-system-deps @@ -106,7 +110,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - uses: ./.github/actions/install-system-deps @@ -126,7 +130,7 @@ jobs: env: RISC0_DEV_MODE: "1" RUST_LOG: "info" - run: cargo nextest run --workspace --exclude integration_tests + run: cargo nextest run --workspace --exclude integration_tests --all-features integration-tests: runs-on: ubuntu-latest @@ -134,7 +138,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - uses: ./.github/actions/install-system-deps @@ -156,35 +160,33 @@ jobs: RUST_LOG: "info" run: cargo nextest run -p integration_tests -- --skip tps_test --skip indexer - # # TODO: Bring this back once we find the source of the errors. - # # - # integration-tests-indexer: - # runs-on: ubuntu-latest - # timeout-minutes: 60 - # steps: - # - uses: actions/checkout@v5 - # with: - # ref: ${{ github.head_ref }} + integration-tests-indexer: + runs-on: ubuntu-latest + timeout-minutes: 60 + 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-system-deps - # - uses: ./.github/actions/install-risc0 + - uses: ./.github/actions/install-risc0 - # - uses: ./.github/actions/install-logos-blockchain-circuits - # with: - # github-token: ${{ secrets.GITHUB_TOKEN }} + - uses: ./.github/actions/install-logos-blockchain-circuits + with: + github-token: ${{ secrets.GITHUB_TOKEN }} - # - name: Install active toolchain - # run: rustup install + - name: Install active toolchain + run: rustup install - # - name: Install nextest - # run: cargo install --locked cargo-nextest + - name: Install nextest + run: cargo install --locked cargo-nextest - # - name: Run tests - # env: - # RISC0_DEV_MODE: "1" - # RUST_LOG: "info" - # run: cargo nextest run -p integration_tests indexer -- --skip tps_test + - name: Run tests + env: + RISC0_DEV_MODE: "1" + RUST_LOG: "info" + run: cargo nextest run -p integration_tests indexer -- --skip tps_test valid-proof-test: runs-on: ubuntu-latest @@ -192,7 +194,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - uses: ./.github/actions/install-system-deps @@ -218,7 +220,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - ref: ${{ github.head_ref }} + ref: ${{ github.event.pull_request.head.sha || github.head_ref }} - uses: ./.github/actions/install-risc0 diff --git a/.github/workflows/publish_images.yml b/.github/workflows/publish_images.yml index 5076a430..dbf6a68d 100644 --- a/.github/workflows/publish_images.yml +++ b/.github/workflows/publish_images.yml @@ -50,7 +50,7 @@ jobs: type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - type=sha,prefix={{branch}}- + type=sha,prefix=sha- type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image diff --git a/Cargo.lock b/Cargo.lock index cf582227..ca46abde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -503,7 +503,7 @@ dependencies = [ "ark-ff 0.4.2", "ark-std 0.4.0", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.2.25", ] [[package]] @@ -515,7 +515,7 @@ dependencies = [ "ark-ff 0.5.0", "ark-std 0.5.0", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.2.25", ] [[package]] @@ -1137,7 +1137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" dependencies = [ "prost 0.14.3", - "prost-types", + "prost-types 0.14.3", "tonic", "tonic-prost", "ureq", @@ -1462,6 +1462,14 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "clock_core" +version = "0.1.0" +dependencies = [ + "borsh", + "nssa_core", +] + [[package]] name = "cobs" version = "0.3.0" @@ -1511,6 +1519,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "borsh", + "clock_core", "hex", "log", "logos-blockchain-common-http-client", @@ -1706,15 +1715,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpp_demangle" version = "0.4.5" @@ -1757,6 +1757,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1950,7 +1959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.117", + "syn 1.0.109", ] [[package]] @@ -3064,6 +3073,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "html-escape" version = "0.2.13" @@ -4370,7 +4390,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "logos-blockchain-blend-crypto" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "blake2", "logos-blockchain-groth16", @@ -4384,10 +4404,11 @@ dependencies = [ [[package]] name = "logos-blockchain-blend-message" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "blake2", "derivative", + "hex", "itertools 0.14.0", "logos-blockchain-blend-crypto", "logos-blockchain-blend-proofs", @@ -4406,7 +4427,7 @@ dependencies = [ [[package]] name = "logos-blockchain-blend-proofs" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "ed25519-dalek", "generic-array 1.3.5", @@ -4419,12 +4440,13 @@ dependencies = [ "num-bigint 0.4.6", "serde", "thiserror 1.0.69", + "zeroize", ] [[package]] name = "logos-blockchain-chain-broadcast-service" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "async-trait", "derivative", @@ -4440,7 +4462,7 @@ dependencies = [ [[package]] name = "logos-blockchain-chain-service" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "async-trait", "bytes", @@ -4455,6 +4477,7 @@ dependencies = [ "logos-blockchain-services-utils", "logos-blockchain-storage-service", "logos-blockchain-time-service", + "logos-blockchain-tracing", "logos-blockchain-utils", "num-bigint 0.4.6", "overwatch", @@ -4470,7 +4493,7 @@ dependencies = [ [[package]] name = "logos-blockchain-circuits-prover" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "logos-blockchain-circuits-utils", "tempfile", @@ -4479,7 +4502,7 @@ dependencies = [ [[package]] name = "logos-blockchain-circuits-utils" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "dirs", ] @@ -4487,7 +4510,7 @@ dependencies = [ [[package]] name = "logos-blockchain-common-http-client" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "futures", "hex", @@ -4507,7 +4530,7 @@ dependencies = [ [[package]] name = "logos-blockchain-core" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "ark-ff 0.4.2", "bincode", @@ -4537,7 +4560,7 @@ dependencies = [ [[package]] name = "logos-blockchain-cryptarchia-engine" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "cfg_eval", "logos-blockchain-pol", @@ -4553,7 +4576,7 @@ dependencies = [ [[package]] name = "logos-blockchain-cryptarchia-sync" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "bytes", "futures", @@ -4570,7 +4593,7 @@ dependencies = [ [[package]] name = "logos-blockchain-groth16" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "ark-bn254 0.4.0", "ark-ec 0.4.2", @@ -4588,11 +4611,12 @@ dependencies = [ [[package]] name = "logos-blockchain-http-api-common" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "axum 0.7.9", "logos-blockchain-core", "logos-blockchain-key-management-system-keys", + "logos-blockchain-tracing", "serde", "serde_json", "serde_with", @@ -4602,7 +4626,7 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-keys" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "async-trait", "bytes", @@ -4628,7 +4652,7 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-macros" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "proc-macro2", "quote", @@ -4638,7 +4662,7 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-operators" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "async-trait", "logos-blockchain-blend-proofs", @@ -4654,12 +4678,13 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-service" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "async-trait", "log", "logos-blockchain-key-management-system-keys", "logos-blockchain-key-management-system-operators", + "logos-blockchain-tracing", "overwatch", "serde", "thiserror 2.0.18", @@ -4670,7 +4695,7 @@ dependencies = [ [[package]] name = "logos-blockchain-ledger" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "derivative", "logos-blockchain-blend-crypto", @@ -4687,6 +4712,7 @@ dependencies = [ "rand 0.8.5", "rpds", "serde", + "serde_arrays", "thiserror 1.0.69", "tracing", ] @@ -4694,12 +4720,13 @@ dependencies = [ [[package]] name = "logos-blockchain-network-service" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "async-trait", "futures", "logos-blockchain-core", "logos-blockchain-cryptarchia-sync", + "logos-blockchain-tracing", "overwatch", "serde", "tokio", @@ -4710,7 +4737,7 @@ dependencies = [ [[package]] name = "logos-blockchain-poc" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "logos-blockchain-circuits-prover", "logos-blockchain-circuits-utils", @@ -4726,7 +4753,7 @@ dependencies = [ [[package]] name = "logos-blockchain-pol" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "astro-float", "logos-blockchain-circuits-prover", @@ -4745,7 +4772,7 @@ dependencies = [ [[package]] name = "logos-blockchain-poq" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "logos-blockchain-circuits-prover", "logos-blockchain-circuits-utils", @@ -4762,7 +4789,7 @@ dependencies = [ [[package]] name = "logos-blockchain-poseidon2" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "ark-bn254 0.4.0", "ark-ff 0.4.2", @@ -4773,7 +4800,7 @@ dependencies = [ [[package]] name = "logos-blockchain-services-utils" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "async-trait", "futures", @@ -4788,13 +4815,14 @@ dependencies = [ [[package]] name = "logos-blockchain-storage-service" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "async-trait", "bytes", "futures", "logos-blockchain-core", "logos-blockchain-cryptarchia-engine", + "logos-blockchain-tracing", "overwatch", "serde", "thiserror 1.0.69", @@ -4805,12 +4833,13 @@ dependencies = [ [[package]] name = "logos-blockchain-time-service" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "async-trait", "futures", "log", "logos-blockchain-cryptarchia-engine", + "logos-blockchain-tracing", "overwatch", "sntpc", "thiserror 2.0.18", @@ -4820,10 +4849,33 @@ dependencies = [ "tracing", ] +[[package]] +name = "logos-blockchain-tracing" +version = "0.2.1" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +dependencies = [ + "opentelemetry", + "opentelemetry-appender-tracing", + "opentelemetry-http", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "rand 0.8.5", + "serde", + "tokio", + "tracing", + "tracing-appender", + "tracing-gelf", + "tracing-loki", + "tracing-opentelemetry", + "tracing-subscriber 0.3.23", + "url", +] + [[package]] name = "logos-blockchain-utils" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "async-trait", "blake2", @@ -4840,7 +4892,7 @@ dependencies = [ [[package]] name = "logos-blockchain-utxotree" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "ark-ff 0.4.2", "logos-blockchain-groth16", @@ -4854,7 +4906,7 @@ dependencies = [ [[package]] name = "logos-blockchain-witness-generator" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "tempfile", ] @@ -4862,7 +4914,7 @@ dependencies = [ [[package]] name = "logos-blockchain-zksign" version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git#81dbb4517aa466358ed425d92fad7d45a0c419fd" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" dependencies = [ "logos-blockchain-circuits-prover", "logos-blockchain-circuits-utils", @@ -4876,6 +4928,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "loki-api" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdc38a304f59a03e6efa3876766a48c70a766a93f88341c3fff4212834b8e327" +dependencies = [ + "prost 0.13.5", + "prost-types 0.13.5", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -4989,6 +5051,21 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.7.3" @@ -5163,11 +5240,11 @@ dependencies = [ [[package]] name = "multihash" -version = "0.19.3" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +checksum = "89ace881e3f514092ce9efbcb8f413d0ad9763860b828981c2de51ddc666936c" dependencies = [ - "core2", + "no_std_io2", "unsigned-varint", ] @@ -5228,6 +5305,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "no_std_io2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6" +dependencies = [ + "memchr", +] + [[package]] name = "no_std_strings" version = "0.1.3" @@ -5259,6 +5345,7 @@ version = "0.1.0" dependencies = [ "anyhow", "borsh", + "clock_core", "env_logger", "hex", "hex-literal 1.1.0", @@ -5295,6 +5382,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "num" version = "0.4.3" @@ -5535,6 +5631,98 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.18", +] + +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6a1ac5ca3accf562b8c306fa8483c85f4390f768185ab775f242f7fe8fdcc2" +dependencies = [ + "opentelemetry", + "tracing", + "tracing-core", + "tracing-subscriber 0.3.23", +] + +[[package]] +name = "opentelemetry-http" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" +dependencies = [ + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost 0.14.3", + "reqwest", + "thiserror 2.0.18", + "tokio", + "tonic", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost 0.14.3", + "tonic", + "tonic-prost", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" + +[[package]] +name = "opentelemetry_sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.2", + "thiserror 2.0.18", + "tokio", + "tokio-stream", +] + [[package]] name = "optfield" version = "0.4.0" @@ -5897,6 +6085,7 @@ dependencies = [ "amm_program", "ata_core", "ata_program", + "clock_core", "nssa_core", "risc0-zkvm", "serde", @@ -5964,6 +6153,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost 0.13.5", +] + [[package]] name = "prost-types" version = "0.14.3" @@ -6934,7 +7132,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6945,9 +7143,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -7151,6 +7349,7 @@ dependencies = [ "serde_json", "storage", "tempfile", + "test_program_methods", "testnet_initial_state", "tokio", "url", @@ -7215,6 +7414,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_arrays" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a16b99c5ea4fe3daccd14853ad260ec00ea043b2708d1fd1da3106dcd8d9df" +dependencies = [ + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -7463,6 +7671,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -7516,6 +7733,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + [[package]] name = "sntpc" version = "0.5.2" @@ -7831,8 +8054,10 @@ dependencies = [ name = "test_programs" version = "0.1.0" dependencies = [ + "clock_core", "nssa_core", "risc0-zkvm", + "serde", ] [[package]] @@ -7919,6 +8144,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "throw_error" version = "0.3.1" @@ -8319,6 +8553,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +dependencies = [ + "crossbeam-channel", + "thiserror 2.0.18", + "time", + "tracing-subscriber 0.3.23", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -8350,6 +8596,82 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-gelf" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c0170f1bf67b749d4377c2da1d99d6e722600051ee53870cfb6f618611e29e" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "hostname", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "tracing-core", + "tracing-futures", + "tracing-subscriber 0.3.23", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-loki" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3beec919fbdf99d719de8eda6adae3281f8a5b71ae40431f44dc7423053d34" +dependencies = [ + "loki-api", + "reqwest", + "serde", + "serde_json", + "snap", + "tokio", + "tokio-stream", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", + "tracing-subscriber 0.3.23", + "url", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac28f2d093c6c477eaa76b23525478f38de514fa9aeb1285738d4b97a9552fc" +dependencies = [ + "js-sys", + "opentelemetry", + "smallvec", + "tracing", + "tracing-core", + "tracing-subscriber 0.3.23", + "web-time", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.2.25" @@ -8359,6 +8681,24 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "triomphe" version = "0.1.15" @@ -8657,6 +8997,7 @@ dependencies = [ "async-stream", "ata_core", "base58", + "bip39", "clap", "common", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index c2853089..5514c300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "nssa/core", "programs/amm/core", "programs/amm", + "programs/clock/core", "programs/token/core", "programs/token", "programs/associated_token_account/core", @@ -56,6 +57,7 @@ indexer_service_protocol = { path = "indexer/service/protocol" } indexer_service_rpc = { path = "indexer/service/rpc" } wallet = { path = "wallet" } wallet-ffi = { path = "wallet-ffi", default-features = false } +clock_core = { path = "programs/clock/core" } token_core = { path = "programs/token/core" } token_program = { path = "programs/token" } amm_core = { path = "programs/amm/core" } @@ -118,11 +120,11 @@ 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" } -logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git" } -logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git" } -logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git" } -logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git" } +logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" } +logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" } +logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" } +logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" } +logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" } rocksdb = { version = "0.24.0", default-features = false, features = [ "snappy", @@ -150,6 +152,14 @@ opt-level = 'z' lto = true codegen-units = 1 +# 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] +debug = "line-tables-only" + +[profile.test] +debug = "line-tables-only" + [workspace.lints.rust] warnings = "deny" diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index b6867713..148a9403 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index bcd528f1..46326067 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 142397f7..ad40805f 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/program_methods/clock.bin index c3f8af15..e2a6f120 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 280a834f..d0460713 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index 4cd06c56..b0f81f79 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index 55880e41..dcbee51a 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index b9c82387..e0358fa4 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin new file mode 100644 index 00000000..9bd40a30 Binary files /dev/null and b/artifacts/test_program_methods/auth_asserting_noop.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index a740bdb8..0353d78f 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 112ca113..cd74cf3f 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/chain_caller_pda_drop.bin b/artifacts/test_program_methods/chain_caller_pda_drop.bin new file mode 100644 index 00000000..91b42aa2 Binary files /dev/null and b/artifacts/test_program_methods/chain_caller_pda_drop.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index a130510b..1f966bef 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index 41a5cb3b..8a48effd 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin new file mode 100644 index 00000000..e08df712 Binary files /dev/null and b/artifacts/test_program_methods/clock_chain_caller.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 3dddebe1..37abf0f7 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 1d682ec3..ebd53621 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin new file mode 100644 index 00000000..29c660cd Binary files /dev/null and b/artifacts/test_program_methods/flash_swap_callback.bin differ diff --git a/artifacts/test_program_methods/flash_swap_initiator.bin b/artifacts/test_program_methods/flash_swap_initiator.bin new file mode 100644 index 00000000..a560d477 Binary files /dev/null and b/artifacts/test_program_methods/flash_swap_initiator.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index c68496ab..c9d0facd 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/malicious_caller_program_id.bin b/artifacts/test_program_methods/malicious_caller_program_id.bin new file mode 100644 index 00000000..9b31fd7e Binary files /dev/null and b/artifacts/test_program_methods/malicious_caller_program_id.bin differ diff --git a/artifacts/test_program_methods/malicious_self_program_id.bin b/artifacts/test_program_methods/malicious_self_program_id.bin new file mode 100644 index 00000000..c4a2c039 Binary files /dev/null and b/artifacts/test_program_methods/malicious_self_program_id.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index ffd29461..42d2171d 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index a2bbecd8..d2b99291 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index b44b1233..f57ac2f1 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index e006fc75..6b79e074 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index da811f60..eb89f4a9 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/pda_claimer.bin b/artifacts/test_program_methods/pda_claimer.bin new file mode 100644 index 00000000..092a2191 Binary files /dev/null and b/artifacts/test_program_methods/pda_claimer.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin new file mode 100644 index 00000000..559adea4 Binary files /dev/null and b/artifacts/test_program_methods/pinata_cooldown.bin differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin new file mode 100644 index 00000000..d7e81a9f Binary files /dev/null and b/artifacts/test_program_methods/private_pda_delegator.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 3963873e..880e03b1 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index 08db47f0..3a4e811f 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/time_locked_transfer.bin b/artifacts/test_program_methods/time_locked_transfer.bin new file mode 100644 index 00000000..eeb80385 Binary files /dev/null and b/artifacts/test_program_methods/time_locked_transfer.bin differ diff --git a/artifacts/test_program_methods/two_pda_claimer.bin b/artifacts/test_program_methods/two_pda_claimer.bin new file mode 100644 index 00000000..b71d87ab Binary files /dev/null and b/artifacts/test_program_methods/two_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index ceb5ae74..8d749f3c 100644 Binary files a/artifacts/test_program_methods/validity_window.bin and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin index a7661f03..109829d2 100644 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and b/artifacts/test_program_methods/validity_window_chain_caller.bin differ diff --git a/bedrock/cfgsync.yaml b/bedrock/cfgsync.yaml deleted file mode 100644 index e09fe586..00000000 --- a/bedrock/cfgsync.yaml +++ /dev/null @@ -1,12 +0,0 @@ -port: 4400 -n_hosts: 4 -timeout: 10 - -# Tracing -tracing_settings: - logger: Stdout - tracing: None - filter: None - metrics: None - console: None - level: DEBUG diff --git a/bedrock/deployment-settings.yaml b/bedrock/deployment-settings.yaml new file mode 100644 index 00000000..d0c05e24 --- /dev/null +++ b/bedrock/deployment-settings.yaml @@ -0,0 +1,82 @@ +blend: + common: + num_blend_layers: 3 + minimum_network_size: 30 + protocol_name: /blend/integration-tests + data_replication_factor: 0 + core: + scheduler: + cover: + message_frequency_per_round: 1.0 + intervals_for_safety_buffer: 100 + delayer: + maximum_release_delay_in_rounds: 3 + 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 +cryptarchia: + epoch_config: + epoch_stake_distribution_stabilization: 3 + epoch_period_nonce_buffer: 3 + epoch_period_nonce_stabilization: 4 + security_param: 10 + slot_activation_coeff: + numerator: 1 + denominator: 2 + learning_rate: 0.1 + sdp_config: + service_params: + BN: + lock_period: 10 + inactivity_period: 1 + retention_period: 1 + timestamp: 0 + min_stake: + threshold: 1 + timestamp: 0 + gossipsub_protocol: /integration/logos-blockchain/cryptarchia/proto/1.0.0 + genesis_state: + mantle_tx: + ops: + - opcode: 0 + payload: + inputs: [ ] + outputs: + - value: 1 + pk: d204000000000000000000000000000000000000000000000000000000000000 + - value: 100 + pk: 2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26 + - opcode: 17 + payload: + channel_id: "0000000000000000000000000000000000000000000000000000000000000000" + inscription: [ 103, 101, 110, 101, 115, 105, 115 ] # "genesis" in bytes + parent: "0000000000000000000000000000000000000000000000000000000000000000" + signer: "0000000000000000000000000000000000000000000000000000000000000000" + execution_gas_price: 0 + storage_gas_price: 0 + ops_proofs: + - !ZkSig + pi_a: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + pi_b: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + pi_c: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + - NoProof +time: + slot_duration: '1.0' + chain_start_time: PLACEHOLDER_CHAIN_START_TIME +mempool: + pubsub_topic: mantle_e2e_tests diff --git a/bedrock/docker-compose.yml b/bedrock/docker-compose.yml index 4f85bf25..73795666 100644 --- a/bedrock/docker-compose.yml +++ b/bedrock/docker-compose.yml @@ -1,46 +1,12 @@ services: - cfgsync: - image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657 - volumes: - - ./scripts:/etc/logos-blockchain/scripts - - ./cfgsync.yaml:/etc/logos-blockchain/cfgsync.yaml:z - entrypoint: /etc/logos-blockchain/scripts/run_cfgsync.sh - logos-blockchain-node-0: - image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657 + image: ghcr.io/logos-blockchain/logos-blockchain@sha256:c5243681b353278cabb562a176f0a5cfbefc2056f18cebc47fe0e3720c29fb12 ports: - "${PORT:-8080}:18080/tcp" volumes: - ./scripts:/etc/logos-blockchain/scripts - ./kzgrs_test_params:/kzgrs_test_params:z - depends_on: - - cfgsync - entrypoint: /etc/logos-blockchain/scripts/run_logos_blockchain_node.sh - - logos-blockchain-node-1: - image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657 - volumes: - - ./scripts:/etc/logos-blockchain/scripts - - ./kzgrs_test_params:/kzgrs_test_params:z - depends_on: - - cfgsync - entrypoint: /etc/logos-blockchain/scripts/run_logos_blockchain_node.sh - - logos-blockchain-node-2: - image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657 - volumes: - - ./scripts:/etc/logos-blockchain/scripts - - ./kzgrs_test_params:/kzgrs_test_params:z - depends_on: - - cfgsync - entrypoint: /etc/logos-blockchain/scripts/run_logos_blockchain_node.sh - - logos-blockchain-node-3: - image: ghcr.io/logos-blockchain/logos-blockchain@sha256:000982e751dfd346ca5346b8025c685fc3abc585079c59cde3bde7fd63100657 - volumes: - - ./scripts:/etc/logos-blockchain/scripts - - ./kzgrs_test_params:/kzgrs_test_params:z - depends_on: - - cfgsync + - ./node-config.yaml:/etc/logos-blockchain/node-config.yaml:z + - ./deployment-settings.yaml:/etc/logos-blockchain/deployment-settings.yaml:z entrypoint: /etc/logos-blockchain/scripts/run_logos_blockchain_node.sh diff --git a/bedrock/node-config.yaml b/bedrock/node-config.yaml new file mode 100644 index 00000000..2c4adc40 --- /dev/null +++ b/bedrock/node-config.yaml @@ -0,0 +1,54 @@ +blend: + non_ephemeral_signing_key_id: 86c8519f00178e9eb1fe5f4247e4bed77d4c9f6da2fb10e8a1fdd7ba6bc79fa0 + core: + zk: + secret_key_kms_id: 64249c75c2cb813578b75d05b215fc95f67cea5862fff047228183f22e63460e +cryptarchia: + service: + bootstrap: + prolonged_bootstrap_period: '1.000000000' + network: + network: + max_connected_peers_to_try_download: 16 + max_discovered_peers_to_try_download: 16 + leader: + wallet: + funding_pk: "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26" +sdp: + wallet: + funding_pk: ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717 +kms: + backend: + keys: + 2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26: !Zk 6c645cd4636d9c4c36a37a9aeabcaa3300000000000000000000000000000000 + 64249c75c2cb813578b75d05b215fc95f67cea5862fff047228183f22e63460e: !Zk 83c851cf4436e8d2fdac33d56d2b235f66431be97e2a20bf241d431713dc720f + ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717: !Zk 7364705cd4636d9c4c36a37a9aeabcaa00000000000000000000000000000000 + 86c8519f00178e9eb1fe5f4247e4bed77d4c9f6da2fb10e8a1fdd7ba6bc79fa0: !Ed25519 5cd4636d9c4c36a37a9aeabcaa332c3ec796226af0af48a0b2e70167205af749 +wallet: + known_keys: + 2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26: "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26" + ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717: "ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717" + voucher_master_key_id: 2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26 + +api: + backend: + listen_address: 0.0.0.0:18080 + cors_origins: [] + timeout: 30 + max_body_size: 10485760 + max_concurrent_requests: 500 + +tracing: + logger: + stdout: true + stderr: false + file: + directory: "./state/logs" + otlp: null + loki: null + gelf: null + tracing: None + filter: None + metrics: None + console: None + level: "INFO" diff --git a/bedrock/scripts/run_logos_blockchain_node.sh b/bedrock/scripts/run_logos_blockchain_node.sh index e318ab4a..ffa02e6d 100755 --- a/bedrock/scripts/run_logos_blockchain_node.sh +++ b/bedrock/scripts/run_logos_blockchain_node.sh @@ -2,12 +2,19 @@ set -e -export CFG_FILE_PATH="/config.yaml" \ - CFG_SERVER_ADDR="http://cfgsync:4400" \ - CFG_HOST_IP=$(hostname -i) \ - CFG_HOST_IDENTIFIER="validator-$(hostname -i)" \ - LOG_LEVEL="INFO" \ - POL_PROOF_DEV_MODE=true +export POL_PROOF_DEV_MODE=true -/usr/bin/logos-blockchain-cfgsync-client && \ - exec /usr/bin/logos-blockchain-node /config.yaml +# 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 diff --git a/common/Cargo.toml b/common/Cargo.toml index 0ae0b220..dbf5ec0c 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] nssa.workspace = true nssa_core.workspace = true +clock_core.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/common/src/block.rs b/common/src/block.rs index 6decc390..92adbdb1 100644 --- a/common/src/block.rs +++ b/common/src/block.rs @@ -1,10 +1,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use nssa_core::{BlockId, Timestamp}; +use nssa_core::BlockId; +pub use nssa_core::Timestamp; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256, digest::FixedOutput as _}; use crate::{HashType, transaction::NSSATransaction}; - pub type MantleMsgId = [u8; 32]; pub type BlockHash = HashType; diff --git a/common/src/transaction.rs b/common/src/transaction.rs index ea0b9819..7ce0e76f 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -1,6 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; use log::warn; -use nssa::{AccountId, V03State}; +use nssa::{AccountId, V03State, ValidatedStateDiff}; use nssa_core::{BlockId, Timestamp}; use serde::{Deserialize, Serialize}; @@ -66,21 +66,53 @@ impl NSSATransaction { } } + /// Validates the transaction against the current state and returns the resulting diff + /// without applying it. Rejects transactions that modify clock system accounts. + pub fn validate_on_state( + &self, + state: &V03State, + block_id: BlockId, + timestamp: Timestamp, + ) -> Result { + let diff = match self { + Self::Public(tx) => { + ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp) + } + Self::PrivacyPreserving(tx) => ValidatedStateDiff::from_privacy_preserving_transaction( + tx, state, block_id, timestamp, + ), + Self::ProgramDeployment(tx) => { + ValidatedStateDiff::from_program_deployment_transaction(tx, state) + } + }?; + + let public_diff = diff.public_diff(); + let touches_clock = nssa::CLOCK_PROGRAM_ACCOUNT_IDS.iter().any(|id| { + public_diff + .get(id) + .is_some_and(|post| *post != state.get_account_by_id(*id)) + }); + if touches_clock { + return Err(nssa::error::NssaError::InvalidInput( + "Transaction modifies system clock accounts".into(), + )); + } + + Ok(diff) + } + + /// Validates the transaction against the current state, rejects modifications to clock + /// system accounts, and applies the resulting diff to the state. pub fn execute_check_on_state( self, state: &mut V03State, block_id: BlockId, timestamp: Timestamp, ) -> Result { - match &self { - Self::Public(tx) => state.transition_from_public_transaction(tx, block_id, timestamp), - Self::PrivacyPreserving(tx) => { - state.transition_from_privacy_preserving_transaction(tx, block_id, timestamp) - } - Self::ProgramDeployment(tx) => state.transition_from_program_deployment_transaction(tx), - } - .inspect_err(|err| warn!("Error at transition {err:#?}"))?; - + let diff = self + .validate_on_state(state, block_id, timestamp) + .inspect_err(|err| warn!("Error at transition {err:#?}"))?; + state.apply_state_diff(diff); Ok(self) } } @@ -121,3 +153,20 @@ pub enum TransactionMalformationError { #[error("Transaction size {size} exceeds maximum allowed size of {max} bytes")] TransactionTooLarge { size: usize, max: usize }, } + +/// Returns the canonical Clock Program invocation transaction for the given block timestamp. +/// Every valid block must end with exactly one occurrence of this transaction. +#[must_use] +pub fn clock_invocation(timestamp: clock_core::Instruction) -> nssa::PublicTransaction { + let message = nssa::public_transaction::Message::try_new( + nssa::program::Program::clock().id(), + clock_core::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(), + vec![], + timestamp, + ) + .expect("Clock invocation message should always be constructable"); + nssa::PublicTransaction::new( + message, + nssa::public_transaction::WitnessSet::from_raw_parts(vec![]), + ) +} diff --git a/completions/bash/wallet b/completions/bash/wallet index 57dd3636..b01e5607 100644 --- a/completions/bash/wallet +++ b/completions/bash/wallet @@ -22,6 +22,20 @@ _wallet_complete_account_id() { fi } +# Helper function to complete account labels +_wallet_complete_account_label() { + local cur="$1" + local labels + + if command -v wallet &>/dev/null; then + labels=$(wallet account list 2>/dev/null | grep -o '\[.*\]' | sed 's/^\[//;s/\]$//') + fi + + if [[ -n "$labels" ]]; then + COMPREPLY=($(compgen -W "$labels" -- "$cur")) + fi +} + _wallet() { local cur prev words cword _init_completion 2>/dev/null || { @@ -91,20 +105,32 @@ _wallet() { --account-id) _wallet_complete_account_id "$cur" ;; + --account-label) + _wallet_complete_account_label "$cur" + ;; *) - COMPREPLY=($(compgen -W "--account-id" -- "$cur")) + COMPREPLY=($(compgen -W "--account-id --account-label" -- "$cur")) ;; esac ;; send) case "$prev" in - --from | --to) + --from) _wallet_complete_account_id "$cur" ;; + --from-label) + _wallet_complete_account_label "$cur" + ;; + --to) + _wallet_complete_account_id "$cur" + ;; + --to-label) + _wallet_complete_account_label "$cur" + ;; --to-npk | --to-vpk | --amount) ;; # no specific completion *) - COMPREPLY=($(compgen -W "--from --to --to-npk --to-vpk --amount" -- "$cur")) + COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --amount" -- "$cur")) ;; esac ;; @@ -147,8 +173,11 @@ _wallet() { -a | --account-id) _wallet_complete_account_id "$cur" ;; + --account-label) + _wallet_complete_account_label "$cur" + ;; *) - COMPREPLY=($(compgen -W "-r --raw -k --keys -a --account-id" -- "$cur")) + COMPREPLY=($(compgen -W "-r --raw -k --keys -a --account-id --account-label" -- "$cur")) ;; esac ;; @@ -186,10 +215,13 @@ _wallet() { -a | --account-id) _wallet_complete_account_id "$cur" ;; + --account-label) + _wallet_complete_account_label "$cur" + ;; -l | --label) ;; # no specific completion for label value *) - COMPREPLY=($(compgen -W "-a --account-id -l --label" -- "$cur")) + COMPREPLY=($(compgen -W "-a --account-id --account-label -l --label" -- "$cur")) ;; esac ;; @@ -206,8 +238,11 @@ _wallet() { --to) _wallet_complete_account_id "$cur" ;; + --to-label) + _wallet_complete_account_label "$cur" + ;; *) - COMPREPLY=($(compgen -W "--to" -- "$cur")) + COMPREPLY=($(compgen -W "--to --to-label" -- "$cur")) ;; esac ;; @@ -221,49 +256,85 @@ _wallet() { ;; new) case "$prev" in - --definition-account-id | --supply-account-id) + --definition-account-id) _wallet_complete_account_id "$cur" ;; + --definition-account-label) + _wallet_complete_account_label "$cur" + ;; + --supply-account-id) + _wallet_complete_account_id "$cur" + ;; + --supply-account-label) + _wallet_complete_account_label "$cur" + ;; -n | --name | -t | --total-supply) ;; # no specific completion *) - COMPREPLY=($(compgen -W "--definition-account-id --supply-account-id -n --name -t --total-supply" -- "$cur")) + COMPREPLY=($(compgen -W "--definition-account-id --definition-account-label --supply-account-id --supply-account-label -n --name -t --total-supply" -- "$cur")) ;; esac ;; send) case "$prev" in - --from | --to) + --from) _wallet_complete_account_id "$cur" ;; + --from-label) + _wallet_complete_account_label "$cur" + ;; + --to) + _wallet_complete_account_id "$cur" + ;; + --to-label) + _wallet_complete_account_label "$cur" + ;; --to-npk | --to-vpk | --amount) ;; # no specific completion *) - COMPREPLY=($(compgen -W "--from --to --to-npk --to-vpk --amount" -- "$cur")) + COMPREPLY=($(compgen -W "--from --from-label --to --to-label --to-npk --to-vpk --amount" -- "$cur")) ;; esac ;; burn) case "$prev" in - --definition | --holder) + --definition) _wallet_complete_account_id "$cur" ;; + --definition-label) + _wallet_complete_account_label "$cur" + ;; + --holder) + _wallet_complete_account_id "$cur" + ;; + --holder-label) + _wallet_complete_account_label "$cur" + ;; --amount) ;; # no specific completion *) - COMPREPLY=($(compgen -W "--definition --holder --amount" -- "$cur")) + COMPREPLY=($(compgen -W "--definition --definition-label --holder --holder-label --amount" -- "$cur")) ;; esac ;; mint) case "$prev" in - --definition | --holder) + --definition) _wallet_complete_account_id "$cur" ;; + --definition-label) + _wallet_complete_account_label "$cur" + ;; + --holder) + _wallet_complete_account_id "$cur" + ;; + --holder-label) + _wallet_complete_account_label "$cur" + ;; --holder-npk | --holder-vpk | --amount) ;; # no specific completion *) - COMPREPLY=($(compgen -W "--definition --holder --holder-npk --holder-vpk --amount" -- "$cur")) + COMPREPLY=($(compgen -W "--definition --definition-label --holder --holder-label --holder-npk --holder-vpk --amount" -- "$cur")) ;; esac ;; @@ -277,49 +348,103 @@ _wallet() { ;; new) case "$prev" in - --user-holding-a | --user-holding-b | --user-holding-lp) + --user-holding-a) _wallet_complete_account_id "$cur" ;; + --user-holding-a-label) + _wallet_complete_account_label "$cur" + ;; + --user-holding-b) + _wallet_complete_account_id "$cur" + ;; + --user-holding-b-label) + _wallet_complete_account_label "$cur" + ;; + --user-holding-lp) + _wallet_complete_account_id "$cur" + ;; + --user-holding-lp-label) + _wallet_complete_account_label "$cur" + ;; --balance-a | --balance-b) ;; # no specific completion *) - COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --user-holding-lp --balance-a --balance-b" -- "$cur")) + COMPREPLY=($(compgen -W "--user-holding-a --user-holding-a-label --user-holding-b --user-holding-b-label --user-holding-lp --user-holding-lp-label --balance-a --balance-b" -- "$cur")) ;; esac ;; swap) case "$prev" in - --user-holding-a | --user-holding-b) + --user-holding-a) _wallet_complete_account_id "$cur" ;; + --user-holding-a-label) + _wallet_complete_account_label "$cur" + ;; + --user-holding-b) + _wallet_complete_account_id "$cur" + ;; + --user-holding-b-label) + _wallet_complete_account_label "$cur" + ;; --amount-in | --min-amount-out | --token-definition) ;; # no specific completion *) - COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --amount-in --min-amount-out --token-definition" -- "$cur")) + COMPREPLY=($(compgen -W "--user-holding-a --user-holding-a-label --user-holding-b --user-holding-b-label --amount-in --min-amount-out --token-definition" -- "$cur")) ;; esac ;; add-liquidity) case "$prev" in - --user-holding-a | --user-holding-b | --user-holding-lp) + --user-holding-a) _wallet_complete_account_id "$cur" ;; + --user-holding-a-label) + _wallet_complete_account_label "$cur" + ;; + --user-holding-b) + _wallet_complete_account_id "$cur" + ;; + --user-holding-b-label) + _wallet_complete_account_label "$cur" + ;; + --user-holding-lp) + _wallet_complete_account_id "$cur" + ;; + --user-holding-lp-label) + _wallet_complete_account_label "$cur" + ;; --max-amount-a | --max-amount-b | --min-amount-lp) ;; # no specific completion *) - COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --user-holding-lp --max-amount-a --max-amount-b --min-amount-lp" -- "$cur")) + COMPREPLY=($(compgen -W "--user-holding-a --user-holding-a-label --user-holding-b --user-holding-b-label --user-holding-lp --user-holding-lp-label --max-amount-a --max-amount-b --min-amount-lp" -- "$cur")) ;; esac ;; remove-liquidity) case "$prev" in - --user-holding-a | --user-holding-b | --user-holding-lp) + --user-holding-a) _wallet_complete_account_id "$cur" ;; + --user-holding-a-label) + _wallet_complete_account_label "$cur" + ;; + --user-holding-b) + _wallet_complete_account_id "$cur" + ;; + --user-holding-b-label) + _wallet_complete_account_label "$cur" + ;; + --user-holding-lp) + _wallet_complete_account_id "$cur" + ;; + --user-holding-lp-label) + _wallet_complete_account_label "$cur" + ;; --balance-lp | --min-amount-a | --min-amount-b) ;; # no specific completion *) - COMPREPLY=($(compgen -W "--user-holding-a --user-holding-b --user-holding-lp --balance-lp --min-amount-a --min-amount-b" -- "$cur")) + COMPREPLY=($(compgen -W "--user-holding-a --user-holding-a-label --user-holding-b --user-holding-b-label --user-holding-lp --user-holding-lp-label --balance-lp --min-amount-a --min-amount-b" -- "$cur")) ;; esac ;; diff --git a/completions/zsh/_wallet b/completions/zsh/_wallet index 6e60cc53..2d4fe26b 100644 --- a/completions/zsh/_wallet +++ b/completions/zsh/_wallet @@ -90,12 +90,15 @@ _wallet_auth_transfer() { case $line[1] in init) _arguments \ - '--account-id[Account ID to initialize]:account_id:_wallet_account_ids' + '--account-id[Account ID to initialize]:account_id:_wallet_account_ids' \ + '--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels' ;; send) _arguments \ '--from[Source account ID]:from_account:_wallet_account_ids' \ + '--from-label[Source account label (alternative to --from)]:label:_wallet_account_labels' \ '--to[Destination account ID (for owned accounts)]:to_account:_wallet_account_ids' \ + '--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' \ '--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \ '--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \ '--amount[Amount of native tokens to send]:amount:' @@ -165,7 +168,8 @@ _wallet_account() { _arguments \ '(-r --raw)'{-r,--raw}'[Get raw account data]' \ '(-k --keys)'{-k,--keys}'[Display keys (pk for public accounts, npk/vpk for private accounts)]' \ - '(-a --account-id)'{-a,--account-id}'[Account ID to query]:account_id:_wallet_account_ids' + '(-a --account-id)'{-a,--account-id}'[Account ID to query]:account_id:_wallet_account_ids' \ + '--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels' ;; list|ls) _arguments \ @@ -189,6 +193,7 @@ _wallet_account() { label) _arguments \ '(-a --account-id)'{-a,--account-id}'[Account ID to label]:account_id:_wallet_account_ids' \ + '--account-label[Account label (alternative to --account-id)]:label:_wallet_account_labels' \ '(-l --label)'{-l,--label}'[The label to assign to the account]:label:' ;; esac @@ -216,7 +221,8 @@ _wallet_pinata() { case $line[1] in claim) _arguments \ - '--to[Destination account ID to receive claimed tokens]:to_account:_wallet_account_ids' + '--to[Destination account ID to receive claimed tokens]:to_account:_wallet_account_ids' \ + '--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' ;; esac ;; @@ -249,12 +255,16 @@ _wallet_token() { '--name[Token name]:name:' \ '--total-supply[Total supply of tokens to mint]:total_supply:' \ '--definition-account-id[Account ID for token definition]:definition_account:_wallet_account_ids' \ - '--supply-account-id[Account ID to receive initial supply]:supply_account:_wallet_account_ids' + '--definition-account-label[Definition account label (alternative to --definition-account-id)]:label:_wallet_account_labels' \ + '--supply-account-id[Account ID to receive initial supply]:supply_account:_wallet_account_ids' \ + '--supply-account-label[Supply account label (alternative to --supply-account-id)]:label:_wallet_account_labels' ;; send) _arguments \ '--from[Source holding account ID]:from_account:_wallet_account_ids' \ + '--from-label[Source account label (alternative to --from)]:label:_wallet_account_labels' \ '--to[Destination holding account ID (for owned accounts)]:to_account:_wallet_account_ids' \ + '--to-label[Destination account label (alternative to --to)]:label:_wallet_account_labels' \ '--to-npk[Destination nullifier public key (for foreign private accounts)]:npk:' \ '--to-vpk[Destination viewing public key (for foreign private accounts)]:vpk:' \ '--amount[Amount of tokens to send]:amount:' @@ -262,13 +272,17 @@ _wallet_token() { burn) _arguments \ '--definition[Definition account ID]:definition_account:_wallet_account_ids' \ + '--definition-label[Definition account label (alternative to --definition)]:label:_wallet_account_labels' \ '--holder[Holder account ID]:holder_account:_wallet_account_ids' \ + '--holder-label[Holder account label (alternative to --holder)]:label:_wallet_account_labels' \ '--amount[Amount of tokens to burn]:amount:' ;; mint) _arguments \ '--definition[Definition account ID]:definition_account:_wallet_account_ids' \ + '--definition-label[Definition account label (alternative to --definition)]:label:_wallet_account_labels' \ '--holder[Holder account ID (for owned accounts)]:holder_account:_wallet_account_ids' \ + '--holder-label[Holder account label (alternative to --holder)]:label:_wallet_account_labels' \ '--holder-npk[Holder nullifier public key (for foreign private accounts)]:npk:' \ '--holder-vpk[Holder viewing public key (for foreign private accounts)]:vpk:' \ '--amount[Amount of tokens to mint]:amount:' @@ -302,15 +316,20 @@ _wallet_amm() { new) _arguments \ '--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \ + '--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \ '--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \ + '--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \ '--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \ + '--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \ '--balance-a[Amount of token A to deposit]:balance_a:' \ '--balance-b[Amount of token B to deposit]:balance_b:' ;; swap) _arguments \ '--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \ + '--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \ '--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \ + '--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \ '--amount-in[Amount of tokens to swap]:amount_in:' \ '--min-amount-out[Minimum tokens expected in return]:min_amount_out:' \ '--token-definition[Definition ID of the token being provided]:token_def:' @@ -318,8 +337,11 @@ _wallet_amm() { add-liquidity) _arguments \ '--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \ + '--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \ '--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \ + '--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \ '--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \ + '--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \ '--max-amount-a[Maximum amount of token A to deposit]:max_amount_a:' \ '--max-amount-b[Maximum amount of token B to deposit]:max_amount_b:' \ '--min-amount-lp[Minimum LP tokens to receive]:min_amount_lp:' @@ -327,8 +349,11 @@ _wallet_amm() { remove-liquidity) _arguments \ '--user-holding-a[User token A holding account ID]:holding_a:_wallet_account_ids' \ + '--user-holding-a-label[User holding A account label (alternative to --user-holding-a)]:label:_wallet_account_labels' \ '--user-holding-b[User token B holding account ID]:holding_b:_wallet_account_ids' \ + '--user-holding-b-label[User holding B account label (alternative to --user-holding-b)]:label:_wallet_account_labels' \ '--user-holding-lp[User LP token holding account ID]:holding_lp:_wallet_account_ids' \ + '--user-holding-lp-label[User holding LP account label (alternative to --user-holding-lp)]:label:_wallet_account_labels' \ '--balance-lp[Amount of LP tokens to burn]:balance_lp:' \ '--min-amount-a[Minimum token A to receive]:min_amount_a:' \ '--min-amount-b[Minimum token B to receive]:min_amount_b:' @@ -424,7 +449,7 @@ _wallet_help() { _wallet_account_ids() { local -a accounts local line - + # Try to get accounts from wallet account list command # Filter to lines starting with /N (numbered accounts) and extract the account ID if command -v wallet &>/dev/null; then @@ -433,14 +458,35 @@ _wallet_account_ids() { [[ -n "$line" ]] && accounts+=("${line%,}") done < <(wallet account list 2>/dev/null | grep '^/[0-9]' | awk '{print $2}') fi - + # Provide type prefixes as fallback if command fails or returns nothing if (( ${#accounts} == 0 )); then compadd -S '' -- 'Public/' 'Private/' return fi - + _multi_parts / accounts } +# Helper function to complete account labels +# Uses `wallet account list` to get available labels +_wallet_account_labels() { + local -a labels + local line + + if command -v wallet &>/dev/null; then + while IFS= read -r line; do + local label + # Extract label from [...] at end of line + label="${line##*\[}" + label="${label%\]}" + [[ -n "$label" && "$label" != "$line" ]] && labels+=("$label") + done < <(wallet account list 2>/dev/null) + fi + + if (( ${#labels} > 0 )); then + compadd -a labels + fi +} + _wallet "$@" diff --git a/docs/LEZ testnet v0.1 tutorials/associated-token-accounts.md b/docs/LEZ testnet v0.1 tutorials/associated-token-accounts.md index 330ae909..7ed95e01 100644 --- a/docs/LEZ testnet v0.1 tutorials/associated-token-accounts.md +++ b/docs/LEZ testnet v0.1 tutorials/associated-token-accounts.md @@ -52,7 +52,7 @@ The derivation works as follows: ``` seed = SHA256(owner_id || definition_id) -ata_address = AccountId::from((ata_program_id, seed)) +ata_address = AccountId::for_public_pda(ata_program_id, seed) ``` Because the computation is pure, anyone who knows the owner and definition can reproduce the exact same ATA address — no network call required. diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world.rs b/examples/program_deployment/methods/guest/src/bin/hello_world.rs index 810e83f3..3e91db0e 100644 --- a/examples/program_deployment/methods/guest/src/bin/hello_world.rs +++ b/examples/program_deployment/methods/guest/src/bin/hello_world.rs @@ -19,6 +19,8 @@ fn main() { // Read inputs let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: greeting, }, @@ -50,5 +52,12 @@ fn main() { // with the NSSA program rules. // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be // called to commit the output. - ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]).write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_data, + vec![pre_state], + vec![post_state], + ) + .write(); } diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs b/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs index 62908870..70dfa2ae 100644 --- a/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs +++ b/examples/program_deployment/methods/guest/src/bin/hello_world_with_authorization.rs @@ -19,6 +19,8 @@ fn main() { // Read inputs let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: greeting, }, @@ -57,5 +59,12 @@ fn main() { // with the NSSA program rules. // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be // called to commit the output. - ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]).write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_data, + vec![pre_state], + vec![post_state], + ) + .write(); } diff --git a/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs b/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs index 7e29b5de..4289349b 100644 --- a/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs +++ b/examples/program_deployment/methods/guest/src/bin/hello_world_with_move_function.rs @@ -66,6 +66,8 @@ fn main() { // Read input accounts. let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: (function_id, data), }, @@ -85,5 +87,12 @@ fn main() { // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be // called to commit the output. - ProgramOutput::new(instruction_words, pre_states, post_states).write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + post_states, + ) + .write(); } diff --git a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs index d2c04083..716e5c29 100644 --- a/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs +++ b/examples/program_deployment/methods/guest/src/bin/simple_tail_call.rs @@ -27,6 +27,8 @@ fn main() { // Read inputs let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: (), }, @@ -55,7 +57,13 @@ fn main() { // Write the outputs. // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be // called to commit the output. - ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]) - .with_chained_calls(vec![chained_call]) - .write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_data, + vec![pre_state], + vec![post_state], + ) + .with_chained_calls(vec![chained_call]) + .write(); } diff --git a/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs b/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs index 564efc2b..5ec9aaab 100644 --- a/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs +++ b/examples/program_deployment/methods/guest/src/bin/tail_call_with_pda.rs @@ -33,6 +33,8 @@ fn main() { // Read inputs let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: (), }, @@ -68,7 +70,13 @@ fn main() { // Write the outputs. // WARNING: constructing a `ProgramOutput` has no effect on its own. `.write()` must be // called to commit the output. - ProgramOutput::new(instruction_data, vec![pre_state], vec![post_state]) - .with_chained_calls(vec![chained_call]) - .write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_data, + vec![pre_state], + vec![post_state], + ) + .with_chained_calls(vec![chained_call]) + .write(); } diff --git a/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs b/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs index e6a8ca99..86c95ebf 100644 --- a/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs +++ b/examples/program_deployment/src/bin/run_hello_world_with_authorization_through_tail_call_with_pda.rs @@ -46,7 +46,7 @@ async fn main() { let program = Program::new(bytecode).unwrap(); // Compute the PDA to pass it as input account to the public execution - let pda = AccountId::from((&program.id(), &PDA_SEED)); + let pda = AccountId::for_public_pda(&program.id(), &PDA_SEED); let account_ids = vec![pda]; let instruction_data = (); let nonces = vec![]; diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index 7faf5376..611dec8d 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -4,7 +4,7 @@ use anyhow::Result; use bedrock_client::HeaderId; use common::{ block::{BedrockStatus, Block}, - transaction::NSSATransaction, + transaction::{NSSATransaction, clock_invocation}, }; use nssa::{Account, AccountId, V03State}; use nssa_core::BlockId; @@ -122,7 +122,18 @@ impl IndexerStore { { let mut state_guard = self.current_state.write().await; - for transaction in &block.body.transactions { + let (clock_tx, user_txs) = block + .body + .transactions + .split_last() + .ok_or_else(|| anyhow::anyhow!("Block has no transactions"))?; + + anyhow::ensure!( + *clock_tx == NSSATransaction::Public(clock_invocation(block.header.timestamp)), + "Last transaction in block must be the clock invocation for the block timestamp" + ); + + for transaction in user_txs { transaction .clone() .transaction_stateless_check()? @@ -132,6 +143,16 @@ impl IndexerStore { block.header.timestamp, )?; } + + // Apply the clock invocation directly (it is expected to modify clock accounts). + let NSSATransaction::Public(clock_public_tx) = clock_tx else { + anyhow::bail!("Clock invocation must be a public transaction"); + }; + state_guard.transition_from_public_transaction( + clock_public_tx, + block.header.block_id, + block.header.timestamp, + )?; } // ToDo: Currently we are fetching only finalized blocks @@ -177,7 +198,11 @@ mod tests { let storage = IndexerStore::open_db_with_genesis( home.as_ref(), &genesis_block(), - &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]), + &nssa::V03State::new_with_genesis_accounts( + &[(acc1(), 10000), (acc2(), 20000)], + vec![], + 0, + ), ) .unwrap(); @@ -195,7 +220,11 @@ mod tests { let storage = IndexerStore::open_db_with_genesis( home.as_ref(), &genesis_block(), - &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]), + &nssa::V03State::new_with_genesis_accounts( + &[(acc1(), 10000), (acc2(), 20000)], + vec![], + 0, + ), ) .unwrap(); @@ -213,11 +242,14 @@ mod tests { 10, &sign_key, ); + let block_id = u64::try_from(i).unwrap(); + let block_timestamp = block_id.saturating_mul(100); + let clock_tx = NSSATransaction::Public(clock_invocation(block_timestamp)); let next_block = common::test_utils::produce_dummy_block( - u64::try_from(i).unwrap(), + block_id, Some(prev_hash), - vec![tx], + vec![tx, clock_tx], ); prev_hash = next_block.header.hash; diff --git a/indexer/core/src/lib.rs b/indexer/core/src/lib.rs index bcd99ad7..10e0834a 100644 --- a/indexer/core/src/lib.rs +++ b/indexer/core/src/lib.rs @@ -57,11 +57,9 @@ impl IndexerCore { let channel_genesis_msg_id = [0; 32]; let genesis_block = hashable_data.into_pending_block(&signing_key, channel_genesis_msg_id); - let initial_commitments: Option> = config - .initial_private_accounts - .as_ref() - .map(|initial_commitments| { - initial_commitments + let initial_private_accounts: Option> = + config.initial_private_accounts.as_ref().map(|accounts| { + accounts .iter() .map(|init_comm_data| { let npk = &init_comm_data.npk; @@ -71,7 +69,10 @@ impl IndexerCore { acc.program_owner = nssa::program::Program::authenticated_transfer_program().id(); - nssa_core::Commitment::new(npk, &acc) + ( + nssa_core::Commitment::new(npk, &acc), + nssa_core::Nullifier::for_account_initialization(npk), + ) }) .collect() }); @@ -88,10 +89,11 @@ impl IndexerCore { // If initial commitments or accounts are present in config, need to construct state from // them - let state = if initial_commitments.is_some() || init_accs.is_some() { + let state = if initial_private_accounts.is_some() || init_accs.is_some() { let mut state = V03State::new_with_genesis_accounts( &init_accs.unwrap_or_default(), - &initial_commitments.unwrap_or_default(), + initial_private_accounts.unwrap_or_default(), + genesis_block.header.timestamp, ); // ToDo: Remove after testnet @@ -142,7 +144,22 @@ impl IndexerCore { info!("Parsed {} L2 blocks with ids {:?}", l2_block_vec.len(), l2_blocks_parsed_ids); for l2_block in l2_block_vec { - self.store.put_block(l2_block.clone(), l1_header).await?; + // TODO: proper fix is to make the sequencer's genesis include a + // trailing `clock_invocation(0)` (and have the indexer's + // `open_db_with_genesis` not pre-apply state transitions) so the + // inscribed genesis can flow through `put_block` like any other + // block. For now we skip re-applying it. + // + // The channel-start (block_id == 1) is the sequencer's genesis + // inscription that we re-discover during initial search. The + // indexer already has its own locally-constructed genesis in + // the store from `open_db_with_genesis`, so re-applying the + // inscribed copy is both redundant and would fail the strict + // block validation in `put_block` (the inscribed genesis lacks + // the trailing clock invocation). + if l2_block.header.block_id != 1 { + self.store.put_block(l2_block.clone(), l1_header).await?; + } yield Ok(l2_block); } diff --git a/indexer/service/protocol/src/lib.rs b/indexer/service/protocol/src/lib.rs index b773b6ec..503d6187 100644 --- a/indexer/service/protocol/src/lib.rs +++ b/indexer/service/protocol/src/lib.rs @@ -138,7 +138,7 @@ pub struct Account { } pub type BlockId = u64; -pub type TimeStamp = u64; +pub type Timestamp = u64; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct Block { @@ -153,7 +153,7 @@ pub struct BlockHeader { pub block_id: BlockId, pub prev_block_hash: HashType, pub hash: HashType, - pub timestamp: TimeStamp, + pub timestamp: Timestamp, pub signature: Signature, } diff --git a/integration_tests/src/config.rs b/integration_tests/src/config.rs index 1dd726eb..008f7700 100644 --- a/integration_tests/src/config.rs +++ b/integration_tests/src/config.rs @@ -121,7 +121,7 @@ impl InitialData { self.private_accounts .iter() .map(|(key_chain, account)| PrivateAccountPublicInitialData { - npk: key_chain.nullifier_public_key.clone(), + npk: key_chain.nullifier_public_key, account: account.clone(), }) .collect() @@ -211,7 +211,7 @@ pub fn sequencer_config( max_block_size, mempool_max_size, block_create_timeout, - retry_pending_blocks_timeout: Duration::from_mins(2), + retry_pending_blocks_timeout: Duration::from_secs(5), initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()), initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()), signing_key: [37; 32], diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 08e7cf9f..a4381acf 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -256,11 +256,11 @@ impl TestContext { let config_overrides = WalletConfigOverrides::default(); let wallet_password = "test_pass".to_owned(); - let wallet = WalletCore::new_init_storage( + let (wallet, _mnemonic) = WalletCore::new_init_storage( config_path, storage_path, Some(config_overrides), - wallet_password.clone(), + &wallet_password, ) .context("Failed to init wallet")?; wallet diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index 42aa5f3f..dde9e7f5 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -113,9 +113,12 @@ async fn amm_public() -> Result<()> { // Create new token let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id_1), - supply_account_id: format_public_account_id(supply_account_id_1), + definition_account_id: Some(format_public_account_id(definition_account_id_1)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id_1)), + supply_account_label: None, name: "A NAM1".to_owned(), + total_supply: 37, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -124,8 +127,10 @@ async fn amm_public() -> Result<()> { // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_1` let subcommand = TokenProgramAgnosticSubcommand::Send { - from: format_public_account_id(supply_account_id_1), + from: Some(format_public_account_id(supply_account_id_1)), + from_label: None, to: Some(format_public_account_id(recipient_account_id_1)), + to_label: None, to_npk: None, to_vpk: None, amount: 7, @@ -137,9 +142,12 @@ async fn amm_public() -> Result<()> { // Create new token let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id_2), - supply_account_id: format_public_account_id(supply_account_id_2), + definition_account_id: Some(format_public_account_id(definition_account_id_2)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id_2)), + supply_account_label: None, name: "A NAM2".to_owned(), + total_supply: 37, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -148,8 +156,10 @@ async fn amm_public() -> Result<()> { // Transfer 7 tokens from `supply_acc` to the account at account_id `recipient_account_id_2` let subcommand = TokenProgramAgnosticSubcommand::Send { - from: format_public_account_id(supply_account_id_2), + from: Some(format_public_account_id(supply_account_id_2)), + from_label: None, to: Some(format_public_account_id(recipient_account_id_2)), + to_label: None, to_npk: None, to_vpk: None, amount: 7, @@ -181,9 +191,12 @@ async fn amm_public() -> Result<()> { // Send creation tx let subcommand = AmmProgramAgnosticSubcommand::New { - user_holding_a: format_public_account_id(recipient_account_id_1), - user_holding_b: format_public_account_id(recipient_account_id_2), - user_holding_lp: format_public_account_id(user_holding_lp), + user_holding_a: Some(format_public_account_id(recipient_account_id_1)), + user_holding_a_label: None, + user_holding_b: Some(format_public_account_id(recipient_account_id_2)), + user_holding_b_label: None, + user_holding_lp: Some(format_public_account_id(user_holding_lp)), + user_holding_lp_label: None, balance_a: 3, balance_b: 3, }; @@ -223,9 +236,11 @@ async fn amm_public() -> Result<()> { // Make swap - let subcommand = AmmProgramAgnosticSubcommand::Swap { - user_holding_a: format_public_account_id(recipient_account_id_1), - user_holding_b: format_public_account_id(recipient_account_id_2), + let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput { + user_holding_a: Some(format_public_account_id(recipient_account_id_1)), + user_holding_a_label: None, + user_holding_b: Some(format_public_account_id(recipient_account_id_2)), + user_holding_b_label: None, amount_in: 2, min_amount_out: 1, token_definition: definition_account_id_1.to_string(), @@ -266,9 +281,11 @@ async fn amm_public() -> Result<()> { // Make swap - let subcommand = AmmProgramAgnosticSubcommand::Swap { - user_holding_a: format_public_account_id(recipient_account_id_1), - user_holding_b: format_public_account_id(recipient_account_id_2), + let subcommand = AmmProgramAgnosticSubcommand::SwapExactInput { + user_holding_a: Some(format_public_account_id(recipient_account_id_1)), + user_holding_a_label: None, + user_holding_b: Some(format_public_account_id(recipient_account_id_2)), + user_holding_b_label: None, amount_in: 2, min_amount_out: 1, token_definition: definition_account_id_2.to_string(), @@ -310,9 +327,12 @@ async fn amm_public() -> Result<()> { // Add liquidity let subcommand = AmmProgramAgnosticSubcommand::AddLiquidity { - user_holding_a: format_public_account_id(recipient_account_id_1), - user_holding_b: format_public_account_id(recipient_account_id_2), - user_holding_lp: format_public_account_id(user_holding_lp), + user_holding_a: Some(format_public_account_id(recipient_account_id_1)), + user_holding_a_label: None, + user_holding_b: Some(format_public_account_id(recipient_account_id_2)), + user_holding_b_label: None, + user_holding_lp: Some(format_public_account_id(user_holding_lp)), + user_holding_lp_label: None, min_amount_lp: 1, max_amount_a: 2, max_amount_b: 2, @@ -354,9 +374,12 @@ async fn amm_public() -> Result<()> { // Remove liquidity let subcommand = AmmProgramAgnosticSubcommand::RemoveLiquidity { - user_holding_a: format_public_account_id(recipient_account_id_1), - user_holding_b: format_public_account_id(recipient_account_id_2), - user_holding_lp: format_public_account_id(user_holding_lp), + user_holding_a: Some(format_public_account_id(recipient_account_id_1)), + user_holding_a_label: None, + user_holding_b: Some(format_public_account_id(recipient_account_id_2)), + user_holding_b_label: None, + user_holding_lp: Some(format_public_account_id(user_holding_lp)), + user_holding_lp_label: None, balance_lp: 2, min_amount_a: 1, min_amount_b: 1, @@ -397,3 +420,188 @@ async fn amm_public() -> Result<()> { Ok(()) } + +#[test] +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 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"); + }; + + // Create holding_a with a label + let holding_a_label = "amm-holding-a-label".to_owned(); + let SubcommandReturnValue::RegisterAccount { + account_id: holding_a_id, + } = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: Some(holding_a_label.clone()), + })), + ) + .await? + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + // 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 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"); + }; + + // Create holding_b with a label + let holding_b_label = "amm-holding-b-label".to_owned(); + let SubcommandReturnValue::RegisterAccount { + account_id: holding_b_id, + } = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: Some(holding_b_label.clone()), + })), + ) + .await? + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + // Create holding_lp with a label + let holding_lp_label = "amm-holding-lp-label".to_owned(); + let SubcommandReturnValue::RegisterAccount { + account_id: holding_lp_id, + } = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: Some(holding_lp_label.clone()), + })), + ) + .await? + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + // Create token 1 and distribute to holding_a + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_account_id: Some(format_public_account_id(definition_account_id_1)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id_1)), + supply_account_label: None, + 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; + + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: Some(format_public_account_id(supply_account_id_1)), + from_label: None, + to: Some(format_public_account_id(holding_a_id)), + to_label: None, + to_npk: None, + to_vpk: None, + 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; + + // Create token 2 and distribute to holding_b + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_account_id: Some(format_public_account_id(definition_account_id_2)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id_2)), + supply_account_label: None, + 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; + + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: Some(format_public_account_id(supply_account_id_2)), + from_label: None, + to: Some(format_public_account_id(holding_b_id)), + to_label: None, + to_npk: None, + to_vpk: None, + 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; + + // Create AMM pool using account labels instead of IDs + let subcommand = AmmProgramAgnosticSubcommand::New { + user_holding_a: None, + user_holding_a_label: Some(holding_a_label), + user_holding_b: None, + user_holding_b_label: Some(holding_b_label), + user_holding_lp: None, + user_holding_lp_label: Some(holding_lp_label), + balance_a: 3, + balance_b: 3, + }; + 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?; + + // LP balance should be 3 (geometric mean of 3, 3) + assert_eq!( + u128::from_le_bytes(holding_lp_acc.data[33..].try_into().unwrap()), + 3 + ); + + info!("Successfully created AMM pool using account labels"); + + Ok(()) +} diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index 94ba98c9..c0918635 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -68,8 +68,10 @@ async fn create_ata_initializes_holding_account() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id), - supply_account_id: format_public_account_id(supply_account_id), + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id)), + supply_account_label: None, name: "TEST".to_owned(), total_supply, }), @@ -130,8 +132,10 @@ async fn create_ata_is_idempotent() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id), - supply_account_id: format_public_account_id(supply_account_id), + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id)), + supply_account_label: None, name: "TEST".to_owned(), total_supply: 100, }), @@ -208,8 +212,10 @@ async fn transfer_and_burn_via_ata() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id), - supply_account_id: format_public_account_id(supply_account_id), + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id)), + supply_account_label: None, name: "TEST".to_owned(), total_supply, }), @@ -256,8 +262,10 @@ async fn transfer_and_burn_via_ata() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::Send { - from: format_public_account_id(supply_account_id), + from: Some(format_public_account_id(supply_account_id)), + from_label: None, to: Some(format_public_account_id(sender_ata_id)), + to_label: None, to_npk: None, to_vpk: None, amount: fund_amount, @@ -362,8 +370,10 @@ async fn create_ata_with_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id), - supply_account_id: format_public_account_id(supply_account_id), + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id)), + supply_account_label: None, name: "TEST".to_owned(), total_supply: 100, }), @@ -434,8 +444,10 @@ async fn transfer_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id), - supply_account_id: format_public_account_id(supply_account_id), + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id)), + supply_account_label: None, name: "TEST".to_owned(), total_supply, }), @@ -482,8 +494,10 @@ async fn transfer_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::Send { - from: format_public_account_id(supply_account_id), + from: Some(format_public_account_id(supply_account_id)), + from_label: None, to: Some(format_public_account_id(sender_ata_id)), + to_label: None, to_npk: None, to_vpk: None, amount: fund_amount, @@ -556,8 +570,10 @@ async fn burn_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id), - supply_account_id: format_public_account_id(supply_account_id), + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id)), + supply_account_label: None, name: "TEST".to_owned(), total_supply, }), @@ -592,8 +608,10 @@ async fn burn_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Token(TokenProgramAgnosticSubcommand::Send { - from: format_public_account_id(supply_account_id), + from: Some(format_public_account_id(supply_account_id)), + from_label: None, to: Some(format_public_account_id(holder_ata_id)), + to_label: None, to_npk: None, to_vpk: None, amount: fund_amount, diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 59b4719a..cf02d0ac 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -24,8 +24,10 @@ async fn private_transfer_to_owned_account() -> Result<()> { let to: AccountId = ctx.existing_private_accounts()[1]; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_private_account_id(from), + from: Some(format_private_account_id(from)), + from_label: None, to: Some(format_private_account_id(to)), + to_label: None, to_npk: None, to_vpk: None, amount: 100, @@ -63,8 +65,10 @@ async fn private_transfer_to_foreign_account() -> Result<()> { let to_vpk = Secp256k1Point::from_scalar(to_npk.0); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_private_account_id(from), + from: Some(format_private_account_id(from)), + from_label: None, to: None, + to_label: None, to_npk: Some(to_npk_string), to_vpk: Some(hex::encode(to_vpk.0)), amount: 100, @@ -111,8 +115,10 @@ async fn deshielded_transfer_to_public_account() -> Result<()> { assert_eq!(from_acc.balance, 10000); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_private_account_id(from), + from: Some(format_private_account_id(from)), + from_label: None, to: Some(format_public_account_id(to)), + to_label: None, to_npk: None, to_vpk: None, amount: 100, @@ -174,8 +180,10 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> { // Send to this account using claiming path (using npk and vpk instead of account ID) let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_private_account_id(from), + from: Some(format_private_account_id(from)), + from_label: None, to: None, + to_label: None, to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)), to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)), amount: 100, @@ -222,8 +230,10 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> { let to: AccountId = ctx.existing_private_accounts()[1]; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(from), + from: Some(format_public_account_id(from)), + from_label: None, to: Some(format_private_account_id(to)), + to_label: None, to_npk: None, to_vpk: None, amount: 100, @@ -264,8 +274,10 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> { let from: AccountId = ctx.existing_public_accounts()[0]; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(from), + from: Some(format_public_account_id(from)), + from_label: None, to: None, + to_label: None, to_npk: Some(to_npk_string), to_vpk: Some(hex::encode(to_vpk.0)), amount: 100, @@ -334,8 +346,10 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> { // Send transfer using nullifier and viewing public keys let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_private_account_id(from), + from: Some(format_private_account_id(from)), + from_label: None, to: None, + to_label: None, to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)), to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)), amount: 100, @@ -383,7 +397,8 @@ async fn initialize_private_account() -> Result<()> { }; let command = Command::AuthTransfer(AuthTransferSubcommand::Init { - account_id: format_private_account_id(account_id), + account_id: Some(format_private_account_id(account_id)), + account_label: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -415,3 +430,100 @@ async fn initialize_private_account() -> Result<()> { Ok(()) } + +#[test] +async fn private_transfer_using_from_label() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let from: AccountId = ctx.existing_private_accounts()[0]; + let to: AccountId = ctx.existing_private_accounts()[1]; + + // Assign a label to the sender account + let label = "private-sender-label".to_owned(); + let command = Command::Account(AccountSubcommand::Label { + account_id: Some(format_private_account_id(from)), + account_label: None, + label: label.clone(), + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + // Send using the label instead of account ID + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: None, + from_label: Some(label), + to: Some(format_private_account_id(to)), + to_label: None, + to_npk: None, + to_vpk: None, + 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 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); + + info!("Successfully transferred privately using from_label"); + + Ok(()) +} + +#[test] +async fn initialize_private_account_using_label() -> Result<()> { + let mut ctx = TestContext::new().await?; + + // Create a new private account with a label + let label = "init-private-label".to_owned(); + let command = Command::Account(AccountSubcommand::New(NewSubcommand::Private { + cci: None, + label: Some(label.clone()), + })); + let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let SubcommandReturnValue::RegisterAccount { account_id } = result else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + // Initialize using the label instead of account ID + let command = Command::AuthTransfer(AuthTransferSubcommand::Init { + account_id: None, + account_label: Some(label), + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + 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?; + + 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); + + let account = ctx + .wallet() + .get_account_private(account_id) + .context("Failed to get private account")?; + + assert_eq!( + account.program_owner, + Program::authenticated_transfer_program().id() + ); + + info!("Successfully initialized private account using label"); + + Ok(()) +} diff --git a/integration_tests/tests/auth_transfer/public.rs b/integration_tests/tests/auth_transfer/public.rs index 7f8c3836..416c4490 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -17,8 +17,10 @@ async fn successful_transfer_to_existing_account() -> Result<()> { let mut ctx = TestContext::new().await?; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(ctx.existing_public_accounts()[0]), + from: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + from_label: None, to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), + to_label: None, to_npk: None, to_vpk: None, amount: 100, @@ -73,8 +75,10 @@ pub async fn successful_transfer_to_new_account() -> Result<()> { .expect("Failed to find newly created account in the wallet storage"); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(ctx.existing_public_accounts()[0]), + from: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + from_label: None, to: Some(format_public_account_id(new_persistent_account_id)), + to_label: None, to_npk: None, to_vpk: None, amount: 100, @@ -109,8 +113,10 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> { let mut ctx = TestContext::new().await?; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(ctx.existing_public_accounts()[0]), + from: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + from_label: None, to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), + to_label: None, to_npk: None, to_vpk: None, amount: 1_000_000, @@ -147,8 +153,10 @@ async fn two_consecutive_successful_transfers() -> Result<()> { // First transfer let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(ctx.existing_public_accounts()[0]), + from: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + from_label: None, to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), + to_label: None, to_npk: None, to_vpk: None, amount: 100, @@ -179,8 +187,10 @@ async fn two_consecutive_successful_transfers() -> Result<()> { // Second transfer let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(ctx.existing_public_accounts()[0]), + from: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + from_label: None, to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), + to_label: None, to_npk: None, to_vpk: None, amount: 100, @@ -226,7 +236,8 @@ async fn initialize_public_account() -> Result<()> { }; let command = Command::AuthTransfer(AuthTransferSubcommand::Init { - account_id: format_public_account_id(account_id), + account_id: Some(format_public_account_id(account_id)), + account_label: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -245,3 +256,97 @@ async fn initialize_public_account() -> Result<()> { Ok(()) } + +#[test] +async fn successful_transfer_using_from_label() -> Result<()> { + let mut ctx = TestContext::new().await?; + + // Assign a label to the sender account + let label = "sender-label".to_owned(); + let command = Command::Account(AccountSubcommand::Label { + account_id: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + account_label: None, + label: label.clone(), + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + // Send using the label instead of account ID + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: None, + from_label: Some(label), + to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), + to_label: None, + to_npk: None, + to_vpk: None, + 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; + + 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?; + + assert_eq!(acc_1_balance, 9900); + assert_eq!(acc_2_balance, 20100); + + info!("Successfully transferred using from_label"); + + Ok(()) +} + +#[test] +async fn successful_transfer_using_to_label() -> Result<()> { + let mut ctx = TestContext::new().await?; + + // Assign a label to the receiver account + let label = "receiver-label".to_owned(); + let command = Command::Account(AccountSubcommand::Label { + account_id: Some(format_public_account_id(ctx.existing_public_accounts()[1])), + account_label: None, + label: label.clone(), + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + // Send using the label for the recipient + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + from_label: None, + to: None, + to_label: Some(label), + to_npk: None, + to_vpk: None, + 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; + + 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?; + + assert_eq!(acc_1_balance, 9900); + assert_eq!(acc_2_balance, 20100); + + info!("Successfully transferred using to_label"); + + Ok(()) +} diff --git a/integration_tests/tests/indexer.rs b/integration_tests/tests/indexer.rs index cb8cf0e9..0aef4a42 100644 --- a/integration_tests/tests/indexer.rs +++ b/integration_tests/tests/indexer.rs @@ -1,38 +1,68 @@ #![expect( + clippy::shadow_unrelated, clippy::tests_outside_test_module, reason = "We don't care about these in tests" )] use std::time::Duration; -use anyhow::Result; +use anyhow::{Context as _, Result}; use indexer_service_rpc::RpcClient as _; -use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_public_account_id}; +use integration_tests::{ + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_private_account_id, + format_public_account_id, verify_commitment_is_in_state, +}; use log::info; +use nssa::AccountId; use tokio::test; use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcommand}; -/// Timeout in milliseconds to reliably await for block finalization. -const L2_TO_L1_TIMEOUT_MILLIS: u64 = 600_000; +/// Maximum time to wait for the indexer to catch up to the sequencer. +const L2_TO_L1_TIMEOUT_MILLIS: u64 = 180_000; + +/// Poll the indexer until its last finalized block id reaches the sequencer's +/// current last block id (and at least the genesis block has been advanced past), +/// or until [`L2_TO_L1_TIMEOUT_MILLIS`] elapses. Returns the last indexer block +/// id observed. +async fn wait_for_indexer_to_catch_up(ctx: &TestContext) -> u64 { + let timeout = Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS); + let mut last_ind: u64 = 1; + let inner = async { + loop { + let seq = sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()) + .await + .unwrap_or(0); + let ind = ctx + .indexer_client() + .get_last_finalized_block_id() + .await + .unwrap_or(1); + last_ind = ind; + if ind >= seq && ind > 1 { + info!("Indexer caught up: seq={seq}, ind={ind}"); + return ind; + } + tokio::time::sleep(Duration::from_secs(2)).await; + } + }; + tokio::time::timeout(timeout, inner) + .await + .unwrap_or_else(|_| { + info!("Indexer catch-up timed out: ind={last_ind}"); + last_ind + }) +} #[test] async fn indexer_test_run() -> Result<()> { let ctx = TestContext::new().await?; - // RUN OBSERVATION - tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; + let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await; let last_block_seq = sequencer_service_rpc::RpcClient::get_last_block_id(ctx.sequencer_client()).await?; info!("Last block on seq now is {last_block_seq}"); - - let last_block_indexer = ctx - .indexer_client() - .get_last_finalized_block_id() - .await - .unwrap(); - info!("Last block on ind now is {last_block_indexer}"); assert!(last_block_indexer > 1); @@ -44,15 +74,8 @@ async fn indexer_test_run() -> Result<()> { async fn indexer_block_batching() -> Result<()> { let ctx = TestContext::new().await?; - // WAIT info!("Waiting for indexer to parse blocks"); - tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; - - let last_block_indexer = ctx - .indexer_client() - .get_last_finalized_block_id() - .await - .unwrap(); + let last_block_indexer = wait_for_indexer_to_catch_up(&ctx).await; info!("Last block on ind now is {last_block_indexer}"); @@ -83,8 +106,10 @@ async fn indexer_state_consistency() -> Result<()> { let mut ctx = TestContext::new().await?; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(ctx.existing_public_accounts()[0]), + from: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + from_label: None, to: Some(format_public_account_id(ctx.existing_public_accounts()[1])), + to_label: None, to_npk: None, to_vpk: None, amount: 100, @@ -113,9 +138,40 @@ async fn indexer_state_consistency() -> Result<()> { assert_eq!(acc_1_balance, 9900); assert_eq!(acc_2_balance, 20100); - // WAIT + let from: AccountId = ctx.existing_private_accounts()[0]; + let to: AccountId = ctx.existing_private_accounts()[1]; + + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: Some(format_private_account_id(from)), + from_label: None, + to: Some(format_private_account_id(to)), + to_label: None, + to_npk: None, + to_vpk: None, + 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 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); + + info!("Successfully transferred privately to owned account"); + info!("Waiting for indexer to parse blocks"); - tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; + wait_for_indexer_to_catch_up(&ctx).await; let acc1_ind_state = ctx .indexer_client() @@ -147,3 +203,76 @@ async fn indexer_state_consistency() -> Result<()> { Ok(()) } + +#[test] +async fn indexer_state_consistency_with_labels() -> Result<()> { + let mut ctx = TestContext::new().await?; + + // Assign labels to both accounts + let from_label = "idx-sender-label".to_owned(); + let to_label_str = "idx-receiver-label".to_owned(); + + let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label { + account_id: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + account_label: None, + label: from_label.clone(), + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd).await?; + + let label_cmd = Command::Account(wallet::cli::account::AccountSubcommand::Label { + account_id: Some(format_public_account_id(ctx.existing_public_accounts()[1])), + account_label: None, + label: to_label_str.clone(), + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), label_cmd).await?; + + // Send using labels instead of account IDs + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: None, + from_label: Some(from_label), + to: None, + to_label: Some(to_label_str), + to_npk: None, + to_vpk: None, + 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 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?; + + assert_eq!(acc_1_balance, 9900); + assert_eq!(acc_2_balance, 20100); + + info!("Waiting for indexer to parse blocks"); + wait_for_indexer_to_catch_up(&ctx).await; + + let acc1_ind_state = ctx + .indexer_client() + .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?; + + assert_eq!(acc1_ind_state, acc1_seq_state.into()); + + info!("Indexer state is consistent after label-based transfer"); + + Ok(()) +} diff --git a/integration_tests/tests/keys_restoration.rs b/integration_tests/tests/keys_restoration.rs index cdbe2e6b..8dca027c 100644 --- a/integration_tests/tests/keys_restoration.rs +++ b/integration_tests/tests/keys_restoration.rs @@ -69,8 +69,10 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> { // Send to this account using claiming path (using npk and vpk instead of account ID) let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_private_account_id(from), + from: Some(format_private_account_id(from)), + from_label: None, to: None, + to_label: None, to_npk: Some(hex::encode(to_keys.nullifier_public_key.0)), to_vpk: Some(hex::encode(to_keys.viewing_public_key.0)), amount: 100, @@ -143,8 +145,10 @@ async fn restore_keys_from_seed() -> Result<()> { // Send to first private account let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_private_account_id(from), + from: Some(format_private_account_id(from)), + from_label: None, to: Some(format_private_account_id(to_account_id1)), + to_label: None, to_npk: None, to_vpk: None, amount: 100, @@ -153,8 +157,10 @@ async fn restore_keys_from_seed() -> Result<()> { // Send to second private account let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_private_account_id(from), + from: Some(format_private_account_id(from)), + from_label: None, to: Some(format_private_account_id(to_account_id2)), + to_label: None, to_npk: None, to_vpk: None, amount: 101, @@ -191,8 +197,10 @@ async fn restore_keys_from_seed() -> Result<()> { // Send to first public account let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(from), + from: Some(format_public_account_id(from)), + from_label: None, to: Some(format_public_account_id(to_account_id3)), + to_label: None, to_npk: None, to_vpk: None, amount: 102, @@ -201,8 +209,10 @@ async fn restore_keys_from_seed() -> Result<()> { // Send to second public account let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(from), + from: Some(format_public_account_id(from)), + from_label: None, to: Some(format_public_account_id(to_account_id4)), + to_label: None, to_npk: None, to_vpk: None, amount: 103, @@ -264,8 +274,10 @@ async fn restore_keys_from_seed() -> Result<()> { // Test that restored accounts can send transactions let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_private_account_id(to_account_id1), + from: Some(format_private_account_id(to_account_id1)), + from_label: None, to: Some(format_private_account_id(to_account_id2)), + to_label: None, to_npk: None, to_vpk: None, amount: 10, @@ -273,8 +285,10 @@ async fn restore_keys_from_seed() -> Result<()> { wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { - from: format_public_account_id(to_account_id3), + from: Some(format_public_account_id(to_account_id3)), + from_label: None, to: Some(format_public_account_id(to_account_id4)), + to_label: None, to_npk: None, to_vpk: None, amount: 11, diff --git a/integration_tests/tests/pinata.rs b/integration_tests/tests/pinata.rs index 3285c216..77c4a646 100644 --- a/integration_tests/tests/pinata.rs +++ b/integration_tests/tests/pinata.rs @@ -52,7 +52,8 @@ async fn claim_pinata_to_uninitialized_public_account_fails_fast() -> Result<()> let claim_result = wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: winner_account_id_formatted, + to: Some(winner_account_id_formatted), + to_label: None, }), ) .await; @@ -106,7 +107,8 @@ async fn claim_pinata_to_uninitialized_private_account_fails_fast() -> Result<() let claim_result = wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: winner_account_id_formatted, + to: Some(winner_account_id_formatted), + to_label: None, }), ) .await; @@ -137,7 +139,8 @@ async fn claim_pinata_to_existing_public_account() -> Result<()> { let pinata_prize = 150; let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: format_public_account_id(ctx.existing_public_accounts()[0]), + to: Some(format_public_account_id(ctx.existing_public_accounts()[0])), + to_label: None, }); let pinata_balance_pre = ctx @@ -175,7 +178,10 @@ async fn claim_pinata_to_existing_private_account() -> Result<()> { let pinata_prize = 150; let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: format_private_account_id(ctx.existing_private_accounts()[0]), + to: Some(format_private_account_id( + ctx.existing_private_accounts()[0], + )), + to_label: None, }); let pinata_balance_pre = ctx @@ -239,7 +245,8 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { // Initialize account under auth transfer program let command = Command::AuthTransfer(AuthTransferSubcommand::Init { - account_id: winner_account_id_formatted.clone(), + account_id: Some(winner_account_id_formatted.clone()), + account_label: None, }); wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; @@ -254,7 +261,8 @@ async fn claim_pinata_to_new_private_account() -> Result<()> { // Claim pinata to the new private account let command = Command::Pinata(PinataProgramAgnosticSubcommand::Claim { - to: winner_account_id_formatted, + to: Some(winner_account_id_formatted), + to_label: None, }); let pinata_balance_pre = ctx diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index b638b6c9..e40e27c8 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -79,8 +79,10 @@ async fn create_and_transfer_public_token() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id), - supply_account_id: format_public_account_id(supply_account_id), + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id)), + supply_account_label: None, name: name.clone(), total_supply, }; @@ -126,8 +128,10 @@ async fn create_and_transfer_public_token() -> Result<()> { // Transfer 7 tokens from supply_acc to recipient_account_id let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: format_public_account_id(supply_account_id), + from: Some(format_public_account_id(supply_account_id)), + from_label: None, to: Some(format_public_account_id(recipient_account_id)), + to_label: None, to_npk: None, to_vpk: None, amount: transfer_amount, @@ -171,8 +175,10 @@ async fn create_and_transfer_public_token() -> Result<()> { // Burn 3 tokens from recipient_acc let burn_amount = 3; let subcommand = TokenProgramAgnosticSubcommand::Burn { - definition: format_public_account_id(definition_account_id), - holder: format_public_account_id(recipient_account_id), + definition: Some(format_public_account_id(definition_account_id)), + definition_label: None, + holder: Some(format_public_account_id(recipient_account_id)), + holder_label: None, amount: burn_amount, }; @@ -215,8 +221,10 @@ async fn create_and_transfer_public_token() -> Result<()> { // Mint 10 tokens at recipient_acc let mint_amount = 10; let subcommand = TokenProgramAgnosticSubcommand::Mint { - definition: format_public_account_id(definition_account_id), + definition: Some(format_public_account_id(definition_account_id)), + definition_label: None, holder: Some(format_public_account_id(recipient_account_id)), + holder_label: None, holder_npk: None, holder_vpk: None, amount: mint_amount, @@ -319,8 +327,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id), - supply_account_id: format_private_account_id(supply_account_id), + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_private_account_id(supply_account_id)), + supply_account_label: None, name: name.clone(), total_supply, }; @@ -356,8 +366,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { // Transfer 7 tokens from supply_acc to recipient_account_id let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: format_private_account_id(supply_account_id), + from: Some(format_private_account_id(supply_account_id)), + from_label: None, to: Some(format_private_account_id(recipient_account_id)), + to_label: None, to_npk: None, to_vpk: None, amount: transfer_amount, @@ -383,8 +395,10 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { // Burn 3 tokens from recipient_acc let burn_amount = 3; let subcommand = TokenProgramAgnosticSubcommand::Burn { - definition: format_public_account_id(definition_account_id), - holder: format_private_account_id(recipient_account_id), + definition: Some(format_public_account_id(definition_account_id)), + definition_label: None, + holder: Some(format_private_account_id(recipient_account_id)), + holder_label: None, amount: burn_amount, }; @@ -475,8 +489,10 @@ async fn create_token_with_private_definition() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: format_private_account_id(definition_account_id), - supply_account_id: format_public_account_id(supply_account_id), + definition_account_id: Some(format_private_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id)), + supply_account_label: None, name: name.clone(), total_supply, }; @@ -544,8 +560,10 @@ async fn create_token_with_private_definition() -> Result<()> { // Mint to public account let mint_amount_public = 10; let subcommand = TokenProgramAgnosticSubcommand::Mint { - definition: format_private_account_id(definition_account_id), + definition: Some(format_private_account_id(definition_account_id)), + definition_label: None, holder: Some(format_public_account_id(recipient_account_id_public)), + holder_label: None, holder_npk: None, holder_vpk: None, amount: mint_amount_public, @@ -590,8 +608,10 @@ async fn create_token_with_private_definition() -> Result<()> { // Mint to private account let mint_amount_private = 5; let subcommand = TokenProgramAgnosticSubcommand::Mint { - definition: format_private_account_id(definition_account_id), + definition: Some(format_private_account_id(definition_account_id)), + definition_label: None, holder: Some(format_private_account_id(recipient_account_id_private)), + holder_label: None, holder_npk: None, holder_vpk: None, amount: mint_amount_private, @@ -669,8 +689,10 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: format_private_account_id(definition_account_id), - supply_account_id: format_private_account_id(supply_account_id), + definition_account_id: Some(format_private_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_private_account_id(supply_account_id)), + supply_account_label: None, name, total_supply, }; @@ -728,8 +750,10 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { // Transfer tokens let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: format_private_account_id(supply_account_id), + from: Some(format_private_account_id(supply_account_id)), + from_label: None, to: Some(format_private_account_id(recipient_account_id)), + to_label: None, to_npk: None, to_vpk: None, amount: transfer_amount, @@ -841,8 +865,10 @@ async fn shielded_token_transfer() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id), - supply_account_id: format_public_account_id(supply_account_id), + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id)), + supply_account_label: None, name, total_supply, }; @@ -855,8 +881,10 @@ async fn shielded_token_transfer() -> Result<()> { // Perform shielded transfer: public supply -> private recipient let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: format_public_account_id(supply_account_id), + from: Some(format_public_account_id(supply_account_id)), + from_label: None, to: Some(format_private_account_id(recipient_account_id)), + to_label: None, to_npk: None, to_vpk: None, amount: transfer_amount, @@ -963,8 +991,10 @@ async fn deshielded_token_transfer() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: format_public_account_id(definition_account_id), - supply_account_id: format_private_account_id(supply_account_id), + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_private_account_id(supply_account_id)), + supply_account_label: None, name, total_supply, }; @@ -977,8 +1007,10 @@ async fn deshielded_token_transfer() -> Result<()> { // Perform deshielded transfer: private supply -> public recipient let transfer_amount = 7; let subcommand = TokenProgramAgnosticSubcommand::Send { - from: format_private_account_id(supply_account_id), + from: Some(format_private_account_id(supply_account_id)), + from_label: None, to: Some(format_public_account_id(recipient_account_id)), + to_label: None, to_npk: None, to_vpk: None, amount: transfer_amount, @@ -1069,8 +1101,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { let name = "A NAME".to_owned(); let total_supply = 37; let subcommand = TokenProgramAgnosticSubcommand::New { - definition_account_id: format_private_account_id(definition_account_id), - supply_account_id: format_private_account_id(supply_account_id), + definition_account_id: Some(format_private_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_private_account_id(supply_account_id)), + supply_account_label: None, name, total_supply, }; @@ -1108,8 +1142,10 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { // Mint using claiming path (foreign account) let mint_amount = 9; let subcommand = TokenProgramAgnosticSubcommand::Mint { - definition: format_private_account_id(definition_account_id), + definition: Some(format_private_account_id(definition_account_id)), + definition_label: None, holder: None, + holder_label: None, holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)), holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.0)), amount: mint_amount, @@ -1149,3 +1185,193 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { Ok(()) } + +#[test] +async fn create_token_using_labels() -> Result<()> { + let mut ctx = TestContext::new().await?; + + // Create definition and supply accounts with labels + let def_label = "token-definition-label".to_owned(); + let supply_label = "token-supply-label".to_owned(); + + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: Some(def_label.clone()), + })), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { + account_id: definition_account_id, + } = result + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: Some(supply_label.clone()), + })), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { + account_id: supply_account_id, + } = result + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + // Create token using account labels instead of IDs + let name = "LABELED TOKEN".to_owned(); + let total_supply = 100; + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_account_id: None, + definition_account_label: Some(def_label), + supply_account_id: None, + supply_account_label: Some(supply_label), + name: name.clone(), + 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; + + let definition_acc = ctx + .sequencer_client() + .get_account(definition_account_id) + .await?; + let token_definition = TokenDefinition::try_from(&definition_acc.data)?; + + assert_eq!(definition_acc.program_owner, Program::token().id()); + assert_eq!( + token_definition, + TokenDefinition::Fungible { + name, + total_supply, + metadata_id: None + } + ); + + let supply_acc = ctx + .sequencer_client() + .get_account(supply_account_id) + .await?; + let token_holding = TokenHolding::try_from(&supply_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: total_supply + } + ); + + info!("Successfully created token using definition and supply account labels"); + + Ok(()) +} + +#[test] +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"); + }; + + // Create supply account with a label + let supply_label = "token-supply-sender".to_owned(); + let result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::New(NewSubcommand::Public { + cci: None, + label: Some(supply_label.clone()), + })), + ) + .await?; + let SubcommandReturnValue::RegisterAccount { + account_id: supply_account_id, + } = result + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + // 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"); + }; + + // Create token + let total_supply = 50; + let subcommand = TokenProgramAgnosticSubcommand::New { + definition_account_id: Some(format_public_account_id(definition_account_id)), + definition_account_label: None, + supply_account_id: Some(format_public_account_id(supply_account_id)), + supply_account_label: None, + name: "LABEL TEST TOKEN".to_owned(), + 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; + + // Transfer token using from_label instead of from + let transfer_amount = 20; + let subcommand = TokenProgramAgnosticSubcommand::Send { + from: None, + from_label: Some(supply_label), + to: Some(format_public_account_id(recipient_account_id)), + to_label: None, + to_npk: None, + to_vpk: None, + 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; + + let recipient_acc = ctx + .sequencer_client() + .get_account(recipient_account_id) + .await?; + let token_holding = TokenHolding::try_from(&recipient_acc.data)?; + assert_eq!( + token_holding, + TokenHolding::Fungible { + definition_id: definition_account_id, + balance: transfer_amount + } + ); + + info!("Successfully transferred token using from_label"); + + Ok(()) +} diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index bd46849e..7d2a6d29 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -249,10 +249,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), vec![1, 2], - vec![ - (sender_npk.clone(), sender_ss), - (recipient_npk.clone(), recipient_ss), - ], + vec![(sender_npk, sender_ss), (recipient_npk, recipient_ss)], vec![sender_nsk], vec![Some(proof)], &program.into(), diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index 6e6b190c..ac548280 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -24,7 +24,6 @@ use log::info; use nssa::{Account, AccountId, PrivateKey, PublicKey, program::Program}; use nssa_core::program::DEFAULT_PROGRAM_ID; use tempfile::tempdir; -use wallet::WalletCore; use wallet_ffi::{ FfiAccount, FfiAccountList, FfiBytes32, FfiPrivateAccountKeys, FfiPublicAccountKey, FfiTransferResult, WalletHandle, error, @@ -211,14 +210,6 @@ fn new_wallet_ffi_with_default_config(password: &str) -> Result<*mut WalletHandl }) } -fn new_wallet_rust_with_default_config(password: &str) -> Result { - let tempdir = tempdir()?; - let config_path = tempdir.path().join("wallet_config.json"); - let storage_path = tempdir.path().join("storage.json"); - - WalletCore::new_init_storage(config_path, storage_path, None, password.to_owned()) -} - 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"); @@ -232,19 +223,8 @@ fn load_existing_ffi_wallet(home: &Path) -> Result<*mut WalletHandle> { fn wallet_ffi_create_public_accounts() -> Result<()> { let password = "password_for_tests"; let n_accounts = 10; - // First `n_accounts` public accounts created with Rust wallet - let new_public_account_ids_rust = { - let mut account_ids = Vec::new(); - let mut wallet_rust = new_wallet_rust_with_default_config(password)?; - for _ in 0..n_accounts { - let account_id = wallet_rust.create_new_account_public(None).0; - account_ids.push(*account_id.value()); - } - account_ids - }; - - // First `n_accounts` public accounts created with wallet FFI + // Create `n_accounts` public accounts with wallet FFI let new_public_account_ids_ffi = unsafe { let mut account_ids = Vec::new(); @@ -258,7 +238,20 @@ fn wallet_ffi_create_public_accounts() -> Result<()> { account_ids }; - assert_eq!(new_public_account_ids_ffi, new_public_account_ids_rust); + // All returned IDs must be unique and non-zero + assert_eq!(new_public_account_ids_ffi.len(), n_accounts); + let unique: HashSet<_> = new_public_account_ids_ffi.iter().collect(); + assert_eq!( + unique.len(), + n_accounts, + "Duplicate public account IDs returned" + ); + assert!( + new_public_account_ids_ffi + .iter() + .all(|id| *id != [0_u8; 32]), + "Zero account ID returned" + ); Ok(()) } @@ -267,19 +260,7 @@ fn wallet_ffi_create_public_accounts() -> Result<()> { fn wallet_ffi_create_private_accounts() -> Result<()> { let password = "password_for_tests"; let n_accounts = 10; - // First `n_accounts` private accounts created with Rust wallet - let new_private_account_ids_rust = { - let mut account_ids = Vec::new(); - - let mut wallet_rust = new_wallet_rust_with_default_config(password)?; - for _ in 0..n_accounts { - let account_id = wallet_rust.create_new_account_private(None).0; - account_ids.push(*account_id.value()); - } - account_ids - }; - - // First `n_accounts` private accounts created with wallet FFI + // Create `n_accounts` private accounts with wallet FFI let new_private_account_ids_ffi = unsafe { let mut account_ids = Vec::new(); @@ -293,7 +274,20 @@ fn wallet_ffi_create_private_accounts() -> Result<()> { account_ids }; - assert_eq!(new_private_account_ids_ffi, new_private_account_ids_rust); + // All returned IDs must be unique and non-zero + assert_eq!(new_private_account_ids_ffi.len(), n_accounts); + let unique: HashSet<_> = new_private_account_ids_ffi.iter().collect(); + assert_eq!( + unique.len(), + n_accounts, + "Duplicate private account IDs returned" + ); + assert!( + new_private_account_ids_ffi + .iter() + .all(|id| *id != [0_u8; 32]), + "Zero account ID returned" + ); Ok(()) } @@ -349,28 +343,23 @@ fn wallet_ffi_save_and_load_persistent_storage() -> Result<()> { fn test_wallet_ffi_list_accounts() -> Result<()> { let password = "password_for_tests"; - // Create the wallet FFI - let wallet_ffi_handle = unsafe { + // Create the wallet FFI and track which account IDs were created as public/private + let (wallet_ffi_handle, created_public_ids, created_private_ids) = unsafe { let handle = new_wallet_ffi_with_default_config(password)?; - // Create 5 public accounts and 5 private accounts + let mut public_ids: Vec<[u8; 32]> = Vec::new(); + let mut private_ids: Vec<[u8; 32]> = Vec::new(); + + // Create 5 public accounts and 5 private accounts, recording their IDs for _ in 0..5 { let mut out_account_id = FfiBytes32::from_bytes([0; 32]); wallet_ffi_create_account_public(handle, &raw mut out_account_id); + public_ids.push(out_account_id.data); + wallet_ffi_create_account_private(handle, &raw mut out_account_id); + private_ids.push(out_account_id.data); } - handle - }; - - // Create the wallet Rust - let wallet_rust = { - let mut wallet = new_wallet_rust_with_default_config(password)?; - // Create 5 public accounts and 5 private accounts - for _ in 0..5 { - wallet.create_new_account_public(None); - wallet.create_new_account_private(None); - } - wallet + (handle, public_ids, private_ids) }; // Get the account list with FFI method @@ -380,15 +369,6 @@ fn test_wallet_ffi_list_accounts() -> Result<()> { out_list }; - let wallet_rust_account_ids = wallet_rust - .storage() - .user_data - .account_ids() - .collect::>(); - - // Assert same number of elements between Rust and FFI result - assert_eq!(wallet_rust_account_ids.len(), wallet_ffi_account_list.count); - let wallet_ffi_account_list_slice = unsafe { core::slice::from_raw_parts( wallet_ffi_account_list.entries, @@ -396,37 +376,38 @@ fn test_wallet_ffi_list_accounts() -> Result<()> { ) }; - // Assert same account ids between Rust and FFI result - assert_eq!( - wallet_rust_account_ids - .iter() - .map(nssa::AccountId::value) - .collect::>(), - wallet_ffi_account_list_slice - .iter() - .map(|entry| &entry.account_id.data) - .collect::>() - ); + // All created accounts must appear in the list + let listed_public_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice + .iter() + .filter(|e| e.is_public) + .map(|e| e.account_id.data) + .collect(); + let listed_private_ids: HashSet<[u8; 32]> = wallet_ffi_account_list_slice + .iter() + .filter(|e| !e.is_public) + .map(|e| e.account_id.data) + .collect(); - // Assert `is_pub` flag is correct in the FFI result - for entry in wallet_ffi_account_list_slice { - let account_id = AccountId::new(entry.account_id.data); - let is_pub_default_in_rust_wallet = wallet_rust - .storage() - .user_data - .default_pub_account_signing_keys - .contains_key(&account_id); - let is_pub_key_tree_wallet_rust = wallet_rust - .storage() - .user_data - .public_key_tree - .account_id_map - .contains_key(&account_id); - - let is_public_in_rust_wallet = is_pub_default_in_rust_wallet || is_pub_key_tree_wallet_rust; - - assert_eq!(entry.is_public, is_public_in_rust_wallet); + for id in &created_public_ids { + assert!( + listed_public_ids.contains(id), + "Created public account not found in list with is_public=true" + ); } + for id in &created_private_ids { + assert!( + listed_private_ids.contains(id), + "Created private account not found in list with is_public=false" + ); + } + + // Total listed accounts must be at least the number we created + assert!( + wallet_ffi_account_list.count >= created_public_ids.len() + created_private_ids.len(), + "Listed account count ({}) is less than the number of created accounts ({})", + wallet_ffi_account_list.count, + created_public_ids.len() + created_private_ids.len() + ); unsafe { wallet_ffi_free_account_list(&raw mut wallet_ffi_account_list); diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index dcdaff45..c038c415 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -42,10 +42,10 @@ impl KeyChain { } #[must_use] - pub fn new_mnemonic(passphrase: String) -> Self { + 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 = SeedHolder::new_mnemonic(passphrase); + 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); @@ -53,12 +53,15 @@ impl KeyChain { 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, - } + ( + Self { + secret_spending_key, + private_key_holder, + nullifier_public_key, + viewing_public_key, + }, + mnemonic, + ) } #[must_use] diff --git a/key_protocol/src/key_management/secret_holders.rs b/key_protocol/src/key_management/secret_holders.rs index 02890631..9804ba39 100644 --- a/key_protocol/src/key_management/secret_holders.rs +++ b/key_protocol/src/key_management/secret_holders.rs @@ -8,8 +8,6 @@ use rand::{RngCore as _, rngs::OsRng}; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, digest::FixedOutput as _}; -const NSSA_ENTROPY_BYTES: [u8; 32] = [0; 32]; - /// Seed holder. Non-clonable to ensure that different holders use different seeds. /// Produces `TopSecretKeyHolder` objects. #[derive(Debug)] @@ -48,9 +46,24 @@ impl SeedHolder { } #[must_use] - pub fn new_mnemonic(passphrase: String) -> Self { - let mnemonic = Mnemonic::from_entropy(&NSSA_ENTROPY_BYTES) - .expect("Enthropy must be a multiple of 32 bytes"); + pub fn new_mnemonic(passphrase: &str) -> (Self, Mnemonic) { + let mut entropy_bytes: [u8; 32] = [0; 32]; + OsRng.fill_bytes(&mut entropy_bytes); + + 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, + ) + } + + #[must_use] + pub fn from_mnemonic(mnemonic: &Mnemonic, passphrase: &str) -> Self { let seed_wide = mnemonic.to_seed(passphrase); Self { @@ -175,12 +188,63 @@ mod tests { } #[test] - fn two_seeds_generated_same_from_same_mnemonic() { - let mnemonic = "test_pass"; + fn two_seeds_recovered_same_from_same_mnemonic() { + let passphrase = "test_pass"; - let seed_holder1 = SeedHolder::new_mnemonic(mnemonic.to_owned()); - let seed_holder2 = SeedHolder::new_mnemonic(mnemonic.to_owned()); + // Generate a mnemonic with random entropy + let (original_seed_holder, mnemonic) = SeedHolder::new_mnemonic(passphrase); - assert_eq!(seed_holder1.seed, seed_holder2.seed); + // Recover from the same mnemonic + let recovered_seed_holder = SeedHolder::from_mnemonic(&mnemonic, passphrase); + + assert_eq!(original_seed_holder.seed, recovered_seed_holder.seed); + } + + #[test] + fn new_mnemonic_generates_different_seeds_each_time() { + let (seed_holder1, mnemonic1) = SeedHolder::new_mnemonic(""); + let (seed_holder2, mnemonic2) = SeedHolder::new_mnemonic(""); + + // Different entropy should produce different mnemonics and seeds + assert_ne!(mnemonic1.to_string(), mnemonic2.to_string()); + assert_ne!(seed_holder1.seed, seed_holder2.seed); + } + + #[test] + fn new_mnemonic_generates_24_word_phrase() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + // 256 bits of entropy produces a 24-word mnemonic + let word_count = mnemonic.to_string().split_whitespace().count(); + assert_eq!(word_count, 24); + } + + #[test] + fn new_mnemonic_produces_valid_seed_length() { + let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic(""); + + assert_eq!(seed_holder.seed.len(), 64); + } + + #[test] + fn different_passphrases_produce_different_seeds() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + let seed_with_pass_a = SeedHolder::from_mnemonic(&mnemonic, "password_a"); + let seed_with_pass_b = SeedHolder::from_mnemonic(&mnemonic, "password_b"); + + // Same mnemonic but different passphrases should produce different seeds + assert_ne!(seed_with_pass_a.seed, seed_with_pass_b.seed); + } + + #[test] + fn empty_passphrase_is_deterministic() { + let (_seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + + let seed1 = SeedHolder::from_mnemonic(&mnemonic, ""); + let seed2 = SeedHolder::from_mnemonic(&mnemonic, ""); + + // Same mnemonic and passphrase should always produce the same seed + assert_eq!(seed1.seed, seed2.seed); } } diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index 8232d9f4..8186865f 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -181,11 +181,12 @@ impl NSSAUserData { impl Default for NSSAUserData { fn default() -> Self { + let (seed_holder, _mnemonic) = SeedHolder::new_mnemonic(""); Self::new_with_accounts( BTreeMap::new(), BTreeMap::new(), - KeyTreePublic::new(&SeedHolder::new_mnemonic("default".to_owned())), - KeyTreePrivate::new(&SeedHolder::new_mnemonic("default".to_owned())), + KeyTreePublic::new(&seed_holder), + KeyTreePrivate::new(&seed_holder), ) .unwrap() } diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index 07f5fe53..d8f0807c 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] nssa_core = { workspace = true, features = ["host"] } +clock_core.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index 998f6d71..215c7db8 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -17,6 +17,7 @@ pub struct PrivacyPreservingCircuitInput { /// - `0` - public account /// - `1` - private account with authentication /// - `2` - private account without authentication + /// - `3` - private PDA account pub visibility_mask: Vec, /// Public keys of private accounts. pub private_account_keys: Vec<(NullifierPublicKey, SharedSecretKey)>, diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index bb11cb4b..fd17b391 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize}; use crate::{Commitment, account::AccountId}; -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] -#[cfg_attr(any(feature = "host", test), derive(Clone, Hash))] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(any(feature = "host", test), derive(Hash))] pub struct NullifierPublicKey(pub [u8; 32]); impl From<&NullifierPublicKey> for AccountId { @@ -55,7 +55,7 @@ pub type NullifierSecretKey = [u8; 32]; #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[cfg_attr( any(feature = "host", test), - derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash) + derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash) )] pub struct Nullifier(pub(super) [u8; 32]); diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 673e09b3..5091cdff 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -6,7 +6,7 @@ use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; use crate::{ - BlockId, Timestamp, + BlockId, NullifierPublicKey, Timestamp, account::{Account, AccountId, AccountWithMetadata}, }; @@ -16,6 +16,8 @@ pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; pub type ProgramId = [u32; 8]; pub type InstructionData = Vec; pub struct ProgramInput { + pub self_program_id: ProgramId, + pub caller_program_id: Option, pub pre_states: Vec, pub instruction: T, } @@ -25,7 +27,7 @@ pub struct ProgramInput { /// Each program can derive up to `2^256` unique account IDs by choosing different /// seeds. PDAs allow programs to control namespaced account identifiers without /// collisions between programs. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct PdaSeed([u8; 32]); impl PdaSeed { @@ -35,8 +37,10 @@ impl PdaSeed { } } -impl From<(&ProgramId, &PdaSeed)> for AccountId { - fn from(value: (&ProgramId, &PdaSeed)) -> Self { +impl AccountId { + /// Derives an [`AccountId`] for a public PDA from the program ID and seed. + #[must_use] + pub fn for_public_pda(program_id: &ProgramId, seed: &PdaSeed) -> Self { use risc0_zkvm::sha::{Impl, Sha256 as _}; const PROGRAM_DERIVED_ACCOUNT_ID_PREFIX: &[u8; 32] = b"/NSSA/v0.2/AccountId/PDA/\x00\x00\x00\x00\x00\x00\x00"; @@ -44,9 +48,38 @@ impl From<(&ProgramId, &PdaSeed)> for AccountId { let mut bytes = [0; 96]; bytes[0..32].copy_from_slice(PROGRAM_DERIVED_ACCOUNT_ID_PREFIX); let program_id_bytes: &[u8] = - bytemuck::try_cast_slice(value.0).expect("ProgramId should be castable to &[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..].copy_from_slice(&value.1.0); + bytes[64..].copy_from_slice(&seed.0); + Self::new( + Impl::hash_bytes(&bytes) + .as_bytes() + .try_into() + .expect("Hash output must be exactly 32 bytes long"), + ) + } + + /// Derives an [`AccountId`] for a private PDA from the program ID, seed, and nullifier + /// public key. + /// + /// Unlike public PDAs ([`AccountId::for_public_pda`]), this includes the `npk` in the + /// derivation, making the address unique per group of controllers sharing viewing keys. + #[must_use] + pub fn for_private_pda( + program_id: &ProgramId, + seed: &PdaSeed, + npk: &NullifierPublicKey, + ) -> 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; 128]; + 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()); Self::new( Impl::hash_bytes(&bytes) .as_bytes() @@ -63,6 +96,9 @@ pub struct ChainedCall { pub pre_states: Vec, /// The instruction data to pass. pub instruction_data: InstructionData, + /// PDA seeds authorized for the callee. For each seed, the callee is authorized to + /// mutate the `AccountId` derived from `(caller_program_id, seed)`, regardless of + /// whether the account is public or private. pub pda_seeds: Vec, } @@ -112,7 +148,9 @@ pub enum Claim { /// This will give no error if program had authorization in pre state and may be useful /// if program decides to give up authorization for a chained call. Authorized, - /// The program requests ownership of the account through a PDA. + /// The program requests ownership of the account through a PDA. The program emits the + /// seed; the `AccountId` is derived from `(program_id, seed)`, regardless of whether the + /// account is public or private. Pda(PdaSeed), } @@ -281,6 +319,11 @@ pub struct InvalidWindow; #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] #[must_use = "ProgramOutput does nothing unless written"] pub struct ProgramOutput { + /// The program ID of the program that produced this output. + pub self_program_id: ProgramId, + /// The program ID of the caller that invoked this program via a chained call, + /// or `None` if this is a top-level call. + pub caller_program_id: Option, /// The instruction data the program received to produce this output. pub instruction_data: InstructionData, /// The account pre states the program received to produce this output. @@ -297,11 +340,15 @@ pub struct ProgramOutput { impl ProgramOutput { pub const fn new( + self_program_id: ProgramId, + caller_program_id: Option, instruction_data: InstructionData, pre_states: Vec, post_states: Vec, ) -> Self { Self { + self_program_id, + caller_program_id, instruction_data, pre_states, post_states, @@ -371,8 +418,8 @@ impl ProgramOutput { } /// Representation of a number as `lo + hi * 2^128`. -#[derive(PartialEq, Eq)] -struct WrappedBalanceSum { +#[derive(Debug, PartialEq, Eq)] +pub struct WrappedBalanceSum { lo: u128, hi: u128, } @@ -382,7 +429,7 @@ impl WrappedBalanceSum { /// /// Returns [`None`] if balance sum overflows `lo + hi * 2^128` representation, which is not /// expected in practical scenarios. - fn from_balances(balances: impl Iterator) -> Option { + pub fn from_balances(balances: impl Iterator) -> Option { let mut wrapped = Self { lo: 0, hi: 0 }; for balance in balances { @@ -397,29 +444,107 @@ impl WrappedBalanceSum { } } +impl std::fmt::Display for WrappedBalanceSum { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.hi == 0 { + write!(f, "{}", self.lo) + } else { + write!(f, "{} * 2^128 + {}", self.hi, self.lo) + } + } +} + +impl From for WrappedBalanceSum { + fn from(value: u128) -> Self { + Self { lo: value, hi: 0 } + } +} + +#[derive(thiserror::Error, Debug)] +pub enum ExecutionValidationError { + #[error("Pre-state account IDs are not unique")] + PreStateAccountIdsNotUnique, + + #[error( + "Pre-state and post-state lengths do not match: pre-state length {pre_state_length}, post-state length {post_state_length}" + )] + MismatchedPreStatePostStateLength { + pre_state_length: usize, + post_state_length: usize, + }, + + #[error("Unallowed modification of nonce for account {account_id}")] + ModifiedNonce { account_id: AccountId }, + + #[error("Unallowed modification of program owner for account {account_id}")] + ModifiedProgramOwner { account_id: AccountId }, + + #[error( + "Trying to decrease balance of account {account_id} owned by {owner_program_id:?} in a program {executing_program_id:?} which is not the owner" + )] + UnauthorizedBalanceDecrease { + account_id: AccountId, + owner_program_id: ProgramId, + executing_program_id: ProgramId, + }, + + #[error( + "Unauthorized modification of data for account {account_id} which is not default and not owned by executing program {executing_program_id:?}" + )] + UnauthorizedDataModification { + account_id: AccountId, + executing_program_id: ProgramId, + }, + + #[error( + "Post-state for account {account_id} has default program owner but pre-state was not default" + )] + NonDefaultAccountWithDefaultOwner { account_id: AccountId }, + + #[error("Total balance across accounts overflowed 2^256 - 1")] + BalanceSumOverflow, + + #[error( + "Total balance across accounts is not preserved: total balance in pre-states {total_balance_pre_states}, total balance in post-states {total_balance_post_states}" + )] + MismatchedTotalBalance { + total_balance_pre_states: WrappedBalanceSum, + total_balance_post_states: WrappedBalanceSum, + }, +} + +/// Computes the set of public-PDA `AccountId`s the callee is authorized to mutate. +/// +/// Returns only public-form derivations, suitable for contexts where all accounts are public +/// (e.g. the public-execution path). The privacy circuit must additionally check each mask-3 +/// `pre_state` against [`AccountId::for_private_pda`] with the supplied npk for that +/// `pre_state`. #[must_use] -pub fn compute_authorized_pdas( +pub fn compute_public_authorized_pdas( caller_program_id: Option, pda_seeds: &[PdaSeed], ) -> HashSet { - caller_program_id - .map(|caller_program_id| { - pda_seeds - .iter() - .map(|pda_seed| AccountId::from((&caller_program_id, pda_seed))) - .collect() - }) - .unwrap_or_default() + let Some(caller) = caller_program_id else { + return HashSet::new(); + }; + pda_seeds + .iter() + .map(|seed| AccountId::for_public_pda(&caller, seed)) + .collect() } /// Reads the NSSA inputs from the guest environment. #[must_use] pub fn read_nssa_inputs() -> (ProgramInput, InstructionData) { + let self_program_id: ProgramId = env::read(); + let caller_program_id: Option = env::read(); let pre_states: Vec = env::read(); let instruction_words: InstructionData = env::read(); let instruction = T::deserialize(&mut Deserializer::new(instruction_words.as_ref())).unwrap(); ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction, }, @@ -433,31 +558,39 @@ pub fn read_nssa_inputs() -> (ProgramInput, InstructionD /// - `pre_states`: The list of input accounts, each annotated with authorization metadata. /// - `post_states`: The list of resulting accounts after executing the program logic. /// - `executing_program_id`: The identifier of the program that was executed. -#[must_use] pub fn validate_execution( pre_states: &[AccountWithMetadata], post_states: &[AccountPostState], executing_program_id: ProgramId, -) -> bool { +) -> Result<(), ExecutionValidationError> { // 1. Check account ids are all different if !validate_uniqueness_of_account_ids(pre_states) { - return false; + return Err(ExecutionValidationError::PreStateAccountIdsNotUnique); } // 2. Lengths must match if pre_states.len() != post_states.len() { - return false; + return Err( + ExecutionValidationError::MismatchedPreStatePostStateLength { + pre_state_length: pre_states.len(), + post_state_length: post_states.len(), + }, + ); } for (pre, post) in pre_states.iter().zip(post_states) { // 3. Nonce must remain unchanged if pre.account.nonce != post.account.nonce { - return false; + return Err(ExecutionValidationError::ModifiedNonce { + account_id: pre.account_id, + }); } // 4. Program ownership changes are not allowed if pre.account.program_owner != post.account.program_owner { - return false; + return Err(ExecutionValidationError::ModifiedProgramOwner { + account_id: pre.account_id, + }); } let account_program_owner = pre.account.program_owner; @@ -466,7 +599,11 @@ pub fn validate_execution( if post.account.balance < pre.account.balance && account_program_owner != executing_program_id { - return false; + return Err(ExecutionValidationError::UnauthorizedBalanceDecrease { + account_id: pre.account_id, + owner_program_id: account_program_owner, + executing_program_id, + }); } // 6. Data changes only allowed if owned by executing program or if account pre state has @@ -475,13 +612,20 @@ pub fn validate_execution( && pre.account != Account::default() && account_program_owner != executing_program_id { - return false; + return Err(ExecutionValidationError::UnauthorizedDataModification { + account_id: pre.account_id, + executing_program_id, + }); } // 7. If a post state has default program owner, the pre state must have been a default // account if post.account.program_owner == DEFAULT_PROGRAM_ID && pre.account != Account::default() { - return false; + return Err( + ExecutionValidationError::NonDefaultAccountWithDefaultOwner { + account_id: pre.account_id, + }, + ); } } @@ -490,20 +634,23 @@ pub fn validate_execution( let Some(total_balance_pre_states) = WrappedBalanceSum::from_balances(pre_states.iter().map(|pre| pre.account.balance)) else { - return false; + return Err(ExecutionValidationError::BalanceSumOverflow); }; let Some(total_balance_post_states) = WrappedBalanceSum::from_balances(post_states.iter().map(|post| post.account.balance)) else { - return false; + return Err(ExecutionValidationError::BalanceSumOverflow); }; if total_balance_pre_states != total_balance_post_states { - return false; + return Err(ExecutionValidationError::MismatchedTotalBalance { + total_balance_pre_states, + total_balance_post_states, + }); } - true + Ok(()) } fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> bool { @@ -620,7 +767,7 @@ mod tests { #[test] fn program_output_try_with_block_validity_window_range() { - let output = ProgramOutput::new(vec![], vec![], vec![]) + 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)); @@ -629,24 +776,24 @@ mod tests { #[test] fn program_output_with_block_validity_window_range_from() { - let output = - ProgramOutput::new(vec![], vec![], vec![]).with_block_validity_window(10_u64..); + 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(vec![], vec![], vec![]).with_block_validity_window(..100_u64); + 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(vec![], vec![], vec![]).try_with_block_validity_window(5_u64..5); + let result = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![]) + .try_with_block_validity_window(5_u64..5); assert!(result.is_err()); } @@ -694,4 +841,108 @@ mod tests { 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)` triple. 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 expected = AccountId::new([ + 132, 198, 103, 173, 244, 211, 188, 217, 249, 99, 126, 205, 152, 120, 192, 47, 13, 53, + 133, 3, 17, 69, 92, 243, 140, 94, 182, 211, 218, 75, 215, 45, + ]); + assert_eq!( + AccountId::for_private_pda(&program_id, &seed, &npk), + 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), + AccountId::for_private_pda(&program_id, &seed, &npk_b), + ); + } + + /// 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), + AccountId::for_private_pda(&program_id, &seed_b, &npk), + ); + } + + /// 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), + AccountId::for_private_pda(&program_id_b, &seed, &npk), + ); + } + + /// 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); + let public_id = AccountId::for_public_pda(&program_id, &seed); + assert_ne!(private_id, public_id); + } + + /// A private PDA address differs from a standard private account address at the same `npk`, + /// because the private PDA formula includes `program_id` and `seed`. + #[test] + fn for_private_pda_differs_from_standard_private() { + let program_id: ProgramId = [1; 8]; + let seed = PdaSeed::new([2; 32]); + let npk = NullifierPublicKey([3; 32]); + let private_pda_id = AccountId::for_private_pda(&program_id, &seed, &npk); + let standard_private_id = AccountId::from(&npk); + assert_ne!(private_pda_id, standard_private_id); + } + + // ---- compute_public_authorized_pdas tests ---- + + /// `compute_public_authorized_pdas` returns the public PDA addresses for the caller's seeds. + #[test] + fn compute_public_authorized_pdas_with_seeds() { + let caller: ProgramId = [1; 8]; + let seed = PdaSeed::new([2; 32]); + let result = compute_public_authorized_pdas(Some(caller), &[seed]); + let expected = AccountId::for_public_pda(&caller, &seed); + assert!(result.contains(&expected)); + assert_eq!(result.len(), 1); + } + + /// With no caller (top-level call), the result is always empty. + #[test] + fn compute_public_authorized_pdas_no_caller_returns_empty() { + let seed = PdaSeed::new([2; 32]); + let result = compute_public_authorized_pdas(None, &[seed]); + assert!(result.is_empty()); + } } diff --git a/nssa/src/error.rs b/nssa/src/error.rs index 61966515..565e02ba 100644 --- a/nssa/src/error.rs +++ b/nssa/src/error.rs @@ -1,12 +1,16 @@ use std::io; +use nssa_core::{ + account::{Account, AccountId}, + program::ProgramId, +}; use thiserror::Error; #[macro_export] macro_rules! ensure { ($cond:expr, $err:expr) => { if !$cond { - return Err($err); + return Err($err.into()); } }; } @@ -17,7 +21,7 @@ pub enum NssaError { InvalidInput(String), #[error("Program violated execution rules")] - InvalidProgramBehavior, + InvalidProgramBehavior(#[from] InvalidProgramBehaviorError), #[error("Serialization error: {0}")] InstructionSerializationError(String), @@ -32,15 +36,15 @@ pub enum NssaError { InvalidPublicKey(#[source] k256::schnorr::Error), #[error("Invalid hex for public key")] - InvalidHexPublicKey(hex::FromHexError), + InvalidHexPublicKey(#[source] hex::FromHexError), - #[error("Risc0 error: {0}")] + #[error("Failed to write program input: {0}")] ProgramWriteInputFailed(String), - #[error("Risc0 error: {0}")] + #[error("Failed to execute program: {0}")] ProgramExecutionFailed(String), - #[error("Risc0 error: {0}")] + #[error("Failed to prove program: {0}")] ProgramProveFailed(String), #[error("Invalid transaction: {0}")] @@ -77,6 +81,61 @@ pub enum NssaError { OutOfValidityWindow, } +#[derive(Error, Debug)] +pub enum InvalidProgramBehaviorError { + #[error( + "Inconsistent pre-state for account {account_id} : expected {expected:?}, actual {actual:?}" + )] + InconsistentAccountPreState { + account_id: AccountId, + // Boxed to reduce the size of the error type + expected: Box, + actual: Box, + }, + + #[error( + "Inconsistent authorization for account {account_id} : expected {expected_authorization}, actual {actual_authorization}" + )] + InconsistentAccountAuthorization { + account_id: AccountId, + expected_authorization: bool, + actual_authorization: bool, + }, + + #[error("Program ID mismatch: expected {expected:?}, actual {actual:?}")] + MismatchedProgramId { + expected: ProgramId, + actual: ProgramId, + }, + + #[error("Caller program ID mismatch: expected {expected:?}, actual {actual:?}")] + MismatchedCallerProgramId { + expected: Option, + actual: Option, + }, + + #[error(transparent)] + ExecutionValidationFailed(#[from] nssa_core::program::ExecutionValidationError), + + #[error("Trying to claim account {account_id} which is not default")] + ClaimedNonDefaultAccount { account_id: AccountId }, + + #[error("Trying to claim account {account_id} which is not authorized")] + ClaimedUnauthorizedAccount { account_id: AccountId }, + + #[error("PDA claim mismatch: expected {expected:?}, actual {actual:?}")] + MismatchedPdaClaim { + expected: AccountId, + actual: AccountId, + }, + + #[error("Default account {account_id} was modified without being claimed")] + DefaultAccountModifiedWithoutClaim { account_id: AccountId }, + + #[error("Called program {program_id:?} which is not listed in dependencies")] + UndeclaredProgramDependency { program_id: ProgramId }, +} + #[cfg(test)] mod tests { diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index ce958354..f4c3be9d 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -16,7 +16,11 @@ pub use program_deployment_transaction::ProgramDeploymentTransaction; pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID; pub use public_transaction::PublicTransaction; pub use signature::{PrivateKey, PublicKey, Signature}; -pub use state::V03State; +pub use state::{ + CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, + CLOCK_PROGRAM_ACCOUNT_IDS, V03State, +}; +pub use validated_state_diff::ValidatedStateDiff; pub mod encoding; pub mod error; @@ -27,6 +31,7 @@ pub mod program_deployment_transaction; pub mod public_transaction; mod signature; mod state; +mod validated_state_diff; pub mod program_methods { include!(concat!(env!("OUT_DIR"), "/program_methods/mod.rs")); diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 0ae7eaac..528bb372 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -10,7 +10,7 @@ use nssa_core::{ use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover}; use crate::{ - error::NssaError, + error::{InvalidProgramBehaviorError, NssaError}, program::Program, program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}, state::MAX_NUMBER_CHAINED_CALLS, @@ -87,15 +87,16 @@ pub fn execute_and_prove( pda_seeds: vec![], }; - let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program)]); + let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program, None)]); let mut chain_calls_counter = 0; - while let Some((chained_call, program)) = chained_calls.pop_front() { + while let Some((chained_call, program, caller_program_id)) = chained_calls.pop_front() { if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS { return Err(NssaError::MaxChainedCallsDepthExceeded); } let inner_receipt = execute_and_prove_program( program, + caller_program_id, &chained_call.pre_states, &chained_call.instruction_data, )?; @@ -112,10 +113,12 @@ pub fn execute_and_prove( env_builder.add_assumption(inner_receipt); for new_call in program_output.chained_calls.into_iter().rev() { - let next_program = dependencies - .get(&new_call.program_id) - .ok_or(NssaError::InvalidProgramBehavior)?; - chained_calls.push_front((new_call, next_program)); + let next_program = dependencies.get(&new_call.program_id).ok_or( + InvalidProgramBehaviorError::UndeclaredProgramDependency { + program_id: new_call.program_id, + }, + )?; + chained_calls.push_front((new_call, next_program, Some(chained_call.program_id))); } chain_calls_counter = chain_calls_counter @@ -153,12 +156,19 @@ pub fn execute_and_prove( fn execute_and_prove_program( program: &Program, + caller_program_id: Option, pre_states: &[AccountWithMetadata], instruction_data: &InstructionData, ) -> Result { // Write inputs to the program let mut env_builder = ExecutorEnv::builder(); - Program::write_inputs(pre_states, instruction_data, &mut env_builder)?; + Program::write_inputs( + program.id(), + caller_program_id, + pre_states, + instruction_data, + &mut env_builder, + )?; let env = env_builder.build().unwrap(); // Prove the program diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index 977bb0d0..2e46f628 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -1,19 +1,10 @@ -use std::{ - collections::{HashMap, HashSet}, - hash::Hash, -}; +use std::collections::HashSet; use borsh::{BorshDeserialize, BorshSerialize}; -use nssa_core::{ - BlockId, PrivacyPreservingCircuitOutput, Timestamp, - account::{Account, AccountWithMetadata}, -}; +use nssa_core::account::AccountId; use sha2::{Digest as _, digest::FixedOutput as _}; use super::{message::Message, witness_set::WitnessSet}; -use crate::{ - AccountId, V03State, error::NssaError, privacy_preserving_transaction::circuit::Proof, -}; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct PrivacyPreservingTransaction { @@ -30,108 +21,6 @@ impl PrivacyPreservingTransaction { } } - pub(crate) fn validate_and_produce_public_state_diff( - &self, - state: &V03State, - block_id: BlockId, - timestamp: Timestamp, - ) -> Result, NssaError> { - let message = &self.message; - let witness_set = &self.witness_set; - - // 1. Commitments or nullifiers are non empty - if message.new_commitments.is_empty() && message.new_nullifiers.is_empty() { - return Err(NssaError::InvalidInput( - "Empty commitments and empty nullifiers found in message".into(), - )); - } - - // 2. Check there are no duplicate account_ids in the public_account_ids list. - if n_unique(&message.public_account_ids) != message.public_account_ids.len() { - return Err(NssaError::InvalidInput( - "Duplicate account_ids found in message".into(), - )); - } - - // Check there are no duplicate nullifiers in the new_nullifiers list - if n_unique(&message.new_nullifiers) != message.new_nullifiers.len() { - return Err(NssaError::InvalidInput( - "Duplicate nullifiers found in message".into(), - )); - } - - // Check there are no duplicate commitments in the new_commitments list - if n_unique(&message.new_commitments) != message.new_commitments.len() { - return Err(NssaError::InvalidInput( - "Duplicate commitments found in message".into(), - )); - } - - // 3. Nonce checks and Valid signatures - // Check exactly one nonce is provided for each signature - if message.nonces.len() != witness_set.signatures_and_public_keys.len() { - return Err(NssaError::InvalidInput( - "Mismatch between number of nonces and signatures/public keys".into(), - )); - } - - // Check the signatures are valid - if !witness_set.signatures_are_valid_for(message) { - return Err(NssaError::InvalidInput( - "Invalid signature for given message and public key".into(), - )); - } - - let signer_account_ids = self.signer_account_ids(); - // Check nonces corresponds to the current nonces on the public state. - for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) { - let current_nonce = state.get_account_by_id(*account_id).nonce; - if current_nonce != *nonce { - return Err(NssaError::InvalidInput("Nonce mismatch".into())); - } - } - - // Verify validity window - if !message.block_validity_window.is_valid_for(block_id) - || !message.timestamp_validity_window.is_valid_for(timestamp) - { - return Err(NssaError::OutOfValidityWindow); - } - - // Build pre_states for proof verification - let public_pre_states: Vec<_> = message - .public_account_ids - .iter() - .map(|account_id| { - AccountWithMetadata::new( - state.get_account_by_id(*account_id), - signer_account_ids.contains(account_id), - *account_id, - ) - }) - .collect(); - - // 4. Proof verification - check_privacy_preserving_circuit_proof_is_valid( - &witness_set.proof, - &public_pre_states, - message, - )?; - - // 5. Commitment freshness - state.check_commitments_are_new(&message.new_commitments)?; - - // 6. Nullifier uniqueness - state.check_nullifiers_are_valid(&message.new_nullifiers)?; - - Ok(message - .public_account_ids - .iter() - .copied() - .zip(message.public_post_states.clone()) - .collect()) - } - #[must_use] pub const fn message(&self) -> &Message { &self.message @@ -170,36 +59,6 @@ impl PrivacyPreservingTransaction { } } -fn check_privacy_preserving_circuit_proof_is_valid( - proof: &Proof, - public_pre_states: &[AccountWithMetadata], - message: &Message, -) -> Result<(), NssaError> { - let output = PrivacyPreservingCircuitOutput { - public_pre_states: public_pre_states.to_vec(), - public_post_states: message.public_post_states.clone(), - ciphertexts: message - .encrypted_private_post_states - .iter() - .cloned() - .map(|value| value.ciphertext) - .collect(), - new_commitments: message.new_commitments.clone(), - new_nullifiers: message.new_nullifiers.clone(), - block_validity_window: message.block_validity_window, - timestamp_validity_window: message.timestamp_validity_window, - }; - proof - .is_valid_for(&output) - .then_some(()) - .ok_or(NssaError::InvalidPrivacyPreservingProof) -} - -fn n_unique(data: &[T]) -> usize { - let set: HashSet<&T> = data.iter().collect(); - set.len() -} - #[cfg(test)] mod tests { use crate::{ diff --git a/nssa/src/program.rs b/nssa/src/program.rs index b87fcf35..b8c3fe77 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -9,7 +9,9 @@ use serde::Serialize; use crate::{ error::NssaError, program_methods::{ - AMM_ELF, ASSOCIATED_TOKEN_ACCOUNT_ELF, AUTHENTICATED_TRANSFER_ELF, PINATA_ELF, TOKEN_ELF, + AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID, + AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF, + PINATA_ID, TOKEN_ELF, TOKEN_ID, }, }; @@ -52,13 +54,20 @@ impl Program { pub(crate) fn execute( &self, + caller_program_id: Option, pre_states: &[AccountWithMetadata], instruction_data: &InstructionData, ) -> Result { // Write inputs to the program let mut env_builder = ExecutorEnv::builder(); env_builder.session_limit(Some(MAX_NUM_CYCLES_PUBLIC_EXECUTION)); - Self::write_inputs(pre_states, instruction_data, &mut env_builder)?; + Self::write_inputs( + self.id, + caller_program_id, + pre_states, + instruction_data, + &mut env_builder, + )?; let env = env_builder.build().unwrap(); // Execute the program (without proving) @@ -78,40 +87,66 @@ impl Program { /// Writes inputs to `env_builder` in the order expected by the programs. pub(crate) fn write_inputs( + program_id: ProgramId, + caller_program_id: Option, pre_states: &[AccountWithMetadata], instruction_data: &[u32], env_builder: &mut ExecutorEnvBuilder, ) -> Result<(), NssaError> { + env_builder + .write(&program_id) + .map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?; + env_builder + .write(&caller_program_id) + .map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?; let pre_states = pre_states.to_vec(); env_builder - .write(&(pre_states, instruction_data)) + .write(&pre_states) + .map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?; + env_builder + .write(&instruction_data) .map_err(|e| NssaError::ProgramWriteInputFailed(e.to_string()))?; Ok(()) } #[must_use] pub fn authenticated_transfer_program() -> Self { - // This unwrap won't panic since the `AUTHENTICATED_TRANSFER_ELF` comes from risc0 build of - // `program_methods` - Self::new(AUTHENTICATED_TRANSFER_ELF.to_vec()).unwrap() + Self { + id: AUTHENTICATED_TRANSFER_ID, + elf: AUTHENTICATED_TRANSFER_ELF.to_vec(), + } } #[must_use] pub fn token() -> Self { - // This unwrap won't panic since the `TOKEN_ELF` comes from risc0 build of - // `program_methods` - Self::new(TOKEN_ELF.to_vec()).unwrap() + Self { + id: TOKEN_ID, + elf: TOKEN_ELF.to_vec(), + } } #[must_use] pub fn amm() -> Self { - Self::new(AMM_ELF.to_vec()).expect("The AMM program must be a valid Risc0 program") + Self { + id: AMM_ID, + elf: AMM_ELF.to_vec(), + } + } + + #[must_use] + pub fn clock() -> Self { + Self { + id: CLOCK_ID, + elf: CLOCK_ELF.to_vec(), + } } #[must_use] pub fn ata() -> Self { - Self::new(ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec()) - .expect("The ATA program must be a valid Risc0 program") + Self { + id: ASSOCIATED_TOKEN_ACCOUNT_ID, + elf: ASSOCIATED_TOKEN_ACCOUNT_ELF.to_vec(), + } } } @@ -119,16 +154,19 @@ impl Program { impl Program { #[must_use] pub fn pinata() -> Self { - // This unwrap won't panic since the `PINATA_ELF` comes from risc0 build of - // `program_methods` - Self::new(PINATA_ELF.to_vec()).unwrap() + Self { + id: PINATA_ID, + elf: PINATA_ELF.to_vec(), + } } #[must_use] - #[expect(clippy::non_ascii_literal, reason = "More readable")] pub fn pinata_token() -> Self { - use crate::program_methods::PINATA_TOKEN_ELF; - Self::new(PINATA_TOKEN_ELF.to_vec()).expect("Piñata program must be a valid R0BF file") + use crate::program_methods::{PINATA_TOKEN_ELF, PINATA_TOKEN_ID}; + Self { + id: PINATA_TOKEN_ID, + elf: PINATA_TOKEN_ELF.to_vec(), + } } } @@ -139,8 +177,9 @@ mod tests { use crate::{ program::Program, program_methods::{ - AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID, - TOKEN_ELF, TOKEN_ID, + AMM_ELF, AMM_ID, ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID, + AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, CLOCK_ELF, CLOCK_ID, PINATA_ELF, + PINATA_ID, PINATA_TOKEN_ELF, PINATA_TOKEN_ID, TOKEN_ELF, TOKEN_ID, }, }; @@ -253,6 +292,36 @@ mod tests { } } + #[must_use] + pub fn pda_claimer() -> Self { + use test_program_methods::{PDA_CLAIMER_ELF, PDA_CLAIMER_ID}; + + Self { + id: PDA_CLAIMER_ID, + elf: PDA_CLAIMER_ELF.to_vec(), + } + } + + #[must_use] + pub fn private_pda_delegator() -> Self { + use test_program_methods::{PRIVATE_PDA_DELEGATOR_ELF, PRIVATE_PDA_DELEGATOR_ID}; + + Self { + id: PRIVATE_PDA_DELEGATOR_ID, + elf: PRIVATE_PDA_DELEGATOR_ELF.to_vec(), + } + } + + #[must_use] + pub fn two_pda_claimer() -> Self { + use test_program_methods::{TWO_PDA_CLAIMER_ELF, TWO_PDA_CLAIMER_ID}; + + Self { + id: TWO_PDA_CLAIMER_ID, + elf: TWO_PDA_CLAIMER_ELF.to_vec(), + } + } + #[must_use] pub fn changer_claimer() -> Self { use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID}; @@ -273,6 +342,16 @@ mod tests { } } + #[must_use] + pub fn auth_asserting_noop() -> Self { + use test_program_methods::{AUTH_ASSERTING_NOOP_ELF, AUTH_ASSERTING_NOOP_ID}; + + Self { + id: AUTH_ASSERTING_NOOP_ID, + elf: AUTH_ASSERTING_NOOP_ELF.to_vec(), + } + } + #[must_use] pub fn malicious_authorization_changer() -> Self { use test_program_methods::{ @@ -287,24 +366,71 @@ mod tests { #[must_use] pub fn modified_transfer_program() -> Self { - use test_program_methods::MODIFIED_TRANSFER_ELF; - // This unwrap won't panic since the `MODIFIED_TRANSFER_ELF` comes from risc0 build of - // `program_methods` - Self::new(MODIFIED_TRANSFER_ELF.to_vec()).unwrap() + use test_program_methods::{MODIFIED_TRANSFER_ELF, MODIFIED_TRANSFER_ID}; + Self { + id: MODIFIED_TRANSFER_ID, + elf: MODIFIED_TRANSFER_ELF.to_vec(), + } } #[must_use] pub fn validity_window() -> Self { - use test_program_methods::VALIDITY_WINDOW_ELF; - // This unwrap won't panic since the `VALIDITY_WINDOW_ELF` comes from risc0 build of - // `program_methods` - Self::new(VALIDITY_WINDOW_ELF.to_vec()).unwrap() + use test_program_methods::{VALIDITY_WINDOW_ELF, VALIDITY_WINDOW_ID}; + Self { + id: VALIDITY_WINDOW_ID, + elf: VALIDITY_WINDOW_ELF.to_vec(), + } } #[must_use] pub fn validity_window_chain_caller() -> Self { - use test_program_methods::VALIDITY_WINDOW_CHAIN_CALLER_ELF; - Self::new(VALIDITY_WINDOW_CHAIN_CALLER_ELF.to_vec()).unwrap() + use test_program_methods::{ + VALIDITY_WINDOW_CHAIN_CALLER_ELF, VALIDITY_WINDOW_CHAIN_CALLER_ID, + }; + Self { + id: VALIDITY_WINDOW_CHAIN_CALLER_ID, + elf: VALIDITY_WINDOW_CHAIN_CALLER_ELF.to_vec(), + } + } + + #[must_use] + pub fn flash_swap_initiator() -> Self { + use test_program_methods::FLASH_SWAP_INITIATOR_ELF; + Self::new(FLASH_SWAP_INITIATOR_ELF.to_vec()) + .expect("flash_swap_initiator must be a valid Risc0 program") + } + + #[must_use] + pub fn flash_swap_callback() -> Self { + use test_program_methods::FLASH_SWAP_CALLBACK_ELF; + Self::new(FLASH_SWAP_CALLBACK_ELF.to_vec()) + .expect("flash_swap_callback must be a valid Risc0 program") + } + + #[must_use] + pub fn malicious_self_program_id() -> Self { + use test_program_methods::MALICIOUS_SELF_PROGRAM_ID_ELF; + Self::new(MALICIOUS_SELF_PROGRAM_ID_ELF.to_vec()) + .expect("malicious_self_program_id must be a valid Risc0 program") + } + + #[must_use] + pub fn malicious_caller_program_id() -> Self { + use test_program_methods::MALICIOUS_CALLER_PROGRAM_ID_ELF; + Self::new(MALICIOUS_CALLER_PROGRAM_ID_ELF.to_vec()) + .expect("malicious_caller_program_id must be a valid Risc0 program") + } + + #[must_use] + pub fn time_locked_transfer() -> Self { + use test_program_methods::TIME_LOCKED_TRANSFER_ELF; + Self::new(TIME_LOCKED_TRANSFER_ELF.to_vec()).unwrap() + } + + #[must_use] + pub fn pinata_cooldown() -> Self { + use test_program_methods::PINATA_COOLDOWN_ELF; + Self::new(PINATA_COOLDOWN_ELF.to_vec()).unwrap() } } @@ -333,7 +459,7 @@ mod tests { ..Account::default() }; let program_output = program - .execute(&[sender, recipient], &instruction_data) + .execute(None, &[sender, recipient], &instruction_data) .unwrap(); let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap(); @@ -355,4 +481,21 @@ mod tests { assert_eq!(pinata_program.id, PINATA_ID); assert_eq!(pinata_program.elf, PINATA_ELF); } + + #[test] + fn builtin_program_ids_match_elfs() { + let cases: &[(&[u8], [u32; 8])] = &[ + (AMM_ELF, AMM_ID), + (AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID), + (ASSOCIATED_TOKEN_ACCOUNT_ELF, ASSOCIATED_TOKEN_ACCOUNT_ID), + (CLOCK_ELF, CLOCK_ID), + (PINATA_ELF, PINATA_ID), + (PINATA_TOKEN_ELF, PINATA_TOKEN_ID), + (TOKEN_ELF, TOKEN_ID), + ]; + for (elf, expected_id) in cases { + let program = Program::new(elf.to_vec()).unwrap(); + assert_eq!(program.id(), *expected_id); + } + } } diff --git a/nssa/src/program_deployment_transaction/transaction.rs b/nssa/src/program_deployment_transaction/transaction.rs index 90387fe6..3fa775a8 100644 --- a/nssa/src/program_deployment_transaction/transaction.rs +++ b/nssa/src/program_deployment_transaction/transaction.rs @@ -2,9 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::account::AccountId; use sha2::{Digest as _, digest::FixedOutput as _}; -use crate::{ - V03State, error::NssaError, program::Program, program_deployment_transaction::message::Message, -}; +use crate::program_deployment_transaction::message::Message; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct ProgramDeploymentTransaction { @@ -22,19 +20,6 @@ impl ProgramDeploymentTransaction { self.message } - pub(crate) fn validate_and_produce_public_state_diff( - &self, - state: &V03State, - ) -> Result { - // TODO: remove clone - let program = Program::new(self.message.bytecode.clone())?; - if state.programs().contains_key(&program.id()) { - Err(NssaError::ProgramAlreadyExists) - } else { - Ok(program) - } - } - #[must_use] pub fn hash(&self) -> [u8; 32] { let bytes = self.to_bytes(); diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index 6a27c0a4..8ab79535 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -1,20 +1,10 @@ -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::HashSet; use borsh::{BorshDeserialize, BorshSerialize}; -use log::debug; -use nssa_core::{ - BlockId, Timestamp, - account::{Account, AccountId, AccountWithMetadata}, - program::{ChainedCall, Claim, DEFAULT_PROGRAM_ID, validate_execution}, -}; +use nssa_core::account::AccountId; use sha2::{Digest as _, digest::FixedOutput as _}; -use crate::{ - V03State, ensure, - error::NssaError, - public_transaction::{Message, WitnessSet}, - state::MAX_NUMBER_CHAINED_CALLS, -}; +use crate::public_transaction::{Message, WitnessSet}; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct PublicTransaction { @@ -67,211 +57,6 @@ impl PublicTransaction { hasher.update(&bytes); hasher.finalize_fixed().into() } - - pub(crate) fn validate_and_produce_public_state_diff( - &self, - state: &V03State, - block_id: BlockId, - timestamp: Timestamp, - ) -> Result, NssaError> { - let message = self.message(); - let witness_set = self.witness_set(); - - // All account_ids must be different - ensure!( - message.account_ids.iter().collect::>().len() == message.account_ids.len(), - NssaError::InvalidInput("Duplicate account_ids found in message".into(),) - ); - - // Check exactly one nonce is provided for each signature - ensure!( - message.nonces.len() == witness_set.signatures_and_public_keys.len(), - NssaError::InvalidInput( - "Mismatch between number of nonces and signatures/public keys".into(), - ) - ); - - // Check the signatures are valid - ensure!( - witness_set.is_valid_for(message), - NssaError::InvalidInput("Invalid signature for given message and public key".into()) - ); - - let signer_account_ids = self.signer_account_ids(); - // Check nonces corresponds to the current nonces on the public state. - for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) { - let current_nonce = state.get_account_by_id(*account_id).nonce; - ensure!( - current_nonce == *nonce, - NssaError::InvalidInput("Nonce mismatch".into()) - ); - } - - // Build pre_states for execution - let input_pre_states: Vec<_> = message - .account_ids - .iter() - .map(|account_id| { - AccountWithMetadata::new( - state.get_account_by_id(*account_id), - signer_account_ids.contains(account_id), - *account_id, - ) - }) - .collect(); - - let mut state_diff: HashMap = HashMap::new(); - - let initial_call = ChainedCall { - program_id: message.program_id, - instruction_data: message.instruction_data.clone(), - pre_states: input_pre_states, - pda_seeds: vec![], - }; - - let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); - let mut chain_calls_counter = 0; - - while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() { - ensure!( - chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS, - NssaError::MaxChainedCallsDepthExceeded - ); - - // Check that the `program_id` corresponds to a deployed program - let Some(program) = state.programs().get(&chained_call.program_id) else { - return Err(NssaError::InvalidInput("Unknown program".into())); - }; - - debug!( - "Program {:?} pre_states: {:?}, instruction_data: {:?}", - chained_call.program_id, chained_call.pre_states, chained_call.instruction_data - ); - let mut program_output = - program.execute(&chained_call.pre_states, &chained_call.instruction_data)?; - debug!( - "Program {:?} output: {:?}", - chained_call.program_id, program_output - ); - - let authorized_pdas = nssa_core::program::compute_authorized_pdas( - caller_program_id, - &chained_call.pda_seeds, - ); - - let is_authorized = |account_id: &AccountId| { - signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id) - }; - - for pre in &program_output.pre_states { - let account_id = pre.account_id; - // Check that the program output pre_states coincide with the values in the public - // state or with any modifications to those values during the chain of calls. - let expected_pre = state_diff - .get(&account_id) - .cloned() - .unwrap_or_else(|| state.get_account_by_id(account_id)); - ensure!( - pre.account == expected_pre, - NssaError::InvalidProgramBehavior - ); - - // Check that authorization flags are consistent with the provided ones or - // authorized by program through the PDA mechanism - ensure!( - pre.is_authorized == is_authorized(&account_id), - NssaError::InvalidProgramBehavior - ); - } - - // Verify execution corresponds to a well-behaved program. - // See the # Programs section for the definition of the `validate_execution` method. - ensure!( - validate_execution( - &program_output.pre_states, - &program_output.post_states, - chained_call.program_id, - ), - NssaError::InvalidProgramBehavior - ); - - // Verify validity window - ensure!( - program_output.block_validity_window.is_valid_for(block_id) - && program_output - .timestamp_validity_window - .is_valid_for(timestamp), - NssaError::OutOfValidityWindow - ); - - for (i, post) in program_output.post_states.iter_mut().enumerate() { - let Some(claim) = post.required_claim() else { - continue; - }; - // The invoked program can only claim accounts with default program id. - ensure!( - post.account().program_owner == DEFAULT_PROGRAM_ID, - NssaError::InvalidProgramBehavior - ); - - let account_id = program_output.pre_states[i].account_id; - - match claim { - Claim::Authorized => { - // The program can only claim accounts that were authorized by the signer. - ensure!( - is_authorized(&account_id), - NssaError::InvalidProgramBehavior - ); - } - Claim::Pda(seed) => { - // The program can only claim accounts that correspond to the PDAs it is - // authorized to claim. - let pda = AccountId::from((&chained_call.program_id, &seed)); - ensure!(account_id == pda, NssaError::InvalidProgramBehavior); - } - } - - post.account_mut().program_owner = chained_call.program_id; - } - - // Update the state diff - for (pre, post) in program_output - .pre_states - .iter() - .zip(program_output.post_states.iter()) - { - state_diff.insert(pre.account_id, post.account().clone()); - } - - for new_call in program_output.chained_calls.into_iter().rev() { - chained_calls.push_front((new_call, Some(chained_call.program_id))); - } - - chain_calls_counter = chain_calls_counter - .checked_add(1) - .expect("we check the max depth at the beginning of the loop"); - } - - // Check that all modified uninitialized accounts where claimed - for post in state_diff.iter().filter_map(|(account_id, post)| { - let pre = state.get_account_by_id(*account_id); - if pre.program_owner != DEFAULT_PROGRAM_ID { - return None; - } - if pre == *post { - return None; - } - Some(post) - }) { - ensure!( - post.program_owner != DEFAULT_PROGRAM_ID, - NssaError::InvalidProgramBehavior - ); - } - - Ok(state_diff) - } } #[cfg(test)] @@ -283,6 +68,7 @@ pub mod tests { error::NssaError, program::Program, public_transaction::{Message, WitnessSet}, + validated_state_diff::ValidatedStateDiff, }; fn keys_for_tests() -> (PrivateKey, PrivateKey, AccountId, AccountId) { @@ -296,7 +82,7 @@ pub mod tests { fn state_for_tests() -> V03State { let (_, _, addr1, addr2) = keys_for_tests(); let initial_data = [(addr1, 10000), (addr2, 20000)]; - V03State::new_with_genesis_accounts(&initial_data, &[]) + V03State::new_with_genesis_accounts(&initial_data, vec![], 0) } fn transaction_for_tests() -> PublicTransaction { @@ -391,7 +177,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key1]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -411,7 +197,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -432,7 +218,7 @@ pub mod tests { let mut witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); witness_set.signatures_and_public_keys[0].0 = Signature::new_for_tests([1; 64]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -452,7 +238,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } @@ -468,7 +254,7 @@ pub mod tests { let witness_set = WitnessSet::for_message(&message, &[&key1, &key2]); let tx = PublicTransaction::new(message, witness_set); - let result = tx.validate_and_produce_public_state_diff(&state, 1, 0); + let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0); assert!(matches!(result, Err(NssaError::InvalidInput(_)))); } } diff --git a/nssa/src/state.rs b/nssa/src/state.rs index ec37884e..f86f429f 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -1,6 +1,11 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use borsh::{BorshDeserialize, BorshSerialize}; +use clock_core::ClockAccountData; +pub use clock_core::{ + CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, + CLOCK_PROGRAM_ACCOUNT_IDS, +}; use nssa_core::{ BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, Timestamp, @@ -9,10 +14,13 @@ use nssa_core::{ }; use crate::{ - error::NssaError, merkle_tree::MerkleTree, - privacy_preserving_transaction::PrivacyPreservingTransaction, program::Program, + error::NssaError, + merkle_tree::MerkleTree, + privacy_preserving_transaction::PrivacyPreservingTransaction, + program::Program, program_deployment_transaction::ProgramDeploymentTransaction, public_transaction::PublicTransaction, + validated_state_diff::{StateDiff, ValidatedStateDiff}, }; pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; @@ -73,7 +81,7 @@ impl NullifierSet { Self(BTreeSet::new()) } - fn extend(&mut self, new_nullifiers: Vec) { + fn extend(&mut self, new_nullifiers: &[Nullifier]) { self.0.extend(new_nullifiers); } @@ -118,7 +126,8 @@ impl V03State { #[must_use] pub fn new_with_genesis_accounts( initial_data: &[(AccountId, u128)], - initial_commitments: &[nssa_core::Commitment], + initial_private_accounts: Vec<(Commitment, Nullifier)>, + genesis_timestamp: nssa_core::Timestamp, ) -> Self { let authenticated_transfer_program = Program::authenticated_transfer_program(); let public_state = initial_data @@ -134,16 +143,24 @@ impl V03State { }) .collect(); - let mut private_state = CommitmentSet::with_capacity(32); - private_state.extend(&[DUMMY_COMMITMENT]); - private_state.extend(initial_commitments); + let mut commitment_set = CommitmentSet::with_capacity(32); + commitment_set.extend(&[DUMMY_COMMITMENT]); + let (commitments, nullifiers): (Vec, Vec) = + initial_private_accounts.into_iter().unzip(); + commitment_set.extend(&commitments); + let mut nullifier_set = NullifierSet::new(); + nullifier_set.extend(&nullifiers); + let private_state = (commitment_set, nullifier_set); let mut this = Self { public_state, - private_state: (private_state, NullifierSet::new()), + private_state, programs: HashMap::new(), }; + this.insert_program(Program::clock()); + this.insert_clock_accounts(genesis_timestamp); + this.insert_program(Program::authenticated_transfer_program()); this.insert_program(Program::token()); this.insert_program(Program::amm()); @@ -152,33 +169,67 @@ impl V03State { this } + fn insert_clock_accounts(&mut self, genesis_timestamp: nssa_core::Timestamp) { + let data = ClockAccountData { + block_id: 0, + timestamp: genesis_timestamp, + } + .to_bytes(); + let clock_program_id = Program::clock().id(); + for account_id in CLOCK_PROGRAM_ACCOUNT_IDS { + self.public_state.insert( + account_id, + Account { + program_owner: clock_program_id, + data: data + .clone() + .try_into() + .expect("Clock account data should fit within accounts data"), + ..Account::default() + }, + ); + } + } + pub(crate) fn insert_program(&mut self, program: Program) { self.programs.insert(program.id(), program); } + pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) { + let StateDiff { + signer_account_ids, + public_diff, + new_commitments, + new_nullifiers, + program, + } = diff.into_state_diff(); + #[expect( + clippy::iter_over_hash_type, + reason = "Iteration order doesn't matter here" + )] + for (account_id, account) in public_diff { + *self.get_account_by_id_mut(account_id) = account; + } + for account_id in signer_account_ids { + self.get_account_by_id_mut(account_id) + .nonce + .public_account_nonce_increment(); + } + self.private_state.0.extend(&new_commitments); + self.private_state.1.extend(&new_nullifiers); + if let Some(program) = program { + self.insert_program(program); + } + } + pub fn transition_from_public_transaction( &mut self, tx: &PublicTransaction, block_id: BlockId, timestamp: Timestamp, ) -> Result<(), NssaError> { - let state_diff = tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; - - #[expect( - clippy::iter_over_hash_type, - reason = "Iteration order doesn't matter here" - )] - for (account_id, post) in state_diff { - let current_account = self.get_account_by_id_mut(account_id); - - *current_account = post; - } - - for account_id in tx.signer_account_ids() { - let current_account = self.get_account_by_id_mut(account_id); - current_account.nonce.public_account_nonce_increment(); - } - + let diff = ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp)?; + self.apply_state_diff(diff); Ok(()) } @@ -188,40 +239,9 @@ impl V03State { block_id: BlockId, timestamp: Timestamp, ) -> Result<(), NssaError> { - // 1. Verify the transaction satisfies acceptance criteria - let public_state_diff = - tx.validate_and_produce_public_state_diff(self, block_id, timestamp)?; - - let message = tx.message(); - - // 2. Add new commitments - self.private_state.0.extend(&message.new_commitments); - - // 3. Add new nullifiers - let new_nullifiers = message - .new_nullifiers - .iter() - .cloned() - .map(|(nullifier, _)| nullifier) - .collect::>(); - self.private_state.1.extend(new_nullifiers); - - // 4. Update public accounts - #[expect( - clippy::iter_over_hash_type, - reason = "Iteration order doesn't matter here" - )] - for (account_id, post) in public_state_diff { - let current_account = self.get_account_by_id_mut(account_id); - *current_account = post; - } - - // 5. Increment nonces for public signers - for account_id in tx.signer_account_ids() { - let current_account = self.get_account_by_id_mut(account_id); - current_account.nonce.public_account_nonce_increment(); - } - + let diff = + ValidatedStateDiff::from_privacy_preserving_transaction(tx, self, block_id, timestamp)?; + self.apply_state_diff(diff); Ok(()) } @@ -229,8 +249,8 @@ impl V03State { &mut self, tx: &ProgramDeploymentTransaction, ) -> Result<(), NssaError> { - let program = tx.validate_and_produce_public_state_diff(self)?; - self.insert_program(program); + let diff = ValidatedStateDiff::from_program_deployment_transaction(tx, self)?; + self.apply_state_diff(diff); Ok(()) } @@ -346,12 +366,15 @@ pub mod tests { Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, - program::{BlockValidityWindow, PdaSeed, ProgramId, TimestampValidityWindow}, + program::{ + BlockValidityWindow, ExecutionValidationError, PdaSeed, ProgramId, + TimestampValidityWindow, WrappedBalanceSum, + }, }; use crate::{ PublicKey, PublicTransaction, V03State, - error::NssaError, + error::{InvalidProgramBehaviorError, NssaError}, execute_and_prove, privacy_preserving_transaction::{ PrivacyPreservingTransaction, @@ -362,7 +385,10 @@ pub mod tests { program::Program, public_transaction, signature::PrivateKey, - state::MAX_NUMBER_CHAINED_CALLS, + state::{ + CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, + CLOCK_PROGRAM_ACCOUNT_IDS, MAX_NUMBER_CHAINED_CALLS, + }, }; impl V03State { @@ -382,6 +408,12 @@ pub mod tests { self.insert_program(Program::claimer()); self.insert_program(Program::changer_claimer()); self.insert_program(Program::validity_window()); + self.insert_program(Program::flash_swap_initiator()); + self.insert_program(Program::flash_swap_callback()); + self.insert_program(Program::malicious_self_program_id()); + self.insert_program(Program::malicious_caller_program_id()); + self.insert_program(Program::time_locked_transfer()); + self.insert_program(Program::pinata_cooldown()); self } @@ -458,6 +490,28 @@ pub mod tests { } } + // ── Flash Swap types (mirrors of guest types for host-side serialisation) ── + + #[derive(serde::Serialize, serde::Deserialize)] + struct CallbackInstruction { + return_funds: bool, + token_program_id: ProgramId, + amount: u128, + } + + #[derive(serde::Serialize, serde::Deserialize)] + enum FlashSwapInstruction { + Initiate { + token_program_id: ProgramId, + callback_program_id: ProgramId, + amount_out: u128, + callback_instruction_data: Vec, + }, + InvariantCheck { + min_vault_balance: u128, + }, + } + fn transfer_transaction( from: AccountId, from_key: &PrivateKey, @@ -477,6 +531,23 @@ pub mod tests { PublicTransaction::new(message, witness_set) } + fn build_flash_swap_tx( + initiator: &Program, + vault_id: AccountId, + receiver_id: AccountId, + instruction: FlashSwapInstruction, + ) -> PublicTransaction { + let message = public_transaction::Message::try_new( + initiator.id(), + vec![vault_id, receiver_id], + vec![], // no signers — vault is PDA-authorised + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set) + } + #[test] fn new_with_genesis() { let key1 = PrivateKey::try_new([1; 32]).unwrap(); @@ -485,6 +556,7 @@ pub mod tests { let addr2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); let initial_data = [(addr1, 100_u128), (addr2, 151_u128)]; let authenticated_transfers_program = Program::authenticated_transfer_program(); + let clock_program = Program::clock(); let expected_public_state = { let mut this = HashMap::new(); this.insert( @@ -503,6 +575,16 @@ pub mod tests { ..Account::default() }, ); + for account_id in CLOCK_PROGRAM_ACCOUNT_IDS { + this.insert( + account_id, + Account { + program_owner: clock_program.id(), + data: [0_u8; 16].to_vec().try_into().unwrap(), + ..Account::default() + }, + ); + } this }; let expected_builtin_programs = { @@ -511,21 +593,52 @@ pub mod tests { authenticated_transfers_program.id(), authenticated_transfers_program, ); + this.insert(clock_program.id(), clock_program); this.insert(Program::token().id(), Program::token()); this.insert(Program::amm().id(), Program::amm()); this.insert(Program::ata().id(), Program::ata()); this }; - let state = V03State::new_with_genesis_accounts(&initial_data, &[]); + let state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); assert_eq!(state.public_state, expected_public_state); assert_eq!(state.programs, expected_builtin_programs); } + #[test] + fn new_with_genesis_includes_nullifiers_for_private_accounts() { + let keys1 = test_private_account_keys_1(); + let keys2 = test_private_account_keys_2(); + + let account = Account { + balance: 100, + program_owner: Program::authenticated_transfer_program().id(), + ..Account::default() + }; + + let npk1 = keys1.npk(); + let npk2 = keys2.npk(); + + let init_commitment1 = Commitment::new(&npk1, &account); + let init_commitment2 = Commitment::new(&npk2, &account); + let init_nullifier1 = Nullifier::for_account_initialization(&npk1); + let init_nullifier2 = Nullifier::for_account_initialization(&npk2); + + let initial_private_accounts = vec![ + (init_commitment1, init_nullifier1), + (init_commitment2, init_nullifier2), + ]; + + let state = V03State::new_with_genesis_accounts(&[], initial_private_accounts, 0); + + assert!(state.private_state.1.contains(&init_nullifier1)); + assert!(state.private_state.1.contains(&init_nullifier2)); + } + #[test] fn insert_program() { - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); let program_to_insert = Program::simple_balance_transfer(); let program_id = program_to_insert.id(); assert!(!state.programs.contains_key(&program_id)); @@ -540,7 +653,7 @@ pub mod tests { let key = PrivateKey::try_new([1; 32]).unwrap(); let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); let initial_data = [(account_id, 100_u128)]; - let state = V03State::new_with_genesis_accounts(&initial_data, &[]); + let state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); let expected_account = &state.public_state[&account_id]; let account = state.get_account_by_id(account_id); @@ -551,7 +664,7 @@ pub mod tests { #[test] fn get_account_by_account_id_default_account() { let addr2 = AccountId::new([0; 32]); - let state = V03State::new_with_genesis_accounts(&[], &[]); + let state = V03State::new_with_genesis_accounts(&[], vec![], 0); let expected_account = Account::default(); let account = state.get_account_by_id(addr2); @@ -561,7 +674,7 @@ pub mod tests { #[test] fn builtin_programs_getter() { - let state = V03State::new_with_genesis_accounts(&[], &[]); + let state = V03State::new_with_genesis_accounts(&[], vec![], 0); let builtin_programs = state.programs(); @@ -573,7 +686,7 @@ pub mod tests { let key = PrivateKey::try_new([1; 32]).unwrap(); let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); let initial_data = [(account_id, 100)]; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); let from = account_id; let to_key = PrivateKey::try_new([2; 32]).unwrap(); let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); @@ -594,7 +707,7 @@ pub mod tests { let key = PrivateKey::try_new([1; 32]).unwrap(); let account_id = AccountId::from(&PublicKey::new_from_private_key(&key)); let initial_data = [(account_id, 100)]; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); let from = account_id; let from_key = key; let to_key = PrivateKey::try_new([2; 32]).unwrap(); @@ -619,7 +732,7 @@ pub mod tests { let account_id1 = AccountId::from(&PublicKey::new_from_private_key(&key1)); let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); let initial_data = [(account_id1, 100), (account_id2, 200)]; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); let from = account_id2; let from_key = key2; let to = account_id1; @@ -643,7 +756,7 @@ pub mod tests { let key2 = PrivateKey::try_new([2; 32]).unwrap(); let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2)); let initial_data = [(account_id1, 100)]; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); let key3 = PrivateKey::try_new([3; 32]).unwrap(); let account_id3 = AccountId::from(&PublicKey::new_from_private_key(&key3)); let balance_to_move = 5; @@ -678,12 +791,156 @@ pub mod tests { assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(1)); } + fn clock_transaction(timestamp: nssa_core::Timestamp) -> PublicTransaction { + let message = public_transaction::Message::try_new( + Program::clock().id(), + CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(), + vec![], + timestamp, + ) + .unwrap(); + PublicTransaction::new( + message, + public_transaction::WitnessSet::from_raw_parts(vec![]), + ) + } + + fn clock_account_data(state: &V03State, account_id: AccountId) -> (u64, nssa_core::Timestamp) { + let data = state.get_account_by_id(account_id).data.into_inner(); + let parsed = clock_core::ClockAccountData::from_bytes(&data); + (parsed.block_id, parsed.timestamp) + } + + #[test] + fn clock_genesis_state_has_zero_block_id_and_genesis_timestamp() { + let genesis_timestamp = 1_000_000_u64; + let state = V03State::new_with_genesis_accounts(&[], vec![], genesis_timestamp); + + let (block_id, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); + + assert_eq!(block_id, 0); + assert_eq!(timestamp, genesis_timestamp); + } + + #[test] + fn clock_invocation_increments_block_id() { + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); + + let tx = clock_transaction(1234); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + let (block_id, _) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); + assert_eq!(block_id, 1); + } + + #[test] + fn clock_invocation_stores_timestamp_from_instruction() { + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); + let block_timestamp = 1_700_000_000_000_u64; + + let tx = clock_transaction(block_timestamp); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + let (_, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); + assert_eq!(timestamp, block_timestamp); + } + + #[test] + fn clock_invocation_sequence_correctly_increments_block_id() { + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); + + for expected_block_id in 1_u64..=5 { + let tx = clock_transaction(expected_block_id * 1000); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + + let (block_id, timestamp) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); + assert_eq!(block_id, expected_block_id); + assert_eq!(timestamp, expected_block_id * 1000); + } + } + + #[test] + fn clock_10_account_not_updated_when_block_id_not_multiple_of_10() { + let genesis_timestamp = 0_u64; + let mut state = V03State::new_with_genesis_accounts(&[], vec![], genesis_timestamp); + + // Run 9 clock ticks (block_ids 1..=9), none of which are multiples of 10. + for tick in 1_u64..=9 { + let tx = clock_transaction(tick * 1000); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + } + + let (block_id_10, timestamp_10) = clock_account_data(&state, CLOCK_10_PROGRAM_ACCOUNT_ID); + // The 10-block account should still reflect genesis state. + assert_eq!(block_id_10, 0); + assert_eq!(timestamp_10, genesis_timestamp); + } + + #[test] + fn clock_10_account_updated_when_block_id_is_multiple_of_10() { + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); + + // Run 10 clock ticks so block_id reaches 10. + for tick in 1_u64..=10 { + let tx = clock_transaction(tick * 1000); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + } + + let (block_id_1, timestamp_1) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); + let (block_id_10, timestamp_10) = clock_account_data(&state, CLOCK_10_PROGRAM_ACCOUNT_ID); + assert_eq!(block_id_1, 10); + assert_eq!(block_id_10, 10); + assert_eq!(timestamp_10, timestamp_1); + } + + #[test] + fn clock_50_account_only_updated_at_multiples_of_50() { + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); + + // After 49 ticks the 50-block account should be unchanged. + for tick in 1_u64..=49 { + let tx = clock_transaction(tick * 1000); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + } + let (block_id_50, _) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID); + assert_eq!(block_id_50, 0); + + // Tick 50 — now the 50-block account should update. + let tx = clock_transaction(50 * 1000); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + let (block_id_50, timestamp_50) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID); + assert_eq!(block_id_50, 50); + assert_eq!(timestamp_50, 50 * 1000); + } + + #[test] + fn all_three_clock_accounts_updated_at_multiple_of_50() { + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); + + // Advance to block 50 (a multiple of both 10 and 50). + for tick in 1_u64..=50 { + let tx = clock_transaction(tick * 1000); + state.transition_from_public_transaction(&tx, 0, 0).unwrap(); + } + + let (block_id_1, ts_1) = clock_account_data(&state, CLOCK_01_PROGRAM_ACCOUNT_ID); + let (block_id_10, ts_10) = clock_account_data(&state, CLOCK_10_PROGRAM_ACCOUNT_ID); + let (block_id_50, ts_50) = clock_account_data(&state, CLOCK_50_PROGRAM_ACCOUNT_ID); + + assert_eq!(block_id_1, 50); + assert_eq!(block_id_10, 50); + assert_eq!(block_id_50, 50); + assert_eq!(ts_1, ts_10); + assert_eq!(ts_1, ts_50); + } + #[test] fn program_should_fail_if_modifies_nonces() { - let initial_data = [(AccountId::new([1; 32]), 100)]; + let account_id = AccountId::new([1; 32]); + let initial_data = [(account_id, 100)]; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); - let account_ids = vec![AccountId::new([1; 32])]; + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); + let account_ids = vec![account_id]; let program_id = Program::nonce_changer_program().id(); let message = public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap(); @@ -692,14 +949,21 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior( + InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::ModifiedNonce { account_id: err_account_id } + ) + )) if err_account_id == account_id + )); } #[test] fn program_should_fail_if_output_accounts_exceed_inputs() { let initial_data = [(AccountId::new([1; 32]), 100)]; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let account_ids = vec![AccountId::new([1; 32])]; let program_id = Program::extra_output_program().id(); let message = @@ -709,14 +973,24 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior( + InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::MismatchedPreStatePostStateLength { + pre_state_length, + post_state_length + } + ) + )) if pre_state_length == 1 && post_state_length == 2 + )); } #[test] fn program_should_fail_with_missing_output_accounts() { let initial_data = [(AccountId::new([1; 32]), 100)]; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let account_ids = vec![AccountId::new([1; 32]), AccountId::new([2; 32])]; let program_id = Program::missing_output_program().id(); let message = @@ -726,14 +1000,24 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior( + InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::MismatchedPreStatePostStateLength { + pre_state_length, + post_state_length + } + ) + )) if pre_state_length == 2 && post_state_length == 1 + )); } #[test] fn program_should_fail_if_modifies_program_owner_with_only_non_default_program_owner() { let initial_data = [(AccountId::new([1; 32]), 0)]; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let account_id = AccountId::new([1; 32]); let account = state.get_account_by_id(account_id); // Assert the target account only differs from the default account in the program owner @@ -750,13 +1034,18 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } + ))) if err_account_id == account_id + )); } #[test] fn program_should_fail_if_modifies_program_owner_with_only_non_default_balance() { let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]) + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0) .with_test_programs() .with_non_default_accounts_but_default_program_owners(); let account_id = AccountId::new([255; 32]); @@ -774,13 +1063,18 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } + ))) if err_account_id == account_id + )); } #[test] fn program_should_fail_if_modifies_program_owner_with_only_non_default_nonce() { let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]) + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0) .with_test_programs() .with_non_default_accounts_but_default_program_owners(); let account_id = AccountId::new([254; 32]); @@ -798,13 +1092,18 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } + ))) if err_account_id == account_id + )); } #[test] fn program_should_fail_if_modifies_program_owner_with_only_non_default_data() { let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]) + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0) .with_test_programs() .with_non_default_accounts_but_default_program_owners(); let account_id = AccountId::new([253; 32]); @@ -822,16 +1121,21 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id } + ))) if err_account_id == account_id + )); } #[test] fn program_should_fail_if_transfers_balance_from_non_owned_account() { - let initial_data = [(AccountId::new([1; 32]), 100)]; - let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); let sender_account_id = AccountId::new([1; 32]); let receiver_account_id = AccountId::new([2; 32]); + let initial_data = [(sender_account_id, 100)]; + let mut state = + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let balance_to_move: u128 = 1; let program_id = Program::simple_balance_transfer().id(); assert_ne!( @@ -850,13 +1154,18 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::UnauthorizedBalanceDecrease { account_id: err_account_id, owner_program_id, executing_program_id } + ))) if err_account_id == sender_account_id && owner_program_id != program_id && executing_program_id == program_id + )); } #[test] fn program_should_fail_if_modifies_data_of_non_owned_account() { let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]) + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0) .with_test_programs() .with_non_default_accounts_but_default_program_owners(); let account_id = AccountId::new([255; 32]); @@ -875,14 +1184,19 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::UnauthorizedDataModification { account_id: err_account_id, executing_program_id } + ))) if err_account_id == account_id && executing_program_id == program_id + )); } #[test] fn program_should_fail_if_does_not_preserve_total_balance_by_minting() { let initial_data = []; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let account_id = AccountId::new([1; 32]); let program_id = Program::minter().id(); @@ -893,13 +1207,18 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states } + ))) if total_balance_pre_states == 0.into() && total_balance_post_states == 1.into() + )); } #[test] fn program_should_fail_if_does_not_preserve_total_balance_by_burning() { let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]) + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0) .with_test_programs() .with_account_owned_by_burner_program(); let program_id = Program::burner().id(); @@ -922,7 +1241,12 @@ pub mod tests { let tx = PublicTransaction::new(message, witness_set); let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states } + ))) if total_balance_pre_states == 100.into() && total_balance_post_states == 99.into() + )); } fn test_public_account_keys_1() -> TestPublicKeys { @@ -1091,7 +1415,7 @@ pub mod tests { let recipient_keys = test_private_account_keys_1(); let mut state = - V03State::new_with_genesis_accounts(&[(sender_keys.account_id(), 200)], &[]); + V03State::new_with_genesis_accounts(&[(sender_keys.account_id(), 200)], vec![], 0); let balance_to_move = 37; @@ -1139,7 +1463,7 @@ pub mod tests { }; let recipient_keys = test_private_account_keys_2(); - let mut state = V03State::new_with_genesis_accounts(&[], &[]) + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0) .with_private_account(&sender_keys, &sender_private_account); let balance_to_move = 37; @@ -1208,7 +1532,8 @@ pub mod tests { let recipient_initial_balance = 400; let mut state = V03State::new_with_genesis_accounts( &[(recipient_keys.account_id(), recipient_initial_balance)], - &[], + vec![], + 0, ) .with_private_account(&sender_keys, &sender_private_account); @@ -1989,9 +2314,16 @@ pub mod tests { assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); } + /// A mask-3 account that no program claims via `Claim::Pda` and no caller authorizes via + /// `ChainedCall.pda_seeds` has no binding between its supplied npk and its `account_id`, + /// so the circuit must reject. Here `simple_balance_transfer` emits no claim for the + /// second account, leaving position 1 unbound. #[test] - fn circuit_should_fail_with_invalid_visibility_mask_value() { + fn private_pda_without_binding_fails() { let program = Program::simple_balance_transfer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); let public_account_1 = AccountWithMetadata::new( Account { program_owner: program.id(), @@ -2001,17 +2333,235 @@ pub mod tests { true, AccountId::new([0; 32]), ); - let public_account_2 = + let private_pda_account = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); let visibility_mask = [0, 3]; let result = execute_and_prove( - vec![public_account_1, public_account_2], + vec![public_account_1, private_pda_account], Program::serialize_instruction(10_u128).unwrap(), visibility_mask.to_vec(), + vec![(npk, shared_secret)], vec![], + vec![None], + &program.into(), + ); + + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } + + /// Happy path: a program claims a new mask-3 account via `Claim::Pda(seed)`. The circuit + /// reads the npk for that `pre_state` from `private_account_keys` at the `pre_state`'s + /// position, derives `AccountId` via `AccountId::for_private_pda(program_id, seed, npk)`, and + /// asserts it equals the `pre_state`'s `account_id`. The equality both validates the claim + /// and binds the supplied npk to the `account_id`. + #[test] + fn private_pda_claim_succeeds() { + let program = Program::pda_claimer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction(seed).unwrap(), + vec![3], + vec![(npk, shared_secret)], vec![], + vec![None], + &program.into(), + ); + + let (output, _proof) = result.expect("mask-3 private PDA claim should succeed"); + assert_eq!(output.new_nullifiers.len(), 1); + assert_eq!(output.new_commitments.len(), 1); + assert_eq!(output.ciphertexts.len(), 1); + assert!(output.public_pre_states.is_empty()); + assert!(output.public_post_states.is_empty()); + } + + /// An npk is supplied that does not match the `pre_state`'s `account_id` under + /// `AccountId::for_private_pda(program, claim_seed, npk)`. The claim equality check rejects. + #[test] + fn private_pda_npk_mismatch_fails() { + // `keys_a` produces the `pre_state`'s `account_id` (the registered pair), `keys_b` is + // the mismatched pair supplied in `private_account_keys` for that pre_state. + let program = Program::pda_claimer(); + let keys_a = test_private_account_keys_1(); + let keys_b = test_private_account_keys_2(); + let npk_a = keys_a.npk(); + let npk_b = keys_b.npk(); + let seed = PdaSeed::new([42; 32]); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys_b.vpk()); + + // `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state. + // `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in + // the circuit must reject. + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk_a); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction(seed).unwrap(), + vec![3], + vec![(npk_b, shared_secret)], vec![], + vec![None], + &program.into(), + ); + + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } + + /// Happy path for the caller-seeds authorization of a mask-3 PDA. The delegator claims a + /// private PDA via `Claim::Pda(seed)`, then chains to a callee (`noop`) delegating the same + /// seed via `ChainedCall.pda_seeds`. In the callee's step, the `pre_state`'s authorization + /// is established via the private derivation + /// `AccountId::for_private_pda(delegator, seed, npk) == pre.account_id`. + #[test] + fn caller_pda_seeds_authorize_private_pda_for_callee() { + let delegator = Program::private_pda_delegator(); + let callee = Program::auth_asserting_noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([77; 32]); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let callee_id = callee.id(); + let program_with_deps = + ProgramWithDependencies::new(delegator, [(callee_id, callee)].into()); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction((seed, seed, callee_id)).unwrap(), + vec![3], + vec![(npk, shared_secret)], + vec![], + vec![None], + &program_with_deps, + ); + + let (output, _proof) = + result.expect("caller-seeds authorization of mask-3 private PDA should succeed"); + assert_eq!(output.new_commitments.len(), 1); + assert_eq!(output.new_nullifiers.len(), 1); + } + + /// The delegator chains with a different seed than the one it claimed with. In the callee + /// step, neither public nor private caller-seeds authorization matches; `pre.is_authorized` + /// was set to `true` by the delegator but no proven source supports it, so the consistency + /// assertion rejects. + #[test] + fn caller_pda_seeds_with_wrong_seed_rejects_private_pda_for_callee() { + let delegator = Program::private_pda_delegator(); + let callee = Program::auth_asserting_noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let claim_seed = PdaSeed::new([77; 32]); + let wrong_delegated_seed = PdaSeed::new([88; 32]); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let callee_id = callee.id(); + let program_with_deps = + ProgramWithDependencies::new(delegator, [(callee_id, callee)].into()); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), + vec![3], + vec![(npk, shared_secret)], + vec![], + vec![None], + &program_with_deps, + ); + + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } + + /// Exploit-scenario pin. A single `(program_id, seed)` pair can derive a family of + /// `AccountId`s, one public PDA and one private PDA per distinct npk. Without the tx-wide + /// family-binding check, a program could claim `PDA_alice` (mask-3, `alice_npk`) and + /// `PDA_bob` (mask-3, `bob_npk`) under the same seed in one transaction, and once reuse + /// is supported a later chained call could delegate both to a callee via + /// `pda_seeds: [S]` and mix balances across them. The binding check rejects the setup + /// here: after the first claim records `(program, seed) → PDA_alice`, the second claim + /// tries to record `(program, seed) → PDA_bob` and panics. + #[test] + fn two_private_pda_claims_under_same_seed_are_rejected() { + let program = Program::two_pda_claimer(); + let keys_a = test_private_account_keys_1(); + let keys_b = test_private_account_keys_2(); + let seed = PdaSeed::new([55; 32]); + let shared_a = SharedSecretKey::new(&[66; 32], &keys_a.vpk()); + let shared_b = SharedSecretKey::new(&[77; 32], &keys_b.vpk()); + + let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk()); + let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk()); + + let pre_a = AccountWithMetadata::new(Account::default(), false, account_a); + let pre_b = AccountWithMetadata::new(Account::default(), false, account_b); + + let result = execute_and_prove( + vec![pre_a, pre_b], + Program::serialize_instruction(seed).unwrap(), + vec![3, 3], + vec![(keys_a.npk(), shared_a), (keys_b.npk(), shared_b)], + vec![], + vec![None, None], + &program.into(), + ); + + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } + + /// Pins the current limitation: a mask-3 PDA that was claimed in a previous transaction + /// cannot be re-used in a new transaction as-is. This PR only binds supplied npks via a + /// fresh `Claim::Pda` or a caller's `ChainedCall.pda_seeds`, neither is present when a + /// program operates on an already-owned private PDA at top level. The reject site is the + /// post-loop `private_pda_bound_positions` assertion in + /// `privacy_preserving_circuit.rs`: `noop` emits no `Claim::Pda` and there is no caller + /// `ChainedCall.pda_seeds`, so position 0 is never bound and the assertion fires. + // TODO: a follow-up PR in the Private PDAs series needs to let the wallet supply a + // `(seed, original_owner_program_id)` side input per mask-3 `pre_state` so the circuit + // can re-verify `AccountId::for_private_pda(owner, seed, npk) == pre.account_id` without a + // claim. + #[test] + fn private_pda_top_level_reuse_rejected_by_binding_check() { + let program = Program::noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let seed = PdaSeed::new([99; 32]); + + // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = + // true, account_id derived via the private formula. + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk); + let owned_pre_state = AccountWithMetadata::new( + Account { + program_owner: program.id(), + ..Account::default() + }, + true, + account_id, + ); + + let result = execute_and_prove( + vec![owned_pre_state], + Program::serialize_instruction(()).unwrap(), + vec![3], + vec![(npk, shared_secret)], + vec![], + vec![None], &program.into(), ); @@ -2160,7 +2710,7 @@ pub mod tests { }; let recipient_keys = test_private_account_keys_2(); - let mut state = V03State::new_with_genesis_accounts(&[], &[]) + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0) .with_private_account(&sender_keys, &sender_private_account); let balance_to_move = 37; @@ -2245,7 +2795,7 @@ pub mod tests { let initial_balance = 100; let initial_data = [(from, initial_balance)]; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let to_key = PrivateKey::try_new([2; 32]).unwrap(); let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); let amount: u128 = 37; @@ -2283,7 +2833,7 @@ pub mod tests { let program = Program::authenticated_transfer_program(); let account_key = PrivateKey::try_new([9; 32]).unwrap(); let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key)); - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); assert_eq!(state.get_account_by_id(account_id), Account::default()); @@ -2304,7 +2854,7 @@ pub mod tests { let program = Program::authenticated_transfer_program(); let account_key = PrivateKey::try_new([10; 32]).unwrap(); let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key)); - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); assert_eq!(state.get_account_by_id(account_id), Account::default()); @@ -2339,7 +2889,7 @@ pub mod tests { let initial_balance = 1000; let initial_data = [(from, initial_balance), (to, 0)]; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let from_key = key; let amount: u128 = 37; let instruction: (u128, ProgramId, u32, Option) = ( @@ -2384,7 +2934,7 @@ pub mod tests { let initial_balance = 100; let initial_data = [(from, initial_balance), (to, 0)]; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let from_key = key; let amount: u128 = 0; let instruction: (u128, ProgramId, u32, Option) = ( @@ -2417,12 +2967,12 @@ pub mod tests { fn execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() { let chain_caller = Program::chain_caller(); let pda_seed = PdaSeed::new([37; 32]); - let from = AccountId::from((&chain_caller.id(), &pda_seed)); + let from = AccountId::for_public_pda(&chain_caller.id(), &pda_seed); let to = AccountId::new([2; 32]); let initial_balance = 1000; let initial_data = [(from, initial_balance), (to, 0)]; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let amount: u128 = 58; let instruction: (u128, ProgramId, u32, Option) = ( amount, @@ -2468,7 +3018,7 @@ pub mod tests { let initial_balance = 100; let initial_data = [(from, initial_balance)]; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let to_key = PrivateKey::try_new([2; 32]).unwrap(); let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); let amount: u128 = 37; @@ -2542,8 +3092,12 @@ pub mod tests { ..Account::default() }; let sender_commitment = Commitment::new(&sender_keys.npk(), &sender_private_account); - let mut state = - V03State::new_with_genesis_accounts(&[], std::slice::from_ref(&sender_commitment)); + let sender_init_nullifier = Nullifier::for_account_initialization(&sender_keys.npk()); + let mut state = V03State::new_with_genesis_accounts( + &[], + vec![(sender_commitment.clone(), sender_init_nullifier)], + 0, + ); let sender_pre = AccountWithMetadata::new(sender_private_account, true, &sender_keys.npk()); let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap(); let recipient_account_id = @@ -2623,9 +3177,15 @@ pub mod tests { let from_commitment = Commitment::new(&from_keys.npk(), &from_account.account); let to_commitment = Commitment::new(&to_keys.npk(), &to_account.account); + let from_init_nullifier = Nullifier::for_account_initialization(&from_keys.npk()); + let to_init_nullifier = Nullifier::for_account_initialization(&to_keys.npk()); let mut state = V03State::new_with_genesis_accounts( &[], - &[from_commitment.clone(), to_commitment.clone()], + vec![ + (from_commitment.clone(), from_init_nullifier), + (to_commitment.clone(), to_init_nullifier), + ], + 0, ) .with_test_programs(); let amount: u128 = 37; @@ -2719,7 +3279,8 @@ pub mod tests { let pinata_definition_id = AccountId::new([1; 32]); let pinata_token_definition_id = AccountId::new([2; 32]); // Total supply of pinata token will be in an account under a PDA. - let pinata_token_holding_id = AccountId::from((&pinata_token.id(), &PdaSeed::new([0; 32]))); + let pinata_token_holding_id = + AccountId::for_public_pda(&pinata_token.id(), &PdaSeed::new([0; 32])); let winner_token_holding_id = AccountId::new([3; 32]); let expected_winner_account_holding = token_core::TokenHolding::Fungible { @@ -2732,7 +3293,7 @@ pub mod tests { ..Account::default() }; - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); state.add_pinata_token_program(pinata_definition_id); // Set up the token accounts directly (bypassing public transactions which @@ -2804,7 +3365,7 @@ pub mod tests { #[test] fn claiming_mechanism_cannot_claim_initialied_accounts() { let claimer = Program::claimer(); - let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let account_id = AccountId::new([2; 32]); // Insert an account with non-default program owner @@ -2824,7 +3385,12 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior( + InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id: err_account_id } + )) if err_account_id == account_id + )); } /// This test ensures that even if a malicious program tries to perform overflow of balances @@ -2844,7 +3410,8 @@ pub mod tests { (sender_id, sender_init_balance), (recipient_id, recipient_init_balance), ], - &[], + vec![], + 0, ); state.insert_program(Program::modified_transfer_program()); @@ -2869,7 +3436,22 @@ pub mod tests { let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); let tx = PublicTransaction::new(message, witness_set); let res = state.transition_from_public_transaction(&tx, 1, 0); - assert!(matches!(res, Err(NssaError::InvalidProgramBehavior))); + let expected_total_balance_pre_states = WrappedBalanceSum::from_balances( + [sender_init_balance, recipient_init_balance].into_iter(), + ) + .unwrap(); + let expected_total_balance_post_states = WrappedBalanceSum::from_balances( + [sender_init_balance, recipient_init_balance, u128::MAX, 1].into_iter(), + ) + .unwrap(); + assert!(matches!( + res, + Err(NssaError::InvalidProgramBehavior( + InvalidProgramBehaviorError::ExecutionValidationFailed( + ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states } + ) + )) if total_balance_pre_states == expected_total_balance_pre_states && total_balance_post_states == expected_total_balance_post_states + )); let sender_post = state.get_account_by_id(sender_id); let recipient_post = state.get_account_by_id(recipient_id); @@ -2894,7 +3476,7 @@ pub mod tests { #[test] fn private_authorized_uninitialized_account() { - let mut state = V03State::new_with_genesis_accounts(&[], &[]); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0); // Set up keys for the authorized private account let private_keys = test_private_account_keys_1(); @@ -2946,7 +3528,7 @@ pub mod tests { #[test] fn private_unauthorized_uninitialized_account_can_still_be_claimed() { - let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let private_keys = test_private_account_keys_1(); // This is intentional: claim authorization was introduced to protect public accounts, @@ -2993,7 +3575,7 @@ pub mod tests { #[test] fn private_account_claimed_then_used_without_init_flag_should_fail() { - let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); // Set up keys for the private account let private_keys = test_private_account_keys_1(); @@ -3074,7 +3656,7 @@ pub mod tests { fn public_changer_claimer_no_data_change_no_claim_succeeds() { let initial_data = []; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let account_id = AccountId::new([1; 32]); let program_id = Program::changer_claimer().id(); // Don't change data (None) and don't claim (false) @@ -3098,7 +3680,7 @@ pub mod tests { fn public_changer_claimer_data_change_no_claim_fails() { let initial_data = []; let mut state = - V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let account_id = AccountId::new([1; 32]); let program_id = Program::changer_claimer().id(); // Change data but don't claim (false) - should fail @@ -3114,7 +3696,14 @@ pub mod tests { let result = state.transition_from_public_transaction(&tx, 1, 0); // Should fail - cannot modify data without claiming the account - assert!(matches!(result, Err(NssaError::InvalidProgramBehavior))); + assert!(matches!( + result, + Err(NssaError::InvalidProgramBehavior( + InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { + account_id: err_account_id + } + )) if err_account_id == account_id + )); } #[test] @@ -3192,9 +3781,11 @@ pub mod tests { let recipient_commitment = Commitment::new(&recipient_keys.npk(), &recipient_account.account); + let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_keys.npk()); let state = V03State::new_with_genesis_accounts( &[(sender_account.account_id, sender_account.account.balance)], - std::slice::from_ref(&recipient_commitment), + vec![(recipient_commitment.clone(), recipient_init_nullifier)], + 0, ) .with_test_programs(); @@ -3244,7 +3835,7 @@ pub mod tests { let validity_window_program = Program::validity_window(); let account_keys = test_public_account_keys_1(); let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id()); - let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { let account_ids = vec![pre.account_id]; let nonces = vec![]; @@ -3296,7 +3887,7 @@ pub mod tests { let validity_window_program = Program::validity_window(); let account_keys = test_public_account_keys_1(); let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id()); - let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { let account_ids = vec![pre.account_id]; let nonces = vec![]; @@ -3349,7 +3940,7 @@ pub mod tests { let validity_window_program = Program::validity_window(); let account_keys = test_private_account_keys_1(); let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk()); - let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { let esk = [3; 32]; let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); @@ -3418,7 +4009,7 @@ pub mod tests { let validity_window_program = Program::validity_window(); let account_keys = test_private_account_keys_1(); let pre = AccountWithMetadata::new(Account::default(), false, &account_keys.npk()); - let mut state = V03State::new_with_genesis_accounts(&[], &[]).with_test_programs(); + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { let esk = [3; 32]; let shared_secret = SharedSecretKey::new(&esk, &account_keys.vpk()); @@ -3467,14 +4058,474 @@ pub mod tests { } } + fn time_locked_transfer_transaction( + from: AccountId, + from_key: &PrivateKey, + from_nonce: u128, + to: AccountId, + clock_account_id: AccountId, + amount: u128, + deadline: u64, + ) -> PublicTransaction { + let program_id = Program::time_locked_transfer().id(); + let message = public_transaction::Message::try_new( + program_id, + vec![from, to, clock_account_id], + vec![Nonce(from_nonce)], + (amount, deadline), + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key]); + PublicTransaction::new(message, witness_set) + } + + #[test] + fn time_locked_transfer_succeeds_when_deadline_has_passed() { + let recipient_id = AccountId::new([42; 32]); + let genesis_timestamp = 500_u64; + let mut state = + V03State::new_with_genesis_accounts(&[(recipient_id, 0)], vec![], genesis_timestamp) + .with_test_programs(); + let key1 = PrivateKey::try_new([1; 32]).unwrap(); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1)); + state.force_insert_account( + sender_id, + Account { + program_owner: Program::time_locked_transfer().id(), + balance: 100, + ..Account::default() + }, + ); + + let amount = 100_u128; + // Deadline in the past: transfer should succeed. + let deadline = 0_u64; + + let tx = time_locked_transfer_transaction( + sender_id, + &key1, + 0, + recipient_id, + CLOCK_01_PROGRAM_ACCOUNT_ID, + amount, + deadline, + ); + + let block_id = 1; + let timestamp = genesis_timestamp + 100; + state + .transition_from_public_transaction(&tx, block_id, timestamp) + .unwrap(); + + // Balances changed. + assert_eq!(state.get_account_by_id(sender_id).balance, 0); + assert_eq!(state.get_account_by_id(recipient_id).balance, 100); + } + + #[test] + fn time_locked_transfer_fails_when_deadline_is_in_the_future() { + let recipient_id = AccountId::new([42; 32]); + let genesis_timestamp = 500_u64; + let mut state = + V03State::new_with_genesis_accounts(&[(recipient_id, 0)], vec![], genesis_timestamp) + .with_test_programs(); + let key1 = PrivateKey::try_new([1; 32]).unwrap(); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&key1)); + state.force_insert_account( + sender_id, + Account { + program_owner: Program::time_locked_transfer().id(), + balance: 100, + ..Account::default() + }, + ); + + let amount = 100_u128; + // Far-future deadline: program should panic. + let deadline = u64::MAX; + + let tx = time_locked_transfer_transaction( + sender_id, + &key1, + 0, + recipient_id, + CLOCK_01_PROGRAM_ACCOUNT_ID, + amount, + deadline, + ); + + let block_id = 1; + let timestamp = genesis_timestamp + 100; + let result = state.transition_from_public_transaction(&tx, block_id, timestamp); + + assert!( + result.is_err(), + "Transfer should fail when deadline is in the future" + ); + // Balances unchanged. + assert_eq!(state.get_account_by_id(sender_id).balance, 100); + assert_eq!(state.get_account_by_id(recipient_id).balance, 0); + } + + fn pinata_cooldown_data(prize: u128, cooldown_ms: u64, last_claim_timestamp: u64) -> Vec { + let mut buf = Vec::with_capacity(32); + buf.extend_from_slice(&prize.to_le_bytes()); + buf.extend_from_slice(&cooldown_ms.to_le_bytes()); + buf.extend_from_slice(&last_claim_timestamp.to_le_bytes()); + buf + } + + fn pinata_cooldown_transaction( + pinata_id: AccountId, + winner_id: AccountId, + clock_account_id: AccountId, + ) -> PublicTransaction { + let program_id = Program::pinata_cooldown().id(); + let message = public_transaction::Message::try_new( + program_id, + vec![pinata_id, winner_id, clock_account_id], + vec![], + (), + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + PublicTransaction::new(message, witness_set) + } + + #[test] + fn pinata_cooldown_claim_succeeds_after_cooldown() { + let winner_id = AccountId::new([11; 32]); + let pinata_id = AccountId::new([99; 32]); + + let genesis_timestamp = 1000_u64; + let mut state = + V03State::new_with_genesis_accounts(&[(winner_id, 0)], vec![], genesis_timestamp) + .with_test_programs(); + + let prize = 50_u128; + let cooldown_ms = 500_u64; + // Last claim was at genesis, so any timestamp >= genesis + cooldown should work. + let last_claim_timestamp = genesis_timestamp; + + state.force_insert_account( + pinata_id, + Account { + program_owner: Program::pinata_cooldown().id(), + balance: 1000, + data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) + .try_into() + .unwrap(), + ..Account::default() + }, + ); + + let tx = pinata_cooldown_transaction(pinata_id, winner_id, CLOCK_01_PROGRAM_ACCOUNT_ID); + + let block_id = 1; + let block_timestamp = genesis_timestamp + cooldown_ms; + // Advance clock so the cooldown check reads an updated timestamp. + let clock_tx = clock_transaction(block_timestamp); + state + .transition_from_public_transaction(&clock_tx, block_id, block_timestamp) + .unwrap(); + + state + .transition_from_public_transaction(&tx, block_id, block_timestamp) + .unwrap(); + + assert_eq!(state.get_account_by_id(pinata_id).balance, 1000 - prize); + assert_eq!(state.get_account_by_id(winner_id).balance, prize); + } + + #[test] + fn pinata_cooldown_claim_fails_during_cooldown() { + let winner_id = AccountId::new([11; 32]); + let pinata_id = AccountId::new([99; 32]); + + let genesis_timestamp = 1000_u64; + let mut state = + V03State::new_with_genesis_accounts(&[(winner_id, 0)], vec![], genesis_timestamp) + .with_test_programs(); + + let prize = 50_u128; + let cooldown_ms = 500_u64; + let last_claim_timestamp = genesis_timestamp; + + state.force_insert_account( + pinata_id, + Account { + balance: 1000, + data: pinata_cooldown_data(prize, cooldown_ms, last_claim_timestamp) + .try_into() + .unwrap(), + ..Account::default() + }, + ); + + let tx = pinata_cooldown_transaction(pinata_id, winner_id, CLOCK_01_PROGRAM_ACCOUNT_ID); + + let block_id = 1; + // Timestamp is only 100ms after last claim, well within the 500ms cooldown. + let block_timestamp = genesis_timestamp + 100; + let clock_tx = clock_transaction(block_timestamp); + state + .transition_from_public_transaction(&clock_tx, block_id, block_timestamp) + .unwrap(); + + let result = state.transition_from_public_transaction(&tx, block_id, block_timestamp); + + assert!(result.is_err(), "Claim should fail during cooldown period"); + assert_eq!(state.get_account_by_id(pinata_id).balance, 1000); + assert_eq!(state.get_account_by_id(winner_id).balance, 0); + } + #[test] fn state_serialization_roundtrip() { let account_id_1 = AccountId::new([1; 32]); let account_id_2 = AccountId::new([2; 32]); let initial_data = [(account_id_1, 100_u128), (account_id_2, 151_u128)]; - let state = V03State::new_with_genesis_accounts(&initial_data, &[]).with_test_programs(); + let state = + V03State::new_with_genesis_accounts(&initial_data, vec![], 0).with_test_programs(); let bytes = borsh::to_vec(&state).unwrap(); let state_from_bytes: V03State = borsh::from_slice(&bytes).unwrap(); assert_eq!(state, state_from_bytes); } + + #[test] + fn flash_swap_successful() { + let initiator = Program::flash_swap_initiator(); + let callback = Program::flash_swap_callback(); + let token = Program::authenticated_transfer_program(); + + let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); + let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32])); + + let initial_balance: u128 = 1000; + let amount_out: u128 = 100; + + let vault_account = Account { + program_owner: token.id(), + balance: initial_balance, + ..Account::default() + }; + let receiver_account = Account { + program_owner: token.id(), + balance: 0, + ..Account::default() + }; + + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); + state.force_insert_account(vault_id, vault_account); + state.force_insert_account(receiver_id, receiver_account); + + // Callback instruction: return funds + let cb_instruction = CallbackInstruction { + return_funds: true, + token_program_id: token.id(), + amount: amount_out, + }; + let cb_data = Program::serialize_instruction(cb_instruction).unwrap(); + + let instruction = FlashSwapInstruction::Initiate { + token_program_id: token.id(), + callback_program_id: callback.id(), + amount_out, + callback_instruction_data: cb_data, + }; + + let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction); + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!(result.is_ok(), "flash swap should succeed: {result:?}"); + + // Vault balance restored, receiver back to 0 + assert_eq!(state.get_account_by_id(vault_id).balance, initial_balance); + assert_eq!(state.get_account_by_id(receiver_id).balance, 0); + } + + #[test] + fn flash_swap_callback_keeps_funds_rollback() { + let initiator = Program::flash_swap_initiator(); + let callback = Program::flash_swap_callback(); + let token = Program::authenticated_transfer_program(); + + let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); + let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32])); + + let initial_balance: u128 = 1000; + let amount_out: u128 = 100; + + let vault_account = Account { + program_owner: token.id(), + balance: initial_balance, + ..Account::default() + }; + let receiver_account = Account { + program_owner: token.id(), + balance: 0, + ..Account::default() + }; + + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); + state.force_insert_account(vault_id, vault_account); + state.force_insert_account(receiver_id, receiver_account); + + // Callback instruction: do NOT return funds + let cb_instruction = CallbackInstruction { + return_funds: false, + token_program_id: token.id(), + amount: amount_out, + }; + let cb_data = Program::serialize_instruction(cb_instruction).unwrap(); + + let instruction = FlashSwapInstruction::Initiate { + token_program_id: token.id(), + callback_program_id: callback.id(), + amount_out, + callback_instruction_data: cb_data, + }; + + let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction); + let result = state.transition_from_public_transaction(&tx, 1, 0); + + // Invariant check fails → entire tx rolls back + assert!( + result.is_err(), + "flash swap should fail when callback keeps funds" + ); + + // State unchanged (rollback) + assert_eq!(state.get_account_by_id(vault_id).balance, initial_balance); + assert_eq!(state.get_account_by_id(receiver_id).balance, 0); + } + + #[test] + fn flash_swap_self_call_targets_correct_program() { + // Zero-amount flash swap: the invariant self-call still runs and succeeds + // because vault balance doesn't decrease. + let initiator = Program::flash_swap_initiator(); + let callback = Program::flash_swap_callback(); + let token = Program::authenticated_transfer_program(); + + let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); + let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32])); + + let initial_balance: u128 = 1000; + + let vault_account = Account { + program_owner: token.id(), + balance: initial_balance, + ..Account::default() + }; + let receiver_account = Account { + program_owner: token.id(), + balance: 0, + ..Account::default() + }; + + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); + state.force_insert_account(vault_id, vault_account); + state.force_insert_account(receiver_id, receiver_account); + + let cb_instruction = CallbackInstruction { + return_funds: true, + token_program_id: token.id(), + amount: 0, + }; + let cb_data = Program::serialize_instruction(cb_instruction).unwrap(); + + let instruction = FlashSwapInstruction::Initiate { + token_program_id: token.id(), + callback_program_id: callback.id(), + amount_out: 0, + callback_instruction_data: cb_data, + }; + + let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction); + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!( + result.is_ok(), + "zero-amount flash swap should succeed: {result:?}" + ); + } + + #[test] + fn flash_swap_standalone_invariant_check_rejected() { + // Calling InvariantCheck directly (not as a chained self-call) should fail + // because caller_program_id will be None. + let initiator = Program::flash_swap_initiator(); + let token = Program::authenticated_transfer_program(); + + let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32])); + + let vault_account = Account { + program_owner: token.id(), + balance: 1000, + ..Account::default() + }; + + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); + state.force_insert_account(vault_id, vault_account); + + let instruction = FlashSwapInstruction::InvariantCheck { + min_vault_balance: 1000, + }; + + let message = public_transaction::Message::try_new( + initiator.id(), + vec![vault_id], + vec![], + instruction, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!( + result.is_err(), + "standalone InvariantCheck should be rejected (caller_program_id is None)" + ); + } + + #[test] + fn malicious_self_program_id_rejected_in_public_execution() { + let program = Program::malicious_self_program_id(); + let acc_id = AccountId::new([99; 32]); + let account = Account::default(); + + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); + state.force_insert_account(acc_id, account); + + let message = + public_transaction::Message::try_new(program.id(), vec![acc_id], vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!( + result.is_err(), + "program with wrong self_program_id in output should be rejected" + ); + } + + #[test] + fn malicious_caller_program_id_rejected_in_public_execution() { + let program = Program::malicious_caller_program_id(); + let acc_id = AccountId::new([99; 32]); + let account = Account::default(); + + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); + state.force_insert_account(acc_id, account); + + let message = + public_transaction::Message::try_new(program.id(), vec![acc_id], vec![], ()).unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[]); + let tx = PublicTransaction::new(message, witness_set); + + let result = state.transition_from_public_transaction(&tx, 1, 0); + assert!( + result.is_err(), + "program with spoofed caller_program_id in output should be rejected" + ); + } } diff --git a/nssa/src/validated_state_diff.rs b/nssa/src/validated_state_diff.rs new file mode 100644 index 00000000..455a13a6 --- /dev/null +++ b/nssa/src/validated_state_diff.rs @@ -0,0 +1,462 @@ +use std::{ + collections::{HashMap, HashSet, VecDeque}, + hash::Hash, +}; + +use log::debug; +use nssa_core::{ + BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp, + account::{Account, AccountId, AccountWithMetadata}, + program::{ + ChainedCall, Claim, DEFAULT_PROGRAM_ID, compute_public_authorized_pdas, validate_execution, + }, +}; + +use crate::{ + V03State, ensure, + error::{InvalidProgramBehaviorError, NssaError}, + privacy_preserving_transaction::{ + PrivacyPreservingTransaction, circuit::Proof, message::Message, + }, + program::Program, + program_deployment_transaction::ProgramDeploymentTransaction, + public_transaction::PublicTransaction, + state::MAX_NUMBER_CHAINED_CALLS, +}; + +pub struct StateDiff { + pub signer_account_ids: Vec, + pub public_diff: HashMap, + pub new_commitments: Vec, + pub new_nullifiers: Vec, + pub program: Option, +} + +/// The validated output of executing or verifying a transaction, ready to be applied to the state. +/// +/// Can only be constructed by the transaction validation functions inside this crate, ensuring the +/// diff has been checked before any state mutation occurs. +pub struct ValidatedStateDiff(StateDiff); + +impl ValidatedStateDiff { + pub fn from_public_transaction( + tx: &PublicTransaction, + state: &V03State, + block_id: BlockId, + timestamp: Timestamp, + ) -> Result { + let message = tx.message(); + let witness_set = tx.witness_set(); + + // All account_ids must be different + ensure!( + message.account_ids.iter().collect::>().len() == message.account_ids.len(), + NssaError::InvalidInput("Duplicate account_ids found in message".into(),) + ); + + // Check exactly one nonce is provided for each signature + ensure!( + message.nonces.len() == witness_set.signatures_and_public_keys.len(), + NssaError::InvalidInput( + "Mismatch between number of nonces and signatures/public keys".into(), + ) + ); + + // Check the signatures are valid + ensure!( + witness_set.is_valid_for(message), + NssaError::InvalidInput("Invalid signature for given message and public key".into()) + ); + + let signer_account_ids = tx.signer_account_ids(); + // Check nonces corresponds to the current nonces on the public state. + for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) { + let current_nonce = state.get_account_by_id(*account_id).nonce; + ensure!( + current_nonce == *nonce, + NssaError::InvalidInput("Nonce mismatch".into()) + ); + } + + // Build pre_states for execution + let input_pre_states: Vec<_> = message + .account_ids + .iter() + .map(|account_id| { + AccountWithMetadata::new( + state.get_account_by_id(*account_id), + signer_account_ids.contains(account_id), + *account_id, + ) + }) + .collect(); + + let mut state_diff: HashMap = HashMap::new(); + + let initial_call = ChainedCall { + program_id: message.program_id, + instruction_data: message.instruction_data.clone(), + pre_states: input_pre_states, + pda_seeds: vec![], + }; + + let mut chained_calls = VecDeque::from_iter([(initial_call, None)]); + let mut chain_calls_counter = 0; + + while let Some((chained_call, caller_program_id)) = chained_calls.pop_front() { + ensure!( + chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS, + NssaError::MaxChainedCallsDepthExceeded + ); + + // Check that the `program_id` corresponds to a deployed program + let Some(program) = state.programs().get(&chained_call.program_id) else { + return Err(NssaError::InvalidInput("Unknown program".into())); + }; + + debug!( + "Program {:?} pre_states: {:?}, instruction_data: {:?}", + chained_call.program_id, chained_call.pre_states, chained_call.instruction_data + ); + let mut program_output = program.execute( + caller_program_id, + &chained_call.pre_states, + &chained_call.instruction_data, + )?; + debug!( + "Program {:?} output: {:?}", + chained_call.program_id, program_output + ); + + let authorized_pdas = + compute_public_authorized_pdas(caller_program_id, &chained_call.pda_seeds); + + let is_authorized = |account_id: &AccountId| { + signer_account_ids.contains(account_id) || authorized_pdas.contains(account_id) + }; + + for pre in &program_output.pre_states { + let account_id = pre.account_id; + // Check that the program output pre_states coincide with the values in the public + // state or with any modifications to those values during the chain of calls. + let expected_pre = state_diff + .get(&account_id) + .cloned() + .unwrap_or_else(|| state.get_account_by_id(account_id)); + ensure!( + pre.account == expected_pre, + InvalidProgramBehaviorError::InconsistentAccountPreState { + account_id, + expected: Box::new(expected_pre), + actual: Box::new(pre.account.clone()) + } + ); + + // Check that authorization flags are consistent with the provided ones or + // authorized by program through the PDA mechanism + let expected_is_authorized = is_authorized(&account_id); + ensure!( + pre.is_authorized == expected_is_authorized, + InvalidProgramBehaviorError::InconsistentAccountAuthorization { + account_id, + expected_authorization: expected_is_authorized, + actual_authorization: pre.is_authorized + } + ); + } + + // Verify that the program output's self_program_id matches the expected program ID. + ensure!( + program_output.self_program_id == chained_call.program_id, + InvalidProgramBehaviorError::MismatchedProgramId { + expected: chained_call.program_id, + actual: program_output.self_program_id + } + ); + + // Verify that the program output's caller_program_id matches the actual caller. + ensure!( + program_output.caller_program_id == caller_program_id, + InvalidProgramBehaviorError::MismatchedCallerProgramId { + expected: caller_program_id, + actual: program_output.caller_program_id, + } + ); + + // Verify execution corresponds to a well-behaved program. + // See the # Programs section for the definition of the `validate_execution` method. + validate_execution( + &program_output.pre_states, + &program_output.post_states, + chained_call.program_id, + ) + .map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?; + + // Verify validity window + ensure!( + program_output.block_validity_window.is_valid_for(block_id) + && program_output + .timestamp_validity_window + .is_valid_for(timestamp), + NssaError::OutOfValidityWindow + ); + + for (i, post) in program_output.post_states.iter_mut().enumerate() { + let Some(claim) = post.required_claim() else { + continue; + }; + let account_id = program_output.pre_states[i].account_id; + + // The invoked program can only claim accounts with default program id. + ensure!( + post.account().program_owner == DEFAULT_PROGRAM_ID, + InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id } + ); + + match claim { + Claim::Authorized => { + // The program can only claim accounts that were authorized by the signer. + ensure!( + is_authorized(&account_id), + InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id } + ); + } + Claim::Pda(seed) => { + // The program can only claim accounts that correspond to the PDAs it is + // authorized to claim. The public-execution path only sees public + // accounts, so the public-PDA derivation is the correct formula here. + let pda = AccountId::for_public_pda(&chained_call.program_id, &seed); + ensure!( + account_id == pda, + InvalidProgramBehaviorError::MismatchedPdaClaim { + expected: pda, + actual: account_id + } + ); + } + } + + post.account_mut().program_owner = chained_call.program_id; + } + + // Update the state diff + for (pre, post) in program_output + .pre_states + .iter() + .zip(program_output.post_states.iter()) + { + state_diff.insert(pre.account_id, post.account().clone()); + } + + for new_call in program_output.chained_calls.into_iter().rev() { + chained_calls.push_front((new_call, Some(chained_call.program_id))); + } + + chain_calls_counter = chain_calls_counter + .checked_add(1) + .expect("we check the max depth at the beginning of the loop"); + } + + // Check that all modified uninitialized accounts where claimed + for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| { + let pre = state.get_account_by_id(*account_id); + if pre.program_owner != DEFAULT_PROGRAM_ID { + return None; + } + if pre == *post { + return None; + } + Some((*account_id, post)) + }) { + ensure!( + post.program_owner != DEFAULT_PROGRAM_ID, + InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id } + ); + } + + Ok(Self(StateDiff { + signer_account_ids, + public_diff: state_diff, + new_commitments: vec![], + new_nullifiers: vec![], + program: None, + })) + } + + pub fn from_privacy_preserving_transaction( + tx: &PrivacyPreservingTransaction, + state: &V03State, + block_id: BlockId, + timestamp: Timestamp, + ) -> Result { + let message = &tx.message; + let witness_set = &tx.witness_set; + + // 1. Commitments or nullifiers are non empty + if message.new_commitments.is_empty() && message.new_nullifiers.is_empty() { + return Err(NssaError::InvalidInput( + "Empty commitments and empty nullifiers found in message".into(), + )); + } + + // 2. Check there are no duplicate account_ids in the public_account_ids list. + if n_unique(&message.public_account_ids) != message.public_account_ids.len() { + return Err(NssaError::InvalidInput( + "Duplicate account_ids found in message".into(), + )); + } + + // Check there are no duplicate nullifiers in the new_nullifiers list + if n_unique(&message.new_nullifiers) != message.new_nullifiers.len() { + return Err(NssaError::InvalidInput( + "Duplicate nullifiers found in message".into(), + )); + } + + // Check there are no duplicate commitments in the new_commitments list + if n_unique(&message.new_commitments) != message.new_commitments.len() { + return Err(NssaError::InvalidInput( + "Duplicate commitments found in message".into(), + )); + } + + // 3. Nonce checks and Valid signatures + // Check exactly one nonce is provided for each signature + if message.nonces.len() != witness_set.signatures_and_public_keys.len() { + return Err(NssaError::InvalidInput( + "Mismatch between number of nonces and signatures/public keys".into(), + )); + } + + // Check the signatures are valid + if !witness_set.signatures_are_valid_for(message) { + return Err(NssaError::InvalidInput( + "Invalid signature for given message and public key".into(), + )); + } + + let signer_account_ids = tx.signer_account_ids(); + // Check nonces corresponds to the current nonces on the public state. + for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) { + let current_nonce = state.get_account_by_id(*account_id).nonce; + if current_nonce != *nonce { + return Err(NssaError::InvalidInput("Nonce mismatch".into())); + } + } + + // Verify validity window + if !message.block_validity_window.is_valid_for(block_id) + || !message.timestamp_validity_window.is_valid_for(timestamp) + { + return Err(NssaError::OutOfValidityWindow); + } + + // Build pre_states for proof verification + let public_pre_states: Vec<_> = message + .public_account_ids + .iter() + .map(|account_id| { + AccountWithMetadata::new( + state.get_account_by_id(*account_id), + signer_account_ids.contains(account_id), + *account_id, + ) + }) + .collect(); + + // 4. Proof verification + check_privacy_preserving_circuit_proof_is_valid( + &witness_set.proof, + &public_pre_states, + message, + )?; + + // 5. Commitment freshness + state.check_commitments_are_new(&message.new_commitments)?; + + // 6. Nullifier uniqueness + state.check_nullifiers_are_valid(&message.new_nullifiers)?; + + let public_diff = message + .public_account_ids + .iter() + .copied() + .zip(message.public_post_states.clone()) + .collect(); + let new_nullifiers = message + .new_nullifiers + .iter() + .copied() + .map(|(nullifier, _)| nullifier) + .collect(); + + Ok(Self(StateDiff { + signer_account_ids, + public_diff, + new_commitments: message.new_commitments.clone(), + new_nullifiers, + program: None, + })) + } + + pub fn from_program_deployment_transaction( + tx: &ProgramDeploymentTransaction, + state: &V03State, + ) -> Result { + // TODO: remove clone + let program = Program::new(tx.message.bytecode.clone())?; + if state.programs().contains_key(&program.id()) { + return Err(NssaError::ProgramAlreadyExists); + } + Ok(Self(StateDiff { + signer_account_ids: vec![], + public_diff: HashMap::new(), + new_commitments: vec![], + new_nullifiers: vec![], + program: Some(program), + })) + } + + /// Returns the public account changes produced by this transaction. + /// + /// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example + /// to enforce that system accounts are not modified by user transactions. + #[must_use] + pub fn public_diff(&self) -> HashMap { + self.0.public_diff.clone() + } + + pub(crate) fn into_state_diff(self) -> StateDiff { + self.0 + } +} + +fn check_privacy_preserving_circuit_proof_is_valid( + proof: &Proof, + public_pre_states: &[AccountWithMetadata], + message: &Message, +) -> Result<(), NssaError> { + let output = PrivacyPreservingCircuitOutput { + public_pre_states: public_pre_states.to_vec(), + public_post_states: message.public_post_states.clone(), + ciphertexts: message + .encrypted_private_post_states + .iter() + .cloned() + .map(|value| value.ciphertext) + .collect(), + new_commitments: message.new_commitments.clone(), + new_nullifiers: message.new_nullifiers.clone(), + block_validity_window: message.block_validity_window, + timestamp_validity_window: message.timestamp_validity_window, + }; + proof + .is_valid_for(&output) + .then_some(()) + .ok_or(NssaError::InvalidPrivacyPreservingProof) +} + +fn n_unique(data: &[T]) -> usize { + let set: HashSet<&T> = data.iter().collect(); + set.len() +} diff --git a/program_methods/guest/Cargo.toml b/program_methods/guest/Cargo.toml index 29ef8304..dc2077b7 100644 --- a/program_methods/guest/Cargo.toml +++ b/program_methods/guest/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] nssa_core.workspace = true +clock_core.workspace = true token_core.workspace = true token_program.workspace = true amm_core.workspace = true diff --git a/program_methods/guest/src/bin/amm.rs b/program_methods/guest/src/bin/amm.rs index 748630d9..bce76c63 100644 --- a/program_methods/guest/src/bin/amm.rs +++ b/program_methods/guest/src/bin/amm.rs @@ -14,6 +14,8 @@ use nssa_core::program::{ProgramInput, ProgramOutput, read_nssa_inputs}; fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction, }, @@ -112,15 +114,15 @@ fn main() { min_amount_to_remove_token_b, ) } - Instruction::Swap { + Instruction::SwapExactInput { swap_amount_in, min_amount_out, token_definition_id_in, } => { let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states .try_into() - .expect("Transfer instruction requires exactly five accounts"); - amm_program::swap::swap( + .expect("SwapExactInput instruction requires exactly five accounts"); + amm_program::swap::swap_exact_input( pool, vault_a, vault_b, @@ -131,9 +133,34 @@ fn main() { token_definition_id_in, ) } + Instruction::SwapExactOutput { + exact_amount_out, + max_amount_in, + token_definition_id_in, + } => { + let [pool, vault_a, vault_b, user_holding_a, user_holding_b] = pre_states + .try_into() + .expect("SwapExactOutput instruction requires exactly five accounts"); + amm_program::swap::swap_exact_output( + pool, + vault_a, + vault_b, + user_holding_a, + user_holding_b, + exact_amount_out, + max_amount_in, + token_definition_id_in, + ) + } }; - ProgramOutput::new(instruction_words, pre_states_clone, post_states) - .with_chained_calls(chained_calls) - .write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states_clone, + post_states, + ) + .with_chained_calls(chained_calls) + .write(); } diff --git a/program_methods/guest/src/bin/associated_token_account.rs b/program_methods/guest/src/bin/associated_token_account.rs index 55d5824b..9b155d7f 100644 --- a/program_methods/guest/src/bin/associated_token_account.rs +++ b/program_methods/guest/src/bin/associated_token_account.rs @@ -4,6 +4,8 @@ use nssa_core::program::{ProgramInput, ProgramOutput, read_nssa_inputs}; fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction, }, @@ -56,7 +58,13 @@ fn main() { } }; - ProgramOutput::new(instruction_words, pre_states_clone, post_states) - .with_chained_calls(chained_calls) - .write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states_clone, + post_states, + ) + .with_chained_calls(chained_calls) + .write(); } diff --git a/program_methods/guest/src/bin/authenticated_transfer.rs b/program_methods/guest/src/bin/authenticated_transfer.rs index 2fb0ea8b..32b69c3a 100644 --- a/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/program_methods/guest/src/bin/authenticated_transfer.rs @@ -67,6 +67,8 @@ fn main() { // Read input accounts. let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: balance_to_move, }, @@ -84,5 +86,12 @@ fn main() { _ => panic!("invalid params"), }; - ProgramOutput::new(instruction_words, pre_states, post_states).write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + post_states, + ) + .write(); } diff --git a/program_methods/guest/src/bin/clock.rs b/program_methods/guest/src/bin/clock.rs new file mode 100644 index 00000000..cb49c384 --- /dev/null +++ b/program_methods/guest/src/bin/clock.rs @@ -0,0 +1,94 @@ +//! Clock Program. +//! +//! A system program that records the current block ID and timestamp into dedicated clock accounts. +//! Three accounts are maintained, updated at different block intervals (every 1, 10, and 50 +//! blocks), allowing programs to read recent timestamps at various granularities. +//! +//! This program can only be invoked exclusively by the sequencer as the last transaction in every +//! block. Clock accounts are assigned to the clock program at genesis, so no claiming is required +//! here. + +use clock_core::{ + CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_10_PROGRAM_ACCOUNT_ID, CLOCK_50_PROGRAM_ACCOUNT_ID, + ClockAccountData, Instruction, +}; +use nssa_core::{ + account::AccountWithMetadata, + program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}, +}; + +fn update_if_multiple( + pre: AccountWithMetadata, + divisor: u64, + current_block_id: u64, + updated_data: &[u8], +) -> (AccountWithMetadata, AccountPostState) { + if current_block_id.is_multiple_of(divisor) { + let mut post_account = pre.account.clone(); + post_account.data = updated_data + .to_vec() + .try_into() + .expect("Clock account data should fit in account data"); + (pre, AccountPostState::new(post_account)) + } else { + let post = AccountPostState::new(pre.account.clone()); + (pre, post) + } +} + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: timestamp, + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([pre_01, pre_10, pre_50]) = <[_; 3]>::try_from(pre_states) else { + panic!("Invalid number of input accounts"); + }; + + // Verify pre-states correspond to the expected clock account IDs. + if pre_01.account_id != CLOCK_01_PROGRAM_ACCOUNT_ID + || pre_10.account_id != CLOCK_10_PROGRAM_ACCOUNT_ID + || pre_50.account_id != CLOCK_50_PROGRAM_ACCOUNT_ID + { + panic!("Invalid input accounts"); + } + + // Verify all clock accounts are owned by this program (assigned at genesis). + if pre_01.account.program_owner != self_program_id + || pre_10.account.program_owner != self_program_id + || pre_50.account.program_owner != self_program_id + { + panic!("Clock accounts must be owned by the clock program"); + } + + let prev_data = ClockAccountData::from_bytes(&pre_01.account.data.clone().into_inner()); + let current_block_id = prev_data + .block_id + .checked_add(1) + .expect("Next block id should be within u64 boundaries"); + + let updated_data = ClockAccountData { + block_id: current_block_id, + timestamp, + } + .to_bytes(); + + let (pre_01, post_01) = update_if_multiple(pre_01, 1, current_block_id, &updated_data); + let (pre_10, post_10) = update_if_multiple(pre_10, 10, current_block_id, &updated_data); + let (pre_50, post_50) = update_if_multiple(pre_50, 50, current_block_id, &updated_data); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![pre_01, pre_10, pre_50], + vec![post_01, post_10, post_50], + ) + .write(); +} diff --git a/program_methods/guest/src/bin/pinata.rs b/program_methods/guest/src/bin/pinata.rs index 2f85f069..dcc76397 100644 --- a/program_methods/guest/src/bin/pinata.rs +++ b/program_methods/guest/src/bin/pinata.rs @@ -46,6 +46,8 @@ fn main() { // It is expected to receive only two accounts: [pinata_account, winner_account] let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: solution, }, @@ -79,6 +81,8 @@ fn main() { .expect("Overflow when adding prize to winner"); ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![pinata, winner], vec![ diff --git a/program_methods/guest/src/bin/pinata_token.rs b/program_methods/guest/src/bin/pinata_token.rs index 3dee05b7..1f7ad9da 100644 --- a/program_methods/guest/src/bin/pinata_token.rs +++ b/program_methods/guest/src/bin/pinata_token.rs @@ -52,6 +52,8 @@ fn main() { // winner_token_holding] let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: solution, }, @@ -97,6 +99,8 @@ fn main() { .with_pda_seeds(vec![PdaSeed::new([0; 32])]); ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![ pinata_definition, diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index e53334f9..8018cd80 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -11,7 +11,7 @@ use nssa_core::{ compute_digest_for_path, program::{ AccountPostState, BlockValidityWindow, ChainedCall, Claim, DEFAULT_PROGRAM_ID, - MAX_NUMBER_CHAINED_CALLS, ProgramId, ProgramOutput, TimestampValidityWindow, + MAX_NUMBER_CHAINED_CALLS, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow, validate_execution, }, }; @@ -23,15 +23,62 @@ struct ExecutionState { post_states: HashMap, block_validity_window: BlockValidityWindow, timestamp_validity_window: TimestampValidityWindow, + /// Positions (in `pre_states`) of mask-3 accounts whose supplied npk has been bound to + /// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk)` + /// 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` + /// under the private derivation. Binding is an idempotent property, not an event: the same + /// position can legitimately be bound through both paths in the same tx (e.g. a program + /// claims a private PDA and then delegates it to a callee), and the set uses `contains`, + /// not `assert!(insert)`. After the main loop, every mask-3 position must appear in this + /// set; otherwise the npk is unbound and the circuit rejects. + private_pda_bound_positions: HashSet, + /// Across the whole transaction, each `(program_id, seed)` pair may resolve to at most one + /// `AccountId`. A seed under a program can derive a family of accounts, one public PDA and + /// one private PDA per distinct npk. Without this check, a single `pda_seeds: [S]` entry in + /// a chained call could authorize multiple family members at once (different npks under the + /// same seed) and let a callee mix balances across them. Every claim and every + /// caller-authorization resolution is recorded here, either as a new `(program, seed)` → + /// `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 mask-3 `pre_state`'s position in `visibility_mask` to the npk supplied for + /// that position in `private_account_keys`. Built once in `derive_from_outputs` by walking + /// `visibility_mask` in lock-step with `private_account_keys`, used later by the claim and + /// caller-seeds authorization paths. + private_pda_npk_by_position: HashMap, } impl ExecutionState { /// Validate program outputs and derive the overall execution state. pub fn derive_from_outputs( visibility_mask: &[u8], + private_account_keys: &[(NullifierPublicKey, SharedSecretKey)], program_id: ProgramId, program_outputs: Vec, ) -> Self { + // Build position → npk map for mask-3 pre_states. `private_account_keys` is consumed in + // pre_state order across all masks 1/2/3, so walk `visibility_mask` in lock-step. The + // downstream `compute_circuit_output` also consumes the same iterator and its trailing + // assertions catch an over-supply of keys; under-supply surfaces here. + let mut private_pda_npk_by_position: HashMap = HashMap::new(); + { + let mut keys_iter = private_account_keys.iter(); + for (pos, &mask) in visibility_mask.iter().enumerate() { + if matches!(mask, 1..=3) { + let (npk, _) = keys_iter.next().unwrap_or_else(|| { + panic!( + "private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})" + ) + }); + if mask == 3 { + private_pda_npk_by_position.insert(pos, *npk); + } + } + } + } + let block_valid_from = program_outputs .iter() .filter_map(|output| output.block_validity_window.start()) @@ -66,6 +113,9 @@ impl ExecutionState { post_states: HashMap::new(), block_validity_window, timestamp_validity_window, + private_pda_bound_positions: HashSet::new(), + pda_family_binding: HashMap::new(), + private_pda_npk_by_position, }; let Some(first_output) = program_outputs.first() else { @@ -107,27 +157,45 @@ impl ExecutionState { |_: Infallible| unreachable!("Infallible error is never constructed"), ); + // Verify that the program output's self_program_id matches the expected program ID. + // This ensures the proof commits to which program produced the output. + assert_eq!( + program_output.self_program_id, chained_call.program_id, + "Program output self_program_id does not match chained call program_id" + ); + + // Verify that the program output's caller_program_id matches the actual caller. + // This prevents a malicious user from privately executing an internal function + // by spoofing caller_program_id (e.g. passing caller_program_id = self_program_id + // to bypass access control checks). + assert_eq!( + program_output.caller_program_id, caller_program_id, + "Program output caller_program_id does not match actual caller" + ); + // Check that the program is well behaved. // See the # Programs section for the definition of the `validate_execution` method. - let execution_valid = validate_execution( + let validated_execution = validate_execution( &program_output.pre_states, &program_output.post_states, chained_call.program_id, ); - assert!(execution_valid, "Bad behaved program"); + if let Err(err) = validated_execution { + panic!( + "Invalid program behavior in program {:?}: {err}", + chained_call.program_id + ); + } for next_call in program_output.chained_calls.iter().rev() { chained_calls.push_front((next_call.clone(), Some(chained_call.program_id))); } - let authorized_pdas = nssa_core::program::compute_authorized_pdas( - caller_program_id, - &chained_call.pda_seeds, - ); execution_state.validate_and_sync_states( visibility_mask, chained_call.program_id, - &authorized_pdas, + caller_program_id, + &chained_call.pda_seeds, program_output.pre_states, program_output.post_states, ); @@ -141,6 +209,19 @@ impl ExecutionState { "Inner call without a chained call found", ); + // Every mask-3 pre_state must have had its npk bound to its account_id, either via a + // `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` matching + // the private derivation. An unbound mask-3 pre_state has no cryptographic link between + // the supplied npk and the account_id, and must be rejected. + for (pos, &mask) in visibility_mask.iter().enumerate() { + if mask == 3 { + assert!( + execution_state.private_pda_bound_positions.contains(&pos), + "private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds" + ); + } + } + // Check that all modified uninitialized accounts were claimed for (account_id, post) in execution_state .pre_states @@ -170,7 +251,8 @@ impl ExecutionState { &mut self, visibility_mask: &[u8], program_id: ProgramId, - authorized_pdas: &HashSet, + caller_program_id: Option, + caller_pda_seeds: &[PdaSeed], pre_states: Vec, post_states: Vec, ) { @@ -197,19 +279,28 @@ impl ExecutionState { "Inconsistent pre state for account {pre_account_id}", ); - let previous_is_authorized = self + let (previous_is_authorized, pre_state_position) = self .pre_states .iter() - .find(|acc| acc.account_id == pre_account_id) + .enumerate() + .find(|(_, acc)| acc.account_id == pre_account_id) .map_or_else( || panic!( "Pre state must exist in execution state for account {pre_account_id}", ), - |acc| acc.is_authorized + |(pos, acc)| (acc.is_authorized, pos) ); - let is_authorized = - previous_is_authorized || authorized_pdas.contains(&pre_account_id); + 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, + pre_account_id, + pre_state_position, + caller_program_id, + caller_pda_seeds, + previous_is_authorized, + ); assert_eq!( pre_is_authorized, is_authorized, @@ -236,9 +327,9 @@ impl ExecutionState { .position(|acc| acc.account_id == pre_account_id) .expect("Pre state must exist at this point"); - let is_public_account = visibility_mask[pre_state_position] == 0; - if is_public_account { - match claim { + let mask = visibility_mask[pre_state_position]; + match mask { + 0 => match claim { Claim::Authorized => { // Note: no need to check authorized pdas because we have already // checked consistency of authorization above. @@ -248,18 +339,52 @@ impl ExecutionState { ); } Claim::Pda(seed) => { - let pda = AccountId::from((&program_id, &seed)); + let pda = AccountId::for_public_pda(&program_id, &seed); assert_eq!( pre_account_id, pda, "Invalid PDA claim for account {pre_account_id} which does not match derived PDA {pda}" ); + assert_family_binding( + &mut self.pda_family_binding, + program_id, + seed, + pre_account_id, + ); + } + }, + 3 => { + match claim { + Claim::Authorized => { + assert!( + pre_is_authorized, + "Cannot claim unauthorized private PDA {pre_account_id}" + ); + } + Claim::Pda(seed) => { + let npk = self + .private_pda_npk_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); + assert_eq!( + pre_account_id, pda, + "Invalid private PDA claim for account {pre_account_id}" + ); + self.private_pda_bound_positions.insert(pre_state_position); + assert_family_binding( + &mut self.pda_family_binding, + program_id, + seed, + pre_account_id, + ); + } } } - } else { - // We don't care about the exact claim mechanism for private accounts. - // This is because the main reason to have it is to protect against PDA griefing - // attacks in public execution, while private PDA doesn't make much sense - // anyway. + _ => { + // Mask 1/2: standard private accounts don't enforce the claim semantics. + // Unauthorized private claiming is intentionally allowed since operating + // these accounts requires the npk/nsk keypair anyway. + } } post.account_mut().program_owner = program_id; @@ -283,6 +408,82 @@ impl ExecutionState { } } +/// Record or re-verify the `(program_id, seed) → account_id` family binding for the +/// transaction. Any claim or caller-seed authorization that resolves a `pre_state` under +/// `(program_id, seed)` must agree with every prior resolution of the same pair; otherwise a +/// single `pda_seeds: [seed]` entry could authorize multiple private-PDA family members at +/// once (different npks under the same seed) and let a callee mix balances across them. Free +/// function so callers can pass `&mut self.pda_family_binding` without holding a borrow on +/// the surrounding struct's other fields. +fn assert_family_binding( + bindings: &mut HashMap<(ProgramId, PdaSeed), AccountId>, + program_id: ProgramId, + seed: PdaSeed, + account_id: AccountId, +) { + match bindings.entry((program_id, seed)) { + Entry::Vacant(e) => { + e.insert(account_id); + } + Entry::Occupied(e) => { + assert_eq!( + *e.get(), + account_id, + "Two different accounts resolved under the same (program, seed) in one transaction: existing {}, new {account_id}", + e.get() + ); + } + } +} + +/// Resolve the authorization state of a `pre_state` seen again in a chained call and record +/// any resulting bindings. Returns `true` if the `pre_state` is authorized through either a +/// previously-seen authorization or a matching caller seed (under the public or private +/// derivation). When a caller seed matches, also records the `(caller, seed) → account_id` +/// family binding and, for the private form, marks the position in +/// `private_pda_bound_positions`. Only reachable when `caller_program_id.is_some()`, +/// top-level flows have no caller-emitted seeds, so binding at top level must come from the +/// claim path. Free function so callers can pass individual `&mut self.*` field borrows +/// without holding a borrow on the surrounding struct's other fields. +#[expect( + clippy::too_many_arguments, + reason = "breaking out a context struct does not buy us anything here" +)] +fn resolve_authorization_and_record_bindings( + pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>, + private_pda_bound_positions: &mut HashSet, + private_pda_npk_by_position: &HashMap, + pre_account_id: AccountId, + pre_state_position: usize, + caller_program_id: Option, + caller_pda_seeds: &[PdaSeed], + previous_is_authorized: bool, +) -> bool { + let matched_caller_seed: Option<(PdaSeed, bool, ProgramId)> = + caller_program_id.and_then(|caller| { + caller_pda_seeds.iter().find_map(|seed| { + if AccountId::for_public_pda(&caller, seed) == pre_account_id { + return Some((*seed, false, caller)); + } + if let Some(npk) = private_pda_npk_by_position.get(&pre_state_position) + && AccountId::for_private_pda(&caller, seed, npk) == pre_account_id + { + return Some((*seed, true, caller)); + } + None + }) + }); + + if let Some((seed, is_private_form, caller)) = matched_caller_seed { + assert_family_binding(pda_family_binding, caller, seed, pre_account_id); + if is_private_form { + private_pda_bound_positions.insert(pre_state_position); + } + } + + previous_is_authorized || matched_caller_seed.is_some() +} + fn compute_circuit_output( execution_state: ExecutionState, visibility_mask: &[u8], @@ -418,6 +619,88 @@ fn compute_circuit_output( .checked_add(1) .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); } + 3 => { + // Private PDA account. The supplied npk has already been bound to + // `pre_state.account_id` upstream in `validate_and_sync_states`, either via a + // `Claim::Pda(seed)` match or via a caller `pda_seeds` match, both of which + // assert `AccountId::for_private_pda(owner, seed, npk) == account_id`. The + // post-loop assertion in `derive_from_outputs` (see the + // `private_pda_bound_positions` check) guarantees that every mask-3 + // position has been through at least one such binding, so this + // branch can safely use the wallet npk without re-verifying. + let Some((npk, shared_secret)) = private_keys_iter.next() else { + panic!("Missing private account key"); + }; + + let (new_nullifier, new_nonce) = if pre_state.is_authorized { + // Existing private PDA with authentication (like mask 1) + let Some(nsk) = private_nsks_iter.next() else { + panic!("Missing private account nullifier secret key"); + }; + assert_eq!( + npk, + &NullifierPublicKey::from(nsk), + "Nullifier public key mismatch" + ); + + let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { + panic!("Missing membership proof"); + }; + + let new_nullifier = compute_nullifier_and_set_digest( + membership_proof_opt.as_ref(), + &pre_state.account, + npk, + nsk, + ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + (new_nullifier, new_nonce) + } else { + // New private PDA (like mask 2). The default + unauthorized requirement + // here rules out use cases like a fully-private multisig, which would need + // a non-default, non-authorized private PDA input account. + // TODO(private-pdas-pr-2/3): relax this once the wallet can supply a + // `(seed, owner)` side input so the npk-to-account_id binding can be + // re-verified for an existing private PDA without a `Claim::Pda` or caller + // `pda_seeds` match. + assert_eq!( + pre_state.account, + Account::default(), + "New private PDA must be default" + ); + + let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { + panic!("Missing membership proof"); + }; + assert!( + membership_proof_opt.is_none(), + "Membership proof must be None for new accounts" + ); + + let nullifier = Nullifier::for_account_initialization(npk); + let new_nonce = Nonce::private_account_nonce_init(npk); + ((nullifier, DUMMY_COMMITMENT_HASH), new_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(npk, &post_with_updated_nonce); + + let encrypted_account = EncryptionScheme::encrypt( + &post_with_updated_nonce, + shared_secret, + &commitment_post, + output_index, + ); + + output.new_commitments.push(commitment_post); + output.ciphertexts.push(encrypted_account); + output_index = output_index + .checked_add(1) + .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); + } _ => panic!("Invalid visibility mask value"), } } @@ -480,8 +763,12 @@ fn main() { program_id, } = env::read(); - let execution_state = - ExecutionState::derive_from_outputs(&visibility_mask, program_id, program_outputs); + let execution_state = ExecutionState::derive_from_outputs( + &visibility_mask, + &private_account_keys, + program_id, + program_outputs, + ); let output = compute_circuit_output( execution_state, diff --git a/program_methods/guest/src/bin/token.rs b/program_methods/guest/src/bin/token.rs index 421d43ef..68205d77 100644 --- a/program_methods/guest/src/bin/token.rs +++ b/program_methods/guest/src/bin/token.rs @@ -12,6 +12,8 @@ use token_program::core::Instruction; fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction, }, @@ -81,5 +83,12 @@ fn main() { } }; - ProgramOutput::new(instruction_words, pre_states_clone, post_states).write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states_clone, + post_states, + ) + .write(); } diff --git a/programs/amm/core/src/lib.rs b/programs/amm/core/src/lib.rs index 85efd00d..6e005c9e 100644 --- a/programs/amm/core/src/lib.rs +++ b/programs/amm/core/src/lib.rs @@ -68,11 +68,27 @@ pub enum Instruction { /// - User Holding Account for Token A /// - User Holding Account for Token B Either User Holding Account for Token A or Token B is /// authorized. - Swap { + SwapExactInput { swap_amount_in: u128, min_amount_out: u128, token_definition_id_in: AccountId, }, + + /// Swap tokens specifying the exact desired output amount, + /// while maintaining the Pool constant product. + /// + /// Required accounts: + /// - AMM Pool (initialized) + /// - Vault Holding Account for Token A (initialized) + /// - Vault Holding Account for Token B (initialized) + /// - User Holding Account for Token A + /// - User Holding Account for Token B Either User Holding Account for Token A or Token B is + /// authorized. + SwapExactOutput { + exact_amount_out: u128, + max_amount_in: u128, + token_definition_id_in: AccountId, + }, } #[derive(Clone, Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] @@ -119,10 +135,10 @@ pub fn compute_pool_pda( definition_token_a_id: AccountId, definition_token_b_id: AccountId, ) -> AccountId { - AccountId::from(( + AccountId::for_public_pda( &amm_program_id, &compute_pool_pda_seed(definition_token_a_id, definition_token_b_id), - )) + ) } #[must_use] @@ -159,10 +175,10 @@ pub fn compute_vault_pda( pool_id: AccountId, definition_token_id: AccountId, ) -> AccountId { - AccountId::from(( + AccountId::for_public_pda( &amm_program_id, &compute_vault_pda_seed(pool_id, definition_token_id), - )) + ) } #[must_use] @@ -183,7 +199,7 @@ pub fn compute_vault_pda_seed(pool_id: AccountId, definition_token_id: AccountId #[must_use] pub fn compute_liquidity_token_pda(amm_program_id: ProgramId, pool_id: AccountId) -> AccountId { - AccountId::from((&amm_program_id, &compute_liquidity_token_pda_seed(pool_id))) + AccountId::for_public_pda(&amm_program_id, &compute_liquidity_token_pda_seed(pool_id)) } #[must_use] diff --git a/programs/amm/src/swap.rs b/programs/amm/src/swap.rs index cb64f5eb..22f3792a 100644 --- a/programs/amm/src/swap.rs +++ b/programs/amm/src/swap.rs @@ -4,21 +4,14 @@ use nssa_core::{ program::{AccountPostState, ChainedCall}, }; -#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] -#[must_use] -pub fn swap( - pool: AccountWithMetadata, - vault_a: AccountWithMetadata, - vault_b: AccountWithMetadata, - user_holding_a: AccountWithMetadata, - user_holding_b: AccountWithMetadata, - swap_amount_in: u128, - min_amount_out: u128, - token_in_id: AccountId, -) -> (Vec, Vec) { - // Verify vaults are in fact vaults +/// Validates swap setup: checks pool is active, vaults match, and reserves are sufficient. +fn validate_swap_setup( + pool: &AccountWithMetadata, + vault_a: &AccountWithMetadata, + vault_b: &AccountWithMetadata, +) -> PoolDefinition { let pool_def_data = PoolDefinition::try_from(&pool.account.data) - .expect("Swap: AMM Program expects a valid Pool Definition Account"); + .expect("AMM Program expects a valid Pool Definition Account"); assert!(pool_def_data.active, "Pool is inactive"); assert_eq!( @@ -30,16 +23,14 @@ pub fn swap( "Vault B was not provided" ); - // fetch pool reserves - // validates reserves is at least the vaults' balances let vault_a_token_holding = token_core::TokenHolding::try_from(&vault_a.account.data) - .expect("Swap: AMM Program expects a valid Token Holding Account for Vault A"); + .expect("AMM Program expects a valid Token Holding Account for Vault A"); let token_core::TokenHolding::Fungible { definition_id: _, balance: vault_a_balance, } = vault_a_token_holding else { - panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault A"); + panic!("AMM Program expects a valid Fungible Token Holding Account for Vault A"); }; assert!( @@ -48,13 +39,13 @@ pub fn swap( ); let vault_b_token_holding = token_core::TokenHolding::try_from(&vault_b.account.data) - .expect("Swap: AMM Program expects a valid Token Holding Account for Vault B"); + .expect("AMM Program expects a valid Token Holding Account for Vault B"); let token_core::TokenHolding::Fungible { definition_id: _, balance: vault_b_balance, } = vault_b_token_holding else { - panic!("Swap: AMM Program expects a valid Fungible Token Holding Account for Vault B"); + panic!("AMM Program expects a valid Fungible Token Holding Account for Vault B"); }; assert!( @@ -62,6 +53,59 @@ pub fn swap( "Reserve for Token B exceeds vault balance" ); + pool_def_data +} + +/// Creates post-state and returns reserves after swap. +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[expect( + clippy::needless_pass_by_value, + reason = "consistent with codebase style" +)] +fn create_swap_post_states( + pool: AccountWithMetadata, + pool_def_data: PoolDefinition, + vault_a: AccountWithMetadata, + vault_b: AccountWithMetadata, + user_holding_a: AccountWithMetadata, + user_holding_b: AccountWithMetadata, + deposit_a: u128, + withdraw_a: u128, + deposit_b: u128, + withdraw_b: u128, +) -> Vec { + let mut pool_post = pool.account; + let pool_post_definition = PoolDefinition { + reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a, + reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b, + ..pool_def_data + }; + + pool_post.data = Data::from(&pool_post_definition); + + vec![ + AccountPostState::new(pool_post), + AccountPostState::new(vault_a.account), + AccountPostState::new(vault_b.account), + AccountPostState::new(user_holding_a.account), + AccountPostState::new(user_holding_b.account), + ] +} + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[must_use] +pub fn swap_exact_input( + pool: AccountWithMetadata, + vault_a: AccountWithMetadata, + vault_b: AccountWithMetadata, + user_holding_a: AccountWithMetadata, + user_holding_b: AccountWithMetadata, + swap_amount_in: u128, + min_amount_out: u128, + token_in_id: AccountId, +) -> (Vec, Vec) { + let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b); + let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) = if token_in_id == pool_def_data.definition_token_a_id { let (chained_calls, deposit_a, withdraw_b) = swap_logic( @@ -95,23 +139,18 @@ pub fn swap( panic!("AccountId is not a token type for the pool"); }; - // Update pool account - let mut pool_post = pool.account; - let pool_post_definition = PoolDefinition { - reserve_a: pool_def_data.reserve_a + deposit_a - withdraw_a, - reserve_b: pool_def_data.reserve_b + deposit_b - withdraw_b, - ..pool_def_data - }; - - pool_post.data = Data::from(&pool_post_definition); - - let post_states = vec![ - AccountPostState::new(pool_post), - AccountPostState::new(vault_a.account), - AccountPostState::new(vault_b.account), - AccountPostState::new(user_holding_a.account), - AccountPostState::new(user_holding_b.account), - ]; + let post_states = create_swap_post_states( + pool, + pool_def_data, + vault_a, + vault_b, + user_holding_a, + user_holding_b, + deposit_a, + withdraw_a, + deposit_b, + withdraw_b, + ); (post_states, chained_calls) } @@ -131,7 +170,9 @@ fn swap_logic( // Compute withdraw amount // Maintains pool constant product // k = pool_def_data.reserve_a * pool_def_data.reserve_b; - let withdraw_amount = (reserve_withdraw_vault_amount * swap_amount_in) + let withdraw_amount = reserve_withdraw_vault_amount + .checked_mul(swap_amount_in) + .expect("reserve * amount_in overflows u128") / (reserve_deposit_vault_amount + swap_amount_in); // Slippage check @@ -175,3 +216,135 @@ fn swap_logic( (chained_calls, swap_amount_in, withdraw_amount) } + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +#[must_use] +pub fn swap_exact_output( + pool: AccountWithMetadata, + vault_a: AccountWithMetadata, + vault_b: AccountWithMetadata, + user_holding_a: AccountWithMetadata, + user_holding_b: AccountWithMetadata, + exact_amount_out: u128, + max_amount_in: u128, + token_in_id: AccountId, +) -> (Vec, Vec) { + let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b); + + let (chained_calls, [deposit_a, withdraw_a], [deposit_b, withdraw_b]) = + if token_in_id == pool_def_data.definition_token_a_id { + let (chained_calls, deposit_a, withdraw_b) = exact_output_swap_logic( + user_holding_a.clone(), + vault_a.clone(), + vault_b.clone(), + user_holding_b.clone(), + exact_amount_out, + max_amount_in, + pool_def_data.reserve_a, + pool_def_data.reserve_b, + pool.account_id, + ); + + (chained_calls, [deposit_a, 0], [0, withdraw_b]) + } else if token_in_id == pool_def_data.definition_token_b_id { + let (chained_calls, deposit_b, withdraw_a) = exact_output_swap_logic( + user_holding_b.clone(), + vault_b.clone(), + vault_a.clone(), + user_holding_a.clone(), + exact_amount_out, + max_amount_in, + pool_def_data.reserve_b, + pool_def_data.reserve_a, + pool.account_id, + ); + + (chained_calls, [0, withdraw_a], [deposit_b, 0]) + } else { + panic!("AccountId is not a token type for the pool"); + }; + + let post_states = create_swap_post_states( + pool, + pool_def_data, + vault_a, + vault_b, + user_holding_a, + user_holding_b, + deposit_a, + withdraw_a, + deposit_b, + withdraw_b, + ); + + (post_states, chained_calls) +} + +#[expect(clippy::too_many_arguments, reason = "TODO: Fix later")] +fn exact_output_swap_logic( + user_deposit: AccountWithMetadata, + vault_deposit: AccountWithMetadata, + vault_withdraw: AccountWithMetadata, + user_withdraw: AccountWithMetadata, + exact_amount_out: u128, + max_amount_in: u128, + reserve_deposit_vault_amount: u128, + reserve_withdraw_vault_amount: u128, + pool_id: AccountId, +) -> (Vec, u128, u128) { + // Guard: exact_amount_out must be nonzero + assert_ne!(exact_amount_out, 0, "Exact amount out must be nonzero"); + + // Guard: exact_amount_out must be less than reserve_withdraw_vault_amount + assert!( + exact_amount_out < reserve_withdraw_vault_amount, + "Exact amount out exceeds reserve" + ); + + // Compute deposit amount using ceiling division + // Formula: amount_in = ceil(reserve_in * exact_amount_out / (reserve_out - exact_amount_out)) + let deposit_amount = reserve_deposit_vault_amount + .checked_mul(exact_amount_out) + .expect("reserve * amount_out overflows u128") + .div_ceil(reserve_withdraw_vault_amount - exact_amount_out); + + // Slippage check + assert!( + deposit_amount <= max_amount_in, + "Required input exceeds maximum amount in" + ); + + let token_program_id = user_deposit.account.program_owner; + + let mut chained_calls = Vec::new(); + chained_calls.push(ChainedCall::new( + token_program_id, + vec![user_deposit, vault_deposit], + &token_core::Instruction::Transfer { + amount_to_transfer: deposit_amount, + }, + )); + + let mut vault_withdraw = vault_withdraw; + vault_withdraw.is_authorized = true; + + let pda_seed = compute_vault_pda_seed( + pool_id, + token_core::TokenHolding::try_from(&vault_withdraw.account.data) + .expect("Exact Output Swap Logic: AMM Program expects valid token data") + .definition_id(), + ); + + chained_calls.push( + ChainedCall::new( + token_program_id, + vec![vault_withdraw, user_withdraw], + &token_core::Instruction::Transfer { + amount_to_transfer: exact_amount_out, + }, + ) + .with_pda_seeds(vec![pda_seed]), + ); + + (chained_calls, deposit_amount, exact_amount_out) +} diff --git a/programs/amm/src/tests.rs b/programs/amm/src/tests.rs index 14638f9d..a10a985d 100644 --- a/programs/amm/src/tests.rs +++ b/programs/amm/src/tests.rs @@ -14,7 +14,10 @@ use nssa_core::{ use token_core::{TokenDefinition, TokenHolding}; use crate::{ - add::add_liquidity, new_definition::new_definition, remove::remove_liquidity, swap::swap, + add::add_liquidity, + new_definition::new_definition, + remove::remove_liquidity, + swap::{swap_exact_input, swap_exact_output}, }; const TOKEN_PROGRAM_ID: ProgramId = [15; 8]; @@ -153,6 +156,10 @@ impl BalanceForTests { 200 } + fn max_amount_in() -> u128 { + 166 + } + fn vault_a_add_successful() -> u128 { 1_400 } @@ -243,6 +250,74 @@ impl ChainedCallForTests { ) } + fn cc_swap_exact_output_token_a_test_1() -> ChainedCall { + let swap_amount: u128 = 498; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::vault_a_init(), + ], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + } + + fn cc_swap_exact_output_token_b_test_1() -> ChainedCall { + let swap_amount: u128 = 166; + + let mut vault_b_auth = AccountWithMetadataForTests::vault_b_init(); + vault_b_auth.is_authorized = true; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_b_auth, AccountWithMetadataForTests::user_holding_b()], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_b_definition_id(), + )]) + } + + fn cc_swap_exact_output_token_a_test_2() -> ChainedCall { + let swap_amount: u128 = 285; + + let mut vault_a_auth = AccountWithMetadataForTests::vault_a_init(); + vault_a_auth.is_authorized = true; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![vault_a_auth, AccountWithMetadataForTests::user_holding_a()], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + .with_pda_seeds(vec![compute_vault_pda_seed( + IdForTests::pool_definition_id(), + IdForTests::token_a_definition_id(), + )]) + } + + fn cc_swap_exact_output_token_b_test_2() -> ChainedCall { + let swap_amount: u128 = 200; + + ChainedCall::new( + TOKEN_PROGRAM_ID, + vec![ + AccountWithMetadataForTests::user_holding_b(), + AccountWithMetadataForTests::vault_b_init(), + ], + &token_core::Instruction::Transfer { + amount_to_transfer: swap_amount, + }, + ) + } + fn cc_add_token_a() -> ChainedCall { ChainedCall::new( TOKEN_PROGRAM_ID, @@ -829,6 +904,54 @@ impl AccountWithMetadataForTests { } } + fn pool_definition_swap_exact_output_test_1() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0_u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::lp_supply_init(), + reserve_a: 1498_u128, + reserve_b: 334_u128, + fees: 0_u128, + active: true, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + + fn pool_definition_swap_exact_output_test_2() -> AccountWithMetadata { + AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0_u128, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: BalanceForTests::lp_supply_init(), + reserve_a: BalanceForTests::vault_a_swap_test_2(), + reserve_b: BalanceForTests::vault_b_swap_test_2(), + fees: 0_u128, + active: true, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + } + } + fn pool_definition_add_zero_lp() -> AccountWithMetadata { AccountWithMetadata { account: Account { @@ -2400,7 +2523,7 @@ fn call_new_definition_chained_call_successful() { #[should_panic(expected = "AccountId is not a token type for the pool")] #[test] fn call_swap_incorrect_token_type() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2415,7 +2538,7 @@ fn call_swap_incorrect_token_type() { #[should_panic(expected = "Vault A was not provided")] #[test] fn call_swap_vault_a_omitted() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_with_wrong_id(), AccountWithMetadataForTests::vault_b_init(), @@ -2430,7 +2553,7 @@ fn call_swap_vault_a_omitted() { #[should_panic(expected = "Vault B was not provided")] #[test] fn call_swap_vault_b_omitted() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_with_wrong_id(), @@ -2445,7 +2568,7 @@ fn call_swap_vault_b_omitted() { #[should_panic(expected = "Reserve for Token A exceeds vault balance")] #[test] fn call_swap_reserves_vault_mismatch_1() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init_low(), AccountWithMetadataForTests::vault_b_init(), @@ -2460,7 +2583,7 @@ fn call_swap_reserves_vault_mismatch_1() { #[should_panic(expected = "Reserve for Token B exceeds vault balance")] #[test] fn call_swap_reserves_vault_mismatch_2() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init_low(), @@ -2475,7 +2598,7 @@ fn call_swap_reserves_vault_mismatch_2() { #[should_panic(expected = "Pool is inactive")] #[test] fn call_swap_ianctive() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_inactive(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2490,7 +2613,7 @@ fn call_swap_ianctive() { #[should_panic(expected = "Withdraw amount is less than minimal amount out")] #[test] fn call_swap_below_min_out() { - let _post_states = swap( + let _post_states = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2504,7 +2627,7 @@ fn call_swap_below_min_out() { #[test] fn call_swap_chained_call_successful_1() { - let (post_states, chained_calls) = swap( + let (post_states, chained_calls) = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2536,7 +2659,7 @@ fn call_swap_chained_call_successful_1() { #[test] fn call_swap_chained_call_successful_2() { - let (post_states, chained_calls) = swap( + let (post_states, chained_calls) = swap_exact_input( AccountWithMetadataForTests::pool_definition_init(), AccountWithMetadataForTests::vault_a_init(), AccountWithMetadataForTests::vault_b_init(), @@ -2566,6 +2689,281 @@ fn call_swap_chained_call_successful_2() { ); } +#[should_panic(expected = "AccountId is not a token type for the pool")] +#[test] +fn call_swap_exact_output_incorrect_token_type() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_lp_definition_id(), + ); +} + +#[should_panic(expected = "Vault A was not provided")] +#[test] +fn call_swap_exact_output_vault_a_omitted() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_with_wrong_id(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Vault B was not provided")] +#[test] +fn call_swap_exact_output_vault_b_omitted() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_with_wrong_id(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Reserve for Token A exceeds vault balance")] +#[test] +fn call_swap_exact_output_reserves_vault_mismatch_1() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init_low(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Reserve for Token B exceeds vault balance")] +#[test] +fn call_swap_exact_output_reserves_vault_mismatch_2() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init_low(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Pool is inactive")] +#[test] +fn call_swap_exact_output_inactive() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_inactive(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::add_max_amount_a(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Required input exceeds maximum amount in")] +#[test] +fn call_swap_exact_output_exceeds_max_in() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + 166_u128, + 100_u128, + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Exact amount out must be nonzero")] +#[test] +fn call_swap_exact_output_zero() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + 0_u128, + 500_u128, + IdForTests::token_a_definition_id(), + ); +} + +#[should_panic(expected = "Exact amount out exceeds reserve")] +#[test] +fn call_swap_exact_output_exceeds_reserve() { + let _post_states = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::vault_b_reserve_init(), + BalanceForTests::max_amount_in(), + IdForTests::token_a_definition_id(), + ); +} + +#[test] +fn call_swap_exact_output_chained_call_successful() { + let (post_states, chained_calls) = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + BalanceForTests::max_amount_in(), + BalanceForTests::vault_b_reserve_init(), + IdForTests::token_a_definition_id(), + ); + + let pool_post = post_states[0].clone(); + + assert!( + AccountWithMetadataForTests::pool_definition_swap_exact_output_test_1().account + == *pool_post.account() + ); + + let chained_call_a = chained_calls[0].clone(); + let chained_call_b = chained_calls[1].clone(); + + assert_eq!( + chained_call_a, + ChainedCallForTests::cc_swap_exact_output_token_a_test_1() + ); + assert_eq!( + chained_call_b, + ChainedCallForTests::cc_swap_exact_output_token_b_test_1() + ); +} + +#[test] +fn call_swap_exact_output_chained_call_successful_2() { + let (post_states, chained_calls) = swap_exact_output( + AccountWithMetadataForTests::pool_definition_init(), + AccountWithMetadataForTests::vault_a_init(), + AccountWithMetadataForTests::vault_b_init(), + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + 285, + 300, + IdForTests::token_b_definition_id(), + ); + + let pool_post = post_states[0].clone(); + + assert!( + AccountWithMetadataForTests::pool_definition_swap_exact_output_test_2().account + == *pool_post.account() + ); + + let chained_call_a = chained_calls[1].clone(); + let chained_call_b = chained_calls[0].clone(); + + assert_eq!( + chained_call_a, + ChainedCallForTests::cc_swap_exact_output_token_a_test_2() + ); + assert_eq!( + chained_call_b, + ChainedCallForTests::cc_swap_exact_output_token_b_test_2() + ); +} + +// Without the fix, `reserve_a * exact_amount_out` silently wraps to 0 in release mode, +// making `deposit_amount = 0`. The slippage check `0 <= max_amount_in` always passes, +// so an attacker receives `exact_amount_out` tokens while paying nothing. +#[should_panic(expected = "reserve * amount_out overflows u128")] +#[test] +fn swap_exact_output_overflow_protection() { + // reserve_a chosen so that reserve_a * 2 overflows u128: + // (u128::MAX / 2 + 1) * 2 = u128::MAX + 1 → wraps to 0 + let large_reserve: u128 = u128::MAX / 2 + 1; + let reserve_b: u128 = 1_000; + + let pool = AccountWithMetadata { + account: Account { + program_owner: ProgramId::default(), + balance: 0, + data: Data::from(&PoolDefinition { + definition_token_a_id: IdForTests::token_a_definition_id(), + definition_token_b_id: IdForTests::token_b_definition_id(), + vault_a_id: IdForTests::vault_a_id(), + vault_b_id: IdForTests::vault_b_id(), + liquidity_pool_id: IdForTests::token_lp_definition_id(), + liquidity_pool_supply: 1, + reserve_a: large_reserve, + reserve_b, + fees: 0, + active: true, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: IdForTests::pool_definition_id(), + }; + + let vault_a = AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_a_definition_id(), + balance: large_reserve, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: IdForTests::vault_a_id(), + }; + + let vault_b = AccountWithMetadata { + account: Account { + program_owner: TOKEN_PROGRAM_ID, + balance: 0, + data: Data::from(&TokenHolding::Fungible { + definition_id: IdForTests::token_b_definition_id(), + balance: reserve_b, + }), + nonce: 0_u128.into(), + }, + is_authorized: true, + account_id: IdForTests::vault_b_id(), + }; + + let _result = swap_exact_output( + pool, + vault_a, + vault_b, + AccountWithMetadataForTests::user_holding_a(), + AccountWithMetadataForTests::user_holding_b(), + 2, // exact_amount_out: small, valid (< reserve_b) + 1, // max_amount_in: tiny — real deposit would be enormous, but + // overflow wraps it to 0, making 0 <= 1 pass silently + IdForTests::token_a_definition_id(), + ); +} + #[test] fn new_definition_lp_asymmetric_amounts() { let (post_states, chained_calls) = new_definition( @@ -2638,7 +3036,7 @@ fn new_definition_lp_symmetric_amounts() { fn state_for_amm_tests() -> V03State { let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); state.force_insert_account( IdForExeTests::pool_definition_id(), AccountsForExeTests::pool_definition_init(), @@ -2681,7 +3079,7 @@ fn state_for_amm_tests() -> V03State { fn state_for_amm_tests_with_new_def() -> V03State { let initial_data = []; - let mut state = V03State::new_with_genesis_accounts(&initial_data, &[]); + let mut state = V03State::new_with_genesis_accounts(&initial_data, vec![], 0); state.force_insert_account( IdForExeTests::token_a_definition_id(), AccountsForExeTests::token_a_definition_account(), @@ -3064,7 +3462,7 @@ fn simple_amm_add() { fn simple_amm_swap_1() { let mut state = state_for_amm_tests(); - let instruction = amm_core::Instruction::Swap { + let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in: BalanceForExeTests::swap_amount_in(), min_amount_out: BalanceForExeTests::swap_min_amount_out(), token_definition_id_in: IdForExeTests::token_b_definition_id(), @@ -3115,7 +3513,7 @@ fn simple_amm_swap_1() { fn simple_amm_swap_2() { let mut state = state_for_amm_tests(); - let instruction = amm_core::Instruction::Swap { + let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in: BalanceForExeTests::swap_amount_in(), min_amount_out: BalanceForExeTests::swap_min_amount_out(), token_definition_id_in: IdForExeTests::token_a_definition_id(), diff --git a/programs/associated_token_account/core/src/lib.rs b/programs/associated_token_account/core/src/lib.rs index 994c632b..8fe6e267 100644 --- a/programs/associated_token_account/core/src/lib.rs +++ b/programs/associated_token_account/core/src/lib.rs @@ -61,7 +61,7 @@ pub fn compute_ata_seed(owner_id: AccountId, definition_id: AccountId) -> PdaSee } pub fn get_associated_token_account_id(ata_program_id: &ProgramId, seed: &PdaSeed) -> AccountId { - AccountId::from((ata_program_id, seed)) + AccountId::for_public_pda(ata_program_id, seed) } /// Verify the ATA's address matches `(ata_program_id, owner, definition)` and return diff --git a/programs/clock/core/Cargo.toml b/programs/clock/core/Cargo.toml new file mode 100644 index 00000000..53a43b6d --- /dev/null +++ b/programs/clock/core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "clock_core" +version = "0.1.0" +edition = "2024" +license = { workspace = true } + +[lints] +workspace = true + +[dependencies] +nssa_core.workspace = true +borsh.workspace = true diff --git a/programs/clock/core/src/lib.rs b/programs/clock/core/src/lib.rs new file mode 100644 index 00000000..5fc03633 --- /dev/null +++ b/programs/clock/core/src/lib.rs @@ -0,0 +1,42 @@ +//! Core data structures and constants for the Clock Program. + +use borsh::{BorshDeserialize, BorshSerialize}; +use nssa_core::{Timestamp, account::AccountId}; + +pub const CLOCK_01_PROGRAM_ACCOUNT_ID: AccountId = + AccountId::new(*b"/LEZ/ClockProgramAccount/0000001"); + +pub const CLOCK_10_PROGRAM_ACCOUNT_ID: AccountId = + AccountId::new(*b"/LEZ/ClockProgramAccount/0000010"); + +pub const CLOCK_50_PROGRAM_ACCOUNT_ID: AccountId = + AccountId::new(*b"/LEZ/ClockProgramAccount/0000050"); + +/// All clock program account ID in the order expected by the clock program. +pub const CLOCK_PROGRAM_ACCOUNT_IDS: [AccountId; 3] = [ + CLOCK_01_PROGRAM_ACCOUNT_ID, + CLOCK_10_PROGRAM_ACCOUNT_ID, + CLOCK_50_PROGRAM_ACCOUNT_ID, +]; + +/// The instruction type for the Clock Program. The sequencer passes the current block timestamp. +pub type Instruction = Timestamp; + +/// The data stored in a clock account. +#[derive(Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct ClockAccountData { + pub block_id: u64, + pub timestamp: Timestamp, +} + +impl ClockAccountData { + #[must_use] + pub fn to_bytes(self) -> Vec { + borsh::to_vec(&self).expect("ClockAccountData serialization should not fail") + } + + #[must_use] + pub fn from_bytes(bytes: &[u8]) -> Self { + borsh::from_slice(bytes).expect("ClockAccountData deserialization should not fail") + } +} diff --git a/sequencer/core/Cargo.toml b/sequencer/core/Cargo.toml index e1ff0895..efd0e359 100644 --- a/sequencer/core/Cargo.toml +++ b/sequencer/core/Cargo.toml @@ -40,3 +40,5 @@ mock = [] [dev-dependencies] futures.workspace = true +test_program_methods.workspace = true +nssa = { workspace = true, features = ["test-utils"] } diff --git a/sequencer/core/src/block_settlement_client.rs b/sequencer/core/src/block_settlement_client.rs index 2f036b98..6b32f8de 100644 --- a/sequencer/core/src/block_settlement_client.rs +++ b/sequencer/core/src/block_settlement_client.rs @@ -3,7 +3,7 @@ use bedrock_client::BedrockClient; pub use common::block::Block; pub use logos_blockchain_core::mantle::{MantleTx, SignedMantleTx, ops::channel::MsgId}; use logos_blockchain_core::mantle::{ - Op, OpProof, Transaction as _, TxHash, ledger, + Op, OpProof, Transaction as _, ops::channel::{ChannelId, inscribe::InscriptionOp}, }; pub use logos_blockchain_key_management_system_service::keys::Ed25519Key; @@ -45,14 +45,11 @@ pub trait BlockSettlementClientTrait: Clone { }; let inscribe_op_id = inscribe_op.id(); - let ledger_tx = ledger::Tx::new(vec![], vec![]); - let inscribe_tx = MantleTx { ops: vec![Op::ChannelInscribe(inscribe_op)], - ledger_tx, // Altruistic test config - storage_gas_price: 0, - execution_gas_price: 0, + storage_gas_price: 0.into(), + execution_gas_price: 0.into(), }; let tx_hash = inscribe_tx.hash(); @@ -67,7 +64,6 @@ pub trait BlockSettlementClientTrait: Clone { let signed_mantle_tx = SignedMantleTx { ops_proofs: vec![OpProof::Ed25519Sig(signature)], - ledger_tx_proof: empty_ledger_signature(&tx_hash), mantle_tx: inscribe_tx, }; Ok((signed_mantle_tx, inscribe_op_id)) @@ -118,10 +114,3 @@ impl BlockSettlementClientTrait for BlockSettlementClient { &self.signing_key } } - -fn empty_ledger_signature( - tx_hash: &TxHash, -) -> logos_blockchain_key_management_system_service::keys::ZkSignature { - logos_blockchain_key_management_system_service::keys::ZkKey::multi_sign(&[], tx_hash.as_ref()) - .expect("multi-sign with empty key set works") -} diff --git a/sequencer/core/src/block_store.rs b/sequencer/core/src/block_store.rs index 9c4c875a..7e47005d 100644 --- a/sequencer/core/src/block_store.rs +++ b/sequencer/core/src/block_store.rs @@ -150,7 +150,7 @@ mod tests { let retrieved_tx = node_store.get_transaction_by_hash(tx.hash()); assert_eq!(None, retrieved_tx); // Add the block with the transaction - let dummy_state = V03State::new_with_genesis_accounts(&[], &[]); + let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0); node_store.update(&block, [1; 32], &dummy_state).unwrap(); // Try again let retrieved_tx = node_store.get_transaction_by_hash(tx.hash()); @@ -209,7 +209,7 @@ mod tests { let block_hash = block.header.hash; let block_msg_id = [1; 32]; - let dummy_state = V03State::new_with_genesis_accounts(&[], &[]); + let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0); node_store .update(&block, block_msg_id, &dummy_state) .unwrap(); @@ -244,7 +244,7 @@ mod tests { let block = common::test_utils::produce_dummy_block(1, None, vec![tx]); let block_id = block.header.block_id; - let dummy_state = V03State::new_with_genesis_accounts(&[], &[]); + let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0); node_store.update(&block, [1; 32], &dummy_state).unwrap(); // Verify initial status is Pending diff --git a/sequencer/core/src/config.rs b/sequencer/core/src/config.rs index 2fb101aa..fa4a2fa7 100644 --- a/sequencer/core/src/config.rs +++ b/sequencer/core/src/config.rs @@ -24,9 +24,10 @@ pub struct SequencerConfig { pub genesis_id: u64, /// If `True`, then adds random sequence of bytes to genesis block. pub is_genesis_random: bool, - /// Maximum number of transactions in block. + /// Maximum number of user transactions in a block (excludes the mandatory clock transaction). pub max_num_tx_in_block: usize, - /// Maximum block size (includes header and transactions). + /// Maximum block size (includes header, user transactions, and the mandatory clock + /// transaction). #[serde(default = "default_max_block_size")] pub max_block_size: ByteSize, /// Mempool maximum size. diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 16667051..47037fbd 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -7,7 +7,7 @@ use common::PINATA_BASE58; use common::{ HashType, block::{BedrockStatus, Block, HashableBlockData}, - transaction::NSSATransaction, + transaction::{NSSATransaction, clock_invocation}, }; use config::SequencerConfig; use log::{error, info, warn}; @@ -16,7 +16,6 @@ use mempool::{MemPool, MemPoolHandle}; #[cfg(feature = "mock")] pub use mock::SequencerCoreWithMockClients; use nssa::V03State; -use nssa_core::{BlockId, Timestamp}; pub use storage::error::DbError; use testnet_initial_state::initial_state; @@ -104,24 +103,26 @@ impl SequencerCore> = config - .initial_private_accounts - .clone() - .map(|initial_commitments| { - initial_commitments - .iter() - .map(|init_comm_data| { - let npk = &init_comm_data.npk; + let initial_private_accounts: Option< + Vec<(nssa_core::Commitment, nssa_core::Nullifier)>, + > = config.initial_private_accounts.clone().map(|accounts| { + accounts + .iter() + .map(|init_comm_data| { + let npk = &init_comm_data.npk; - let mut acc = init_comm_data.account.clone(); + let mut acc = init_comm_data.account.clone(); - acc.program_owner = - nssa::program::Program::authenticated_transfer_program().id(); + acc.program_owner = + nssa::program::Program::authenticated_transfer_program().id(); - nssa_core::Commitment::new(npk, &acc) - }) - .collect() - }); + ( + nssa_core::Commitment::new(npk, &acc), + nssa_core::Nullifier::for_account_initialization(npk), + ) + }) + .collect() + }); let init_accs: Option> = config .initial_public_accounts @@ -135,10 +136,11 @@ impl SequencerCore SequencerCore Result { - match &tx { - NSSATransaction::Public(tx) => self - .state - .transition_from_public_transaction(tx, block_id, timestamp), - NSSATransaction::PrivacyPreserving(tx) => self - .state - .transition_from_privacy_preserving_transaction(tx, block_id, timestamp), - NSSATransaction::ProgramDeployment(tx) => self - .state - .transition_from_program_deployment_transaction(tx), - } - .inspect_err(|err| warn!("Error at transition {err:#?}"))?; - - Ok(tx) - } - pub async fn produce_new_block(&mut self) -> Result { let (tx, _msg_id) = self .produce_new_block_with_mempool_transactions() @@ -224,12 +204,20 @@ impl SequencerCore SequencerCore { - valid_transactions.push(valid_tx); - - info!("Validated transaction with hash {tx_hash}, including it in block"); - - if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block { - break; - } - } + let validated_diff = match tx.validate_on_state( + &self.state, + new_block_height, + new_block_timestamp, + ) { + Ok(diff) => diff, Err(err) => { error!( "Transaction with hash {tx_hash} failed execution check with error: {err:#?}, skipping it", ); - // TODO: Probably need to handle unsuccessful transaction execution? + continue; } + }; + + self.state.apply_state_diff(validated_diff); + + valid_transactions.push(tx); + info!("Validated transaction with hash {tx_hash}, including it in block"); + if valid_transactions.len() >= self.sequencer_config.max_num_tx_in_block { + break; } } + // Append the Clock Program invocation as the mandatory last transaction. + self.state + .transition_from_public_transaction(&clock_tx, new_block_height, new_block_timestamp) + .context("Clock transaction failed. Aborting block production.")?; + valid_transactions.push(clock_nssa_tx); + let hashable_data = HashableBlockData { block_id: new_block_height, transactions: valid_transactions, @@ -395,7 +392,10 @@ mod tests { use std::{pin::pin, time::Duration}; use bedrock_client::BackoffConfig; - use common::{test_utils::sequencer_sign_key_for_testing, transaction::NSSATransaction}; + use common::{ + test_utils::sequencer_sign_key_for_testing, + transaction::{NSSATransaction, clock_invocation}, + }; use logos_blockchain_core::mantle::ops::channel::ChannelId; use mempool::MemPoolHandle; use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys}; @@ -524,7 +524,7 @@ mod tests { let tx = tx.transaction_stateless_check().unwrap(); // Signature is not from sender. Execution fails - let result = sequencer.execute_check_transaction_on_state(tx, 0, 0); + let result = tx.execute_check_on_state(&mut sequencer.state, 0, 0); assert!(matches!( result, @@ -550,7 +550,9 @@ mod tests { // Passed pre-check assert!(result.is_ok()); - let result = sequencer.execute_check_transaction_on_state(result.unwrap(), 0, 0); + let result = result + .unwrap() + .execute_check_on_state(&mut sequencer.state, 0, 0); let is_failed_at_balance_mismatch = matches!( result.err().unwrap(), nssa::error::NssaError::ProgramExecutionFailed(_) @@ -572,8 +574,7 @@ mod tests { acc1, 0, acc2, 100, &sign_key1, ); - sequencer - .execute_check_transaction_on_state(tx, 0, 0) + tx.execute_check_on_state(&mut sequencer.state, 0, 0) .unwrap(); let bal_from = sequencer.state.get_account_by_id(acc1).balance; @@ -652,8 +653,14 @@ mod tests { .unwrap() .unwrap(); - // Only one should be included in the block - assert_eq!(block.body.transactions, vec![tx.clone()]); + // Only one user tx should be included; the clock tx is always appended last. + assert_eq!( + block.body.transactions, + vec![ + tx.clone(), + NSSATransaction::Public(clock_invocation(block.header.timestamp)) + ] + ); } #[tokio::test] @@ -679,7 +686,13 @@ mod tests { .get_block_at_id(sequencer.chain_height) .unwrap() .unwrap(); - assert_eq!(block.body.transactions, vec![tx.clone()]); + assert_eq!( + block.body.transactions, + vec![ + tx.clone(), + NSSATransaction::Public(clock_invocation(block.header.timestamp)) + ] + ); // Add same transaction should fail mempool_handle.push(tx.clone()).await.unwrap(); @@ -691,7 +704,13 @@ mod tests { .get_block_at_id(sequencer.chain_height) .unwrap() .unwrap(); - assert!(block.body.transactions.is_empty()); + // The replay is rejected, so only the clock tx is in the block. + assert_eq!( + block.body.transactions, + vec![NSSATransaction::Public(clock_invocation( + block.header.timestamp + ))] + ); } #[tokio::test] @@ -726,7 +745,13 @@ mod tests { .get_block_at_id(sequencer.chain_height) .unwrap() .unwrap(); - assert_eq!(block.body.transactions, vec![tx.clone()]); + assert_eq!( + block.body.transactions, + vec![ + tx.clone(), + NSSATransaction::Public(clock_invocation(block.header.timestamp)) + ] + ); } // Instantiating a new sequencer from the same config. This should load the existing block @@ -856,8 +881,54 @@ mod tests { ); assert_eq!( new_block.body.transactions, - vec![tx], - "New block should contain the submitted transaction" + vec![ + tx, + NSSATransaction::Public(clock_invocation(new_block.header.timestamp)) + ], + "New block should contain the submitted transaction and the clock invocation" + ); + } + + #[tokio::test] + async fn transactions_touching_clock_account_are_dropped_from_block() { + let (mut sequencer, mempool_handle) = common_setup().await; + + // Canonical clock invocation and a crafted variant with a different timestamp — both must + // be dropped because their diffs touch the clock accounts. + let crafted_clock_tx = { + let message = nssa::public_transaction::Message::try_new( + nssa::program::Program::clock().id(), + nssa::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(), + vec![], + 42_u64, + ) + .unwrap(); + NSSATransaction::Public(nssa::PublicTransaction::new( + message, + nssa::public_transaction::WitnessSet::from_raw_parts(vec![]), + )) + }; + mempool_handle + .push(NSSATransaction::Public(clock_invocation(0))) + .await + .unwrap(); + mempool_handle.push(crafted_clock_tx).await.unwrap(); + sequencer + .produce_new_block_with_mempool_transactions() + .unwrap(); + + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + + // Both transactions were dropped. Only the system-appended clock tx remains. + assert_eq!( + block.body.transactions, + vec![NSSATransaction::Public(clock_invocation( + block.header.timestamp + ))] ); } @@ -909,4 +980,158 @@ mod tests { "Chain height should NOT match the modified config.genesis_id" ); } + + #[tokio::test] + async fn user_tx_that_chain_calls_clock_is_dropped() { + let (mut sequencer, mempool_handle) = common_setup().await; + + // Deploy the clock_chain_caller test program. + let deploy_tx = + NSSATransaction::ProgramDeployment(nssa::ProgramDeploymentTransaction::new( + nssa::program_deployment_transaction::Message::new( + test_program_methods::CLOCK_CHAIN_CALLER_ELF.to_vec(), + ), + )); + mempool_handle.push(deploy_tx).await.unwrap(); + sequencer + .produce_new_block_with_mempool_transactions() + .unwrap(); + + // Build a user transaction that invokes clock_chain_caller, which in turn chain-calls the + // clock program with the clock accounts. The sequencer should detect that the resulting + // state diff modifies clock accounts and drop the transaction. + let clock_chain_caller_id = + nssa::program::Program::new(test_program_methods::CLOCK_CHAIN_CALLER_ELF.to_vec()) + .unwrap() + .id(); + let clock_program_id = nssa::program::Program::clock().id(); + let timestamp: u64 = 0; + + let message = nssa::public_transaction::Message::try_new( + clock_chain_caller_id, + nssa::CLOCK_PROGRAM_ACCOUNT_IDS.to_vec(), + vec![], // no signers + (clock_program_id, timestamp), + ) + .unwrap(); + let user_tx = NSSATransaction::Public(nssa::PublicTransaction::new( + message, + nssa::public_transaction::WitnessSet::from_raw_parts(vec![]), + )); + + mempool_handle.push(user_tx).await.unwrap(); + sequencer + .produce_new_block_with_mempool_transactions() + .unwrap(); + + let block = sequencer + .store + .get_block_at_id(sequencer.chain_height) + .unwrap() + .unwrap(); + + // The user tx must have been dropped; only the mandatory clock invocation remains. + assert_eq!( + block.body.transactions, + vec![NSSATransaction::Public(clock_invocation( + block.header.timestamp + ))] + ); + } + + #[tokio::test] + async fn block_production_aborts_when_clock_account_data_is_corrupted() { + let (mut sequencer, mempool_handle) = common_setup().await; + + // Corrupt the clock 01 account data so the clock program panics on deserialization. + let clock_account_id = nssa::CLOCK_01_PROGRAM_ACCOUNT_ID; + let mut corrupted = sequencer.state.get_account_by_id(clock_account_id); + corrupted.data = vec![0xff; 3].try_into().unwrap(); + sequencer + .state + .force_insert_account(clock_account_id, corrupted); + + // Push a dummy transaction so the mempool is non-empty. + let tx = common::test_utils::produce_dummy_empty_transaction(); + mempool_handle.push(tx).await.unwrap(); + + // Block production must fail because the appended clock tx cannot execute. + let result = sequencer.produce_new_block_with_mempool_transactions(); + assert!( + result.is_err(), + "Block production should abort when clock account data is corrupted" + ); + } + + #[tokio::test] + async fn genesis_private_account_cannot_be_re_initialized() { + use common::transaction::NSSATransaction; + use nssa::{ + Account, + privacy_preserving_transaction::{ + PrivacyPreservingTransaction, circuit::execute_and_prove, message::Message, + witness_set::WitnessSet, + }, + program::Program, + }; + use nssa_core::{ + SharedSecretKey, + account::AccountWithMetadata, + encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}, + }; + use testnet_initial_state::PrivateAccountPublicInitialData; + + let nsk: nssa_core::NullifierSecretKey = [7; 32]; + let npk = nssa_core::NullifierPublicKey::from(&nsk); + let vsk: EphemeralSecretKey = [8; 32]; + let vpk = ViewingPublicKey::from_scalar(vsk); + + let genesis_account = Account { + program_owner: Program::authenticated_transfer_program().id(), + ..Account::default() + }; + + // Start a sequencer from config with a preconfigured private genesis account + let mut config = setup_sequencer_config(); + config.initial_private_accounts = Some(vec![PrivateAccountPublicInitialData { + npk, + account: genesis_account, + }]); + + let (mut sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + + // Attempt to re-initialize the same genesis account via a privacy-preserving transaction + let esk = [9; 32]; + let shared_secret = SharedSecretKey::new(&esk, &vpk); + let epk = EphemeralPublicKey::from_scalar(esk); + + let (output, proof) = execute_and_prove( + vec![AccountWithMetadata::new(Account::default(), true, &npk)], + Program::serialize_instruction(0_u128).unwrap(), + vec![1], + vec![(npk, shared_secret)], + vec![nsk], + vec![None], + &Program::authenticated_transfer_program().into(), + ) + .unwrap(); + + let message = + Message::try_from_circuit_output(vec![], vec![], vec![(npk, vpk, epk)], output) + .unwrap(); + + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = NSSATransaction::PrivacyPreserving(PrivacyPreservingTransaction::new( + message, + witness_set, + )); + + let result = tx.execute_check_on_state(&mut sequencer.state, 2, 0); + + assert!( + result.is_err_and(|e| e.to_string().contains("Nullifier already seen")), + "re-initializing a genesis private account must be rejected by the sequencer" + ); + } } diff --git a/storage/src/cells/mod.rs b/storage/src/cells/mod.rs new file mode 100644 index 00000000..76c1ff8c --- /dev/null +++ b/storage/src/cells/mod.rs @@ -0,0 +1,96 @@ +use std::sync::Arc; + +use borsh::{BorshDeserialize, BorshSerialize}; +use rocksdb::{BoundColumnFamily, DBWithThreadMode, MultiThreaded, WriteBatch}; + +use crate::{DbResult, error::DbError}; + +pub mod shared_cells; + +pub trait SimpleStorableCell { + const CF_NAME: &'static str; + const CELL_NAME: &'static str; + type KeyParams; + + fn key_constructor(_params: Self::KeyParams) -> DbResult> { + borsh::to_vec(&Self::CELL_NAME).map_err(|err| { + DbError::borsh_cast_message( + err, + Some(format!("Failed to serialize {:?}", Self::CELL_NAME)), + ) + }) + } + + fn column_ref(db: &DBWithThreadMode) -> Arc> { + db.cf_handle(Self::CF_NAME) + .unwrap_or_else(|| panic!("Column family {:?} must be present", Self::CF_NAME)) + } +} + +pub trait SimpleReadableCell: SimpleStorableCell + BorshDeserialize { + fn get(db: &DBWithThreadMode, params: Self::KeyParams) -> DbResult { + let res = Self::get_opt(db, params)?; + + res.ok_or_else(|| DbError::db_interaction_error(format!("{:?} not found", Self::CELL_NAME))) + } + + fn get_opt( + db: &DBWithThreadMode, + params: Self::KeyParams, + ) -> DbResult> { + let cf_ref = Self::column_ref(db); + let res = db + .get_cf(&cf_ref, Self::key_constructor(params)?) + .map_err(|rerr| { + DbError::rocksdb_cast_message( + rerr, + Some(format!("Failed to read {:?}", Self::CELL_NAME)), + ) + })?; + + res.map(|data| { + borsh::from_slice::(&data).map_err(|err| { + DbError::borsh_cast_message( + err, + Some(format!("Failed to deserialize {:?}", Self::CELL_NAME)), + ) + }) + }) + .transpose() + } +} + +pub trait SimpleWritableCell: SimpleStorableCell + BorshSerialize { + fn value_constructor(&self) -> DbResult>; + + fn put(&self, db: &DBWithThreadMode, params: Self::KeyParams) -> DbResult<()> { + let cf_ref = Self::column_ref(db); + db.put_cf( + &cf_ref, + Self::key_constructor(params)?, + self.value_constructor()?, + ) + .map_err(|rerr| { + DbError::rocksdb_cast_message( + rerr, + Some(format!("Failed to write {:?}", Self::CELL_NAME)), + ) + })?; + Ok(()) + } + + fn put_batch( + &self, + db: &DBWithThreadMode, + params: Self::KeyParams, + write_batch: &mut WriteBatch, + ) -> DbResult<()> { + let cf_ref = Self::column_ref(db); + write_batch.put_cf( + &cf_ref, + Self::key_constructor(params)?, + self.value_constructor()?, + ); + Ok(()) + } +} diff --git a/storage/src/cells/shared_cells.rs b/storage/src/cells/shared_cells.rs new file mode 100644 index 00000000..2a76edf3 --- /dev/null +++ b/storage/src/cells/shared_cells.rs @@ -0,0 +1,89 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use common::block::Block; + +use crate::{ + BLOCK_CELL_NAME, CF_BLOCK_NAME, CF_META_NAME, DB_META_FIRST_BLOCK_IN_DB_KEY, + DB_META_FIRST_BLOCK_SET_KEY, DB_META_LAST_BLOCK_IN_DB_KEY, DbResult, + cells::{SimpleReadableCell, SimpleStorableCell, SimpleWritableCell}, + error::DbError, +}; + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct LastBlockCell(pub u64); + +impl SimpleStorableCell for LastBlockCell { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_LAST_BLOCK_IN_DB_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for LastBlockCell {} + +impl SimpleWritableCell for LastBlockCell { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize last block id".to_owned())) + }) + } +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct FirstBlockSetCell(pub bool); + +impl SimpleStorableCell for FirstBlockSetCell { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_FIRST_BLOCK_SET_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for FirstBlockSetCell {} + +impl SimpleWritableCell for FirstBlockSetCell { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize first block set flag".to_owned()), + ) + }) + } +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct FirstBlockCell(pub u64); + +impl SimpleStorableCell for FirstBlockCell { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_FIRST_BLOCK_IN_DB_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for FirstBlockCell {} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct BlockCell(pub Block); + +impl SimpleStorableCell for BlockCell { + type KeyParams = u64; + + const CELL_NAME: &'static str = BLOCK_CELL_NAME; + const CF_NAME: &'static str = CF_BLOCK_NAME; + + fn key_constructor(params: Self::KeyParams) -> DbResult> { + // ToDo: Replace with increasing ordering serialization + borsh::to_vec(¶ms).map_err(|err| { + DbError::borsh_cast_message( + err, + Some(format!( + "Failed to serialize {:?} key params", + Self::CELL_NAME + )), + ) + }) + } +} + +impl SimpleReadableCell for BlockCell {} diff --git a/storage/src/indexer/indexer_cells.rs b/storage/src/indexer/indexer_cells.rs new file mode 100644 index 00000000..76a2c035 --- /dev/null +++ b/storage/src/indexer/indexer_cells.rs @@ -0,0 +1,230 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use nssa::V03State; + +use crate::{ + CF_META_NAME, DbResult, + cells::{SimpleReadableCell, SimpleStorableCell, SimpleWritableCell}, + error::DbError, + indexer::{ + ACC_NUM_CELL_NAME, BLOCK_HASH_CELL_NAME, BREAKPOINT_CELL_NAME, CF_ACC_META, + CF_BREAKPOINT_NAME, CF_HASH_TO_ID, CF_TX_TO_ID, DB_META_LAST_BREAKPOINT_ID, + DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY, TX_HASH_CELL_NAME, + }, +}; + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct LastObservedL1LibHeaderCell(pub [u8; 32]); + +impl SimpleStorableCell for LastObservedL1LibHeaderCell { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for LastObservedL1LibHeaderCell {} + +impl SimpleWritableCell for LastObservedL1LibHeaderCell { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize last observed l1 header".to_owned()), + ) + }) + } +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct LastBreakpointIdCell(pub u64); + +impl SimpleStorableCell for LastBreakpointIdCell { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_LAST_BREAKPOINT_ID; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for LastBreakpointIdCell {} + +impl SimpleWritableCell for LastBreakpointIdCell { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize last breakpoint id".to_owned()), + ) + }) + } +} + +#[derive(BorshDeserialize)] +pub struct BreakpointCellOwned(pub V03State); + +impl SimpleStorableCell for BreakpointCellOwned { + type KeyParams = u64; + + const CELL_NAME: &'static str = BREAKPOINT_CELL_NAME; + const CF_NAME: &'static str = CF_BREAKPOINT_NAME; + + fn key_constructor(params: Self::KeyParams) -> DbResult> { + borsh::to_vec(¶ms).map_err(|err| { + DbError::borsh_cast_message( + err, + Some(format!( + "Failed to serialize {:?} key params", + Self::CELL_NAME + )), + ) + }) + } +} + +impl SimpleReadableCell for BreakpointCellOwned {} + +#[derive(BorshSerialize)] +pub struct BreakpointCellRef<'state>(pub &'state V03State); + +impl SimpleStorableCell for BreakpointCellRef<'_> { + type KeyParams = u64; + + const CELL_NAME: &'static str = BREAKPOINT_CELL_NAME; + const CF_NAME: &'static str = CF_BREAKPOINT_NAME; + + fn key_constructor(params: Self::KeyParams) -> DbResult> { + borsh::to_vec(¶ms).map_err(|err| { + DbError::borsh_cast_message( + err, + Some(format!( + "Failed to serialize {:?} key params", + Self::CELL_NAME + )), + ) + }) + } +} + +impl SimpleWritableCell for BreakpointCellRef<'_> { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize breakpoint".to_owned())) + }) + } +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct BlockHashToBlockIdMapCell(pub u64); + +impl SimpleStorableCell for BlockHashToBlockIdMapCell { + type KeyParams = [u8; 32]; + + const CELL_NAME: &'static str = BLOCK_HASH_CELL_NAME; + const CF_NAME: &'static str = CF_HASH_TO_ID; + + fn key_constructor(params: Self::KeyParams) -> DbResult> { + borsh::to_vec(¶ms).map_err(|err| { + DbError::borsh_cast_message( + err, + Some(format!( + "Failed to serialize {:?} key params", + Self::CELL_NAME + )), + ) + }) + } +} + +impl SimpleReadableCell for BlockHashToBlockIdMapCell {} + +impl SimpleWritableCell for BlockHashToBlockIdMapCell { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize block id".to_owned())) + }) + } +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct TxHashToBlockIdMapCell(pub u64); + +impl SimpleStorableCell for TxHashToBlockIdMapCell { + type KeyParams = [u8; 32]; + + const CELL_NAME: &'static str = TX_HASH_CELL_NAME; + const CF_NAME: &'static str = CF_TX_TO_ID; + + fn key_constructor(params: Self::KeyParams) -> DbResult> { + borsh::to_vec(¶ms).map_err(|err| { + DbError::borsh_cast_message( + err, + Some(format!( + "Failed to serialize {:?} key params", + Self::CELL_NAME + )), + ) + }) + } +} + +impl SimpleReadableCell for TxHashToBlockIdMapCell {} + +impl SimpleWritableCell for TxHashToBlockIdMapCell { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize block id".to_owned())) + }) + } +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct AccNumTxCell(pub u64); + +impl SimpleStorableCell for AccNumTxCell { + type KeyParams = [u8; 32]; + + const CELL_NAME: &'static str = ACC_NUM_CELL_NAME; + const CF_NAME: &'static str = CF_ACC_META; + + fn key_constructor(params: Self::KeyParams) -> DbResult> { + borsh::to_vec(¶ms).map_err(|err| { + DbError::borsh_cast_message( + err, + Some(format!( + "Failed to serialize {:?} key params", + Self::CELL_NAME + )), + ) + }) + } +} + +impl SimpleReadableCell for AccNumTxCell {} + +impl SimpleWritableCell for AccNumTxCell { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize number of transactions".to_owned()), + ) + }) + } +} + +#[cfg(test)] +mod uniform_tests { + use crate::{ + cells::SimpleStorableCell as _, + indexer::indexer_cells::{BreakpointCellOwned, BreakpointCellRef}, + }; + + #[test] + fn breakpoint_ref_and_owned_is_aligned() { + assert_eq!(BreakpointCellRef::CELL_NAME, BreakpointCellOwned::CELL_NAME); + assert_eq!(BreakpointCellRef::CF_NAME, BreakpointCellOwned::CF_NAME); + assert_eq!( + BreakpointCellRef::key_constructor(1000).unwrap(), + BreakpointCellOwned::key_constructor(1000).unwrap() + ); + } +} diff --git a/storage/src/indexer/mod.rs b/storage/src/indexer/mod.rs index 6a9a67b6..85f2a278 100644 --- a/storage/src/indexer/mod.rs +++ b/storage/src/indexer/mod.rs @@ -6,44 +6,29 @@ use rocksdb::{ BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, }; -use crate::error::DbError; +use crate::{BREAKPOINT_INTERVAL, CF_BLOCK_NAME, CF_META_NAME, DBIO, DbResult, error::DbError}; +pub mod indexer_cells; pub mod read_multiple; pub mod read_once; pub mod write_atomic; pub mod write_non_atomic; -/// Maximal size of stored blocks in base. -/// -/// Used to control db size. -/// -/// Currently effectively unbounded. -pub const BUFF_SIZE_ROCKSDB: usize = usize::MAX; - -/// Size of stored blocks cache in memory. -/// -/// Keeping small to not run out of memory. -pub const CACHE_SIZE: usize = 1000; - -/// Key base for storing metainformation about id of first block in db. -pub const DB_META_FIRST_BLOCK_IN_DB_KEY: &str = "first_block_in_db"; -/// Key base for storing metainformation about id of last current block in db. -pub const DB_META_LAST_BLOCK_IN_DB_KEY: &str = "last_block_in_db"; /// Key base for storing metainformation about id of last observed L1 lib header in db. pub const DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY: &str = "last_observed_l1_lib_header_in_db"; -/// Key base for storing metainformation which describe if first block has been set. -pub const DB_META_FIRST_BLOCK_SET_KEY: &str = "first_block_set"; /// Key base for storing metainformation about the last breakpoint. pub const DB_META_LAST_BREAKPOINT_ID: &str = "last_breakpoint_id"; -/// Interval between state breakpoints. -pub const BREAKPOINT_INTERVAL: u8 = 100; +/// Cell name for a breakpoint. +pub const BREAKPOINT_CELL_NAME: &str = "breakpoint"; +/// Cell name for a block hash to block id map. +pub const BLOCK_HASH_CELL_NAME: &str = "block hash"; +/// Cell name for a tx hash to block id map. +pub const TX_HASH_CELL_NAME: &str = "tx hash"; +/// Cell name for a account number of transactions. +pub const ACC_NUM_CELL_NAME: &str = "acc id"; -/// Name of block column family. -pub const CF_BLOCK_NAME: &str = "cf_block"; -/// Name of meta column family. -pub const CF_META_NAME: &str = "cf_meta"; /// Name of breakpoint column family. pub const CF_BREAKPOINT_NAME: &str = "cf_breakpoint"; /// Name of hash to id map column family. @@ -55,12 +40,16 @@ pub const CF_ACC_META: &str = "cf_acc_meta"; /// Name of account id to tx hash map column family. pub const CF_ACC_TO_TX: &str = "cf_acc_to_tx"; -pub type DbResult = Result; - pub struct RocksDBIO { pub db: DBWithThreadMode, } +impl DBIO for RocksDBIO { + fn db(&self) -> &DBWithThreadMode { + &self.db + } +} + impl RocksDBIO { pub fn open_or_create( path: &Path, @@ -257,7 +246,11 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, &genesis_block(), - &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]), + &nssa::V03State::new_with_genesis_accounts( + &[(acc1(), 10000), (acc2(), 20000)], + vec![], + 0, + ), ) .unwrap(); @@ -294,7 +287,11 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, &genesis_block(), - &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]), + &nssa::V03State::new_with_genesis_accounts( + &[(acc1(), 10000), (acc2(), 20000)], + vec![], + 0, + ), ) .unwrap(); @@ -347,7 +344,11 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, &genesis_block(), - &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]), + &nssa::V03State::new_with_genesis_accounts( + &[(acc1(), 10000), (acc2(), 20000)], + vec![], + 0, + ), ) .unwrap(); @@ -420,7 +421,11 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, &genesis_block(), - &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]), + &nssa::V03State::new_with_genesis_accounts( + &[(acc1(), 10000), (acc2(), 20000)], + vec![], + 0, + ), ) .unwrap(); @@ -503,7 +508,11 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, &genesis_block(), - &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]), + &nssa::V03State::new_with_genesis_accounts( + &[(acc1(), 10000), (acc2(), 20000)], + vec![], + 0, + ), ) .unwrap(); @@ -599,7 +608,11 @@ mod tests { let dbio = RocksDBIO::open_or_create( temdir_path, &genesis_block(), - &nssa::V03State::new_with_genesis_accounts(&[(acc1(), 10000), (acc2(), 20000)], &[]), + &nssa::V03State::new_with_genesis_accounts( + &[(acc1(), 10000), (acc2(), 20000)], + vec![], + 0, + ), ) .unwrap(); diff --git a/storage/src/indexer/read_once.rs b/storage/src/indexer/read_once.rs index 74d1afe9..b1ae0ada 100644 --- a/storage/src/indexer/read_once.rs +++ b/storage/src/indexer/read_once.rs @@ -1,7 +1,11 @@ -use super::{ - Block, DB_META_FIRST_BLOCK_IN_DB_KEY, DB_META_FIRST_BLOCK_SET_KEY, - DB_META_LAST_BLOCK_IN_DB_KEY, DB_META_LAST_BREAKPOINT_ID, - DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY, DbError, DbResult, RocksDBIO, V03State, +use super::{Block, DbResult, RocksDBIO, V03State}; +use crate::{ + DBIO as _, + cells::shared_cells::{BlockCell, FirstBlockCell, FirstBlockSetCell, LastBlockCell}, + indexer::indexer_cells::{ + AccNumTxCell, BlockHashToBlockIdMapCell, BreakpointCellOwned, LastBreakpointIdCell, + LastObservedL1LibHeaderCell, TxHashToBlockIdMapCell, + }, }; #[expect(clippy::multiple_inherent_impl, reason = "Readability")] @@ -9,264 +13,55 @@ impl RocksDBIO { // Meta pub fn get_meta_first_block_in_db(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize first block".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "First block not found".to_owned(), - )) - } + self.get::(()).map(|cell| cell.0) } pub fn get_meta_last_block_in_db(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize last block".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "Last block not found".to_owned(), - )) - } + self.get::(()).map(|cell| cell.0) } pub fn get_meta_last_observed_l1_lib_header_in_db(&self) -> DbResult> { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY).map_err( - |err| { - DbError::borsh_cast_message( - err, - Some( - "Failed to serialize DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY" - .to_owned(), - ), - ) - }, - )?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - res.map(|data| { - borsh::from_slice::<[u8; 32]>(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize last l1 lib header".to_owned()), - ) - }) - }) - .transpose() + self.get_opt::(()) + .map(|opt| opt.map(|val| val.0)) } pub fn get_meta_is_first_block_set(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_SET_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_SET_KEY".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - Ok(res.is_some()) + Ok(self.get_opt::(())?.is_some()) } pub fn get_meta_last_breakpoint_id(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BREAKPOINT_ID).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BREAKPOINT_ID".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize last breakpoint id".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "Last breakpoint id not found".to_owned(), - )) - } + self.get::(()).map(|cell| cell.0) } // Block pub fn get_block(&self, block_id: u64) -> DbResult> { - let cf_block = self.block_column(); - let res = self - .db - .get_cf( - &cf_block, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(Some(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message( - serr, - Some("Failed to deserialize block data".to_owned()), - ) - })?)) - } else { - Ok(None) - } + self.get_opt::(block_id) + .map(|opt| opt.map(|val| val.0)) } // State pub fn get_breakpoint(&self, br_id: u64) -> DbResult { - let cf_br = self.breakpoint_column(); - let res = self - .db - .get_cf( - &cf_br, - borsh::to_vec(&br_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize breakpoint id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message( - serr, - Some("Failed to deserialize breakpoint data".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "Breakpoint on this id not found".to_owned(), - )) - } + self.get::(br_id).map(|cell| cell.0) } // Mappings pub fn get_block_id_by_hash(&self, hash: [u8; 32]) -> DbResult> { - let cf_hti = self.hash_to_id_column(); - let res = self - .db - .get_cf( - &cf_hti, - borsh::to_vec(&hash).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block hash".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(Some(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message(serr, Some("Failed to deserialize block id".to_owned())) - })?)) - } else { - Ok(None) - } + self.get_opt::(hash) + .map(|opt| opt.map(|cell| cell.0)) } pub fn get_block_id_by_tx_hash(&self, tx_hash: [u8; 32]) -> DbResult> { - let cf_tti = self.tx_hash_to_id_column(); - let res = self - .db - .get_cf( - &cf_tti, - borsh::to_vec(&tx_hash).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize transaction hash".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(Some(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message(serr, Some("Failed to deserialize block id".to_owned())) - })?)) - } else { - Ok(None) - } + self.get_opt::(tx_hash) + .map(|opt| opt.map(|cell| cell.0)) } // Accounts meta pub(crate) fn get_acc_meta_num_tx(&self, acc_id: [u8; 32]) -> DbResult> { - let cf_ameta = self.account_meta_column(); - let res = self.db.get_cf(&cf_ameta, acc_id).map_err(|rerr| { - DbError::rocksdb_cast_message(rerr, Some("Failed to read from acc meta cf".to_owned())) - })?; - - res.map(|data| { - borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message(serr, Some("Failed to deserialize num tx".to_owned())) - }) - }) - .transpose() + self.get_opt::(acc_id) + .map(|opt| opt.map(|cell| cell.0)) } } diff --git a/storage/src/indexer/write_atomic.rs b/storage/src/indexer/write_atomic.rs index 161d763a..9b661f3b 100644 --- a/storage/src/indexer/write_atomic.rs +++ b/storage/src/indexer/write_atomic.rs @@ -2,10 +2,14 @@ use std::collections::HashMap; use rocksdb::WriteBatch; -use super::{ - Arc, BREAKPOINT_INTERVAL, Block, BoundColumnFamily, DB_META_FIRST_BLOCK_IN_DB_KEY, - DB_META_FIRST_BLOCK_SET_KEY, DB_META_LAST_BLOCK_IN_DB_KEY, DB_META_LAST_BREAKPOINT_ID, - DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY, DbError, DbResult, RocksDBIO, +use super::{BREAKPOINT_INTERVAL, Block, DbError, DbResult, RocksDBIO}; +use crate::{ + DB_META_FIRST_BLOCK_IN_DB_KEY, DBIO as _, + cells::shared_cells::{FirstBlockSetCell, LastBlockCell}, + indexer::indexer_cells::{ + AccNumTxCell, BlockHashToBlockIdMapCell, LastBreakpointIdCell, LastObservedL1LibHeaderCell, + TxHashToBlockIdMapCell, + }, }; #[expect(clippy::multiple_inherent_impl, reason = "Readability")] @@ -18,22 +22,27 @@ impl RocksDBIO { num_tx: u64, write_batch: &mut WriteBatch, ) -> DbResult<()> { - let cf_ameta = self.account_meta_column(); + self.put_batch(&AccNumTxCell(num_tx), acc_id, write_batch) + } - write_batch.put_cf( - &cf_ameta, - borsh::to_vec(&acc_id).map_err(|err| { - DbError::borsh_cast_message(err, Some("Failed to serialize account id".to_owned())) - })?, - borsh::to_vec(&num_tx).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize acc metadata".to_owned()), - ) - })?, - ); + // Mappings - Ok(()) + pub fn put_block_id_by_hash_batch( + &self, + hash: [u8; 32], + block_id: u64, + write_batch: &mut WriteBatch, + ) -> DbResult<()> { + self.put_batch(&BlockHashToBlockIdMapCell(block_id), hash, write_batch) + } + + pub fn put_block_id_by_tx_hash_batch( + &self, + tx_hash: [u8; 32], + block_id: u64, + write_batch: &mut WriteBatch, + ) -> DbResult<()> { + self.put_batch(&TxHashToBlockIdMapCell(block_id), tx_hash, write_batch) } // Account @@ -163,23 +172,7 @@ impl RocksDBIO { block_id: u64, write_batch: &mut WriteBatch, ) -> DbResult<()> { - let cf_meta = self.meta_column(); - write_batch.put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last block id".to_owned()), - ) - })?, - ); - Ok(()) + self.put_batch(&LastBlockCell(block_id), (), write_batch) } pub fn put_meta_last_observed_l1_lib_header_in_db_batch( @@ -187,26 +180,7 @@ impl RocksDBIO { l1_lib_header: [u8; 32], write_batch: &mut WriteBatch, ) -> DbResult<()> { - let cf_meta = self.meta_column(); - write_batch.put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some( - "Failed to serialize DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY" - .to_owned(), - ), - ) - })?, - borsh::to_vec(&l1_lib_header).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last l1 block header".to_owned()), - ) - })?, - ); - Ok(()) + self.put_batch(&LastObservedL1LibHeaderCell(l1_lib_header), (), write_batch) } pub fn put_meta_last_breakpoint_id_batch( @@ -214,46 +188,17 @@ impl RocksDBIO { br_id: u64, write_batch: &mut WriteBatch, ) -> DbResult<()> { - let cf_meta = self.meta_column(); - write_batch.put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BREAKPOINT_ID).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BREAKPOINT_ID".to_owned()), - ) - })?, - borsh::to_vec(&br_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last block id".to_owned()), - ) - })?, - ); - Ok(()) + self.put_batch(&LastBreakpointIdCell(br_id), (), write_batch) } pub fn put_meta_is_first_block_set_batch(&self, write_batch: &mut WriteBatch) -> DbResult<()> { - let cf_meta = self.meta_column(); - write_batch.put_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_SET_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_SET_KEY".to_owned()), - ) - })?, - [1_u8; 1], - ); - Ok(()) + self.put_batch(&FirstBlockSetCell(true), (), write_batch) } // Block pub fn put_block(&self, block: &Block, l1_lib_header: [u8; 32]) -> DbResult<()> { let cf_block = self.block_column(); - let cf_hti = self.hash_to_id_column(); - let cf_tti: Arc> = self.tx_hash_to_id_column(); let last_curr_block = self.get_meta_last_block_in_db()?; let mut write_batch = WriteBatch::default(); @@ -272,33 +217,22 @@ impl RocksDBIO { self.put_meta_last_observed_l1_lib_header_in_db_batch(l1_lib_header, &mut write_batch)?; } - write_batch.put_cf( - &cf_hti, - borsh::to_vec(&block.header.hash).map_err(|err| { - DbError::borsh_cast_message(err, Some("Failed to serialize block hash".to_owned())) - })?, - borsh::to_vec(&block.header.block_id).map_err(|err| { - DbError::borsh_cast_message(err, Some("Failed to serialize block id".to_owned())) - })?, - ); + self.put_block_id_by_hash_batch( + block.header.hash.into(), + block.header.block_id, + &mut write_batch, + )?; let mut acc_to_tx_map: HashMap<[u8; 32], Vec<[u8; 32]>> = HashMap::new(); for tx in &block.body.transactions { let tx_hash = tx.hash(); - write_batch.put_cf( - &cf_tti, - borsh::to_vec(&tx_hash).map_err(|err| { - DbError::borsh_cast_message(err, Some("Failed to serialize tx hash".to_owned())) - })?, - borsh::to_vec(&block.header.block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block id".to_owned()), - ) - })?, - ); + self.put_block_id_by_tx_hash_batch( + tx_hash.into(), + block.header.block_id, + &mut write_batch, + )?; let acc_ids = tx .affected_public_account_ids() diff --git a/storage/src/indexer/write_non_atomic.rs b/storage/src/indexer/write_non_atomic.rs index 17c1be18..62b466a2 100644 --- a/storage/src/indexer/write_non_atomic.rs +++ b/storage/src/indexer/write_non_atomic.rs @@ -1,7 +1,10 @@ -use super::{ - BREAKPOINT_INTERVAL, DB_META_FIRST_BLOCK_SET_KEY, DB_META_LAST_BLOCK_IN_DB_KEY, - DB_META_LAST_BREAKPOINT_ID, DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY, DbError, - DbResult, RocksDBIO, V03State, +use super::{BREAKPOINT_INTERVAL, DbError, DbResult, RocksDBIO, V03State}; +use crate::{ + DBIO as _, + cells::shared_cells::{FirstBlockSetCell, LastBlockCell}, + indexer::indexer_cells::{ + BreakpointCellRef, LastBreakpointIdCell, LastObservedL1LibHeaderCell, + }, }; #[expect(clippy::multiple_inherent_impl, reason = "Readability")] @@ -9,118 +12,28 @@ impl RocksDBIO { // Meta pub fn put_meta_last_block_in_db(&self, block_id: u64) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) + self.put(&LastBlockCell(block_id), ()) } pub fn put_meta_last_observed_l1_lib_header_in_db( &self, l1_lib_header: [u8; 32], ) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY).map_err( - |err| { - DbError::borsh_cast_message( - err, - Some( - "Failed to serialize DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY" - .to_owned(), - ), - ) - }, - )?, - borsh::to_vec(&l1_lib_header).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last l1 block header".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) + self.put(&LastObservedL1LibHeaderCell(l1_lib_header), ()) } pub fn put_meta_last_breakpoint_id(&self, br_id: u64) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BREAKPOINT_ID).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BREAKPOINT_ID".to_owned()), - ) - })?, - borsh::to_vec(&br_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) + self.put(&LastBreakpointIdCell(br_id), ()) } pub fn put_meta_is_first_block_set(&self) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_SET_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_SET_KEY".to_owned()), - ) - })?, - [1_u8; 1], - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) + self.put(&FirstBlockSetCell(true), ()) } // State pub fn put_breakpoint(&self, br_id: u64, breakpoint: &V03State) -> DbResult<()> { - let cf_br = self.breakpoint_column(); - - self.db - .put_cf( - &cf_br, - borsh::to_vec(&br_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize breakpoint id".to_owned()), - ) - })?, - borsh::to_vec(breakpoint).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize breakpoint data".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None)) + self.put(&BreakpointCellRef(breakpoint), br_id) } pub fn put_next_breakpoint(&self) -> DbResult<()> { diff --git a/storage/src/lib.rs b/storage/src/lib.rs index 05c4a374..2edb0ee3 100644 --- a/storage/src/lib.rs +++ b/storage/src/lib.rs @@ -1,3 +1,69 @@ +use rocksdb::{DBWithThreadMode, MultiThreaded, WriteBatch}; + +use crate::{ + cells::{SimpleReadableCell, SimpleWritableCell}, + error::DbError, +}; + +pub mod cells; pub mod error; pub mod indexer; pub mod sequencer; + +/// Maximal size of stored blocks in base. +/// +/// Used to control db size. +/// +/// Currently effectively unbounded. +pub const BUFF_SIZE_ROCKSDB: usize = usize::MAX; + +/// Size of stored blocks cache in memory. +/// +/// Keeping small to not run out of memory. +pub const CACHE_SIZE: usize = 1000; + +/// Key base for storing metainformation which describe if first block has been set. +pub const DB_META_FIRST_BLOCK_SET_KEY: &str = "first_block_set"; +/// Key base for storing metainformation about id of first block in db. +pub const DB_META_FIRST_BLOCK_IN_DB_KEY: &str = "first_block_in_db"; +/// Key base for storing metainformation about id of last current block in db. +pub const DB_META_LAST_BLOCK_IN_DB_KEY: &str = "last_block_in_db"; + +/// Cell name for a block. +pub const BLOCK_CELL_NAME: &str = "block"; + +/// Interval between state breakpoints. +pub const BREAKPOINT_INTERVAL: u8 = 100; + +/// Name of block column family. +pub const CF_BLOCK_NAME: &str = "cf_block"; +/// Name of meta column family. +pub const CF_META_NAME: &str = "cf_meta"; + +pub type DbResult = Result; + +/// Minimal requirements for DB IO. +pub trait DBIO { + fn db(&self) -> &DBWithThreadMode; + + fn get(&self, params: T::KeyParams) -> DbResult { + T::get(self.db(), params) + } + + fn get_opt(&self, params: T::KeyParams) -> DbResult> { + T::get_opt(self.db(), params) + } + + fn put(&self, cell: &T, params: T::KeyParams) -> DbResult<()> { + cell.put(self.db(), params) + } + + fn put_batch( + &self, + cell: &T, + params: T::KeyParams, + write_batch: &mut WriteBatch, + ) -> DbResult<()> { + cell.put_batch(self.db(), params, write_batch) + } +} diff --git a/storage/src/sequencer.rs b/storage/src/sequencer.rs deleted file mode 100644 index 17d0e73e..00000000 --- a/storage/src/sequencer.rs +++ /dev/null @@ -1,596 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use common::block::{BedrockStatus, Block, BlockMeta, MantleMsgId}; -use nssa::V03State; -use rocksdb::{ - BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, WriteBatch, -}; - -use crate::error::DbError; - -/// Maximal size of stored blocks in base. -/// -/// Used to control db size. -/// -/// Currently effectively unbounded. -pub const BUFF_SIZE_ROCKSDB: usize = usize::MAX; - -/// Size of stored blocks cache in memory. -/// -/// Keeping small to not run out of memory. -pub const CACHE_SIZE: usize = 1000; - -/// Key base for storing metainformation about id of first block in db. -pub const DB_META_FIRST_BLOCK_IN_DB_KEY: &str = "first_block_in_db"; -/// Key base for storing metainformation about id of last current block in db. -pub const DB_META_LAST_BLOCK_IN_DB_KEY: &str = "last_block_in_db"; -/// Key base for storing metainformation which describe if first block has been set. -pub const DB_META_FIRST_BLOCK_SET_KEY: &str = "first_block_set"; -/// Key base for storing metainformation about the last finalized block on Bedrock. -pub const DB_META_LAST_FINALIZED_BLOCK_ID: &str = "last_finalized_block_id"; -/// Key base for storing metainformation about the latest block meta. -pub const DB_META_LATEST_BLOCK_META_KEY: &str = "latest_block_meta"; - -/// Key base for storing the NSSA state. -pub const DB_NSSA_STATE_KEY: &str = "nssa_state"; - -/// Name of block column family. -pub const CF_BLOCK_NAME: &str = "cf_block"; -/// Name of meta column family. -pub const CF_META_NAME: &str = "cf_meta"; -/// Name of state column family. -pub const CF_NSSA_STATE_NAME: &str = "cf_nssa_state"; - -pub type DbResult = Result; - -pub struct RocksDBIO { - pub db: DBWithThreadMode, -} - -impl RocksDBIO { - pub fn open_or_create( - path: &Path, - genesis_block: &Block, - genesis_msg_id: MantleMsgId, - ) -> DbResult { - let mut cf_opts = Options::default(); - cf_opts.set_max_write_buffer_number(16); - // ToDo: Add more column families for different data - let cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); - let cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); - let cfstate = ColumnFamilyDescriptor::new(CF_NSSA_STATE_NAME, cf_opts.clone()); - - let mut db_opts = Options::default(); - db_opts.create_missing_column_families(true); - db_opts.create_if_missing(true); - let db = DBWithThreadMode::::open_cf_descriptors( - &db_opts, - path, - vec![cfb, cfmeta, cfstate], - ) - .map_err(|err| DbError::RocksDbError { - error: err, - additional_info: Some("Failed to open or create DB".to_owned()), - })?; - - let dbio = Self { db }; - - let is_start_set = dbio.get_meta_is_first_block_set()?; - if !is_start_set { - let block_id = genesis_block.header.block_id; - dbio.put_meta_first_block_in_db(genesis_block, genesis_msg_id)?; - dbio.put_meta_is_first_block_set()?; - dbio.put_meta_last_block_in_db(block_id)?; - dbio.put_meta_last_finalized_block_id(None)?; - dbio.put_meta_latest_block_meta(&BlockMeta { - id: genesis_block.header.block_id, - hash: genesis_block.header.hash, - msg_id: genesis_msg_id, - })?; - } - - Ok(dbio) - } - - pub fn destroy(path: &Path) -> DbResult<()> { - let mut cf_opts = Options::default(); - cf_opts.set_max_write_buffer_number(16); - // ToDo: Add more column families for different data - let _cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); - let _cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); - let _cfstate = ColumnFamilyDescriptor::new(CF_NSSA_STATE_NAME, cf_opts.clone()); - - let mut db_opts = Options::default(); - db_opts.create_missing_column_families(true); - db_opts.create_if_missing(true); - DBWithThreadMode::::destroy(&db_opts, path) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None)) - } - - pub fn meta_column(&self) -> Arc> { - self.db.cf_handle(CF_META_NAME).unwrap() - } - - pub fn block_column(&self) -> Arc> { - self.db.cf_handle(CF_BLOCK_NAME).unwrap() - } - - pub fn nssa_state_column(&self) -> Arc> { - self.db.cf_handle(CF_NSSA_STATE_NAME).unwrap() - } - - pub fn get_meta_first_block_in_db(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize first block".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "First block not found".to_owned(), - )) - } - } - - pub fn get_meta_last_block_in_db(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize last block".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "Last block not found".to_owned(), - )) - } - } - - pub fn get_meta_is_first_block_set(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_SET_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_SET_KEY".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - Ok(res.is_some()) - } - - pub fn put_nssa_state_in_db(&self, state: &V03State, batch: &mut WriteBatch) -> DbResult<()> { - let cf_nssa_state = self.nssa_state_column(); - batch.put_cf( - &cf_nssa_state, - borsh::to_vec(&DB_NSSA_STATE_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_NSSA_STATE_KEY".to_owned()), - ) - })?, - borsh::to_vec(state).map_err(|err| { - DbError::borsh_cast_message(err, Some("Failed to serialize NSSA state".to_owned())) - })?, - ); - - Ok(()) - } - - pub fn put_meta_first_block_in_db(&self, block: &Block, msg_id: MantleMsgId) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - borsh::to_vec(&block.header.block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize first block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - let mut batch = WriteBatch::default(); - self.put_block(block, msg_id, true, &mut batch)?; - self.db.write(batch).map_err(|rerr| { - DbError::rocksdb_cast_message( - rerr, - Some("Failed to write first block in db".to_owned()), - ) - })?; - - Ok(()) - } - - pub fn put_meta_last_block_in_db(&self, block_id: u64) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) - } - - fn put_meta_last_block_in_db_batch( - &self, - block_id: u64, - batch: &mut WriteBatch, - ) -> DbResult<()> { - let cf_meta = self.meta_column(); - batch.put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_BLOCK_IN_DB_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_BLOCK_IN_DB_KEY".to_owned()), - ) - })?, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last block id".to_owned()), - ) - })?, - ); - Ok(()) - } - - pub fn put_meta_last_finalized_block_id(&self, block_id: Option) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LAST_FINALIZED_BLOCK_ID).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LAST_FINALIZED_BLOCK_ID".to_owned()), - ) - })?, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize last block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) - } - - pub fn put_meta_is_first_block_set(&self) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_FIRST_BLOCK_SET_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_FIRST_BLOCK_SET_KEY".to_owned()), - ) - })?, - [1_u8; 1], - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) - } - - fn put_meta_latest_block_meta(&self, block_meta: &BlockMeta) -> DbResult<()> { - let cf_meta = self.meta_column(); - self.db - .put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LATEST_BLOCK_META_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LATEST_BLOCK_META_KEY".to_owned()), - ) - })?, - borsh::to_vec(&block_meta).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize latest block meta".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - Ok(()) - } - - fn put_meta_latest_block_meta_batch( - &self, - block_meta: &BlockMeta, - batch: &mut WriteBatch, - ) -> DbResult<()> { - let cf_meta = self.meta_column(); - batch.put_cf( - &cf_meta, - borsh::to_vec(&DB_META_LATEST_BLOCK_META_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LATEST_BLOCK_META_KEY".to_owned()), - ) - })?, - borsh::to_vec(&block_meta).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize latest block meta".to_owned()), - ) - })?, - ); - Ok(()) - } - - pub fn latest_block_meta(&self) -> DbResult { - let cf_meta = self.meta_column(); - let res = self - .db - .get_cf( - &cf_meta, - borsh::to_vec(&DB_META_LATEST_BLOCK_META_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize DB_META_LATEST_BLOCK_META_KEY".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize latest block meta".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "Latest block meta not found".to_owned(), - )) - } - } - - pub fn put_block( - &self, - block: &Block, - msg_id: MantleMsgId, - first: bool, - batch: &mut WriteBatch, - ) -> DbResult<()> { - let cf_block = self.block_column(); - - if !first { - let last_curr_block = self.get_meta_last_block_in_db()?; - - if block.header.block_id > last_curr_block { - self.put_meta_last_block_in_db_batch(block.header.block_id, batch)?; - self.put_meta_latest_block_meta_batch( - &BlockMeta { - id: block.header.block_id, - hash: block.header.hash, - msg_id, - }, - batch, - )?; - } - } - - batch.put_cf( - &cf_block, - borsh::to_vec(&block.header.block_id).map_err(|err| { - DbError::borsh_cast_message(err, Some("Failed to serialize block id".to_owned())) - })?, - borsh::to_vec(block).map_err(|err| { - DbError::borsh_cast_message(err, Some("Failed to serialize block data".to_owned())) - })?, - ); - Ok(()) - } - - pub fn get_block(&self, block_id: u64) -> DbResult> { - let cf_block = self.block_column(); - let res = self - .db - .get_cf( - &cf_block, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(Some(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message( - serr, - Some("Failed to deserialize block data".to_owned()), - ) - })?)) - } else { - Ok(None) - } - } - - pub fn get_nssa_state(&self) -> DbResult { - let cf_nssa_state = self.nssa_state_column(); - let res = self - .db - .get_cf( - &cf_nssa_state, - borsh::to_vec(&DB_NSSA_STATE_KEY).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block id".to_owned()), - ) - })?, - ) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - if let Some(data) = res { - Ok(borsh::from_slice::(&data).map_err(|serr| { - DbError::borsh_cast_message( - serr, - Some("Failed to deserialize block data".to_owned()), - ) - })?) - } else { - Err(DbError::db_interaction_error( - "NSSA state not found".to_owned(), - )) - } - } - - pub fn delete_block(&self, block_id: u64) -> DbResult<()> { - let cf_block = self.block_column(); - let key = borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message(err, Some("Failed to serialize block id".to_owned())) - })?; - - if self - .db - .get_cf(&cf_block, &key) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))? - .is_none() - { - return Err(DbError::db_interaction_error(format!( - "Block with id {block_id} not found" - ))); - } - - self.db - .delete_cf(&cf_block, key) - .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; - - Ok(()) - } - - pub fn mark_block_as_finalized(&self, block_id: u64) -> DbResult<()> { - let mut block = self.get_block(block_id)?.ok_or_else(|| { - DbError::db_interaction_error(format!("Block with id {block_id} not found")) - })?; - block.bedrock_status = BedrockStatus::Finalized; - - let cf_block = self.block_column(); - self.db - .put_cf( - &cf_block, - borsh::to_vec(&block_id).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block id".to_owned()), - ) - })?, - borsh::to_vec(&block).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to serialize block data".to_owned()), - ) - })?, - ) - .map_err(|rerr| { - DbError::rocksdb_cast_message( - rerr, - Some(format!("Failed to mark block {block_id} as finalized")), - ) - })?; - - Ok(()) - } - - pub fn get_all_blocks(&self) -> impl Iterator> { - let cf_block = self.block_column(); - self.db - .iterator_cf(&cf_block, rocksdb::IteratorMode::Start) - .map(|res| { - let (_key, value) = res.map_err(|rerr| { - DbError::rocksdb_cast_message( - rerr, - Some("Failed to get key value pair".to_owned()), - ) - })?; - - borsh::from_slice::(&value).map_err(|err| { - DbError::borsh_cast_message( - err, - Some("Failed to deserialize block data".to_owned()), - ) - }) - }) - } - - pub fn atomic_update( - &self, - block: &Block, - msg_id: MantleMsgId, - state: &V03State, - ) -> DbResult<()> { - let block_id = block.header.block_id; - let mut batch = WriteBatch::default(); - self.put_block(block, msg_id, false, &mut batch)?; - self.put_nssa_state_in_db(state, &mut batch)?; - self.db.write(batch).map_err(|rerr| { - DbError::rocksdb_cast_message( - rerr, - Some(format!("Failed to udpate db with block {block_id}")), - ) - }) - } -} diff --git a/storage/src/sequencer/mod.rs b/storage/src/sequencer/mod.rs new file mode 100644 index 00000000..508f6c29 --- /dev/null +++ b/storage/src/sequencer/mod.rs @@ -0,0 +1,349 @@ +use std::{path::Path, sync::Arc}; + +use common::block::{BedrockStatus, Block, BlockMeta, MantleMsgId}; +use nssa::V03State; +use rocksdb::{ + BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, WriteBatch, +}; + +use crate::{ + CF_BLOCK_NAME, CF_META_NAME, DB_META_FIRST_BLOCK_IN_DB_KEY, DBIO, DbResult, + cells::shared_cells::{BlockCell, FirstBlockCell, FirstBlockSetCell, LastBlockCell}, + error::DbError, + sequencer::sequencer_cells::{ + LastFinalizedBlockIdCell, LatestBlockMetaCellOwned, LatestBlockMetaCellRef, + NSSAStateCellOwned, NSSAStateCellRef, + }, +}; + +pub mod sequencer_cells; + +/// Key base for storing metainformation about the last finalized block on Bedrock. +pub const DB_META_LAST_FINALIZED_BLOCK_ID: &str = "last_finalized_block_id"; +/// Key base for storing metainformation about the latest block meta. +pub const DB_META_LATEST_BLOCK_META_KEY: &str = "latest_block_meta"; + +/// Key base for storing the NSSA state. +pub const DB_NSSA_STATE_KEY: &str = "nssa_state"; + +/// Name of state column family. +pub const CF_NSSA_STATE_NAME: &str = "cf_nssa_state"; + +pub struct RocksDBIO { + pub db: DBWithThreadMode, +} + +impl DBIO for RocksDBIO { + fn db(&self) -> &DBWithThreadMode { + &self.db + } +} + +impl RocksDBIO { + pub fn open_or_create( + path: &Path, + genesis_block: &Block, + genesis_msg_id: MantleMsgId, + ) -> DbResult { + let mut cf_opts = Options::default(); + cf_opts.set_max_write_buffer_number(16); + // ToDo: Add more column families for different data + let cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); + let cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); + let cfstate = ColumnFamilyDescriptor::new(CF_NSSA_STATE_NAME, cf_opts.clone()); + + let mut db_opts = Options::default(); + db_opts.create_missing_column_families(true); + db_opts.create_if_missing(true); + let db = DBWithThreadMode::::open_cf_descriptors( + &db_opts, + path, + vec![cfb, cfmeta, cfstate], + ) + .map_err(|err| DbError::RocksDbError { + error: err, + additional_info: Some("Failed to open or create DB".to_owned()), + })?; + + let dbio = Self { db }; + + let is_start_set = dbio.get_meta_is_first_block_set()?; + if !is_start_set { + let block_id = genesis_block.header.block_id; + dbio.put_meta_first_block_in_db(genesis_block, genesis_msg_id)?; + dbio.put_meta_is_first_block_set()?; + dbio.put_meta_last_block_in_db(block_id)?; + dbio.put_meta_last_finalized_block_id(None)?; + dbio.put_meta_latest_block_meta(&BlockMeta { + id: genesis_block.header.block_id, + hash: genesis_block.header.hash, + msg_id: genesis_msg_id, + })?; + } + + Ok(dbio) + } + + pub fn destroy(path: &Path) -> DbResult<()> { + let mut cf_opts = Options::default(); + cf_opts.set_max_write_buffer_number(16); + // ToDo: Add more column families for different data + let _cfb = ColumnFamilyDescriptor::new(CF_BLOCK_NAME, cf_opts.clone()); + let _cfmeta = ColumnFamilyDescriptor::new(CF_META_NAME, cf_opts.clone()); + let _cfstate = ColumnFamilyDescriptor::new(CF_NSSA_STATE_NAME, cf_opts.clone()); + + let mut db_opts = Options::default(); + db_opts.create_missing_column_families(true); + db_opts.create_if_missing(true); + DBWithThreadMode::::destroy(&db_opts, path) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None)) + } + + // Columns + + pub fn meta_column(&self) -> Arc> { + self.db + .cf_handle(CF_META_NAME) + .expect("Meta column should exist") + } + + pub fn block_column(&self) -> Arc> { + self.db + .cf_handle(CF_BLOCK_NAME) + .expect("Block column should exist") + } + + pub fn nssa_state_column(&self) -> Arc> { + self.db + .cf_handle(CF_NSSA_STATE_NAME) + .expect("State should exist") + } + + // Meta + + pub fn get_meta_first_block_in_db(&self) -> DbResult { + self.get::(()).map(|cell| cell.0) + } + + pub fn get_meta_last_block_in_db(&self) -> DbResult { + self.get::(()).map(|cell| cell.0) + } + + pub fn get_meta_is_first_block_set(&self) -> DbResult { + Ok(self.get_opt::(())?.is_some()) + } + + pub fn put_nssa_state_in_db(&self, state: &V03State, batch: &mut WriteBatch) -> DbResult<()> { + self.put_batch(&NSSAStateCellRef(state), (), batch) + } + + pub fn put_meta_first_block_in_db(&self, block: &Block, msg_id: MantleMsgId) -> DbResult<()> { + let cf_meta = self.meta_column(); + self.db + .put_cf( + &cf_meta, + borsh::to_vec(&DB_META_FIRST_BLOCK_IN_DB_KEY).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize DB_META_FIRST_BLOCK_IN_DB_KEY".to_owned()), + ) + })?, + borsh::to_vec(&block.header.block_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize first block id".to_owned()), + ) + })?, + ) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + let mut batch = WriteBatch::default(); + self.put_block(block, msg_id, true, &mut batch)?; + self.db.write(batch).map_err(|rerr| { + DbError::rocksdb_cast_message( + rerr, + Some("Failed to write first block in db".to_owned()), + ) + })?; + + Ok(()) + } + + pub fn put_meta_last_block_in_db(&self, block_id: u64) -> DbResult<()> { + self.put(&LastBlockCell(block_id), ()) + } + + fn put_meta_last_block_in_db_batch( + &self, + block_id: u64, + batch: &mut WriteBatch, + ) -> DbResult<()> { + self.put_batch(&LastBlockCell(block_id), (), batch) + } + + pub fn put_meta_last_finalized_block_id(&self, block_id: Option) -> DbResult<()> { + self.put(&LastFinalizedBlockIdCell(block_id), ()) + } + + pub fn put_meta_is_first_block_set(&self) -> DbResult<()> { + self.put(&FirstBlockSetCell(true), ()) + } + + fn put_meta_latest_block_meta(&self, block_meta: &BlockMeta) -> DbResult<()> { + self.put(&LatestBlockMetaCellRef(block_meta), ()) + } + + fn put_meta_latest_block_meta_batch( + &self, + block_meta: &BlockMeta, + batch: &mut WriteBatch, + ) -> DbResult<()> { + self.put_batch(&LatestBlockMetaCellRef(block_meta), (), batch) + } + + pub fn latest_block_meta(&self) -> DbResult { + self.get::(()).map(|val| val.0) + } + + pub fn put_block( + &self, + block: &Block, + msg_id: MantleMsgId, + first: bool, + batch: &mut WriteBatch, + ) -> DbResult<()> { + let cf_block = self.block_column(); + + if !first { + let last_curr_block = self.get_meta_last_block_in_db()?; + + if block.header.block_id > last_curr_block { + self.put_meta_last_block_in_db_batch(block.header.block_id, batch)?; + self.put_meta_latest_block_meta_batch( + &BlockMeta { + id: block.header.block_id, + hash: block.header.hash, + msg_id, + }, + batch, + )?; + } + } + + batch.put_cf( + &cf_block, + borsh::to_vec(&block.header.block_id).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize block id".to_owned())) + })?, + borsh::to_vec(block).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize block data".to_owned())) + })?, + ); + Ok(()) + } + + pub fn get_block(&self, block_id: u64) -> DbResult> { + self.get_opt::(block_id) + .map(|opt| opt.map(|val| val.0)) + } + + pub fn get_nssa_state(&self) -> DbResult { + self.get::(()).map(|val| val.0) + } + + pub fn delete_block(&self, block_id: u64) -> DbResult<()> { + let cf_block = self.block_column(); + let key = borsh::to_vec(&block_id).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize block id".to_owned())) + })?; + + if self + .db + .get_cf(&cf_block, &key) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))? + .is_none() + { + return Err(DbError::db_interaction_error(format!( + "Block with id {block_id} not found" + ))); + } + + self.db + .delete_cf(&cf_block, key) + .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; + + Ok(()) + } + + pub fn mark_block_as_finalized(&self, block_id: u64) -> DbResult<()> { + let mut block = self.get_block(block_id)?.ok_or_else(|| { + DbError::db_interaction_error(format!("Block with id {block_id} not found")) + })?; + block.bedrock_status = BedrockStatus::Finalized; + + let cf_block = self.block_column(); + self.db + .put_cf( + &cf_block, + borsh::to_vec(&block_id).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize block id".to_owned()), + ) + })?, + borsh::to_vec(&block).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize block data".to_owned()), + ) + })?, + ) + .map_err(|rerr| { + DbError::rocksdb_cast_message( + rerr, + Some(format!("Failed to mark block {block_id} as finalized")), + ) + })?; + + Ok(()) + } + + pub fn get_all_blocks(&self) -> impl Iterator> { + let cf_block = self.block_column(); + self.db + .iterator_cf(&cf_block, rocksdb::IteratorMode::Start) + .map(|res| { + let (_key, value) = res.map_err(|rerr| { + DbError::rocksdb_cast_message( + rerr, + Some("Failed to get key value pair".to_owned()), + ) + })?; + + borsh::from_slice::(&value).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to deserialize block data".to_owned()), + ) + }) + }) + } + + pub fn atomic_update( + &self, + block: &Block, + msg_id: MantleMsgId, + state: &V03State, + ) -> DbResult<()> { + let block_id = block.header.block_id; + let mut batch = WriteBatch::default(); + self.put_block(block, msg_id, false, &mut batch)?; + self.put_nssa_state_in_db(state, &mut batch)?; + self.db.write(batch).map_err(|rerr| { + DbError::rocksdb_cast_message( + rerr, + Some(format!("Failed to udpate db with block {block_id}")), + ) + }) + } +} diff --git a/storage/src/sequencer/sequencer_cells.rs b/storage/src/sequencer/sequencer_cells.rs new file mode 100644 index 00000000..0ad092d7 --- /dev/null +++ b/storage/src/sequencer/sequencer_cells.rs @@ -0,0 +1,132 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use common::block::BlockMeta; +use nssa::V03State; + +use crate::{ + CF_META_NAME, DbResult, + cells::{SimpleReadableCell, SimpleStorableCell, SimpleWritableCell}, + error::DbError, + sequencer::{ + CF_NSSA_STATE_NAME, DB_META_LAST_FINALIZED_BLOCK_ID, DB_META_LATEST_BLOCK_META_KEY, + DB_NSSA_STATE_KEY, + }, +}; + +#[derive(BorshDeserialize)] +pub struct NSSAStateCellOwned(pub V03State); + +impl SimpleStorableCell for NSSAStateCellOwned { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_NSSA_STATE_KEY; + const CF_NAME: &'static str = CF_NSSA_STATE_NAME; +} + +impl SimpleReadableCell for NSSAStateCellOwned {} + +#[derive(BorshSerialize)] +pub struct NSSAStateCellRef<'state>(pub &'state V03State); + +impl SimpleStorableCell for NSSAStateCellRef<'_> { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_NSSA_STATE_KEY; + const CF_NAME: &'static str = CF_NSSA_STATE_NAME; +} + +impl SimpleWritableCell for NSSAStateCellRef<'_> { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize last state".to_owned())) + }) + } +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub struct LastFinalizedBlockIdCell(pub Option); + +impl SimpleStorableCell for LastFinalizedBlockIdCell { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_LAST_FINALIZED_BLOCK_ID; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for LastFinalizedBlockIdCell {} + +impl SimpleWritableCell for LastFinalizedBlockIdCell { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize last finalized block id".to_owned()), + ) + }) + } +} + +#[derive(BorshDeserialize)] +pub struct LatestBlockMetaCellOwned(pub BlockMeta); + +impl SimpleStorableCell for LatestBlockMetaCellOwned { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_LATEST_BLOCK_META_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for LatestBlockMetaCellOwned {} + +#[derive(BorshSerialize)] +pub struct LatestBlockMetaCellRef<'blockmeta>(pub &'blockmeta BlockMeta); + +impl SimpleStorableCell for LatestBlockMetaCellRef<'_> { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_LATEST_BLOCK_META_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleWritableCell for LatestBlockMetaCellRef<'_> { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message(err, Some("Failed to serialize last block meta".to_owned())) + }) + } +} + +#[cfg(test)] +mod uniform_tests { + use crate::{ + cells::SimpleStorableCell as _, + sequencer::sequencer_cells::{ + LatestBlockMetaCellOwned, LatestBlockMetaCellRef, NSSAStateCellOwned, NSSAStateCellRef, + }, + }; + + #[test] + fn state_ref_and_owned_is_aligned() { + assert_eq!(NSSAStateCellRef::CELL_NAME, NSSAStateCellOwned::CELL_NAME); + assert_eq!(NSSAStateCellRef::CF_NAME, NSSAStateCellOwned::CF_NAME); + assert_eq!( + NSSAStateCellRef::key_constructor(()).unwrap(), + NSSAStateCellOwned::key_constructor(()).unwrap() + ); + } + + #[test] + fn block_meta_ref_and_owned_is_aligned() { + assert_eq!( + LatestBlockMetaCellRef::CELL_NAME, + LatestBlockMetaCellOwned::CELL_NAME + ); + assert_eq!( + LatestBlockMetaCellRef::CF_NAME, + LatestBlockMetaCellOwned::CF_NAME + ); + assert_eq!( + LatestBlockMetaCellRef::key_constructor(()).unwrap(), + LatestBlockMetaCellOwned::key_constructor(()).unwrap() + ); + } +} diff --git a/test_program_methods/guest/Cargo.toml b/test_program_methods/guest/Cargo.toml index 1ca958b3..46edeb61 100644 --- a/test_program_methods/guest/Cargo.toml +++ b/test_program_methods/guest/Cargo.toml @@ -9,5 +9,7 @@ workspace = true [dependencies] nssa_core.workspace = true +clock_core.workspace = true risc0-zkvm.workspace = true +serde = { workspace = true, default-features = false } diff --git a/test_program_methods/guest/src/bin/auth_asserting_noop.rs b/test_program_methods/guest/src/bin/auth_asserting_noop.rs new file mode 100644 index 00000000..0b6d9176 --- /dev/null +++ b/test_program_methods/guest/src/bin/auth_asserting_noop.rs @@ -0,0 +1,40 @@ +use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; + +/// A variant of `noop` that asserts every `pre_state.is_authorized == true` before echoing +/// the `post_states`. Any unauthorized `pre_state` panics the guest, failing the whole +/// circuit proof. Used as a callee in private-PDA delegation tests to actually exercise the +/// authorization propagated through `ChainedCall.pda_seeds`. +type Instruction = (); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + .. + }, + instruction_words, + ) = read_nssa_inputs::(); + + for pre in &pre_states { + assert!( + pre.is_authorized, + "auth_asserting_noop: pre_state {} is not authorized", + pre.account_id + ); + } + + let post_states = pre_states + .iter() + .map(|account| AccountPostState::new(account.account.clone())) + .collect(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + post_states, + ) + .write(); +} diff --git a/test_program_methods/guest/src/bin/burner.rs b/test_program_methods/guest/src/bin/burner.rs index 991091c0..02be2d38 100644 --- a/test_program_methods/guest/src/bin/burner.rs +++ b/test_program_methods/guest/src/bin/burner.rs @@ -5,6 +5,8 @@ type Instruction = u128; fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: balance_to_burn, }, @@ -20,6 +22,8 @@ fn main() { account_post.balance = account_post.balance.saturating_sub(balance_to_burn); ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![pre], vec![AccountPostState::new(account_post)], diff --git a/test_program_methods/guest/src/bin/chain_caller.rs b/test_program_methods/guest/src/bin/chain_caller.rs index c5780665..5c124bed 100644 --- a/test_program_methods/guest/src/bin/chain_caller.rs +++ b/test_program_methods/guest/src/bin/chain_caller.rs @@ -13,6 +13,8 @@ type Instruction = (u128, ProgramId, u32, Option); fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: (balance, auth_transfer_id, num_chain_calls, pda_seed), }, @@ -55,6 +57,8 @@ fn main() { } ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![sender_pre.clone(), recipient_pre.clone()], vec![ diff --git a/test_program_methods/guest/src/bin/changer_claimer.rs b/test_program_methods/guest/src/bin/changer_claimer.rs index ee82ec16..6d2b51b4 100644 --- a/test_program_methods/guest/src/bin/changer_claimer.rs +++ b/test_program_methods/guest/src/bin/changer_claimer.rs @@ -6,6 +6,8 @@ type Instruction = (Option>, bool); fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: (data_opt, should_claim), }, @@ -33,5 +35,12 @@ fn main() { AccountPostState::new(account_post) }; - ProgramOutput::new(instruction_words, vec![pre], vec![post_state]).write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![pre], + vec![post_state], + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/claimer.rs b/test_program_methods/guest/src/bin/claimer.rs index e6239381..a3a7fb19 100644 --- a/test_program_methods/guest/src/bin/claimer.rs +++ b/test_program_methods/guest/src/bin/claimer.rs @@ -5,6 +5,8 @@ type Instruction = (); fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: (), }, @@ -17,5 +19,12 @@ fn main() { let account_post = AccountPostState::new_claimed(pre.account.clone(), Claim::Authorized); - ProgramOutput::new(instruction_words, vec![pre], vec![account_post]).write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![pre], + vec![account_post], + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/clock_chain_caller.rs b/test_program_methods/guest/src/bin/clock_chain_caller.rs new file mode 100644 index 00000000..cdbe5214 --- /dev/null +++ b/test_program_methods/guest/src/bin/clock_chain_caller.rs @@ -0,0 +1,46 @@ +use nssa_core::{ + Timestamp, + program::{ + AccountPostState, ChainedCall, ProgramId, ProgramInput, ProgramOutput, read_nssa_inputs, + }, +}; +use risc0_zkvm::serde::to_vec; + +type Instruction = (ProgramId, Timestamp); // (clock_program_id, timestamp) + +/// A program that chain-calls the clock program with the clock accounts it received as pre-states. +/// Used in tests to verify that user transactions cannot modify clock accounts, even indirectly +/// via chain calls. +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (clock_program_id, timestamp), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let post_states: Vec<_> = pre_states + .iter() + .map(|pre| AccountPostState::new(pre.account.clone())) + .collect(); + + let chained_call = ChainedCall { + program_id: clock_program_id, + instruction_data: to_vec(×tamp).unwrap(), + pre_states: pre_states.clone(), + pda_seeds: vec![], + }; + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + post_states, + ) + .with_chained_calls(vec![chained_call]) + .write(); +} diff --git a/test_program_methods/guest/src/bin/data_changer.rs b/test_program_methods/guest/src/bin/data_changer.rs index 730a7180..3969d7f6 100644 --- a/test_program_methods/guest/src/bin/data_changer.rs +++ b/test_program_methods/guest/src/bin/data_changer.rs @@ -6,6 +6,8 @@ type Instruction = Vec; fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: data, }, @@ -23,6 +25,8 @@ fn main() { .expect("provided data should fit into data limit"); ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![pre], vec![AccountPostState::new_claimed( diff --git a/test_program_methods/guest/src/bin/extra_output.rs b/test_program_methods/guest/src/bin/extra_output.rs index 3adc591c..3a5df556 100644 --- a/test_program_methods/guest/src/bin/extra_output.rs +++ b/test_program_methods/guest/src/bin/extra_output.rs @@ -6,7 +6,15 @@ use nssa_core::{ type Instruction = (); fn main() { - let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + .. + }, + instruction_words, + ) = read_nssa_inputs::(); let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { return; @@ -15,6 +23,8 @@ fn main() { let account_pre = pre.account.clone(); ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![pre], vec![ diff --git a/test_program_methods/guest/src/bin/flash_swap_callback.rs b/test_program_methods/guest/src/bin/flash_swap_callback.rs new file mode 100644 index 00000000..251833bb --- /dev/null +++ b/test_program_methods/guest/src/bin/flash_swap_callback.rs @@ -0,0 +1,94 @@ +//! Flash swap callback, the user logic step in the "prep → callback → assert" pattern. +//! +//! # Role +//! +//! This program is called as chained call 2 in the flash swap sequence: +//! 1. Token transfer out (vault → receiver) +//! 2. **This callback** (user logic) +//! 3. Invariant check (assert vault balance restored) +//! +//! In a real flash swap, this would contain the user's arbitrage or other logic. +//! In this test program, it is controlled by `return_funds`: +//! +//! - `return_funds = true`: emits a token transfer (receiver → vault) to return the funds. The +//! invariant check will pass and the transaction will succeed. +//! +//! - `return_funds = false`: emits no transfers. Funds stay with the receiver. The invariant check +//! will fail (vault balance < initial), causing full atomic rollback. This simulates a malicious +//! or buggy callback that does not repay the flash loan. +//! +//! # Note on `caller_program_id` +//! +//! This program does not enforce any access control on `caller_program_id`. +//! It is designed to be called by the flash swap initiator but could in principle be +//! called by any program. In production, a callback would typically verify the caller +//! if it needs to trust the context it is called from. + +use nssa_core::program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct CallbackInstruction { + /// If true, return the borrowed funds to the vault (happy path). + /// If false, keep the funds (simulates a malicious callback, triggers rollback). + pub return_funds: bool, + pub token_program_id: ProgramId, + pub amount: u128, +} + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, // not enforced in this callback + pre_states, + instruction, + }, + instruction_words, + ) = read_nssa_inputs::(); + + // pre_states[0] = vault (after transfer out), pre_states[1] = receiver (after transfer out) + let Ok([vault_pre, receiver_pre]) = <[_; 2]>::try_from(pre_states) else { + panic!("Callback requires exactly 2 accounts: vault, receiver"); + }; + + let mut chained_calls = Vec::new(); + + if instruction.return_funds { + // Happy path: return the borrowed funds via a token transfer (receiver → vault). + // The receiver is a PDA of this callback program (seed = [1_u8; 32]). + // Mark the receiver as authorized since it will be PDA-authorized in this chained call. + let mut receiver_authorized = receiver_pre.clone(); + receiver_authorized.is_authorized = true; + let transfer_instruction = risc0_zkvm::serde::to_vec(&instruction.amount) + .expect("transfer instruction serialization"); + + chained_calls.push(ChainedCall { + program_id: instruction.token_program_id, + pre_states: vec![receiver_authorized, vault_pre.clone()], + instruction_data: transfer_instruction, + pda_seeds: vec![PdaSeed::new([1_u8; 32])], + }); + } + // Malicious path (return_funds = false): emit no chained calls. + // The vault balance will not be restored, so the invariant check in the initiator + // will panic, rolling back the entire transaction including the initial transfer out. + + // The callback itself makes no direct state changes, accounts pass through unchanged. + // All mutations go through the token program via chained calls. + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![vault_pre.clone(), receiver_pre.clone()], + vec![ + AccountPostState::new(vault_pre.account), + AccountPostState::new(receiver_pre.account), + ], + ) + .with_chained_calls(chained_calls) + .write(); +} diff --git a/test_program_methods/guest/src/bin/flash_swap_initiator.rs b/test_program_methods/guest/src/bin/flash_swap_initiator.rs new file mode 100644 index 00000000..27d1f317 --- /dev/null +++ b/test_program_methods/guest/src/bin/flash_swap_initiator.rs @@ -0,0 +1,216 @@ +//! Flash swap initiator, demonstrates the "prep → callback → assert" pattern using +//! generalized multi tail-calls with `self_program_id` and `caller_program_id`. +//! +//! # Pattern +//! +//! A flash swap lets a program optimistically transfer tokens out, run arbitrary user +//! logic (the callback), then assert that invariants hold after the callback. The entire +//! sequence is a single atomic transaction: if any step fails, all state changes roll back. +//! +//! # How it works +//! +//! This program handles two instruction variants: +//! +//! - `Initiate` (external): the top-level entrypoint. Emits 3 chained calls: +//! 1. Token transfer out (vault → receiver) +//! 2. User callback (arbitrary logic, e.g. arbitrage) +//! 3. Self-call to `InvariantCheck` (using `self_program_id` to reference itself) +//! +//! - `InvariantCheck` (internal): enforces that the vault balance was restored after the callback. +//! Uses `caller_program_id == Some(self_program_id)` to prevent standalone calls (this is the +//! visibility enforcement mechanism). +//! +//! # What this demonstrates +//! +//! - `self_program_id`: enables a program to chain back to itself (step 3 above) +//! - `caller_program_id`: enables a program to restrict which callers can invoke an instruction +//! - Computed intermediate states: the initiator computes expected intermediate account states from +//! the `pre_states` and amount, keeping the instruction minimal. +//! - Atomic rollback: if the callback doesn't return funds, the invariant check fails, and all +//! state changes from steps 1 and 2 are rolled back automatically. +//! +//! # Tests +//! +//! See `nssa/src/state.rs` for integration tests: +//! - `flash_swap_successful`: full round-trip, funds returned, state unchanged +//! - `flash_swap_callback_keeps_funds_rollback`: callback keeps funds, full rollback +//! - `flash_swap_self_call_targets_correct_program`: zero-amount self-call isolation test +//! - `flash_swap_standalone_invariant_check_rejected`: `caller_program_id` access control + +use nssa_core::program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub enum FlashSwapInstruction { + /// External entrypoint: initiate a flash swap. + /// + /// Emits 3 chained calls: + /// 1. Token transfer (vault → receiver, `amount_out`) + /// 2. Callback (user logic, e.g. arbitrage) + /// 3. Self-call `InvariantCheck` (verify vault balance did not decrease) + /// + /// Intermediate account states are computed inside the program from `pre_states` and + /// `amount_out`. + Initiate { + token_program_id: ProgramId, + callback_program_id: ProgramId, + amount_out: u128, + callback_instruction_data: Vec, + }, + /// Internal: verify the vault invariant holds after callback execution. + /// + /// Access control: only callable as a chained call from this program itself. + /// This is enforced by checking `caller_program_id == Some(self_program_id)`. + /// Any attempt to call this instruction as a standalone top-level transaction + /// will be rejected because `caller_program_id` will be `None`. + InvariantCheck { min_vault_balance: u128 }, +} + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction, + }, + instruction_words, + ) = read_nssa_inputs::(); + + match instruction { + FlashSwapInstruction::Initiate { + token_program_id, + callback_program_id, + amount_out, + callback_instruction_data, + } => { + let Ok([vault_pre, receiver_pre]) = <[_; 2]>::try_from(pre_states) else { + panic!("Initiate requires exactly 2 accounts: vault, receiver"); + }; + + // Capture initial vault balance, the invariant check will verify it is restored. + let min_vault_balance = vault_pre.account.balance; + + // Compute intermediate account states from pre_states and amount_out. + let mut vault_after_transfer = vault_pre.clone(); + vault_after_transfer.account.balance = vault_pre + .account + .balance + .checked_sub(amount_out) + .expect("vault has insufficient balance for flash swap"); + + let mut receiver_after_transfer = receiver_pre.clone(); + receiver_after_transfer.account.balance = receiver_pre + .account + .balance + .checked_add(amount_out) + .expect("receiver balance overflow"); + + let mut vault_after_callback = vault_after_transfer.clone(); + vault_after_callback.account.balance = vault_after_transfer + .account + .balance + .checked_add(amount_out) + .expect("vault balance overflow after callback"); + + // Chained call 1: Token transfer (vault → receiver). + // The vault is a PDA of this initiator program (seed = [0_u8; 32]), so we provide + // the PDA seed to authorize the token program to debit the vault on our behalf. + // Mark the vault as authorized since it will be PDA-authorized in this chained call. + let mut vault_authorized = vault_pre.clone(); + vault_authorized.is_authorized = true; + let transfer_instruction = + risc0_zkvm::serde::to_vec(&amount_out).expect("transfer instruction serialization"); + let call_1 = ChainedCall { + program_id: token_program_id, + pre_states: vec![vault_authorized, receiver_pre.clone()], + instruction_data: transfer_instruction, + pda_seeds: vec![PdaSeed::new([0_u8; 32])], + }; + + // Chained call 2: User callback. + // Receives the post-transfer states as its pre_states. The callback may run + // arbitrary logic (arbitrage, etc.) and is expected to return funds to the vault. + let call_2 = ChainedCall { + program_id: callback_program_id, + pre_states: vec![vault_after_transfer, receiver_after_transfer], + instruction_data: callback_instruction_data, + pda_seeds: vec![], + }; + + // Chained call 3: Self-call to enforce the invariant. + // Uses `self_program_id` to reference this program, the key feature that enables + // the "prep → callback → assert" pattern without a separate checker program. + // If the callback did not return funds, vault_after_callback.balance < + // min_vault_balance and this call will panic, rolling back the entire + // transaction. + let invariant_instruction = + risc0_zkvm::serde::to_vec(&FlashSwapInstruction::InvariantCheck { + min_vault_balance, + }) + .expect("invariant instruction serialization"); + let call_3 = ChainedCall { + program_id: self_program_id, // self-referential chained call + pre_states: vec![vault_after_callback], + instruction_data: invariant_instruction, + pda_seeds: vec![], + }; + + // The initiator itself makes no direct state changes. + // All mutations happen inside the chained calls (token transfers). + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![vault_pre.clone(), receiver_pre.clone()], + vec![ + AccountPostState::new(vault_pre.account), + AccountPostState::new(receiver_pre.account), + ], + ) + .with_chained_calls(vec![call_1, call_2, call_3]) + .write(); + } + + FlashSwapInstruction::InvariantCheck { min_vault_balance } => { + // Visibility enforcement: `InvariantCheck` is an internal instruction. + // It must only be called as a chained call from this program itself (via `Initiate`). + // When called as a top-level transaction, `caller_program_id` is `None` → panics. + // When called as a chained call from `Initiate`, `caller_program_id` is + // `Some(self_program_id)` → passes. + assert_eq!( + caller_program_id, + Some(self_program_id), + "InvariantCheck is an internal instruction: must be called by flash_swap_initiator \ + via a chained call", + ); + + let Ok([vault]) = <[_; 1]>::try_from(pre_states) else { + panic!("InvariantCheck requires exactly 1 account: vault"); + }; + + // The core invariant: vault balance must not have decreased. + // If the callback returned funds, this passes. If not, this panics and + // the entire transaction (including the prior token transfer) rolls back. + assert!( + vault.account.balance >= min_vault_balance, + "Flash swap invariant violated: vault balance {} < minimum {}", + vault.account.balance, + min_vault_balance + ); + + // Pass-through: no state changes in the invariant check step. + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![vault.clone()], + vec![AccountPostState::new(vault.account)], + ) + .write(); + } + } +} diff --git a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs b/test_program_methods/guest/src/bin/malicious_authorization_changer.rs index 7452d337..f7aba4a0 100644 --- a/test_program_methods/guest/src/bin/malicious_authorization_changer.rs +++ b/test_program_methods/guest/src/bin/malicious_authorization_changer.rs @@ -14,6 +14,8 @@ type Instruction = (u128, ProgramId); fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: (balance, transfer_program_id), }, @@ -40,6 +42,8 @@ fn main() { }; ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![sender.clone(), receiver.clone()], vec![ diff --git a/test_program_methods/guest/src/bin/malicious_caller_program_id.rs b/test_program_methods/guest/src/bin/malicious_caller_program_id.rs new file mode 100644 index 00000000..2326190e --- /dev/null +++ b/test_program_methods/guest/src/bin/malicious_caller_program_id.rs @@ -0,0 +1,34 @@ +use nssa_core::program::{ + AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs, +}; + +type Instruction = (); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id: _, // ignore the actual caller + pre_states, + instruction: (), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let post_states = pre_states + .iter() + .map(|a| AccountPostState::new(a.account.clone())) + .collect(); + + // Deliberately output wrong caller_program_id. + // A real caller_program_id is None for a top-level call, so we spoof Some(DEFAULT_PROGRAM_ID) + // to simulate a program claiming it was invoked by another program when it was not. + ProgramOutput::new( + self_program_id, + Some(DEFAULT_PROGRAM_ID), // WRONG: should be None for a top-level call + instruction_words, + pre_states, + post_states, + ) + .write(); +} diff --git a/test_program_methods/guest/src/bin/malicious_self_program_id.rs b/test_program_methods/guest/src/bin/malicious_self_program_id.rs new file mode 100644 index 00000000..be447ab9 --- /dev/null +++ b/test_program_methods/guest/src/bin/malicious_self_program_id.rs @@ -0,0 +1,32 @@ +use nssa_core::program::{ + AccountPostState, DEFAULT_PROGRAM_ID, ProgramInput, ProgramOutput, read_nssa_inputs, +}; + +type Instruction = (); + +fn main() { + let ( + ProgramInput { + self_program_id: _, // ignore the correct ID + caller_program_id, + pre_states, + instruction: (), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let post_states = pre_states + .iter() + .map(|a| AccountPostState::new(a.account.clone())) + .collect(); + + // Deliberately output wrong self_program_id + ProgramOutput::new( + DEFAULT_PROGRAM_ID, // WRONG: should be self_program_id + caller_program_id, + instruction_words, + pre_states, + post_states, + ) + .write(); +} diff --git a/test_program_methods/guest/src/bin/minter.rs b/test_program_methods/guest/src/bin/minter.rs index ac29e4d3..1f31ca05 100644 --- a/test_program_methods/guest/src/bin/minter.rs +++ b/test_program_methods/guest/src/bin/minter.rs @@ -3,7 +3,15 @@ use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nss type Instruction = (); fn main() { - let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + .. + }, + instruction_words, + ) = read_nssa_inputs::(); let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { return; @@ -17,6 +25,8 @@ fn main() { .expect("Balance overflow"); ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![pre], vec![AccountPostState::new(account_post)], diff --git a/test_program_methods/guest/src/bin/missing_output.rs b/test_program_methods/guest/src/bin/missing_output.rs index b485e87a..d7d2778d 100644 --- a/test_program_methods/guest/src/bin/missing_output.rs +++ b/test_program_methods/guest/src/bin/missing_output.rs @@ -3,7 +3,15 @@ use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nss type Instruction = (); fn main() { - let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + .. + }, + instruction_words, + ) = read_nssa_inputs::(); let Ok([pre1, pre2]) = <[_; 2]>::try_from(pre_states) else { return; @@ -12,6 +20,8 @@ fn main() { let account_pre1 = pre1.account.clone(); ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![pre1, pre2], vec![AccountPostState::new(account_pre1)], diff --git a/test_program_methods/guest/src/bin/modified_transfer.rs b/test_program_methods/guest/src/bin/modified_transfer.rs index a89c72fb..2c05921c 100644 --- a/test_program_methods/guest/src/bin/modified_transfer.rs +++ b/test_program_methods/guest/src/bin/modified_transfer.rs @@ -64,6 +64,8 @@ fn main() { // Read input accounts. let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: balance_to_move, }, @@ -80,5 +82,12 @@ fn main() { } _ => panic!("invalid params"), }; - ProgramOutput::new(instruction_data, pre_states, post_states).write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_data, + pre_states, + post_states, + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/nonce_changer.rs b/test_program_methods/guest/src/bin/nonce_changer.rs index 0cecdc81..c6e851fe 100644 --- a/test_program_methods/guest/src/bin/nonce_changer.rs +++ b/test_program_methods/guest/src/bin/nonce_changer.rs @@ -3,7 +3,15 @@ use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nss type Instruction = (); fn main() { - let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + .. + }, + instruction_words, + ) = read_nssa_inputs::(); let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { return; @@ -14,6 +22,8 @@ fn main() { account_post.nonce.public_account_nonce_increment(); ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![pre], vec![AccountPostState::new(account_post)], diff --git a/test_program_methods/guest/src/bin/noop.rs b/test_program_methods/guest/src/bin/noop.rs index 35a07765..fc92aebe 100644 --- a/test_program_methods/guest/src/bin/noop.rs +++ b/test_program_methods/guest/src/bin/noop.rs @@ -3,11 +3,26 @@ use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nss type Instruction = (); fn main() { - let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + .. + }, + instruction_words, + ) = read_nssa_inputs::(); let post_states = pre_states .iter() .map(|account| AccountPostState::new(account.account.clone())) .collect(); - ProgramOutput::new(instruction_words, pre_states, post_states).write(); + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + post_states, + ) + .write(); } diff --git a/test_program_methods/guest/src/bin/pda_claimer.rs b/test_program_methods/guest/src/bin/pda_claimer.rs new file mode 100644 index 00000000..5dec4da4 --- /dev/null +++ b/test_program_methods/guest/src/bin/pda_claimer.rs @@ -0,0 +1,32 @@ +use nssa_core::program::{ + AccountPostState, Claim, PdaSeed, ProgramInput, ProgramOutput, read_nssa_inputs, +}; + +type Instruction = PdaSeed; + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: seed, + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { + return; + }; + + let account_post = AccountPostState::new_claimed(pre.account.clone(), Claim::Pda(seed)); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![pre], + vec![account_post], + ) + .write(); +} diff --git a/test_program_methods/guest/src/bin/pinata_cooldown.rs b/test_program_methods/guest/src/bin/pinata_cooldown.rs new file mode 100644 index 00000000..9e8bde3b --- /dev/null +++ b/test_program_methods/guest/src/bin/pinata_cooldown.rs @@ -0,0 +1,116 @@ +//! Cooldown-based pinata program. +//! +//! A Piñata program that uses the on-chain clock to prevent abuse. +//! After each prize claim the program records the current timestamp; the next claim is only +//! allowed once a configurable cooldown period has elapsed. +//! +//! Expected pre-states (in order): +//! 0 - pinata account (authorized, owned by this program) +//! 1 - winner account +//! 2 - clock account `CLOCK_01`. +//! +//! Pinata account data layout (24 bytes): +//! [prize: u64 LE | `cooldown_ms`: u64 LE | `last_claim_timestamp`: u64 LE]. + +use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData}; +use nssa_core::program::{AccountPostState, Claim, ProgramInput, ProgramOutput, read_nssa_inputs}; + +type Instruction = (); + +struct PinataState { + prize: u128, + cooldown_ms: u64, + last_claim_timestamp: u64, +} + +impl PinataState { + fn from_bytes(bytes: &[u8]) -> Self { + assert!(bytes.len() >= 32, "Pinata account data too short"); + let prize = u128::from_le_bytes(bytes[..16].try_into().unwrap()); + let cooldown_ms = u64::from_le_bytes(bytes[16..24].try_into().unwrap()); + let last_claim_timestamp = u64::from_le_bytes(bytes[24..32].try_into().unwrap()); + Self { + prize, + cooldown_ms, + last_claim_timestamp, + } + } + + fn to_bytes(&self) -> Vec { + let mut buf = Vec::with_capacity(32); + buf.extend_from_slice(&self.prize.to_le_bytes()); + buf.extend_from_slice(&self.cooldown_ms.to_le_bytes()); + buf.extend_from_slice(&self.last_claim_timestamp.to_le_bytes()); + buf + } +} + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([pinata, winner, clock_pre]) = <[_; 3]>::try_from(pre_states) else { + panic!("Expected exactly 3 input accounts: pinata, winner, clock"); + }; + + // Check the clock account is the system clock account + assert_eq!(clock_pre.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID); + + let clock_data = ClockAccountData::from_bytes(&clock_pre.account.data.clone().into_inner()); + let current_timestamp = clock_data.timestamp; + + let pinata_state = PinataState::from_bytes(&pinata.account.data.clone().into_inner()); + + // Enforce cooldown: the elapsed time since the last claim must exceed the cooldown period. + let elapsed = current_timestamp.saturating_sub(pinata_state.last_claim_timestamp); + assert!( + elapsed >= pinata_state.cooldown_ms, + "Cooldown not elapsed: {elapsed}ms since last claim, need {}ms", + pinata_state.cooldown_ms, + ); + + let mut pinata_post = pinata.account.clone(); + let mut winner_post = winner.account.clone(); + + pinata_post.balance = pinata_post + .balance + .checked_sub(pinata_state.prize) + .expect("Not enough balance in the pinata"); + winner_post.balance = winner_post + .balance + .checked_add(pinata_state.prize) + .expect("Overflow when adding prize to winner"); + + // Update the last claim timestamp. + let updated_state = PinataState { + last_claim_timestamp: current_timestamp, + ..pinata_state + }; + pinata_post.data = updated_state + .to_bytes() + .try_into() + .expect("Pinata state should fit in account data"); + + // Clock account is read-only. + let clock_post = clock_pre.account.clone(); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![pinata, winner, clock_pre], + vec![ + AccountPostState::new_claimed_if_default(pinata_post, Claim::Authorized), + AccountPostState::new(winner_post), + AccountPostState::new(clock_post), + ], + ) + .write(); +} diff --git a/test_program_methods/guest/src/bin/private_pda_delegator.rs b/test_program_methods/guest/src/bin/private_pda_delegator.rs new file mode 100644 index 00000000..fe55045e --- /dev/null +++ b/test_program_methods/guest/src/bin/private_pda_delegator.rs @@ -0,0 +1,51 @@ +use nssa_core::program::{ + AccountPostState, ChainedCall, Claim, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, +}; +use risc0_zkvm::serde::to_vec; + +/// Claims the sole `pre_state` as a PDA with `claim_seed`, then chains to `callee_program_id` +/// delegating authorization with `delegated_seed` in `pda_seeds`. When `claim_seed == +/// delegated_seed` this exercises the happy caller-seeds authorization path for mask-3 private +/// PDAs in `validate_and_sync_states`; when they differ, the callee's mask-3 `pre_state` has +/// no matching authorization source and the circuit must reject. +type Instruction = (PdaSeed, PdaSeed, ProgramId); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (claim_seed, delegated_seed, callee_program_id), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { + return; + }; + + let claimed = AccountPostState::new_claimed(pre.account.clone(), Claim::Pda(claim_seed)); + + let mut pre_for_callee = pre.clone(); + pre_for_callee.is_authorized = true; + pre_for_callee.account.program_owner = self_program_id; + + let chained_call = ChainedCall { + program_id: callee_program_id, + instruction_data: to_vec(&()).unwrap(), + pre_states: vec![pre_for_callee], + pda_seeds: vec![delegated_seed], + }; + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![pre], + vec![claimed], + ) + .with_chained_calls(vec![chained_call]) + .write(); +} diff --git a/test_program_methods/guest/src/bin/program_owner_changer.rs b/test_program_methods/guest/src/bin/program_owner_changer.rs index 7e421351..0282b5cc 100644 --- a/test_program_methods/guest/src/bin/program_owner_changer.rs +++ b/test_program_methods/guest/src/bin/program_owner_changer.rs @@ -3,7 +3,15 @@ use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nss type Instruction = (); fn main() { - let (ProgramInput { pre_states, .. }, instruction_words) = read_nssa_inputs::(); + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + .. + }, + instruction_words, + ) = read_nssa_inputs::(); let Ok([pre]) = <[_; 1]>::try_from(pre_states) else { return; @@ -14,6 +22,8 @@ fn main() { account_post.program_owner = [0, 1, 2, 3, 4, 5, 6, 7]; ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![pre], vec![AccountPostState::new(account_post)], diff --git a/test_program_methods/guest/src/bin/simple_balance_transfer.rs b/test_program_methods/guest/src/bin/simple_balance_transfer.rs index 9ee715e8..f324b371 100644 --- a/test_program_methods/guest/src/bin/simple_balance_transfer.rs +++ b/test_program_methods/guest/src/bin/simple_balance_transfer.rs @@ -5,6 +5,8 @@ type Instruction = u128; fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: balance, }, @@ -27,6 +29,8 @@ fn main() { .expect("Overflow when adding balance"); ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![sender_pre, receiver_pre], vec![ diff --git a/test_program_methods/guest/src/bin/time_locked_transfer.rs b/test_program_methods/guest/src/bin/time_locked_transfer.rs new file mode 100644 index 00000000..25595661 --- /dev/null +++ b/test_program_methods/guest/src/bin/time_locked_transfer.rs @@ -0,0 +1,72 @@ +//! Time-locked transfer program. +//! +//! Demonstrates how a program can include a clock account among its inputs and use the on-chain +//! timestamp in its logic. The transfer only executes when the clock timestamp is at or past a +//! caller-supplied deadline; otherwise the program panics. +//! +//! Expected pre-states (in order): +//! 0 - sender account (authorized) +//! 1 - receiver account +//! 2 - clock account (read-only, e.g. `CLOCK_01`). + +use clock_core::{CLOCK_01_PROGRAM_ACCOUNT_ID, ClockAccountData}; +use nssa_core::program::{AccountPostState, ProgramInput, ProgramOutput, read_nssa_inputs}; + +/// (`amount`, `deadline_timestamp`). +type Instruction = (u128, u64); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (amount, deadline), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([sender_pre, receiver_pre, clock_pre]) = <[_; 3]>::try_from(pre_states) else { + panic!("Expected exactly 3 input accounts: sender, receiver, clock"); + }; + + // Check the clock account is the system clock account + assert_eq!(clock_pre.account_id, CLOCK_01_PROGRAM_ACCOUNT_ID); + + // Read the current timestamp from the clock account. + let clock_data = ClockAccountData::from_bytes(&clock_pre.account.data.clone().into_inner()); + + assert!( + clock_data.timestamp >= deadline, + "Transfer is time-locked until timestamp {deadline}, current is {}", + clock_data.timestamp, + ); + + let mut sender_post = sender_pre.account.clone(); + let mut receiver_post = receiver_pre.account.clone(); + + sender_post.balance = sender_post + .balance + .checked_sub(amount) + .expect("Insufficient balance"); + receiver_post.balance = receiver_post + .balance + .checked_add(amount) + .expect("Balance overflow"); + + // Clock account is read-only: post state equals pre state. + let clock_post = clock_pre.account.clone(); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![sender_pre, receiver_pre, clock_pre], + vec![ + AccountPostState::new(sender_post), + AccountPostState::new(receiver_post), + AccountPostState::new(clock_post), + ], + ) + .write(); +} diff --git a/test_program_methods/guest/src/bin/two_pda_claimer.rs b/test_program_methods/guest/src/bin/two_pda_claimer.rs new file mode 100644 index 00000000..53aae666 --- /dev/null +++ b/test_program_methods/guest/src/bin/two_pda_claimer.rs @@ -0,0 +1,37 @@ +use nssa_core::program::{ + AccountPostState, Claim, PdaSeed, ProgramInput, ProgramOutput, read_nssa_inputs, +}; + +/// Claims two `pre_states` under the same `seed`. Used to exercise the tx-wide +/// `(program_id, seed) → AccountId` family-binding check: when both `pre_states` are mask-3 +/// with different npks, each `Claim::Pda(seed)` resolves to a different `AccountId` under the +/// same `(program, seed)` key, and the circuit must reject. +type Instruction = PdaSeed; + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: seed, + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([pre_a, pre_b]) = <[_; 2]>::try_from(pre_states) else { + return; + }; + + let claim_a = AccountPostState::new_claimed(pre_a.account.clone(), Claim::Pda(seed)); + let claim_b = AccountPostState::new_claimed(pre_b.account.clone(), Claim::Pda(seed)); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![pre_a, pre_b], + vec![claim_a, claim_b], + ) + .write(); +} diff --git a/test_program_methods/guest/src/bin/validity_window.rs b/test_program_methods/guest/src/bin/validity_window.rs index a0ff9f36..03100e8e 100644 --- a/test_program_methods/guest/src/bin/validity_window.rs +++ b/test_program_methods/guest/src/bin/validity_window.rs @@ -8,6 +8,8 @@ type Instruction = (BlockValidityWindow, TimestampValidityWindow); fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: (block_validity_window, timestamp_validity_window), }, @@ -21,6 +23,8 @@ fn main() { let post = pre.account.clone(); ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![pre], vec![AccountPostState::new(post)], diff --git a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs index 39f8ad69..212418a2 100644 --- a/test_program_methods/guest/src/bin/validity_window_chain_caller.rs +++ b/test_program_methods/guest/src/bin/validity_window_chain_caller.rs @@ -16,6 +16,8 @@ type Instruction = (BlockValidityWindow, ProgramId, BlockValidityWindow); fn main() { let ( ProgramInput { + self_program_id, + caller_program_id, pre_states, instruction: (block_validity_window, chained_program_id, chained_block_validity_window), }, @@ -38,6 +40,8 @@ fn main() { }; ProgramOutput::new( + self_program_id, + caller_program_id, instruction_words, vec![pre], vec![AccountPostState::new(post)], diff --git a/testnet_initial_state/src/lib.rs b/testnet_initial_state/src/lib.rs index 6224d71e..07d546fe 100644 --- a/testnet_initial_state/src/lib.rs +++ b/testnet_initial_state/src/lib.rs @@ -169,7 +169,7 @@ pub fn initial_commitments() -> Vec { initial_priv_accounts_private_keys() .into_iter() .map(|data| PrivateAccountPublicInitialData { - npk: data.key_chain.nullifier_public_key.clone(), + npk: data.key_chain.nullifier_public_key, account: data.account, }) .collect() @@ -196,25 +196,29 @@ pub fn initial_accounts() -> Vec { #[must_use] pub fn initial_state() -> V03State { - let initial_commitments: Vec = initial_commitments() - .iter() - .map(|init_comm_data| { - let npk = &init_comm_data.npk; + let initial_private_accounts: Vec<(nssa_core::Commitment, nssa_core::Nullifier)> = + initial_commitments() + .iter() + .map(|init_comm_data| { + let npk = &init_comm_data.npk; - let mut acc = init_comm_data.account.clone(); + let mut acc = init_comm_data.account.clone(); - acc.program_owner = nssa::program::Program::authenticated_transfer_program().id(); + acc.program_owner = nssa::program::Program::authenticated_transfer_program().id(); - nssa_core::Commitment::new(npk, &acc) - }) - .collect(); + ( + nssa_core::Commitment::new(npk, &acc), + nssa_core::Nullifier::for_account_initialization(npk), + ) + }) + .collect(); let init_accs: Vec<(nssa::AccountId, u128)> = initial_accounts() .iter() .map(|acc_data| (acc_data.account_id, acc_data.balance)) .collect(); - nssa::V03State::new_with_genesis_accounts(&init_accs, &initial_commitments) + nssa::V03State::new_with_genesis_accounts(&init_accs, initial_private_accounts, 0) } #[must_use] diff --git a/wallet-ffi/src/wallet.rs b/wallet-ffi/src/wallet.rs index 9117d0ee..93fc20aa 100644 --- a/wallet-ffi/src/wallet.rs +++ b/wallet-ffi/src/wallet.rs @@ -111,8 +111,8 @@ pub unsafe extern "C" fn wallet_ffi_create_new( return ptr::null_mut(); }; - match WalletCore::new_init_storage(config_path, storage_path, None, password) { - Ok(core) => { + match WalletCore::new_init_storage(config_path, storage_path, None, &password) { + Ok((core, _mnemonic)) => { let wrapper = Box::new(WalletWrapper { core: Mutex::new(core), }); diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index f77988a0..4e98b8ef 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -17,6 +17,7 @@ token_core.workspace = true amm_core.workspace = true testnet_initial_state.workspace = true ata_core.workspace = true +bip39.workspace = true anyhow.workspace = true thiserror.workspace = true diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index ebfe9896..3699609b 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, HashMap, btree_map::Entry}; use anyhow::Result; +use bip39::Mnemonic; use key_protocol::{ key_management::{ key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, @@ -95,7 +96,7 @@ impl WalletChainStore { }) } - pub fn new_storage(config: WalletConfig, password: String) -> Result { + pub fn new_storage(config: WalletConfig, password: &str) -> Result<(Self, Mnemonic)> { let mut public_init_acc_map = BTreeMap::new(); let mut private_init_acc_map = BTreeMap::new(); @@ -121,13 +122,43 @@ impl WalletChainStore { } } - let public_tree = KeyTreePublic::new(&SeedHolder::new_mnemonic(password.clone())); - let private_tree = KeyTreePrivate::new(&SeedHolder::new_mnemonic(password)); + // TODO: Use password for storage encryption + let _ = password; + let (seed_holder, mnemonic) = SeedHolder::new_mnemonic(""); + let public_tree = KeyTreePublic::new(&seed_holder); + let private_tree = KeyTreePrivate::new(&seed_holder); + + Ok(( + Self { + user_data: NSSAUserData::new_with_accounts( + public_init_acc_map, + private_init_acc_map, + public_tree, + private_tree, + )?, + wallet_config: config, + labels: HashMap::new(), + }, + mnemonic, + )) + } + + /// Restore storage from an existing mnemonic phrase. + pub fn restore_storage( + config: WalletConfig, + mnemonic: &Mnemonic, + password: &str, + ) -> Result { + // TODO: Use password for storage encryption + let _ = password; + let seed_holder = SeedHolder::from_mnemonic(mnemonic, ""); + let public_tree = KeyTreePublic::new(&seed_holder); + let private_tree = KeyTreePrivate::new(&seed_holder); Ok(Self { user_data: NSSAUserData::new_with_accounts( - public_init_acc_map, - private_init_acc_map, + BTreeMap::new(), + BTreeMap::new(), public_tree, private_tree, )?, diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 2a8ed2c7..86ae7e35 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -10,7 +10,10 @@ use crate::{ WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, config::Label, - helperfunctions::{AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix}, + helperfunctions::{ + AccountPrivacyKind, HumanReadableAccount, parse_addr_with_privacy_prefix, + resolve_id_or_label, + }, }; /// Represents generic chain CLI subcommand. @@ -25,8 +28,16 @@ pub enum AccountSubcommand { #[arg(short, long)] keys: bool, /// Valid 32 byte base58 string with privacy prefix. - #[arg(short, long)] - account_id: String, + #[arg( + short, + long, + conflicts_with = "account_label", + required_unless_present = "account_label" + )] + account_id: Option, + /// Account label (alternative to --account-id). + #[arg(long, conflicts_with = "account_id")] + account_label: Option, }, /// Produce new public or private account. #[command(subcommand)] @@ -43,8 +54,16 @@ pub enum AccountSubcommand { /// Set a label for an account. Label { /// Valid 32 byte base58 string with privacy prefix. - #[arg(short, long)] - account_id: String, + #[arg( + short, + long, + conflicts_with = "account_label", + required_unless_present = "account_label" + )] + account_id: Option, + /// Account label (alternative to --account-id). + #[arg(long = "account-label", conflicts_with = "account_id")] + account_label: Option, /// The label to assign to the account. #[arg(short, long)] label: String, @@ -171,8 +190,15 @@ impl WalletSubcommand for AccountSubcommand { raw, keys, account_id, + account_label, } => { - let (account_id_str, addr_kind) = parse_addr_with_privacy_prefix(&account_id)?; + let resolved = resolve_id_or_label( + account_id, + account_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let (account_id_str, addr_kind) = parse_addr_with_privacy_prefix(&resolved)?; let account_id: nssa::AccountId = account_id_str.parse()?; @@ -371,8 +397,18 @@ impl WalletSubcommand for AccountSubcommand { Ok(SubcommandReturnValue::Empty) } - Self::Label { account_id, label } => { - let (account_id_str, _) = parse_addr_with_privacy_prefix(&account_id)?; + Self::Label { + account_id, + account_label, + label, + } => { + let resolved = resolve_id_or_label( + account_id, + account_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let (account_id_str, _) = parse_addr_with_privacy_prefix(&resolved)?; // Check if label is already used by a different account if let Some(existing_account) = wallet_core diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 6463dee8..1653e938 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -1,6 +1,7 @@ -use std::{io::Write as _, path::PathBuf}; +use std::{io::Write as _, path::PathBuf, str::FromStr as _}; use anyhow::{Context as _, Result}; +use bip39::Mnemonic; use clap::{Parser, Subcommand}; use common::{HashType, transaction::NSSATransaction}; use futures::TryFutureExt as _; @@ -167,8 +168,9 @@ pub async fn execute_subcommand( config_subcommand.handle_subcommand(wallet_core).await? } Command::RestoreKeys { depth } => { + let mnemonic = read_mnemonic_from_stdin()?; let password = read_password_from_stdin()?; - wallet_core.reset_storage(password)?; + wallet_core.restore_storage(&mnemonic, &password)?; execute_keys_restoration(wallet_core, depth).await?; SubcommandReturnValue::Empty @@ -212,6 +214,16 @@ pub fn read_password_from_stdin() -> Result { Ok(password.trim().to_owned()) } +pub fn read_mnemonic_from_stdin() -> Result { + let mut phrase = String::new(); + + print!("Input recovery phrase: "); + std::io::stdout().flush()?; + std::io::stdin().read_line(&mut phrase)?; + + Mnemonic::from_str(phrase.trim()).context("Invalid mnemonic phrase") +} + pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) -> Result<()> { wallet_core .storage diff --git a/wallet/src/cli/programs/amm.rs b/wallet/src/cli/programs/amm.rs index 7307569d..0f8a0fff 100644 --- a/wallet/src/cli/programs/amm.rs +++ b/wallet/src/cli/programs/amm.rs @@ -5,7 +5,7 @@ use nssa::AccountId; use crate::{ WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, - helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, + helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix, resolve_id_or_label}, program_facades::amm::Amm, }; @@ -19,25 +19,80 @@ pub enum AmmProgramAgnosticSubcommand { /// Only public execution allowed. New { /// `user_holding_a` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - user_holding_a: String, + #[arg( + long, + conflicts_with = "user_holding_a_label", + required_unless_present = "user_holding_a_label" + )] + user_holding_a: Option, + /// User holding A account label (alternative to --user-holding-a). + #[arg(long, conflicts_with = "user_holding_a")] + user_holding_a_label: Option, /// `user_holding_b` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - user_holding_b: String, + #[arg( + long, + conflicts_with = "user_holding_b_label", + required_unless_present = "user_holding_b_label" + )] + user_holding_b: Option, + /// User holding B account label (alternative to --user-holding-b). + #[arg(long, conflicts_with = "user_holding_b")] + user_holding_b_label: Option, /// `user_holding_lp` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - user_holding_lp: String, + #[arg( + long, + conflicts_with = "user_holding_lp_label", + required_unless_present = "user_holding_lp_label" + )] + user_holding_lp: Option, + /// User holding LP account label (alternative to --user-holding-lp). + #[arg(long, conflicts_with = "user_holding_lp")] + user_holding_lp_label: Option, #[arg(long)] balance_a: u128, #[arg(long)] balance_b: u128, }, - /// Swap. + /// Swap specifying exact input amount. /// /// The account associated with swapping token must be owned. /// /// Only public execution allowed. - Swap { + SwapExactInput { + /// `user_holding_a` - valid 32 byte base58 string with privacy prefix. + #[arg( + long, + conflicts_with = "user_holding_a_label", + required_unless_present = "user_holding_a_label" + )] + user_holding_a: Option, + /// User holding A account label (alternative to --user-holding-a). + #[arg(long, conflicts_with = "user_holding_a")] + user_holding_a_label: Option, + /// `user_holding_b` - valid 32 byte base58 string with privacy prefix. + #[arg( + long, + conflicts_with = "user_holding_b_label", + required_unless_present = "user_holding_b_label" + )] + user_holding_b: Option, + /// User holding B account label (alternative to --user-holding-b). + #[arg(long, conflicts_with = "user_holding_b")] + user_holding_b_label: Option, + #[arg(long)] + amount_in: u128, + #[arg(long)] + min_amount_out: u128, + /// `token_definition` - valid 32 byte base58 string WITHOUT privacy prefix. + #[arg(long)] + token_definition: String, + }, + /// Swap specifying exact output amount. + /// + /// The account associated with swapping token must be owned. + /// + /// Only public execution allowed. + SwapExactOutput { /// `user_holding_a` - valid 32 byte base58 string with privacy prefix. #[arg(long)] user_holding_a: String, @@ -45,9 +100,9 @@ pub enum AmmProgramAgnosticSubcommand { #[arg(long)] user_holding_b: String, #[arg(long)] - amount_in: u128, + exact_amount_out: u128, #[arg(long)] - min_amount_out: u128, + max_amount_in: u128, /// `token_definition` - valid 32 byte base58 string WITHOUT privacy prefix. #[arg(long)] token_definition: String, @@ -59,14 +114,35 @@ pub enum AmmProgramAgnosticSubcommand { /// Only public execution allowed. AddLiquidity { /// `user_holding_a` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - user_holding_a: String, + #[arg( + long, + conflicts_with = "user_holding_a_label", + required_unless_present = "user_holding_a_label" + )] + user_holding_a: Option, + /// User holding A account label (alternative to --user-holding-a). + #[arg(long, conflicts_with = "user_holding_a")] + user_holding_a_label: Option, /// `user_holding_b` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - user_holding_b: String, + #[arg( + long, + conflicts_with = "user_holding_b_label", + required_unless_present = "user_holding_b_label" + )] + user_holding_b: Option, + /// User holding B account label (alternative to --user-holding-b). + #[arg(long, conflicts_with = "user_holding_b")] + user_holding_b_label: Option, /// `user_holding_lp` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - user_holding_lp: String, + #[arg( + long, + conflicts_with = "user_holding_lp_label", + required_unless_present = "user_holding_lp_label" + )] + user_holding_lp: Option, + /// User holding LP account label (alternative to --user-holding-lp). + #[arg(long, conflicts_with = "user_holding_lp")] + user_holding_lp_label: Option, #[arg(long)] min_amount_lp: u128, #[arg(long)] @@ -81,14 +157,35 @@ pub enum AmmProgramAgnosticSubcommand { /// Only public execution allowed. RemoveLiquidity { /// `user_holding_a` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - user_holding_a: String, + #[arg( + long, + conflicts_with = "user_holding_a_label", + required_unless_present = "user_holding_a_label" + )] + user_holding_a: Option, + /// User holding A account label (alternative to --user-holding-a). + #[arg(long, conflicts_with = "user_holding_a")] + user_holding_a_label: Option, /// `user_holding_b` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - user_holding_b: String, + #[arg( + long, + conflicts_with = "user_holding_b_label", + required_unless_present = "user_holding_b_label" + )] + user_holding_b: Option, + /// User holding B account label (alternative to --user-holding-b). + #[arg(long, conflicts_with = "user_holding_b")] + user_holding_b_label: Option, /// `user_holding_lp` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - user_holding_lp: String, + #[arg( + long, + conflicts_with = "user_holding_lp_label", + required_unless_present = "user_holding_lp_label" + )] + user_holding_lp: Option, + /// User holding LP account label (alternative to --user-holding-lp). + #[arg(long, conflicts_with = "user_holding_lp")] + user_holding_lp_label: Option, #[arg(long)] balance_lp: u128, #[arg(long)] @@ -106,11 +203,32 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { match self { Self::New { user_holding_a, + user_holding_a_label, user_holding_b, + user_holding_b_label, user_holding_lp, + user_holding_lp_label, balance_a, balance_b, } => { + let user_holding_a = resolve_id_or_label( + user_holding_a, + user_holding_a_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let user_holding_b = resolve_id_or_label( + user_holding_b, + user_holding_b_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let user_holding_lp = resolve_id_or_label( + user_holding_lp, + user_holding_lp_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; let (user_holding_b, user_holding_b_privacy) = @@ -150,13 +268,27 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { } } } - Self::Swap { + Self::SwapExactInput { user_holding_a, + user_holding_a_label, user_holding_b, + user_holding_b_label, amount_in, min_amount_out, token_definition, } => { + let user_holding_a = resolve_id_or_label( + user_holding_a, + user_holding_a_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let user_holding_b = resolve_id_or_label( + user_holding_b, + user_holding_b_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; let (user_holding_b, user_holding_b_privacy) = @@ -168,7 +300,7 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { match (user_holding_a_privacy, user_holding_b_privacy) { (AccountPrivacyKind::Public, AccountPrivacyKind::Public) => { Amm(wallet_core) - .send_swap( + .send_swap_exact_input( user_holding_a, user_holding_b, amount_in, @@ -185,14 +317,70 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { } } } - Self::AddLiquidity { + Self::SwapExactOutput { user_holding_a, user_holding_b, + exact_amount_out, + max_amount_in, + token_definition, + } => { + let (user_holding_a, user_holding_a_privacy) = + parse_addr_with_privacy_prefix(&user_holding_a)?; + let (user_holding_b, user_holding_b_privacy) = + parse_addr_with_privacy_prefix(&user_holding_b)?; + + let user_holding_a: AccountId = user_holding_a.parse()?; + let user_holding_b: AccountId = user_holding_b.parse()?; + + match (user_holding_a_privacy, user_holding_b_privacy) { + (AccountPrivacyKind::Public, AccountPrivacyKind::Public) => { + Amm(wallet_core) + .send_swap_exact_output( + user_holding_a, + user_holding_b, + exact_amount_out, + max_amount_in, + token_definition.parse()?, + ) + .await?; + + Ok(SubcommandReturnValue::Empty) + } + _ => { + // ToDo: Implement after private multi-chain calls is available + anyhow::bail!("Only public execution allowed for Amm calls"); + } + } + } + Self::AddLiquidity { + user_holding_a, + user_holding_a_label, + user_holding_b, + user_holding_b_label, user_holding_lp, + user_holding_lp_label, min_amount_lp, max_amount_a, max_amount_b, } => { + let user_holding_a = resolve_id_or_label( + user_holding_a, + user_holding_a_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let user_holding_b = resolve_id_or_label( + user_holding_b, + user_holding_b_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let user_holding_lp = resolve_id_or_label( + user_holding_lp, + user_holding_lp_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; let (user_holding_b, user_holding_b_privacy) = @@ -235,12 +423,33 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { } Self::RemoveLiquidity { user_holding_a, + user_holding_a_label, user_holding_b, + user_holding_b_label, user_holding_lp, + user_holding_lp_label, balance_lp, min_amount_a, min_amount_b, } => { + let user_holding_a = resolve_id_or_label( + user_holding_a, + user_holding_a_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let user_holding_b = resolve_id_or_label( + user_holding_b, + user_holding_b_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let user_holding_lp = resolve_id_or_label( + user_holding_lp, + user_holding_lp_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; let (user_holding_a, user_holding_a_privacy) = parse_addr_with_privacy_prefix(&user_holding_a)?; let (user_holding_b, user_holding_b_privacy) = diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index b3f833ac..41008eac 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -7,7 +7,10 @@ use crate::{ AccDecodeData::Decode, WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, - helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, + helperfunctions::{ + AccountPrivacyKind, parse_addr_with_privacy_prefix, resolve_account_label, + resolve_id_or_label, + }, program_facades::native_token_transfer::NativeTokenTransfer, }; @@ -17,8 +20,15 @@ pub enum AuthTransferSubcommand { /// Initialize account under authenticated transfer program. Init { /// `account_id` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - account_id: String, + #[arg( + long, + conflicts_with = "account_label", + required_unless_present = "account_label" + )] + account_id: Option, + /// Account label (alternative to --account-id). + #[arg(long, conflicts_with = "account_id")] + account_label: Option, }, /// Send native tokens from one account to another with variable privacy. /// @@ -28,11 +38,21 @@ pub enum AuthTransferSubcommand { /// First is used for owned accounts, second otherwise. Send { /// from - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - from: String, + #[arg( + long, + conflicts_with = "from_label", + required_unless_present = "from_label" + )] + from: Option, + /// From account label (alternative to --from). + #[arg(long, conflicts_with = "from")] + from_label: Option, /// to - valid 32 byte base58 string with privacy prefix. - #[arg(long)] + #[arg(long, conflicts_with = "to_label")] to: Option, + /// To account label (alternative to --to). + #[arg(long, conflicts_with = "to")] + to_label: Option, /// `to_npk` - valid 32 byte hex string. #[arg(long)] to_npk: Option, @@ -51,8 +71,17 @@ impl WalletSubcommand for AuthTransferSubcommand { wallet_core: &mut WalletCore, ) -> Result { match self { - Self::Init { account_id } => { - let (account_id, addr_privacy) = parse_addr_with_privacy_prefix(&account_id)?; + Self::Init { + account_id, + account_label, + } => { + let resolved = resolve_id_or_label( + account_id, + account_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let (account_id, addr_privacy) = parse_addr_with_privacy_prefix(&resolved)?; match addr_privacy { AccountPrivacyKind::Public => { @@ -98,11 +127,30 @@ impl WalletSubcommand for AuthTransferSubcommand { } Self::Send { from, + from_label, to, + to_label, to_npk, to_vpk, amount, } => { + let from = resolve_id_or_label( + from, + from_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let to = match (to, to_label) { + (v, None) => v, + (None, Some(label)) => Some(resolve_account_label( + &label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?), + (Some(_), Some(_)) => { + anyhow::bail!("Provide only one of --to or --to-label") + } + }; let underlying_subcommand = match (to, to_npk, to_vpk) { (None, None, None) => { anyhow::bail!( diff --git a/wallet/src/cli/programs/pinata.rs b/wallet/src/cli/programs/pinata.rs index 94cb0649..6171a0f2 100644 --- a/wallet/src/cli/programs/pinata.rs +++ b/wallet/src/cli/programs/pinata.rs @@ -7,7 +7,7 @@ use crate::{ AccDecodeData::Decode, WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, - helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, + helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix, resolve_id_or_label}, program_facades::pinata::Pinata, }; @@ -17,8 +17,15 @@ pub enum PinataProgramAgnosticSubcommand { /// Claim pinata. Claim { /// to - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - to: String, + #[arg( + long, + conflicts_with = "to_label", + required_unless_present = "to_label" + )] + to: Option, + /// To account label (alternative to --to). + #[arg(long, conflicts_with = "to")] + to_label: Option, }, } @@ -28,7 +35,13 @@ impl WalletSubcommand for PinataProgramAgnosticSubcommand { wallet_core: &mut WalletCore, ) -> Result { let underlying_subcommand = match self { - Self::Claim { to } => { + Self::Claim { to, to_label } => { + let to = resolve_id_or_label( + to, + to_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; let (to, to_addr_privacy) = parse_addr_with_privacy_prefix(&to)?; match to_addr_privacy { diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index 4274b1da..0575da09 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -7,7 +7,10 @@ use crate::{ AccDecodeData::Decode, WalletCore, cli::{SubcommandReturnValue, WalletSubcommand}, - helperfunctions::{AccountPrivacyKind, parse_addr_with_privacy_prefix}, + helperfunctions::{ + AccountPrivacyKind, parse_addr_with_privacy_prefix, resolve_account_label, + resolve_id_or_label, + }, program_facades::token::Token, }; @@ -17,11 +20,25 @@ pub enum TokenProgramAgnosticSubcommand { /// Produce a new token. New { /// `definition_account_id` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - definition_account_id: String, + #[arg( + long, + conflicts_with = "definition_account_label", + required_unless_present = "definition_account_label" + )] + definition_account_id: Option, + /// Definition account label (alternative to --definition-account-id). + #[arg(long, conflicts_with = "definition_account_id")] + definition_account_label: Option, /// `supply_account_id` - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - supply_account_id: String, + #[arg( + long, + conflicts_with = "supply_account_label", + required_unless_present = "supply_account_label" + )] + supply_account_id: Option, + /// Supply account label (alternative to --supply-account-id). + #[arg(long, conflicts_with = "supply_account_id")] + supply_account_label: Option, #[arg(short, long)] name: String, #[arg(short, long)] @@ -35,11 +52,21 @@ pub enum TokenProgramAgnosticSubcommand { /// First is used for owned accounts, second otherwise. Send { /// from - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - from: String, + #[arg( + long, + conflicts_with = "from_label", + required_unless_present = "from_label" + )] + from: Option, + /// From account label (alternative to --from). + #[arg(long, conflicts_with = "from")] + from_label: Option, /// to - valid 32 byte base58 string with privacy prefix. - #[arg(long)] + #[arg(long, conflicts_with = "to_label")] to: Option, + /// To account label (alternative to --to). + #[arg(long, conflicts_with = "to")] + to_label: Option, /// `to_npk` - valid 32 byte hex string. #[arg(long)] to_npk: Option, @@ -58,11 +85,25 @@ pub enum TokenProgramAgnosticSubcommand { /// we can not modify foreign accounts. Burn { /// definition - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - definition: String, + #[arg( + long, + conflicts_with = "definition_label", + required_unless_present = "definition_label" + )] + definition: Option, + /// Definition account label (alternative to --definition). + #[arg(long, conflicts_with = "definition")] + definition_label: Option, /// holder - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - holder: String, + #[arg( + long, + conflicts_with = "holder_label", + required_unless_present = "holder_label" + )] + holder: Option, + /// Holder account label (alternative to --holder). + #[arg(long, conflicts_with = "holder")] + holder_label: Option, /// amount - amount of balance to burn. #[arg(long)] amount: u128, @@ -77,11 +118,21 @@ pub enum TokenProgramAgnosticSubcommand { /// First is used for owned accounts, second otherwise. Mint { /// definition - valid 32 byte base58 string with privacy prefix. - #[arg(long)] - definition: String, + #[arg( + long, + conflicts_with = "definition_label", + required_unless_present = "definition_label" + )] + definition: Option, + /// Definition account label (alternative to --definition). + #[arg(long, conflicts_with = "definition")] + definition_label: Option, /// holder - valid 32 byte base58 string with privacy prefix. - #[arg(long)] + #[arg(long, conflicts_with = "holder_label")] holder: Option, + /// Holder account label (alternative to --holder). + #[arg(long, conflicts_with = "holder")] + holder_label: Option, /// `holder_npk` - valid 32 byte hex string. #[arg(long)] holder_npk: Option, @@ -102,10 +153,24 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { match self { Self::New { definition_account_id, + definition_account_label, supply_account_id, + supply_account_label, name, total_supply, } => { + let definition_account_id = resolve_id_or_label( + definition_account_id, + definition_account_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let supply_account_id = resolve_id_or_label( + supply_account_id, + supply_account_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; let (definition_account_id, definition_addr_privacy) = parse_addr_with_privacy_prefix(&definition_account_id)?; let (supply_account_id, supply_addr_privacy) = @@ -158,11 +223,30 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { } Self::Send { from, + from_label, to, + to_label, to_npk, to_vpk, amount, } => { + let from = resolve_id_or_label( + from, + from_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let to = match (to, to_label) { + (v, None) => v, + (None, Some(label)) => Some(resolve_account_label( + &label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?), + (Some(_), Some(_)) => { + anyhow::bail!("Provide only one of --to or --to-label") + } + }; let underlying_subcommand = match (to, to_npk, to_vpk) { (None, None, None) => { anyhow::bail!( @@ -248,9 +332,23 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { } Self::Burn { definition, + definition_label, holder, + holder_label, amount, } => { + let definition = resolve_id_or_label( + definition, + definition_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let holder = resolve_id_or_label( + holder, + holder_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; let underlying_subcommand = { let (definition, definition_privacy) = parse_addr_with_privacy_prefix(&definition)?; @@ -300,11 +398,30 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { } Self::Mint { definition, + definition_label, holder, + holder_label, holder_npk, holder_vpk, amount, } => { + let definition = resolve_id_or_label( + definition, + definition_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?; + let holder = match (holder, holder_label) { + (v, None) => v, + (None, Some(label)) => Some(resolve_account_label( + &label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + )?), + (Some(_), Some(_)) => { + anyhow::bail!("Provide only one of --holder or --holder-label") + } + }; let underlying_subcommand = match (holder, holder_npk, holder_vpk) { (None, None, None) => { anyhow::bail!( diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index d82dedaf..37a27409 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -50,6 +50,66 @@ impl From for HumanReadableAccount { } } +/// Resolve an account id-or-label pair to a `Privacy/id` string. +/// +/// Exactly one of `id` or `label` must be `Some`. If `id` is provided it is +/// returned as-is; if `label` is provided it is resolved via +/// [`resolve_account_label`]. Any other combination returns an error. +pub fn resolve_id_or_label( + id: Option, + label: Option, + labels: &HashMap, + user_data: &NSSAUserData, +) -> Result { + match (id, label) { + (Some(id), None) => Ok(id), + (None, Some(label)) => resolve_account_label(&label, labels, user_data), + _ => anyhow::bail!("provide exactly one of account id or account label"), + } +} + +/// Resolve an account label to its full `Privacy/id` string representation. +/// +/// Looks up the label in the labels map and determines whether the account is +/// public or private by checking the user data key trees. +pub fn resolve_account_label( + label: &str, + labels: &HashMap, + user_data: &NSSAUserData, +) -> Result { + let account_id_str = labels + .iter() + .find(|(_, l)| l.to_string() == label) + .map(|(k, _)| k.clone()) + .ok_or_else(|| anyhow::anyhow!("No account found with label '{label}'"))?; + + let account_id: nssa::AccountId = account_id_str.parse()?; + + let privacy = if user_data + .public_key_tree + .account_id_map + .contains_key(&account_id) + || user_data + .default_pub_account_signing_keys + .contains_key(&account_id) + { + "Public" + } else if user_data + .private_key_tree + .account_id_map + .contains_key(&account_id) + || user_data + .default_user_private_accounts + .contains_key(&account_id) + { + "Private" + } else { + anyhow::bail!("Account with label '{label}' not found in wallet"); + }; + + Ok(format!("{privacy}/{account_id_str}")) +} + /// Get home dir for wallet. Env var `NSSA_WALLET_HOME_DIR` must be set before execution to succeed. fn get_home_nssa_var() -> Result { Ok(PathBuf::from_str(&std::env::var(HOME_DIR_ENV_VAR)?)?) diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index a09d477e..460cfcfd 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -11,6 +11,7 @@ use std::path::PathBuf; use anyhow::{Context as _, Result}; +use bip39::Mnemonic; use chain_storage::WalletChainStore; use common::{HashType, transaction::NSSATransaction}; use config::WalletConfig; @@ -117,15 +118,24 @@ impl WalletCore { config_path: PathBuf, storage_path: PathBuf, config_overrides: Option, - password: String, - ) -> Result { - Self::new( + password: &str, + ) -> Result<(Self, Mnemonic)> { + let mut mnemonic_out = None; + let wallet = Self::new( config_path, storage_path, config_overrides, - |config| WalletChainStore::new_storage(config, password), + |config| { + let (storage, mnemonic) = WalletChainStore::new_storage(config, password)?; + mnemonic_out = Some(mnemonic); + Ok(storage) + }, 0, - ) + )?; + Ok(( + wallet, + mnemonic_out.expect("mnemonic should be set after new_storage"), + )) } fn new( @@ -191,9 +201,13 @@ impl WalletCore { &self.storage } - /// Reset storage. - pub fn reset_storage(&mut self, password: String) -> Result<()> { - self.storage = WalletChainStore::new_storage(self.storage.wallet_config.clone(), password)?; + /// Restore storage from an existing mnemonic phrase. + pub fn restore_storage(&mut self, mnemonic: &Mnemonic, password: &str) -> Result<()> { + self.storage = WalletChainStore::restore_storage( + self.storage.wallet_config.clone(), + mnemonic, + password, + )?; Ok(()) } @@ -379,7 +393,7 @@ impl WalletCore { acc_manager.visibility_mask().to_vec(), private_account_keys .iter() - .map(|keys| (keys.npk.clone(), keys.ssk)) + .map(|keys| (keys.npk, keys.ssk)) .collect::>(), acc_manager.private_account_auth(), acc_manager.private_account_membership_proofs(), @@ -393,7 +407,7 @@ impl WalletCore { Vec::from_iter(acc_manager.public_account_nonces()), private_account_keys .iter() - .map(|keys| (keys.npk.clone(), keys.vpk.clone(), keys.epk.clone())) + .map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone())) .collect(), output, ) diff --git a/wallet/src/main.rs b/wallet/src/main.rs index e055bd63..cf8356db 100644 --- a/wallet/src/main.rs +++ b/wallet/src/main.rs @@ -46,13 +46,21 @@ async fn main() -> Result<()> { println!("Persistent storage not found, need to execute setup"); let password = read_password_from_stdin()?; - let wallet = WalletCore::new_init_storage( + let (wallet, mnemonic) = WalletCore::new_init_storage( config_path, storage_path, Some(config_overrides), - password, + &password, )?; + println!(); + println!("IMPORTANT: Write down your recovery phrase and store it securely."); + println!("This is the only way to recover your wallet if you lose access."); + println!(); + println!("Recovery phrase:"); + println!(" {mnemonic}"); + println!(); + wallet.store_persistent_data().await?; wallet }; diff --git a/wallet/src/pinata_interactions.rs b/wallet/src/pinata_interactions.rs index b883e7e6..77549772 100644 --- a/wallet/src/pinata_interactions.rs +++ b/wallet/src/pinata_interactions.rs @@ -59,7 +59,7 @@ impl WalletCore { &nssa::program::Program::serialize_instruction(solution).unwrap(), &[0, 1], &produce_random_nonces(1), - &[(winner_npk.clone(), shared_secret_winner.clone())], + &[(winner_npk, shared_secret_winner.clone())], &[(winner_nsk.unwrap())], &[winner_proof], &program.into(), @@ -71,7 +71,7 @@ impl WalletCore { vec![pinata_account_id], vec![], vec![( - winner_npk.clone(), + winner_npk, winner_vpk.clone(), eph_holder_winner.generate_ephemeral_public_key(), )], @@ -126,7 +126,7 @@ impl WalletCore { &nssa::program::Program::serialize_instruction(solution).unwrap(), &[0, 2], &produce_random_nonces(1), - &[(winner_npk.clone(), shared_secret_winner.clone())], + &[(winner_npk, shared_secret_winner.clone())], &[], &[], &program.into(), @@ -138,7 +138,7 @@ impl WalletCore { vec![pinata_account_id], vec![], vec![( - winner_npk.clone(), + winner_npk, winner_vpk.clone(), eph_holder_winner.generate_ephemeral_public_key(), )], diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 04056111..14a805c7 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -138,7 +138,7 @@ impl AccountManager { let eph_holder = EphemeralKeyHolder::new(&pre.npk); Some(PrivateAccountKeys { - npk: pre.npk.clone(), + npk: pre.npk, ssk: eph_holder.calculate_shared_secret_sender(&pre.vpk), vpk: pre.vpk.clone(), epk: eph_holder.generate_ephemeral_public_key(), diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs index d68de7a5..b31d0658 100644 --- a/wallet/src/program_facades/amm.rs +++ b/wallet/src/program_facades/amm.rs @@ -121,7 +121,7 @@ impl Amm<'_> { .await?) } - pub async fn send_swap( + pub async fn send_swap_exact_input( &self, user_holding_a: AccountId, user_holding_b: AccountId, @@ -129,7 +129,7 @@ impl Amm<'_> { min_amount_out: u128, token_definition_id_in: AccountId, ) -> Result { - let instruction = amm_core::Instruction::Swap { + let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in, min_amount_out, token_definition_id_in, @@ -168,34 +168,105 @@ impl Amm<'_> { user_holding_b, ]; - let account_id_auth; + let account_id_auth = if definition_token_a_id == token_definition_id_in { + user_holding_a + } else if definition_token_b_id == token_definition_id_in { + user_holding_b + } else { + return Err(ExecutionFailureKind::AccountDataError( + token_definition_id_in, + )); + }; - // Checking, which account are associated with TokenDefinition - let token_holder_acc_a = self + let nonces = self + .0 + .get_accounts_nonces(vec![account_id_auth]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let signing_key = self + .0 + .storage + .user_data + .get_pub_account_signing_key(account_id_auth) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + ) + .unwrap(); + + let witness_set = + nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]); + + let tx = nssa::PublicTransaction::new(message, witness_set); + + Ok(self + .0 + .sequencer_client + .send_transaction(NSSATransaction::Public(tx)) + .await?) + } + + pub async fn send_swap_exact_output( + &self, + user_holding_a: AccountId, + user_holding_b: AccountId, + exact_amount_out: u128, + max_amount_in: u128, + token_definition_id_in: AccountId, + ) -> Result { + let instruction = amm_core::Instruction::SwapExactOutput { + exact_amount_out, + max_amount_in, + token_definition_id_in, + }; + let program = Program::amm(); + let amm_program_id = Program::amm().id(); + + let user_a_acc = self .0 .get_account_public(user_holding_a) .await .map_err(ExecutionFailureKind::SequencerError)?; - let token_holder_acc_b = self + let user_b_acc = self .0 .get_account_public(user_holding_b) .await .map_err(ExecutionFailureKind::SequencerError)?; - let token_holder_a = TokenHolding::try_from(&token_holder_acc_a.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_a))?; - let token_holder_b = TokenHolding::try_from(&token_holder_acc_b.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_b))?; + let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data) + .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_a))? + .definition_id(); + let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data) + .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_b))? + .definition_id(); - if token_holder_a.definition_id() == token_definition_id_in { - account_id_auth = user_holding_a; - } else if token_holder_b.definition_id() == token_definition_id_in { - account_id_auth = user_holding_b; + let amm_pool = + compute_pool_pda(amm_program_id, definition_token_a_id, definition_token_b_id); + let vault_holding_a = compute_vault_pda(amm_program_id, amm_pool, definition_token_a_id); + let vault_holding_b = compute_vault_pda(amm_program_id, amm_pool, definition_token_b_id); + + let account_ids = vec![ + amm_pool, + vault_holding_a, + vault_holding_b, + user_holding_a, + user_holding_b, + ]; + + let account_id_auth = if definition_token_a_id == token_definition_id_in { + user_holding_a + } else if definition_token_b_id == token_definition_id_in { + user_holding_b } else { return Err(ExecutionFailureKind::AccountDataError( token_definition_id_in, )); - } + }; let nonces = self .0 diff --git a/wallet/src/transaction_utils.rs b/wallet/src/transaction_utils.rs index 1bcb971f..f2def802 100644 --- a/wallet/src/transaction_utils.rs +++ b/wallet/src/transaction_utils.rs @@ -108,8 +108,8 @@ impl WalletCore { &[1, 1], &produce_random_nonces(2), &[ - (from_npk.clone(), shared_secret_from.clone()), - (to_npk.clone(), shared_secret_to.clone()), + (from_npk, shared_secret_from.clone()), + (to_npk, shared_secret_to.clone()), ], &[ (from_nsk.unwrap(), from_proof.unwrap()), @@ -124,12 +124,12 @@ impl WalletCore { vec![], vec![ ( - from_npk.clone(), + from_npk, from_vpk.clone(), eph_holder_from.generate_ephemeral_public_key(), ), ( - to_npk.clone(), + to_npk, to_vpk.clone(), eph_holder_to.generate_ephemeral_public_key(), ), @@ -185,8 +185,8 @@ impl WalletCore { &[1, 2], &produce_random_nonces(2), &[ - (from_npk.clone(), shared_secret_from.clone()), - (to_npk.clone(), shared_secret_to.clone()), + (from_npk, shared_secret_from.clone()), + (to_npk, shared_secret_to.clone()), ], &[(from_nsk.unwrap(), from_proof.unwrap())], &program.into(), @@ -198,12 +198,12 @@ impl WalletCore { vec![], vec![ ( - from_npk.clone(), + from_npk, from_vpk.clone(), eph_holder_from.generate_ephemeral_public_key(), ), ( - to_npk.clone(), + to_npk, to_vpk.clone(), eph_holder_to.generate_ephemeral_public_key(), ), @@ -255,8 +255,8 @@ impl WalletCore { &[1, 2], &produce_random_nonces(2), &[ - (from_npk.clone(), shared_secret_from.clone()), - (to_npk.clone(), shared_secret_to.clone()), + (from_npk, shared_secret_from.clone()), + (to_npk, shared_secret_to.clone()), ], &[(from_nsk.unwrap(), from_proof.unwrap())], &program.into(), @@ -268,12 +268,12 @@ impl WalletCore { vec![], vec![ ( - from_npk.clone(), + from_npk, from_vpk.clone(), eph_holder.generate_ephemeral_public_key(), ), ( - to_npk.clone(), + to_npk, to_vpk.clone(), eph_holder.generate_ephemeral_public_key(), ), @@ -324,7 +324,7 @@ impl WalletCore { &instruction_data, &[1, 0], &produce_random_nonces(1), - &[(from_npk.clone(), shared_secret.clone())], + &[(from_npk, shared_secret.clone())], &[(from_nsk.unwrap(), from_proof.unwrap())], &program.into(), ) @@ -334,7 +334,7 @@ impl WalletCore { vec![to], vec![], vec![( - from_npk.clone(), + from_npk, from_vpk.clone(), eph_holder.generate_ephemeral_public_key(), )], @@ -385,7 +385,7 @@ impl WalletCore { &instruction_data, &[0, 1], &produce_random_nonces(1), - &[(to_npk.clone(), shared_secret.clone())], + &[(to_npk, shared_secret.clone())], &[(to_nsk.unwrap(), to_proof)], &program.into(), ) @@ -395,7 +395,7 @@ impl WalletCore { vec![from], vec![from_acc.nonce], vec![( - to_npk.clone(), + to_npk, to_vpk.clone(), eph_holder.generate_ephemeral_public_key(), )], @@ -451,7 +451,7 @@ impl WalletCore { &instruction_data, &[0, 2], &produce_random_nonces(1), - &[(to_npk.clone(), shared_secret.clone())], + &[(to_npk, shared_secret.clone())], &[], &program.into(), ) @@ -461,7 +461,7 @@ impl WalletCore { vec![from], vec![from_acc.nonce], vec![( - to_npk.clone(), + to_npk, to_vpk.clone(), eph_holder.generate_ephemeral_public_key(), )], @@ -513,7 +513,7 @@ impl WalletCore { &instruction_data, &[0, 2], &produce_random_nonces(1), - &[(to_npk.clone(), shared_secret.clone())], + &[(to_npk, shared_secret.clone())], &[], &program.into(), ) @@ -523,7 +523,7 @@ impl WalletCore { vec![from], vec![from_acc.nonce], vec![( - to_npk.clone(), + to_npk, to_vpk.clone(), eph_holder.generate_ephemeral_public_key(), )], @@ -565,7 +565,7 @@ impl WalletCore { &Program::serialize_instruction(instruction).unwrap(), &[2], &produce_random_nonces(1), - &[(from_npk.clone(), shared_secret_from.clone())], + &[(from_npk, shared_secret_from.clone())], &[], &Program::authenticated_transfer_program().into(), ) @@ -575,7 +575,7 @@ impl WalletCore { vec![], vec![], vec![( - from_npk.clone(), + from_npk, from_vpk.clone(), eph_holder_from.generate_ephemeral_public_key(), )],