From ea6e337840ce0ff8f94cbc9a08d63ad978a07c6d Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Wed, 14 Jan 2026 12:52:48 +0100 Subject: [PATCH 01/33] feat: compression --- .github/workflows/ci-test-integration.yml | 19 + .github/workflows/publish-packages.yml | 2 +- Cargo.lock | 1628 ++++++++++---- Cargo.toml | 11 +- compressed-delegation-client/Cargo.toml | 28 + .../accounts/compressed_delegation_record.rs | 169 ++ .../src/generated/accounts/mod.rs | 10 + .../src/generated/errors/mod.rs | 6 + .../src/generated/instructions/commit.rs | 380 ++++ .../src/generated/instructions/delegate.rs | 374 ++++ .../instructions/delegate_compressed.rs | 523 +++++ .../src/generated/instructions/finalize.rs | 380 ++++ .../src/generated/instructions/mod.rs | 20 + .../src/generated/instructions/undelegate.rs | 466 ++++ .../instructions/undelegate_compressed.rs | 376 ++++ .../src/generated/mod.rs | 15 + .../src/generated/programs.rs | 12 + .../src/generated/shared.rs | 21 + .../src/generated/types/commit_args.rs | 22 + .../src/generated/types/delegate_args.rs | 32 + .../types/delegate_compressed_args.rs | 35 + .../types/external_undelegate_args.rs | 16 + .../src/generated/types/finalize_args.rs | 19 + .../src/generated/types/mod.rs | 22 + .../src/generated/types/undelegate_args.rs | 20 + .../types/undelegate_compressed_args.rs | 30 + compressed-delegation-client/src/lib.rs | 63 + compressed-delegation-client/src/utils.rs | 15 + config.example.toml | 22 +- docs/compression-documentation.md | 353 +++ .../src/bpf_loader_v1.rs | 2 + magicblock-account-cloner/src/lib.rs | 1 + .../src/scheduled_commits_processor.rs | 2 + magicblock-aperture/src/state/mod.rs | 2 + magicblock-api/Cargo.toml | 1 + magicblock-api/src/magic_validator.rs | 22 +- magicblock-chainlink/Cargo.toml | 5 +- .../chainlink/fetch_cloner/ata_projection.rs | 9 +- .../src/chainlink/fetch_cloner/delegation.rs | 21 +- .../src/chainlink/fetch_cloner/mod.rs | 138 +- .../src/chainlink/fetch_cloner/pipeline.rs | 202 +- .../chainlink/fetch_cloner/program_loader.rs | 6 +- .../chainlink/fetch_cloner/subscription.rs | 11 +- .../src/chainlink/fetch_cloner/tests.rs | 345 +-- .../src/chainlink/fetch_cloner/types.rs | 1 + magicblock-chainlink/src/chainlink/mod.rs | 36 +- .../chain_laser_actor.rs | 3 +- .../chain_updates_client.rs | 5 + .../src/remote_account_provider/config.rs | 4 +- .../src/remote_account_provider/endpoint.rs | 26 +- .../src/remote_account_provider/errors.rs | 9 + .../src/remote_account_provider/mod.rs | 758 ++++++- .../remote_account_provider/photon_client.rs | 160 ++ .../remote_account_provider/remote_account.rs | 28 +- magicblock-chainlink/src/testing/mod.rs | 2 + .../src/testing/photon_client_mock.rs | 83 + magicblock-chainlink/src/testing/utils.rs | 1 + .../tests/01_ensure-accounts.rs | 137 +- .../tests/03_deleg_after_sub.rs | 116 + .../tests/04_redeleg_other_separate_slots.rs | 123 ++ .../tests/05_redeleg_other_same_slot.rs | 108 +- .../tests/06_redeleg_us_separate_slots.rs | 141 ++ .../tests/07_redeleg_us_same_slot.rs | 123 +- magicblock-chainlink/tests/utils/accounts.rs | 31 + .../tests/utils/test_context.rs | 18 +- .../bin/magicblock_committor_program.so | Bin 127480 -> 128696 bytes .../src/instruction_builder/close_buffer.rs | 8 +- .../src/instruction_builder/init_buffer.rs | 12 +- .../src/instruction_builder/mod.rs | 17 + .../src/instruction_builder/realloc_buffer.rs | 6 +- .../src/instruction_builder/write_buffer.rs | 8 +- magicblock-committor-program/src/processor.rs | 6 +- .../src/state/changeset_chunks.rs | 2 +- .../src/state/chunks.rs | 2 +- magicblock-committor-service/Cargo.toml | 5 + .../src/committor_processor.rs | 3 + .../src/intent_execution_manager.rs | 9 +- .../src/intent_executor/error.rs | 8 +- .../intent_executor_factory.rs | 3 + .../src/intent_executor/mod.rs | 45 +- .../intent_executor/single_stage_executor.rs | 4 + .../src/intent_executor/task_info_fetcher.rs | 147 +- .../src/intent_executor/two_stage_executor.rs | 34 +- .../src/pubkeys_provider.rs | 14 +- magicblock-committor-service/src/service.rs | 5 + .../src/tasks/args_task.rs | 202 +- .../src/tasks/buffer_task.rs | 60 +- magicblock-committor-service/src/tasks/mod.rs | 70 +- .../src/tasks/task_builder.rs | 402 +++- .../src/tasks/task_strategist.rs | 141 +- .../delivery_preparator.rs | 196 +- .../src/transaction_preparator/error.rs | 5 + .../src/transaction_preparator/mod.rs | 20 +- magicblock-committor-service/src/types.rs | 4 + magicblock-config/README.md | 16 +- magicblock-config/src/config/compression.rs | 11 + magicblock-config/src/config/mod.rs | 2 + magicblock-config/src/lib.rs | 5 +- magicblock-config/src/tests.rs | 2 +- magicblock-core/Cargo.toml | 9 +- magicblock-core/src/compression/mod.rs | 43 + magicblock-core/src/lib.rs | 1 + .../src/instruction.rs | 34 + magicblock-metrics/src/metrics/mod.rs | 125 +- magicblock-rpc-client/src/lib.rs | 4 + programs/elfs/guinea.so | Bin 143720 -> 150296 bytes programs/magicblock/src/magic_context.rs | 2 +- .../src/magic_scheduled_base_intent.rs | 26 + .../magicblock/src/magicblock_processor.rs | 71 +- .../process_mutate_accounts.rs | 10 + .../process_schedule_commit.rs | 43 +- .../magicblock/src/utils/instruction_utils.rs | 112 +- test-integration/Cargo.lock | 1946 ++++++++++++----- test-integration/Cargo.toml | 11 +- .../configs/chainlink-conf.devnet.toml | 4 + .../configs/committor-conf.devnet.toml | 4 + .../compressed_delegation.so | Bin 0 -> 219624 bytes .../programs/flexi-counter/Cargo.toml | 7 +- .../programs/flexi-counter/src/instruction.rs | 148 +- .../programs/flexi-counter/src/processor.rs | 224 +- .../programs/flexi-counter/src/state.rs | 13 +- .../programs/schedulecommit/src/api.rs | 61 +- .../programs/schedulecommit/src/lib.rs | 1 - .../schedulecommit/src/magicblock_program.rs | 25 - .../programs/schedulecommit/src/order_book.rs | 8 +- .../schedulecommit/test-scenarios/Cargo.toml | 2 +- .../test-scenarios/tests/01_commits.rs | 6 +- .../schedulecommit/test-security/Cargo.toml | 1 + .../test-security/tests/utils/mod.rs | 26 +- test-integration/test-chainlink/Cargo.toml | 11 +- .../scripts/miniv2-json-from-so.js | 4 +- .../test-chainlink/src/ixtest_context.rs | 404 +++- .../test-chainlink/src/programs.rs | 2 +- .../test-chainlink/src/test_context.rs | 16 +- .../tests/ix_01_ensure-accounts.rs | 40 + .../tests/ix_03_deleg_after_sub.rs | 83 + .../tests/ix_06_redeleg_us_separate_slots.rs | 103 +- .../tests/ix_07_redeleg_us_same_slot.rs | 69 + .../tests/ix_remote_account_provider.rs | 24 +- .../test-committor-service/Cargo.toml | 7 + .../test-committor-service/tests/common.rs | 53 +- .../tests/test_delivery_preparator.rs | 107 +- .../tests/test_intent_executor.rs | 38 +- .../tests/test_ix_commit_local.rs | 560 ++++- .../tests/test_transaction_preparator.rs | 15 + .../tests/utils/instructions.rs | 109 +- .../test-committor-service/tests/utils/mod.rs | 2 + .../tests/utils/transactions.rs | 150 +- test-integration/test-runner/bin/run_tests.rs | 68 +- test-integration/test-runner/src/cleanup.rs | 23 + test-integration/test-runner/src/signal.rs | 7 +- test-integration/test-tools/Cargo.toml | 1 + .../src/integration_test_context.rs | 17 +- test-integration/test-tools/src/validator.rs | 163 +- .../configs/helius-config-template.toml | 6 +- .../configs/triton-config-template.toml | 6 +- .../helius-laser/sh/04_start-validator.sh | 2 +- .../helius-laser/sh/05_run-laser-test.sh | 4 +- 158 files changed, 12289 insertions(+), 2015 deletions(-) create mode 100644 compressed-delegation-client/Cargo.toml create mode 100644 compressed-delegation-client/src/generated/accounts/compressed_delegation_record.rs create mode 100644 compressed-delegation-client/src/generated/accounts/mod.rs create mode 100644 compressed-delegation-client/src/generated/errors/mod.rs create mode 100644 compressed-delegation-client/src/generated/instructions/commit.rs create mode 100644 compressed-delegation-client/src/generated/instructions/delegate.rs create mode 100644 compressed-delegation-client/src/generated/instructions/delegate_compressed.rs create mode 100644 compressed-delegation-client/src/generated/instructions/finalize.rs create mode 100644 compressed-delegation-client/src/generated/instructions/mod.rs create mode 100644 compressed-delegation-client/src/generated/instructions/undelegate.rs create mode 100644 compressed-delegation-client/src/generated/instructions/undelegate_compressed.rs create mode 100644 compressed-delegation-client/src/generated/mod.rs create mode 100644 compressed-delegation-client/src/generated/programs.rs create mode 100644 compressed-delegation-client/src/generated/shared.rs create mode 100644 compressed-delegation-client/src/generated/types/commit_args.rs create mode 100644 compressed-delegation-client/src/generated/types/delegate_args.rs create mode 100644 compressed-delegation-client/src/generated/types/delegate_compressed_args.rs create mode 100644 compressed-delegation-client/src/generated/types/external_undelegate_args.rs create mode 100644 compressed-delegation-client/src/generated/types/finalize_args.rs create mode 100644 compressed-delegation-client/src/generated/types/mod.rs create mode 100644 compressed-delegation-client/src/generated/types/undelegate_args.rs create mode 100644 compressed-delegation-client/src/generated/types/undelegate_compressed_args.rs create mode 100644 compressed-delegation-client/src/lib.rs create mode 100644 compressed-delegation-client/src/utils.rs create mode 100644 docs/compression-documentation.md create mode 100644 magicblock-chainlink/src/remote_account_provider/photon_client.rs create mode 100644 magicblock-chainlink/src/testing/photon_client_mock.rs create mode 100644 magicblock-config/src/config/compression.rs create mode 100644 magicblock-core/src/compression/mod.rs create mode 100755 test-integration/programs/compressed_delegation/compressed_delegation.so delete mode 100644 test-integration/programs/schedulecommit/src/magicblock_program.rs diff --git a/.github/workflows/ci-test-integration.yml b/.github/workflows/ci-test-integration.yml index 00b9dfbc1..7dc1a4a57 100644 --- a/.github/workflows/ci-test-integration.yml +++ b/.github/workflows/ci-test-integration.yml @@ -71,6 +71,25 @@ jobs: - uses: ./magicblock-validator/.github/actions/setup-solana + - name: Install Photon indexer + if: matrix.batch_tests == 'table_mania' || matrix.batch_tests == 'committor' || matrix.batch_tests == 'chainlink' + shell: bash + env: + RUSTFLAGS: "-A dead-code" + run: cargo install --git https://github.com/lightprotocol/photon.git --rev ac7df6c388db847b7693a7a1cb766a7c9d7809b5 --locked --force + + - name: Setup Node.js + if: matrix.batch_tests == 'table_mania' || matrix.batch_tests == 'committor' || matrix.batch_tests == 'chainlink' + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install zk-compression CLI + if: matrix.batch_tests == 'table_mania' || matrix.batch_tests == 'committor' || matrix.batch_tests == 'chainlink' + run: | + npm i -g @lightprotocol/zk-compression-cli@0.27.1-alpha.10 + shell: bash + - name: Run integration tests - ${{ matrix.batch_tests }} run: | sudo prlimit --pid $$ --nofile=1048576:1048576 diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 7371799c3..bb284b7ee 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -244,4 +244,4 @@ jobs: cargo publish $DRY_RUN_FLAG --manifest-path=./Cargo.toml --token $CRATES_TOKEN $NO_VERIFY_FLAG env: CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} - DRY_RUN: ${{ env.DRY_RUN }} + DRY_RUN: ${{ env.DRY_RUN }} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 6c10b8752..db3c6ce8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,9 +94,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -198,9 +198,12 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] [[package]] name = "ark-bn254" @@ -208,9 +211,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" dependencies = [ - "ark-ec", - "ark-ff", - "ark-std", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-std 0.4.0", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-std 0.5.0", ] [[package]] @@ -219,10 +233,10 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" dependencies = [ - "ark-ff", - "ark-poly", - "ark-serialize", - "ark-std", + "ark-ff 0.4.2", + "ark-poly 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "derivative", "hashbrown 0.13.2", "itertools 0.10.5", @@ -230,16 +244,37 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash 0.8.12", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.2", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "zeroize", +] + [[package]] name = "ark-ff" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" dependencies = [ - "ark-ff-asm", - "ark-ff-macros", - "ark-serialize", - "ark-std", + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "derivative", "digest 0.10.7", "itertools 0.10.5", @@ -250,6 +285,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-traits", + "paste", + "zeroize", +] + [[package]] name = "ark-ff-asm" version = "0.4.2" @@ -260,6 +315,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.111", +] + [[package]] name = "ark-ff-macros" version = "0.4.2" @@ -273,27 +338,68 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "ark-poly" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" dependencies = [ - "ark-ff", - "ark-serialize", - "ark-std", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "derivative", "hashbrown 0.13.2", ] +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash 0.8.12", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.2", +] + [[package]] name = "ark-serialize" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ - "ark-serialize-derive", - "ark-std", + "ark-serialize-derive 0.4.2", + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint 0.4.6", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive 0.5.0", + "ark-std 0.5.0", + "arrayvec", "digest 0.10.7", "num-bigint 0.4.6", ] @@ -309,6 +415,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "ark-std" version = "0.4.0" @@ -319,6 +436,17 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", + "rayon", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -459,11 +587,11 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "axum-core 0.5.5", + "axum-core 0.5.6", "bytes", "futures-util", "http 1.4.0", @@ -504,9 +632,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -526,6 +654,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -576,7 +710,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.10.5", "proc-macro2", "quote", "regex", @@ -782,18 +916,18 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.8.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", @@ -824,9 +958,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -975,6 +1109,23 @@ dependencies = [ "unreachable", ] +[[package]] +name = "compressed-delegation-client" +version = "0.6.1" +dependencies = [ + "borsh 0.10.4", + "light-compressed-account", + "light-hasher", + "light-sdk", + "light-sdk-types", + "serde", + "solana-account-info", + "solana-cpi", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", +] + [[package]] name = "compression-codecs" version = "0.4.35" @@ -1134,6 +1285,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1288,18 +1458,18 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", @@ -1387,6 +1557,18 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "either" version = "1.15.0" @@ -1428,6 +1610,26 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -1537,7 +1739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.59.0", ] @@ -1569,9 +1771,18 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "five8" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "23f76610e969fa1784327ded240f1e28a3fd9520c9cec93b636fcf62dd37f772" +dependencies = [ + "five8_core 1.0.0", +] [[package]] name = "five8_const" @@ -1579,7 +1790,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" dependencies = [ - "five8_core", + "five8_core 0.1.2", +] + +[[package]] +name = "five8_const" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0f1728185f277989ca573a402716ae0beaaea3f76a8ff87ef9dd8fb19436c5" +dependencies = [ + "five8_core 1.0.0", ] [[package]] @@ -1588,12 +1808,24 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" +[[package]] +name = "five8_core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" + [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.5" @@ -1777,7 +2009,7 @@ dependencies = [ "clap 4.5.53", "magicblock-accounts-db", "solana-commitment-config", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client", "sonic-rs", "tempfile", @@ -2011,7 +2243,7 @@ dependencies = [ "prost 0.12.6", "prost-types 0.12.6", "rand 0.8.5", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "sha2 0.10.9", @@ -2232,6 +2464,22 @@ dependencies = [ "tokio-rustls 0.24.1", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.35", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -2258,12 +2506,29 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -2271,12 +2536,16 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "hyper 1.8.1", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.6.1", + "system-configuration 0.6.1", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2468,6 +2737,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2502,6 +2781,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2513,15 +2801,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" dependencies = [ "jiff-static", "log", @@ -2532,9 +2820,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" dependencies = [ "proc-macro2", "quote", @@ -2617,7 +2905,7 @@ dependencies = [ "solana-account", "solana-clock", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-transaction-status", "structopt", @@ -2715,26 +3003,345 @@ dependencies = [ ] [[package]] -name = "libz-sys" -version = "1.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +name = "libz-sys" +version = "1.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "light-account-checks" +version = "0.3.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "solana-account-info", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "solana-sysvar", + "thiserror 2.0.17", +] + +[[package]] +name = "light-bounded-vec" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cfa375d028164719e3ffef93d2e5c27855cc8a5bb5bf257b868d17c12a3e66" +dependencies = [ + "bytemuck", + "memoffset", + "solana-program-error 2.2.2", + "thiserror 1.0.69", +] + +[[package]] +name = "light-client" +version = "0.13.1" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "async-trait", + "base64 0.13.1", + "borsh 0.10.4", + "bs58", + "bytemuck", + "lazy_static", + "light-compressed-account", + "light-concurrent-merkle-tree", + "light-hasher", + "light-indexed-merkle-tree", + "light-merkle-tree-metadata", + "light-prover-client", + "light-sdk", + "num-bigint 0.4.6", + "num-traits", + "photon-api", + "rand 0.8.5", + "solana-account", + "solana-account-decoder-client-types", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-commitment-config", + "solana-compute-budget-interface", + "solana-epoch-info", + "solana-hash", + "solana-instruction 2.2.1", + "solana-keypair", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-signature", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "light-compressed-account" +version = "0.3.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "borsh 0.10.4", + "bytemuck", + "light-hasher", + "light-macros", + "light-program-profiler", + "light-zero-copy", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "thiserror 2.0.17", + "zerocopy", +] + +[[package]] +name = "light-concurrent-merkle-tree" +version = "2.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "borsh 0.10.4", + "light-bounded-vec", + "light-hasher", + "memoffset", + "solana-program-error 2.2.2", + "thiserror 2.0.17", +] + +[[package]] +name = "light-hasher" +version = "3.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "ark-bn254 0.5.0", + "ark-ff 0.5.0", + "arrayvec", + "borsh 0.10.4", + "light-poseidon 0.3.0", + "num-bigint 0.4.6", + "sha2 0.10.9", + "sha3", + "solana-nostd-keccak", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "thiserror 2.0.17", +] + +[[package]] +name = "light-indexed-array" +version = "0.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "light-hasher", + "num-bigint 0.4.6", + "num-traits", + "thiserror 2.0.17", +] + +[[package]] +name = "light-indexed-merkle-tree" +version = "2.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "light-bounded-vec", + "light-concurrent-merkle-tree", + "light-hasher", + "light-merkle-tree-reference", + "num-bigint 0.4.6", + "num-traits", + "solana-program-error 2.2.2", + "thiserror 2.0.17", +] + +[[package]] +name = "light-macros" +version = "2.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "light-merkle-tree-metadata" +version = "0.3.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "borsh 0.10.4", + "bytemuck", + "light-compressed-account", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-sysvar", + "thiserror 2.0.17", + "zerocopy", +] + +[[package]] +name = "light-merkle-tree-reference" +version = "2.0.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "light-hasher", + "light-indexed-array", + "num-bigint 0.4.6", + "num-traits", + "thiserror 2.0.17", +] + +[[package]] +name = "light-poseidon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee" +dependencies = [ + "ark-bn254 0.4.0", + "ark-ff 0.4.2", + "num-bigint 0.4.6", + "thiserror 1.0.69", +] + +[[package]] +name = "light-poseidon" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3d87542063daaccbfecd78b60f988079b6ec4e089249658b9455075c78d42" +dependencies = [ + "ark-bn254 0.5.0", + "ark-ff 0.5.0", + "num-bigint 0.4.6", + "thiserror 1.0.69", +] + +[[package]] +name = "light-profiler-macro" +version = "0.1.0" +source = "git+https://github.com/Lightprotocol/light-program-profiler?rev=36a75e14f54dd862bf2f338c97435ffc7e3e8de9#36a75e14f54dd862bf2f338c97435ffc7e3e8de9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "light-program-profiler" +version = "0.1.0" +source = "git+https://github.com/Lightprotocol/light-program-profiler?rev=36a75e14f54dd862bf2f338c97435ffc7e3e8de9#36a75e14f54dd862bf2f338c97435ffc7e3e8de9" +dependencies = [ + "light-profiler-macro", +] + +[[package]] +name = "light-prover-client" +version = "2.0.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "ark-bn254 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "light-hasher", + "light-indexed-array", + "light-sparse-merkle-tree", + "num-bigint 0.4.6", + "num-traits", + "reqwest 0.11.27", + "serde", + "serde_json", + "solana-bn254", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "light-sdk" +version = "0.13.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "borsh 0.10.4", + "light-account-checks", + "light-compressed-account", + "light-hasher", + "light-macros", + "light-sdk-macros", + "light-sdk-types", + "light-zero-copy", + "num-bigint 0.4.6", + "solana-account-info", + "solana-cpi", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "thiserror 2.0.17", +] + +[[package]] +name = "light-sdk-macros" +version = "0.13.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "light-hasher", + "light-poseidon 0.3.0", + "proc-macro2", + "quote", + "solana-pubkey 2.2.1", + "syn 2.0.111", +] + +[[package]] +name = "light-sdk-types" +version = "0.13.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "borsh 0.10.4", + "light-account-checks", + "light-compressed-account", + "light-hasher", + "light-macros", + "light-zero-copy", + "solana-msg 2.2.1", + "thiserror 2.0.17", +] + +[[package]] +name = "light-sparse-merkle-tree" +version = "0.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" dependencies = [ - "cc", - "pkg-config", - "vcpkg", + "light-hasher", + "light-indexed-array", + "num-bigint 0.4.6", + "num-traits", + "thiserror 2.0.17", ] [[package]] -name = "light-poseidon" +name = "light-zero-copy" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" dependencies = [ - "ark-bn254", - "ark-ff", - "num-bigint 0.4.6", - "thiserror 1.0.69", + "light-zero-copy-derive", + "zerocopy", +] + +[[package]] +name = "light-zero-copy-derive" +version = "0.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -2820,7 +3427,7 @@ dependencies = [ "borsh 1.6.0", "bytemuck_derive", "solana-program", - "solana-system-interface", + "solana-system-interface 3.0.0", ] [[package]] @@ -2842,10 +3449,10 @@ dependencies = [ "rand 0.9.2", "solana-account", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-loader-v3-interface", "solana-loader-v4-interface", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-signature", "solana-signer", @@ -2868,7 +3475,7 @@ dependencies = [ "magicblock-core", "magicblock-program", "solana-hash", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-transaction", "solana-transaction-error", "thiserror 1.0.69", @@ -2890,7 +3497,7 @@ dependencies = [ "parking_lot", "reflink-copy", "solana-account", - "solana-pubkey", + "solana-pubkey 2.2.1", "tempfile", "thiserror 1.0.69", ] @@ -2921,7 +3528,7 @@ dependencies = [ "magicblock-version", "parking_lot", "rand 0.9.2", - "reqwest", + "reqwest 0.11.27", "scc", "serde", "solana-account", @@ -2932,7 +3539,7 @@ dependencies = [ "solana-fee-structure", "solana-keypair", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-pubsub-client", "solana-rpc-client", "solana-rpc-client-api", @@ -2955,6 +3562,7 @@ dependencies = [ "anyhow", "borsh 1.6.0", "fd-lock", + "light-client", "log", "magic-domain-program", "magicblock-account-cloner", @@ -2983,12 +3591,12 @@ dependencies = [ "solana-genesis-config", "solana-hash", "solana-inline-spl", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", "solana-native-token", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-rpc-client", "solana-sha256-hasher", @@ -3011,9 +3619,12 @@ dependencies = [ "assert_matches", "async-trait", "bincode", + "borsh 0.10.4", + "compressed-delegation-client", "env_logger 0.11.8", "futures-util", "helius-laserstream", + "light-client", "log", "lru", "magicblock-chainlink", @@ -3032,20 +3643,20 @@ dependencies = [ "solana-commitment-config", "solana-feature-set", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-message", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-pubsub-client", "solana-rpc-client", "solana-rpc-client-api", "solana-sdk-ids", "solana-signature", "solana-signer", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-sysvar", "solana-transaction", "solana-transaction-error", @@ -3062,11 +3673,11 @@ dependencies = [ name = "magicblock-committor-program" version = "0.6.1" dependencies = [ - "borsh 1.6.0", + "borsh 0.10.4", "paste", "solana-account", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "thiserror 1.0.69", ] @@ -3077,13 +3688,18 @@ dependencies = [ "async-trait", "base64 0.21.7", "bincode", - "borsh 1.6.0", + "borsh 0.10.4", + "compressed-delegation-client", "dyn-clone", "futures-util", "lazy_static", + "light-client", + "light-sdk", "log", "lru", "magicblock-committor-program", + "magicblock-config", + "magicblock-core", "magicblock-delegation-program", "magicblock-metrics", "magicblock-program", @@ -3097,11 +3713,11 @@ dependencies = [ "solana-commitment-config", "solana-compute-budget-interface", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", @@ -3130,7 +3746,7 @@ dependencies = [ "serde_with", "serial_test", "solana-keypair", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signer", "tempfile", "toml 0.8.23", @@ -3141,13 +3757,16 @@ dependencies = [ name = "magicblock-core" version = "0.6.1" dependencies = [ + "compressed-delegation-client", "flume", + "light-compressed-account", + "light-sdk", "magicblock-magic-program-api", "solana-account", "solana-account-decoder", "solana-hash", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-transaction", "solana-transaction-context", @@ -3172,7 +3791,7 @@ dependencies = [ "pinocchio-pubkey", "pinocchio-system", "rkyv 0.7.45", - "solana-curve25519", + "solana-curve25519 3.1.5", "solana-program", "solana-security-txt", "static_assertions", @@ -3201,12 +3820,12 @@ dependencies = [ "solana-account-decoder", "solana-clock", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-measure", "solana-message", "solana-metrics", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-signer", "solana-storage-proto", @@ -3269,7 +3888,7 @@ dependencies = [ "solana-loader-v4-program", "solana-program", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent-collector", "solana-sdk-ids", "solana-signature", @@ -3305,11 +3924,11 @@ dependencies = [ "solana-clock", "solana-fee-calculator", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-log-collector", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-seed-derivable", "solana-signature", @@ -3332,8 +3951,8 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-hash", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", @@ -3357,10 +3976,10 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-compute-budget-interface", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-signer", "solana-slot-hashes", @@ -3383,9 +4002,9 @@ dependencies = [ "magicblock-ledger", "magicblock-program", "rusqlite", - "solana-instruction", + "solana-instruction 2.2.1", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", @@ -3439,7 +4058,7 @@ dependencies = [ "solana-feature-set", "solana-frozen-abi-macro", "solana-rpc-client-api", - "solana-sanitize", + "solana-sanitize 2.2.1", ] [[package]] @@ -3557,6 +4176,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "munge" version = "0.4.7" @@ -3595,7 +4220,7 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", "security-framework 2.11.1", @@ -3655,6 +4280,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "serde", ] [[package]] @@ -3823,6 +4449,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" + [[package]] name = "openssl-sys" version = "0.9.111" @@ -3917,10 +4549,34 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset", + "fixedbitset 0.4.2", + "indexmap 2.12.1", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset 0.5.7", "indexmap 2.12.1", ] +[[package]] +name = "photon-api" +version = "0.51.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "reqwest 0.12.28", + "serde", + "serde_derive", + "serde_json", + "serde_with", + "url", + "uuid", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -3985,7 +4641,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0225638cadcbebae8932cb7f49cb5da7c15c21beb19f048f05a5ca7d93f065" dependencies = [ - "five8_const", + "five8_const 0.1.4", "pinocchio", "sha2-const-stable", ] @@ -4020,9 +4676,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "portable-atomic-util" @@ -4075,9 +4731,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn 2.0.111", @@ -4098,7 +4754,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.9", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -4127,9 +4783,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -4213,8 +4869,8 @@ dependencies = [ "itertools 0.10.5", "lazy_static", "log", - "multimap", - "petgraph", + "multimap 0.8.3", + "petgraph 0.6.5", "prettyplease 0.1.25", "prost 0.11.9", "prost-types 0.11.9", @@ -4234,10 +4890,10 @@ dependencies = [ "heck 0.5.0", "itertools 0.12.1", "log", - "multimap", + "multimap 0.10.1", "once_cell", - "petgraph", - "prettyplease 0.2.36", + "petgraph 0.6.5", + "prettyplease 0.2.37", "prost 0.12.6", "prost-types 0.12.6", "regex", @@ -4254,10 +4910,10 @@ dependencies = [ "heck 0.5.0", "itertools 0.14.0", "log", - "multimap", + "multimap 0.10.1", "once_cell", - "petgraph", - "prettyplease 0.2.36", + "petgraph 0.7.1", + "prettyplease 0.2.37", "prost 0.13.5", "prost-types 0.13.5", "regex", @@ -4558,6 +5214,26 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4595,7 +5271,7 @@ checksum = "23bbed272e39c47a095a5242218a67412a220006842558b03fe2935e8f3d7b92" dependencies = [ "cfg-if", "libc", - "rustix 1.1.2", + "rustix 1.1.3", "windows", ] @@ -4659,8 +5335,8 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-rustls", - "hyper-tls", + "hyper-rustls 0.24.2", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -4676,7 +5352,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", @@ -4690,6 +5366,48 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.12", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.7", + "hyper-tls 0.6.0", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-native-tls", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reqwest-middleware" version = "0.2.5" @@ -4699,7 +5417,7 @@ dependencies = [ "anyhow", "async-trait", "http 0.2.12", - "reqwest", + "reqwest 0.11.27", "serde", "task-local-extensions", "thiserror 1.0.69", @@ -4842,9 +5560,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", @@ -4882,14 +5600,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.0", "rustls-pki-types", "schannel", - "security-framework 3.3.0", + "security-framework 3.5.1", ] [[package]] @@ -4912,9 +5630,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ "zeroize", ] @@ -4948,9 +5666,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "scc" @@ -4984,9 +5702,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -5037,9 +5755,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.3.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags 2.10.0", "core-foundation 0.10.1", @@ -5115,15 +5833,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -5159,7 +5877,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.0", "serde_core", "serde_json", "serde_with_macros", @@ -5281,10 +5999,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -5356,8 +6075,8 @@ dependencies = [ "serde_derive", "solana-account-info", "solana-clock", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-sysvar", ] @@ -5383,11 +6102,11 @@ dependencies = [ "solana-config-program", "solana-epoch-schedule", "solana-fee-calculator", - "solana-instruction", + "solana-instruction 2.2.1", "solana-nonce", "solana-program", "solana-program-pack", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", "solana-slot-hashes", @@ -5413,7 +6132,7 @@ dependencies = [ "serde_derive", "serde_json", "solana-account", - "solana-pubkey", + "solana-pubkey 2.2.1", "zstd", ] @@ -5425,9 +6144,24 @@ checksum = "e0c17d606a298a205fae325489fbed88ee6dc4463c111672172327e741c8905d" dependencies = [ "bincode", "serde", - "solana-program-error", + "solana-program-error 2.2.2", "solana-program-memory", - "solana-pubkey", + "solana-pubkey 2.2.1", +] + +[[package]] +name = "solana-address" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37320fd2945c5d654b2c6210624a52d66c3f1f73b653ed211ab91a703b35bdd" +dependencies = [ + "five8", + "five8_const 1.0.0", + "serde", + "serde_derive", + "solana-define-syscall 4.0.1", + "solana-program-error 3.0.0", + "solana-sanitize 3.0.1", ] [[package]] @@ -5441,8 +6175,8 @@ dependencies = [ "serde", "serde_derive", "solana-clock", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-slot-hashes", ] @@ -5462,12 +6196,12 @@ dependencies = [ "solana-bincode", "solana-clock", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-packet", "solana-program-runtime", - "solana-pubkey", - "solana-system-interface", + "solana-pubkey 2.2.1", + "solana-system-interface 1.0.0", "solana-transaction-context", "thiserror 2.0.17", ] @@ -5489,7 +6223,7 @@ checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" dependencies = [ "num-bigint 0.4.6", "num-traits", - "solana-define-syscall", + "solana-define-syscall 2.2.1", ] [[package]] @@ -5500,7 +6234,7 @@ checksum = "19a3787b8cf9c9fe3dd360800e8b70982b9e5a8af9e11c354b6665dd4a003adc" dependencies = [ "bincode", "serde", - "solana-instruction", + "solana-instruction 2.2.1", ] [[package]] @@ -5510,9 +6244,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" dependencies = [ "blake3", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "solana-hash", - "solana-sanitize", + "solana-sanitize 2.2.1", ] [[package]] @@ -5521,12 +6255,12 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9abc69625158faaab02347370b91c0d8e0fe347bf9287239f0fbe8f5864d91da" dependencies = [ - "ark-bn254", - "ark-ec", - "ark-ff", - "ark-serialize", + "ark-bn254 0.4.0", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", "bytemuck", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "thiserror 2.0.17", ] @@ -5559,10 +6293,10 @@ dependencies = [ "solana-clock", "solana-compute-budget", "solana-cpi", - "solana-curve25519", + "solana-curve25519 2.2.1", "solana-feature-set", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keccak-hasher", "solana-loader-v3-interface", "solana-loader-v4-interface", @@ -5574,13 +6308,13 @@ dependencies = [ "solana-program-entrypoint", "solana-program-memory", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sbpf", "solana-sdk-ids", "solana-secp256k1-recover", "solana-sha256-hasher", "solana-stable-layout", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-sysvar", "solana-sysvar-id", "solana-timings", @@ -5605,7 +6339,7 @@ dependencies = [ "solana-config-program", "solana-feature-set", "solana-loader-v4-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-stake-program", "solana-system-program", @@ -5622,13 +6356,13 @@ dependencies = [ "solana-commitment-config", "solana-epoch-info", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-signer", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction", "solana-transaction-error", ] @@ -5689,9 +6423,9 @@ dependencies = [ "solana-compute-budget", "solana-compute-budget-interface", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-packet", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-svm-transaction", "solana-transaction-error", @@ -5707,7 +6441,7 @@ dependencies = [ "borsh 1.6.0", "serde", "serde_derive", - "solana-instruction", + "solana-instruction 2.2.1", "solana-sdk-ids", ] @@ -5733,15 +6467,15 @@ dependencies = [ "serde_derive", "solana-account", "solana-bincode", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-packet", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-short-vec", "solana-stake-interface", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction-context", ] @@ -5752,10 +6486,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8dc71126edddc2ba014622fc32d0f5e2e78ec6c5a1e0eb511b85618c09e9ea11" dependencies = [ "solana-account-info", - "solana-define-syscall", - "solana-instruction", - "solana-program-error", - "solana-pubkey", + "solana-define-syscall 2.2.1", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "solana-stable-layout", ] @@ -5768,7 +6502,21 @@ dependencies = [ "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", - "solana-define-syscall", + "solana-define-syscall 2.2.1", + "subtle", + "thiserror 2.0.17", +] + +[[package]] +name = "solana-curve25519" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebca352e7716ff1a0877272f87c772c958489c1d568a92d318dc0c75939d2884" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "solana-define-syscall 3.0.0", "subtle", "thiserror 2.0.17", ] @@ -5788,6 +6536,18 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf784bb2cb3e02cac9801813c30187344228d2ae952534902108f6150573a33d" +[[package]] +name = "solana-define-syscall" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" + +[[package]] +name = "solana-define-syscall" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" + [[package]] name = "solana-derivation-path" version = "2.2.1" @@ -5809,7 +6569,7 @@ dependencies = [ "bytemuck_derive", "ed25519-dalek", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-precompile-error", "solana-sdk-ids", ] @@ -5846,7 +6606,7 @@ checksum = "96c5fd2662ae7574810904585fd443545ed2b568dbd304b25a31e79ccc76e81b" dependencies = [ "siphasher", "solana-hash", - "solana-pubkey", + "solana-pubkey 2.2.1", ] [[package]] @@ -5873,13 +6633,13 @@ dependencies = [ "solana-address-lookup-table-interface", "solana-clock", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keccak-hasher", "solana-message", "solana-nonce", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", "thiserror 2.0.17", ] @@ -5894,12 +6654,12 @@ dependencies = [ "serde_derive", "solana-account", "solana-account-info", - "solana-instruction", - "solana-program-error", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", ] [[package]] @@ -5912,7 +6672,7 @@ dependencies = [ "lazy_static", "solana-epoch-schedule", "solana-hash", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sha256-hasher", ] @@ -5983,7 +6743,7 @@ dependencies = [ "solana-logger", "solana-native-token", "solana-poh-config", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", "solana-sha256-hasher", @@ -6016,7 +6776,7 @@ dependencies = [ "serde", "serde_derive", "solana-atomic-u64", - "solana-sanitize", + "solana-sanitize 2.2.1", "wasm-bindgen", ] @@ -6037,7 +6797,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "951545bd7d0ab4a878cfc7375ac9f1a475cb6936626677b2ba1d25e7b9f3910b" dependencies = [ "bytemuck", - "solana-pubkey", + "solana-pubkey 2.2.1", ] [[package]] @@ -6053,11 +6813,35 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-define-syscall", - "solana-pubkey", + "solana-define-syscall 2.2.1", + "solana-pubkey 2.2.1", "wasm-bindgen", ] +[[package]] +name = "solana-instruction" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1b699a2c1518028a9982e255e0eca10c44d90006542d9d7f9f40dbce3f7c78" +dependencies = [ + "bincode", + "borsh 1.6.0", + "serde", + "solana-define-syscall 4.0.1", + "solana-instruction-error", + "solana-pubkey 4.0.0", +] + +[[package]] +name = "solana-instruction-error" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04259e03c05faf38a8c24217b5cfe4c90572ae6184ab49cddb1584fdd756d3f" +dependencies = [ + "num-traits", + "solana-program-error 3.0.0", +] + [[package]] name = "solana-instructions-sysvar" version = "2.2.1" @@ -6066,10 +6850,10 @@ checksum = "427f2d0d6dc0bb49f16cef5e7f975180d2e80aab9bdd3b2af68e2d029ec63f43" dependencies = [ "bitflags 2.10.0", "solana-account-info", - "solana-instruction", - "solana-program-error", - "solana-pubkey", - "solana-sanitize", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-serialize-utils", "solana-sysvar-id", @@ -6082,9 +6866,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" dependencies = [ "sha3", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "solana-hash", - "solana-sanitize", + "solana-sanitize 2.2.1", ] [[package]] @@ -6098,7 +6882,7 @@ dependencies = [ "ed25519-dalek-bip32", "rand 0.7.3", "solana-derivation-path", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-seed-derivable", "solana-seed-phrase", "solana-signature", @@ -6128,8 +6912,8 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", ] @@ -6142,10 +6926,10 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", ] [[package]] @@ -6157,10 +6941,10 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", ] [[package]] @@ -6175,14 +6959,14 @@ dependencies = [ "solana-bincode", "solana-bpf-loader-program", "solana-compute-budget", - "solana-instruction", + "solana-instruction 2.2.1", "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-log-collector", "solana-measure", "solana-packet", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sbpf", "solana-sdk-ids", "solana-transaction-context", @@ -6230,12 +7014,12 @@ dependencies = [ "serde_derive", "solana-bincode", "solana-hash", - "solana-instruction", - "solana-pubkey", - "solana-sanitize", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-short-vec", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction-error", "wasm-bindgen", ] @@ -6250,7 +7034,7 @@ dependencies = [ "gethostname", "lazy_static", "log", - "reqwest", + "reqwest 0.11.27", "solana-clock", "solana-cluster-type", "solana-sha256-hasher", @@ -6264,7 +7048,16 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36a1a14399afaabc2781a1db09cb14ee4cc4ee5c7a5a3cfcc601811379a8092" dependencies = [ - "solana-define-syscall", + "solana-define-syscall 2.2.1", +] + +[[package]] +name = "solana-msg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "264275c556ea7e22b9d3f87d56305546a38d4eee8ec884f3b126236cb7dcbbb4" +dependencies = [ + "solana-define-syscall 3.0.0", ] [[package]] @@ -6283,7 +7076,7 @@ dependencies = [ "serde_derive", "solana-fee-calculator", "solana-hash", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sha256-hasher", ] @@ -6299,6 +7092,15 @@ dependencies = [ "solana-sdk-ids", ] +[[package]] +name = "solana-nostd-keccak" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ced70920435b1baa58f76e6f84bbc1110ddd1d6161ec76b6d731ae8431e9c4" +dependencies = [ + "sha3", +] + [[package]] name = "solana-offchain-message" version = "2.2.1" @@ -6308,8 +7110,8 @@ dependencies = [ "num_enum", "solana-hash", "solana-packet", - "solana-pubkey", - "solana-sanitize", + "solana-pubkey 2.2.1", + "solana-sanitize 2.2.1", "solana-sha256-hasher", "solana-signature", "solana-signer", @@ -6345,9 +7147,9 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ad1ea160d08dc423c35021fa3e437a5783eb256f5ab8bc3024e27db913acf42" dependencies = [ - "ark-bn254", - "light-poseidon", - "solana-define-syscall", + "ark-bn254 0.4.0", + "light-poseidon 0.2.0", + "solana-define-syscall 2.2.1", "thiserror 2.0.17", ] @@ -6372,7 +7174,7 @@ dependencies = [ "solana-feature-set", "solana-message", "solana-precompile-error", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-secp256k1-program", "solana-secp256r1-program", @@ -6384,7 +7186,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81a57a24e6a4125fc69510b6774cd93402b943191b6cddad05de7281491c90fe" dependencies = [ - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-signer", ] @@ -6424,14 +7226,14 @@ dependencies = [ "solana-clock", "solana-cpi", "solana-decode-error", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "solana-epoch-rewards", "solana-epoch-schedule", "solana-example-mocks", "solana-feature-gate-interface", "solana-fee-calculator", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-instructions-sysvar", "solana-keccak-hasher", "solana-last-restart-slot", @@ -6439,17 +7241,17 @@ dependencies = [ "solana-loader-v3-interface", "solana-loader-v4-interface", "solana-message", - "solana-msg", + "solana-msg 2.2.1", "solana-native-token", "solana-nonce", "solana-program-entrypoint", - "solana-program-error", + "solana-program-error 2.2.2", "solana-program-memory", "solana-program-option", "solana-program-pack", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-sdk-macro", "solana-secp256k1-recover", @@ -6461,7 +7263,7 @@ dependencies = [ "solana-slot-history", "solana-stable-layout", "solana-stake-interface", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-sysvar", "solana-sysvar-id", "solana-vote-interface", @@ -6476,9 +7278,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "473ffe73c68d93e9f2aa726ad2985fe52760052709aaab188100a42c618060ec" dependencies = [ "solana-account-info", - "solana-msg", - "solana-program-error", - "solana-pubkey", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", ] [[package]] @@ -6492,11 +7294,17 @@ dependencies = [ "serde", "serde_derive", "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-pubkey 2.2.1", ] +[[package]] +name = "solana-program-error" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" + [[package]] name = "solana-program-memory" version = "2.2.1" @@ -6504,7 +7312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b0268f6c89825fb634a34bd0c3b8fdaeaecfc3728be1d622a8ee6dd577b60d4" dependencies = [ "num-traits", - "solana-define-syscall", + "solana-define-syscall 2.2.1", ] [[package]] @@ -6519,7 +7327,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" dependencies = [ - "solana-program-error", + "solana-program-error 2.2.2", ] [[package]] @@ -6543,13 +7351,13 @@ dependencies = [ "solana-epoch-schedule", "solana-feature-set", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-last-restart-slot", "solana-log-collector", "solana-measure", "solana-metrics", "solana-precompiles", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sbpf", "solana-sdk-ids", @@ -6575,7 +7383,7 @@ dependencies = [ "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", - "five8_const", + "five8_const 0.1.4", "getrandom 0.2.16", "js-sys", "num-traits", @@ -6584,12 +7392,21 @@ dependencies = [ "serde_derive", "solana-atomic-u64", "solana-decode-error", - "solana-define-syscall", - "solana-sanitize", + "solana-define-syscall 2.2.1", + "solana-sanitize 2.2.1", "solana-sha256-hasher", "wasm-bindgen", ] +[[package]] +name = "solana-pubkey" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f7104d456b58e1418c21a8581e89810278d1190f70f27ece7fc0b2c9282a57" +dependencies = [ + "solana-address", +] + [[package]] name = "solana-pubsub-client" version = "2.2.1" @@ -6599,14 +7416,14 @@ dependencies = [ "crossbeam-channel", "futures-util", "log", - "reqwest", + "reqwest 0.11.27", "semver", "serde", "serde_derive", "serde_json", "solana-account-decoder-client-types", "solana-clock", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client-api", "solana-signature", "thiserror 2.0.17", @@ -6651,7 +7468,7 @@ dependencies = [ "solana-clock", "solana-epoch-schedule", "solana-genesis-config", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", ] @@ -6662,7 +7479,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f6f9113c6003492e74438d1288e30cffa8ccfdc2ef7b49b9e816d8034da18cd" dependencies = [ - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-reward-info", ] @@ -6674,7 +7491,7 @@ checksum = "2b293f4246626c0e0a991531f08848a713ada965612e99dc510963f04d12cae7" dependencies = [ "lazy_static", "solana-feature-set", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", ] @@ -6700,7 +7517,7 @@ dependencies = [ "bs58", "indicatif", "log", - "reqwest", + "reqwest 0.11.27", "reqwest-middleware", "semver", "serde", @@ -6714,9 +7531,9 @@ dependencies = [ "solana-epoch-schedule", "solana-feature-gate-interface", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client-api", "solana-signature", "solana-transaction", @@ -6736,7 +7553,7 @@ dependencies = [ "base64 0.22.1", "bs58", "jsonrpc-core", - "reqwest", + "reqwest 0.11.27", "reqwest-middleware", "semver", "serde", @@ -6749,7 +7566,7 @@ dependencies = [ "solana-fee-calculator", "solana-inflation", "solana-inline-spl", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signer", "solana-transaction-error", "solana-transaction-status-client-types", @@ -6763,6 +7580,12 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" +[[package]] +name = "solana-sanitize" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" + [[package]] name = "solana-sbpf" version = "0.10.0" @@ -6808,7 +7631,7 @@ dependencies = [ "solana-genesis-config", "solana-hard-forks", "solana-inflation", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", "solana-native-token", @@ -6821,13 +7644,13 @@ dependencies = [ "solana-presigner", "solana-program", "solana-program-memory", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-quic-definitions", "solana-rent-collector", "solana-rent-debits", "solana-reserved-account-keys", "solana-reward-info", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-sdk-macro", "solana-secp256k1-program", @@ -6857,7 +7680,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" dependencies = [ - "solana-pubkey", + "solana-pubkey 2.2.1", ] [[package]] @@ -6885,7 +7708,7 @@ dependencies = [ "serde_derive", "sha3", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-precompile-error", "solana-sdk-ids", ] @@ -6898,7 +7721,7 @@ checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" dependencies = [ "borsh 1.6.0", "libsecp256k1", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "thiserror 2.0.17", ] @@ -6911,7 +7734,7 @@ dependencies = [ "bytemuck", "openssl", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-precompile-error", "solana-sdk-ids", ] @@ -6969,9 +7792,9 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" dependencies = [ - "solana-instruction", - "solana-pubkey", - "solana-sanitize", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", + "solana-sanitize 2.2.1", ] [[package]] @@ -6981,7 +7804,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0037386961c0d633421f53560ad7c80675c0447cba4d1bb66d60974dd486c7ea" dependencies = [ "sha2 0.10.9", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "solana-hash", ] @@ -7017,7 +7840,7 @@ dependencies = [ "serde", "serde-big-array", "serde_derive", - "solana-sanitize", + "solana-sanitize 2.2.1", ] [[package]] @@ -7026,7 +7849,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c41991508a4b02f021c1342ba00bcfa098630b213726ceadc7cb032e051975b" dependencies = [ - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-transaction-error", ] @@ -7063,8 +7886,8 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" dependencies = [ - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", ] [[package]] @@ -7081,10 +7904,10 @@ dependencies = [ "solana-clock", "solana-cpi", "solana-decode-error", - "solana-instruction", - "solana-program-error", - "solana-pubkey", - "solana-system-interface", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "solana-system-interface 1.0.0", "solana-sysvar-id", ] @@ -7102,12 +7925,12 @@ dependencies = [ "solana-config-program", "solana-feature-set", "solana-genesis-config", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-native-token", "solana-packet", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", "solana-stake-interface", @@ -7129,9 +7952,9 @@ dependencies = [ "serde", "solana-account-decoder", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-transaction", "solana-transaction-context", @@ -7159,7 +7982,7 @@ dependencies = [ "solana-feature-set", "solana-fee-structure", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-instructions-sysvar", "solana-loader-v4-program", "solana-log-collector", @@ -7170,7 +7993,7 @@ dependencies = [ "solana-precompiles", "solana-program", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-rent-debits", "solana-reserved-account-keys", @@ -7202,7 +8025,7 @@ checksum = "4fc4392f0eed412141a376e99dfb052069b96f13697a9abb335504babe29387a" dependencies = [ "solana-hash", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-signature", "solana-transaction", @@ -7219,11 +8042,26 @@ dependencies = [ "serde", "serde_derive", "solana-decode-error", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "wasm-bindgen", ] +[[package]] +name = "solana-system-interface" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14591d6508042ebefb110305d3ba761615927146a26917ade45dc332d8e1ecde" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-address", + "solana-instruction 3.1.0", + "solana-msg 3.0.0", + "solana-program-error 3.0.0", +] + [[package]] name = "solana-system-program" version = "2.2.1" @@ -7236,15 +8074,15 @@ dependencies = [ "serde_derive", "solana-account", "solana-bincode", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-nonce", "solana-nonce-account", "solana-packet", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-sysvar", "solana-transaction-context", "solana-type-overrides", @@ -7259,9 +8097,9 @@ dependencies = [ "solana-hash", "solana-keypair", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signer", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction", ] @@ -7280,20 +8118,20 @@ dependencies = [ "serde_derive", "solana-account-info", "solana-clock", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-instructions-sysvar", "solana-last-restart-slot", "solana-program-entrypoint", - "solana-program-error", + "solana-program-error 2.2.2", "solana-program-memory", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-sdk-macro", "solana-slot-hashes", @@ -7308,7 +8146,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" dependencies = [ - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", ] @@ -7326,7 +8164,7 @@ checksum = "49d9eabdce318cb07c60a23f1cc367b43e177c79225b5c2a081869ad182172ad" dependencies = [ "eager", "enum-iterator", - "solana-pubkey", + "solana-pubkey 2.2.1", ] [[package]] @@ -7341,18 +8179,18 @@ dependencies = [ "solana-bincode", "solana-feature-set", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", "solana-precompiles", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-reserved-account-keys", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-short-vec", "solana-signature", "solana-signer", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction-error", "wasm-bindgen", ] @@ -7367,8 +8205,8 @@ dependencies = [ "serde", "serde_derive", "solana-account", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-rent", "solana-signature", ] @@ -7381,8 +8219,8 @@ checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" dependencies = [ "serde", "serde_derive", - "solana-instruction", - "solana-sanitize", + "solana-instruction 2.2.1", + "solana-sanitize 2.2.1", ] [[package]] @@ -7404,16 +8242,16 @@ dependencies = [ "solana-account-decoder", "solana-clock", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-loader-v2-interface", "solana-message", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-reserved-account-keys", "solana-reward-info", "solana-sdk-ids", "solana-signature", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction", "solana-transaction-error", "solana-transaction-status-client-types", @@ -7475,7 +8313,7 @@ dependencies = [ "serde", "serde_derive", "solana-feature-set", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-serde-varint", ] @@ -7493,14 +8331,14 @@ dependencies = [ "solana-clock", "solana-decode-error", "solana-hash", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", "solana-serde-varint", "solana-serialize-utils", "solana-short-vec", - "solana-system-interface", + "solana-system-interface 1.0.0", ] [[package]] @@ -7521,11 +8359,11 @@ dependencies = [ "solana-epoch-schedule", "solana-feature-set", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-packet", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", "solana-signer", @@ -7560,8 +8398,8 @@ dependencies = [ "serde_json", "sha3", "solana-derivation-path", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-seed-derivable", "solana-seed-phrase", @@ -7643,8 +8481,8 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f8349dbcbe575f354f9a533a21f272f3eb3808a49e2fdc1c34393b88ba76cb" dependencies = [ - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", ] [[package]] @@ -7654,7 +8492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7398da23554a31660f17718164e31d31900956054f54f52d5ec1be51cb4f4b3" dependencies = [ "bytemuck", - "solana-program-error", + "solana-program-error 2.2.2", "solana-sha256-hasher", "spl-discriminator-derive", ] @@ -7703,11 +8541,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f09647c0974e33366efeb83b8e2daebb329f0420149e74d3a4bd2c08cf9f7cb" dependencies = [ "solana-account-info", - "solana-instruction", - "solana-msg", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", "solana-program-entrypoint", - "solana-program-error", - "solana-pubkey", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", ] [[package]] @@ -7722,10 +8560,10 @@ dependencies = [ "num-derive", "num-traits", "solana-decode-error", - "solana-msg", - "solana-program-error", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", "solana-program-option", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-zk-sdk", "thiserror 2.0.17", ] @@ -7766,10 +8604,10 @@ dependencies = [ "num-traits", "solana-account-info", "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "spl-discriminator", "spl-pod", "spl-program-error", @@ -7856,7 +8694,7 @@ checksum = "170378693c5516090f6d37ae9bad2b9b6125069be68d9acd4865bbe9fc8499fd" dependencies = [ "base64 0.22.1", "bytemuck", - "solana-curve25519", + "solana-curve25519 2.2.1", "solana-zk-sdk", ] @@ -7867,7 +8705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eff2d6a445a147c9d6dd77b8301b1e116c8299601794b558eafa409b342faf96" dependencies = [ "bytemuck", - "solana-curve25519", + "solana-curve25519 2.2.1", "solana-program", "solana-zk-sdk", "spl-pod", @@ -7906,10 +8744,10 @@ dependencies = [ "num-derive", "num-traits", "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "spl-discriminator", "spl-pod", "thiserror 1.0.69", @@ -7926,10 +8764,10 @@ dependencies = [ "num-traits", "solana-borsh", "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "spl-discriminator", "spl-pod", "spl-type-length-value", @@ -7949,10 +8787,10 @@ dependencies = [ "solana-account-info", "solana-cpi", "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "spl-discriminator", "spl-pod", "spl-program-error", @@ -7972,8 +8810,8 @@ dependencies = [ "num-traits", "solana-account-info", "solana-decode-error", - "solana-msg", - "solana-program-error", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", "spl-discriminator", "spl-pod", "thiserror 1.0.69", @@ -8087,6 +8925,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -8107,7 +8948,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -8120,6 +8972,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tabular" version = "0.2.0" @@ -8146,14 +9008,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -8178,7 +9040,7 @@ dependencies = [ "magicblock-ledger", "magicblock-processor", "solana-account", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-program", "solana-rpc-client", @@ -8437,9 +9299,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -8460,21 +9322,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap 2.12.1", - "toml_datetime 0.7.3", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -8527,7 +9389,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" dependencies = [ "async-trait", - "axum 0.8.7", + "axum 0.8.8", "base64 0.22.1", "bytes", "h2 0.4.12", @@ -8568,7 +9430,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" dependencies = [ - "prettyplease 0.2.36", + "prettyplease 0.2.37", "proc-macro2", "prost-build 0.12.6", "quote", @@ -8581,7 +9443,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" dependencies = [ - "prettyplease 0.2.36", + "prettyplease 0.2.37", "proc-macro2", "prost-build 0.13.5", "prost-types 0.13.5", @@ -8652,6 +9514,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -8666,9 +9546,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -8688,9 +9568,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -8876,6 +9756,7 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -9158,6 +10039,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -9492,7 +10384,7 @@ dependencies = [ "solana-clock", "solana-hash", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-transaction", "solana-transaction-context", @@ -9577,9 +10469,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", @@ -9619,6 +10511,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "zmij" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9747e91771f56fd7893e1164abd78febd14a670ceec257caad15e051de35f06" + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 9b197ee7f..f33aa0aa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ split-debuginfo = "packed" [workspace] members = [ + "compressed-delegation-client", "magicblock-account-cloner", "magicblock-accounts", "magicblock-accounts-db", @@ -53,11 +54,12 @@ assert_matches = "1.5.0" async-trait = "0.1.77" base64 = "0.21.7" bincode = "1.3.3" -borsh = { version = "1.5.1", features = ["derive", "unstable__schema"] } +borsh = "0.10.4" bs58 = "0.5.1" byteorder = "1.5.0" chrono = "0.4" clap = "4.5.40" +compressed-delegation-client = { path = "./compressed-delegation-client" } console-subscriber = "0.5.0" derive_more = "2.0" dyn-clone = "1.0.20" @@ -84,6 +86,11 @@ json = { package = "sonic-rs", version = "0.5.3" } lazy_static = "1.4.0" libloading = "0.8" libc = "0.2.153" +light-client = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } +light-compressed-account = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } +light-hasher = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } +light-sdk = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } +light-sdk-types = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } log = { version = "0.4.20" } lru = "0.16.0" magic-domain-program = { git = "https://github.com/magicblock-labs/magic-domain-program.git", rev = "cffcfeb", default-features = false, features = [ @@ -148,6 +155,7 @@ solana-bpf-loader-program = { version = "2.2" } solana-clock = { version = "2.2" } solana-commitment-config = { version = "2.2" } solana-compute-budget-instruction = { version = "2.2" } +solana-cpi = { version = "2.2" } solana-compute-budget-interface = { version = "2.2" } solana-compute-budget-program = { version = "2.2" } solana-feature-gate-interface = { version = "2.2" } @@ -170,6 +178,7 @@ solana-message = { version = "2.2" } solana-metrics = { version = "2.2" } solana-native-token = { version = "2.2" } solana-program = "2.2" +solana-program-error = { version = "2.2" } solana-program-runtime = { version = "2.2" } solana-pubkey = { version = "2.2" } solana-pubsub-client = { version = "2.2" } diff --git a/compressed-delegation-client/Cargo.toml b/compressed-delegation-client/Cargo.toml new file mode 100644 index 000000000..e791abbe4 --- /dev/null +++ b/compressed-delegation-client/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "compressed-delegation-client" +version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +license.workspace = true +edition.workspace = true + +[features] +default = [] +serde = ["dep:serde"] +anchor = [] +anchor-idl-build = [] +fetch = [] + +[dependencies] +borsh = { workspace = true } +light-compressed-account = { workspace = true } +light-hasher = { workspace = true } +light-sdk = { workspace = true } +light-sdk-types = { workspace = true } +serde = { workspace = true, optional = true } +solana-account-info = { workspace = true } +solana-cpi = { workspace = true } +solana-instruction = { workspace = true } +solana-program-error = { workspace = true } +solana-pubkey = { workspace = true } diff --git a/compressed-delegation-client/src/generated/accounts/compressed_delegation_record.rs b/compressed-delegation-client/src/generated/accounts/compressed_delegation_record.rs new file mode 100644 index 000000000..93020e05b --- /dev/null +++ b/compressed-delegation-client/src/generated/accounts/compressed_delegation_record.rs @@ -0,0 +1,169 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use borsh::BorshDeserialize; +use borsh::BorshSerialize; +use solana_pubkey::Pubkey; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CompressedDelegationRecord { + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub pda: Pubkey, + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub authority: Pubkey, + pub last_update_nonce: u64, + pub is_undelegatable: bool, + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub owner: Pubkey, + pub delegation_slot: u64, + pub lamports: u64, + pub data: Vec, +} + +impl CompressedDelegationRecord { + #[inline(always)] + pub fn from_bytes(data: &[u8]) -> Result { + let mut data = data; + Self::deserialize(&mut data) + } +} + +impl<'a> TryFrom<&solana_account_info::AccountInfo<'a>> + for CompressedDelegationRecord +{ + type Error = std::io::Error; + + fn try_from( + account_info: &solana_account_info::AccountInfo<'a>, + ) -> Result { + let mut data: &[u8] = &(*account_info.data).borrow(); + Self::deserialize(&mut data) + } +} + +#[cfg(feature = "fetch")] +pub fn fetch_compressed_delegation_record( + rpc: &solana_client::rpc_client::RpcClient, + address: &solana_pubkey::Pubkey, +) -> Result< + crate::shared::DecodedAccount, + std::io::Error, +> { + let accounts = fetch_all_compressed_delegation_record(rpc, &[*address])?; + Ok(accounts[0].clone()) +} + +#[cfg(feature = "fetch")] +pub fn fetch_all_compressed_delegation_record( + rpc: &solana_client::rpc_client::RpcClient, + addresses: &[solana_pubkey::Pubkey], +) -> Result< + Vec>, + std::io::Error, +> { + let accounts = rpc.get_multiple_accounts(addresses).map_err(|e| { + std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) + })?; + let mut decoded_accounts: Vec< + crate::shared::DecodedAccount, + > = Vec::new(); + for i in 0..addresses.len() { + let address = addresses[i]; + let account = accounts[i].as_ref().ok_or(std::io::Error::new( + std::io::ErrorKind::Other, + format!("Account not found: {}", address), + ))?; + let data = CompressedDelegationRecord::from_bytes(&account.data)?; + decoded_accounts.push(crate::shared::DecodedAccount { + address, + account: account.clone(), + data, + }); + } + Ok(decoded_accounts) +} + +#[cfg(feature = "fetch")] +pub fn fetch_maybe_compressed_delegation_record( + rpc: &solana_client::rpc_client::RpcClient, + address: &solana_pubkey::Pubkey, +) -> Result< + crate::shared::MaybeAccount, + std::io::Error, +> { + let accounts = + fetch_all_maybe_compressed_delegation_record(rpc, &[*address])?; + Ok(accounts[0].clone()) +} + +#[cfg(feature = "fetch")] +pub fn fetch_all_maybe_compressed_delegation_record( + rpc: &solana_client::rpc_client::RpcClient, + addresses: &[solana_pubkey::Pubkey], +) -> Result< + Vec>, + std::io::Error, +> { + let accounts = rpc.get_multiple_accounts(addresses).map_err(|e| { + std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) + })?; + let mut decoded_accounts: Vec< + crate::shared::MaybeAccount, + > = Vec::new(); + for i in 0..addresses.len() { + let address = addresses[i]; + if let Some(account) = accounts[i].as_ref() { + let data = CompressedDelegationRecord::from_bytes(&account.data)?; + decoded_accounts.push(crate::shared::MaybeAccount::Exists( + crate::shared::DecodedAccount { + address, + account: account.clone(), + data, + }, + )); + } else { + decoded_accounts + .push(crate::shared::MaybeAccount::NotFound(address)); + } + } + Ok(decoded_accounts) +} + +#[cfg(feature = "anchor")] +impl anchor_lang::AccountDeserialize for CompressedDelegationRecord { + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + Ok(Self::deserialize(buf)?) + } +} + +#[cfg(feature = "anchor")] +impl anchor_lang::AccountSerialize for CompressedDelegationRecord {} + +#[cfg(feature = "anchor")] +impl anchor_lang::Owner for CompressedDelegationRecord { + fn owner() -> Pubkey { + crate::COMPRESSED_DELEGATION_ID + } +} + +#[cfg(feature = "anchor-idl-build")] +impl anchor_lang::IdlBuild for CompressedDelegationRecord {} + +#[cfg(feature = "anchor-idl-build")] +impl anchor_lang::Discriminator for CompressedDelegationRecord { + const DISCRIMINATOR: &[u8] = &[0; 8]; +} diff --git a/compressed-delegation-client/src/generated/accounts/mod.rs b/compressed-delegation-client/src/generated/accounts/mod.rs new file mode 100644 index 000000000..a0b7453af --- /dev/null +++ b/compressed-delegation-client/src/generated/accounts/mod.rs @@ -0,0 +1,10 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +pub(crate) mod r#compressed_delegation_record; + +pub use self::r#compressed_delegation_record::*; diff --git a/compressed-delegation-client/src/generated/errors/mod.rs b/compressed-delegation-client/src/generated/errors/mod.rs new file mode 100644 index 000000000..6172ba60a --- /dev/null +++ b/compressed-delegation-client/src/generated/errors/mod.rs @@ -0,0 +1,6 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! diff --git a/compressed-delegation-client/src/generated/instructions/commit.rs b/compressed-delegation-client/src/generated/instructions/commit.rs new file mode 100644 index 000000000..804198148 --- /dev/null +++ b/compressed-delegation-client/src/generated/instructions/commit.rs @@ -0,0 +1,380 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::CommitArgs; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +pub const COMMIT_DISCRIMINATOR: u64 = 2; + +/// Accounts. +#[derive(Debug)] +pub struct Commit { + pub validator: solana_pubkey::Pubkey, + + pub delegated_account: solana_pubkey::Pubkey, +} + +impl Commit { + pub fn instruction( + &self, + args: CommitInstructionArgs, + ) -> solana_instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: CommitInstructionArgs, + remaining_accounts: &[solana_instruction::AccountMeta], + ) -> solana_instruction::Instruction { + let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); + accounts + .push(solana_instruction::AccountMeta::new(self.validator, true)); + accounts.push(solana_instruction::AccountMeta::new_readonly( + self.delegated_account, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = borsh::to_vec(&CommitInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&args).unwrap(); + data.append(&mut args); + + solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + } + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CommitInstructionData { + discriminator: u64, +} + +impl CommitInstructionData { + pub fn new() -> Self { + Self { discriminator: 2 } + } +} + +impl Default for CommitInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CommitInstructionArgs { + pub args: CommitArgs, +} + +/// Instruction builder for `Commit`. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` validator +/// 1. `[]` delegated_account +#[derive(Clone, Debug, Default)] +pub struct CommitBuilder { + validator: Option, + delegated_account: Option, + args: Option, + __remaining_accounts: Vec, +} + +impl CommitBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn validator(&mut self, validator: solana_pubkey::Pubkey) -> &mut Self { + self.validator = Some(validator); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: solana_pubkey::Pubkey, + ) -> &mut Self { + self.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn args(&mut self, args: CommitArgs) -> &mut Self { + self.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_instruction::Instruction { + let accounts = Commit { + validator: self.validator.expect("validator is not set"), + delegated_account: self + .delegated_account + .expect("delegated_account is not set"), + }; + let args = CommitInstructionArgs { + args: self.args.clone().expect("args is not set"), + }; + + accounts.instruction_with_remaining_accounts( + args, + &self.__remaining_accounts, + ) + } +} + +/// `commit` CPI accounts. +pub struct CommitCpiAccounts<'a, 'b> { + pub validator: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, +} + +/// `commit` CPI instruction. +pub struct CommitCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_account_info::AccountInfo<'a>, + + pub validator: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: CommitInstructionArgs, +} + +impl<'a, 'b> CommitCpi<'a, 'b> { + pub fn new( + program: &'b solana_account_info::AccountInfo<'a>, + accounts: CommitCpiAccounts<'a, 'b>, + args: CommitInstructionArgs, + ) -> Self { + Self { + __program: program, + validator: accounts.validator, + delegated_account: accounts.delegated_account, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); + accounts.push(solana_instruction::AccountMeta::new( + *self.validator.key, + true, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + *self.delegated_account.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = borsh::to_vec(&CommitInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&self.__args).unwrap(); + data.append(&mut args); + + let instruction = solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + }; + let mut account_infos = + Vec::with_capacity(3 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.validator.clone()); + account_infos.push(self.delegated_account.clone()); + remaining_accounts.iter().for_each(|remaining_account| { + account_infos.push(remaining_account.0.clone()) + }); + + if signers_seeds.is_empty() { + solana_cpi::invoke(&instruction, &account_infos) + } else { + solana_cpi::invoke_signed( + &instruction, + &account_infos, + signers_seeds, + ) + } + } +} + +/// Instruction builder for `Commit` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` validator +/// 1. `[]` delegated_account +#[derive(Clone, Debug)] +pub struct CommitCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> CommitCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(CommitCpiBuilderInstruction { + __program: program, + validator: None, + delegated_account: None, + args: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn validator( + &mut self, + validator: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.validator = Some(validator); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn args(&mut self, args: CommitArgs) -> &mut Self { + self.instruction.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction.__remaining_accounts.push(( + account, + is_writable, + is_signer, + )); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[(&'b solana_account_info::AccountInfo<'a>, bool, bool)], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + let args = CommitInstructionArgs { + args: self.instruction.args.clone().expect("args is not set"), + }; + let instruction = CommitCpi { + __program: self.instruction.__program, + + validator: self + .instruction + .validator + .expect("validator is not set"), + + delegated_account: self + .instruction + .delegated_account + .expect("delegated_account is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct CommitCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_account_info::AccountInfo<'a>, + validator: Option<&'b solana_account_info::AccountInfo<'a>>, + delegated_account: Option<&'b solana_account_info::AccountInfo<'a>>, + args: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: + Vec<(&'b solana_account_info::AccountInfo<'a>, bool, bool)>, +} diff --git a/compressed-delegation-client/src/generated/instructions/delegate.rs b/compressed-delegation-client/src/generated/instructions/delegate.rs new file mode 100644 index 000000000..983d47e27 --- /dev/null +++ b/compressed-delegation-client/src/generated/instructions/delegate.rs @@ -0,0 +1,374 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::DelegateArgs; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +pub const DELEGATE_DISCRIMINATOR: u64 = 0; + +/// Accounts. +#[derive(Debug)] +pub struct Delegate { + pub payer: solana_pubkey::Pubkey, + + pub delegated_account: solana_pubkey::Pubkey, +} + +impl Delegate { + pub fn instruction( + &self, + args: DelegateInstructionArgs, + ) -> solana_instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: DelegateInstructionArgs, + remaining_accounts: &[solana_instruction::AccountMeta], + ) -> solana_instruction::Instruction { + let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); + accounts.push(solana_instruction::AccountMeta::new(self.payer, true)); + accounts.push(solana_instruction::AccountMeta::new_readonly( + self.delegated_account, + true, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = borsh::to_vec(&DelegateInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&args).unwrap(); + data.append(&mut args); + + solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + } + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DelegateInstructionData { + discriminator: u64, +} + +impl DelegateInstructionData { + pub fn new() -> Self { + Self { discriminator: 0 } + } +} + +impl Default for DelegateInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DelegateInstructionArgs { + pub args: DelegateArgs, +} + +/// Instruction builder for `Delegate`. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[signer]` delegated_account +#[derive(Clone, Debug, Default)] +pub struct DelegateBuilder { + payer: Option, + delegated_account: Option, + args: Option, + __remaining_accounts: Vec, +} + +impl DelegateBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn payer(&mut self, payer: solana_pubkey::Pubkey) -> &mut Self { + self.payer = Some(payer); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: solana_pubkey::Pubkey, + ) -> &mut Self { + self.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn args(&mut self, args: DelegateArgs) -> &mut Self { + self.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_instruction::Instruction { + let accounts = Delegate { + payer: self.payer.expect("payer is not set"), + delegated_account: self + .delegated_account + .expect("delegated_account is not set"), + }; + let args = DelegateInstructionArgs { + args: self.args.clone().expect("args is not set"), + }; + + accounts.instruction_with_remaining_accounts( + args, + &self.__remaining_accounts, + ) + } +} + +/// `delegate` CPI accounts. +pub struct DelegateCpiAccounts<'a, 'b> { + pub payer: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, +} + +/// `delegate` CPI instruction. +pub struct DelegateCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_account_info::AccountInfo<'a>, + + pub payer: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: DelegateInstructionArgs, +} + +impl<'a, 'b> DelegateCpi<'a, 'b> { + pub fn new( + program: &'b solana_account_info::AccountInfo<'a>, + accounts: DelegateCpiAccounts<'a, 'b>, + args: DelegateInstructionArgs, + ) -> Self { + Self { + __program: program, + payer: accounts.payer, + delegated_account: accounts.delegated_account, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); + accounts + .push(solana_instruction::AccountMeta::new(*self.payer.key, true)); + accounts.push(solana_instruction::AccountMeta::new_readonly( + *self.delegated_account.key, + true, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = borsh::to_vec(&DelegateInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&self.__args).unwrap(); + data.append(&mut args); + + let instruction = solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + }; + let mut account_infos = + Vec::with_capacity(3 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.payer.clone()); + account_infos.push(self.delegated_account.clone()); + remaining_accounts.iter().for_each(|remaining_account| { + account_infos.push(remaining_account.0.clone()) + }); + + if signers_seeds.is_empty() { + solana_cpi::invoke(&instruction, &account_infos) + } else { + solana_cpi::invoke_signed( + &instruction, + &account_infos, + signers_seeds, + ) + } + } +} + +/// Instruction builder for `Delegate` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[signer]` delegated_account +#[derive(Clone, Debug)] +pub struct DelegateCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> DelegateCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(DelegateCpiBuilderInstruction { + __program: program, + payer: None, + delegated_account: None, + args: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn payer( + &mut self, + payer: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.payer = Some(payer); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn args(&mut self, args: DelegateArgs) -> &mut Self { + self.instruction.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction.__remaining_accounts.push(( + account, + is_writable, + is_signer, + )); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[(&'b solana_account_info::AccountInfo<'a>, bool, bool)], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + let args = DelegateInstructionArgs { + args: self.instruction.args.clone().expect("args is not set"), + }; + let instruction = DelegateCpi { + __program: self.instruction.__program, + + payer: self.instruction.payer.expect("payer is not set"), + + delegated_account: self + .instruction + .delegated_account + .expect("delegated_account is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct DelegateCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_account_info::AccountInfo<'a>, + payer: Option<&'b solana_account_info::AccountInfo<'a>>, + delegated_account: Option<&'b solana_account_info::AccountInfo<'a>>, + args: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: + Vec<(&'b solana_account_info::AccountInfo<'a>, bool, bool)>, +} diff --git a/compressed-delegation-client/src/generated/instructions/delegate_compressed.rs b/compressed-delegation-client/src/generated/instructions/delegate_compressed.rs new file mode 100644 index 000000000..bdc1676dd --- /dev/null +++ b/compressed-delegation-client/src/generated/instructions/delegate_compressed.rs @@ -0,0 +1,523 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::DelegateCompressedArgs; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +pub const DELEGATE_COMPRESSED_DISCRIMINATOR: u64 = 1; + +/// Accounts. +#[derive(Debug)] +pub struct DelegateCompressed { + pub payer: solana_pubkey::Pubkey, + + pub delegated_account: solana_pubkey::Pubkey, + + pub compressed_delegation_program: solana_pubkey::Pubkey, + + pub compressed_delegation_cpi_signer: solana_pubkey::Pubkey, + + pub light_system_program: solana_pubkey::Pubkey, +} + +impl DelegateCompressed { + pub fn instruction( + &self, + args: DelegateCompressedInstructionArgs, + ) -> solana_instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: DelegateCompressedInstructionArgs, + remaining_accounts: &[solana_instruction::AccountMeta], + ) -> solana_instruction::Instruction { + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); + accounts.push(solana_instruction::AccountMeta::new(self.payer, true)); + accounts.push(solana_instruction::AccountMeta::new_readonly( + self.delegated_account, + true, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + self.compressed_delegation_program, + false, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + self.compressed_delegation_cpi_signer, + false, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + self.light_system_program, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = + borsh::to_vec(&DelegateCompressedInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&args).unwrap(); + data.append(&mut args); + + solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + } + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DelegateCompressedInstructionData { + discriminator: u64, +} + +impl DelegateCompressedInstructionData { + pub fn new() -> Self { + Self { discriminator: 1 } + } +} + +impl Default for DelegateCompressedInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DelegateCompressedInstructionArgs { + pub args: DelegateCompressedArgs, +} + +/// Instruction builder for `DelegateCompressed`. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[signer]` delegated_account +/// 2. `[]` compressed_delegation_program +/// 3. `[]` compressed_delegation_cpi_signer +/// 4. `[]` light_system_program +#[derive(Clone, Debug, Default)] +pub struct DelegateCompressedBuilder { + payer: Option, + delegated_account: Option, + compressed_delegation_program: Option, + compressed_delegation_cpi_signer: Option, + light_system_program: Option, + args: Option, + __remaining_accounts: Vec, +} + +impl DelegateCompressedBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn payer(&mut self, payer: solana_pubkey::Pubkey) -> &mut Self { + self.payer = Some(payer); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: solana_pubkey::Pubkey, + ) -> &mut Self { + self.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn compressed_delegation_program( + &mut self, + compressed_delegation_program: solana_pubkey::Pubkey, + ) -> &mut Self { + self.compressed_delegation_program = + Some(compressed_delegation_program); + self + } + #[inline(always)] + pub fn compressed_delegation_cpi_signer( + &mut self, + compressed_delegation_cpi_signer: solana_pubkey::Pubkey, + ) -> &mut Self { + self.compressed_delegation_cpi_signer = + Some(compressed_delegation_cpi_signer); + self + } + #[inline(always)] + pub fn light_system_program( + &mut self, + light_system_program: solana_pubkey::Pubkey, + ) -> &mut Self { + self.light_system_program = Some(light_system_program); + self + } + #[inline(always)] + pub fn args(&mut self, args: DelegateCompressedArgs) -> &mut Self { + self.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_instruction::Instruction { + let accounts = DelegateCompressed { + payer: self.payer.expect("payer is not set"), + delegated_account: self + .delegated_account + .expect("delegated_account is not set"), + compressed_delegation_program: self + .compressed_delegation_program + .expect("compressed_delegation_program is not set"), + compressed_delegation_cpi_signer: self + .compressed_delegation_cpi_signer + .expect("compressed_delegation_cpi_signer is not set"), + light_system_program: self + .light_system_program + .expect("light_system_program is not set"), + }; + let args = DelegateCompressedInstructionArgs { + args: self.args.clone().expect("args is not set"), + }; + + accounts.instruction_with_remaining_accounts( + args, + &self.__remaining_accounts, + ) + } +} + +/// `delegate_compressed` CPI accounts. +pub struct DelegateCompressedCpiAccounts<'a, 'b> { + pub payer: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, + + pub compressed_delegation_program: &'b solana_account_info::AccountInfo<'a>, + + pub compressed_delegation_cpi_signer: + &'b solana_account_info::AccountInfo<'a>, + + pub light_system_program: &'b solana_account_info::AccountInfo<'a>, +} + +/// `delegate_compressed` CPI instruction. +pub struct DelegateCompressedCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_account_info::AccountInfo<'a>, + + pub payer: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, + + pub compressed_delegation_program: &'b solana_account_info::AccountInfo<'a>, + + pub compressed_delegation_cpi_signer: + &'b solana_account_info::AccountInfo<'a>, + + pub light_system_program: &'b solana_account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: DelegateCompressedInstructionArgs, +} + +impl<'a, 'b> DelegateCompressedCpi<'a, 'b> { + pub fn new( + program: &'b solana_account_info::AccountInfo<'a>, + accounts: DelegateCompressedCpiAccounts<'a, 'b>, + args: DelegateCompressedInstructionArgs, + ) -> Self { + Self { + __program: program, + payer: accounts.payer, + delegated_account: accounts.delegated_account, + compressed_delegation_program: accounts + .compressed_delegation_program, + compressed_delegation_cpi_signer: accounts + .compressed_delegation_cpi_signer, + light_system_program: accounts.light_system_program, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + let mut accounts = Vec::with_capacity(5 + remaining_accounts.len()); + accounts + .push(solana_instruction::AccountMeta::new(*self.payer.key, true)); + accounts.push(solana_instruction::AccountMeta::new_readonly( + *self.delegated_account.key, + true, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + *self.compressed_delegation_program.key, + false, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + *self.compressed_delegation_cpi_signer.key, + false, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + *self.light_system_program.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = + borsh::to_vec(&DelegateCompressedInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&self.__args).unwrap(); + data.append(&mut args); + + let instruction = solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + }; + let mut account_infos = + Vec::with_capacity(6 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.payer.clone()); + account_infos.push(self.delegated_account.clone()); + account_infos.push(self.compressed_delegation_program.clone()); + account_infos.push(self.compressed_delegation_cpi_signer.clone()); + account_infos.push(self.light_system_program.clone()); + remaining_accounts.iter().for_each(|remaining_account| { + account_infos.push(remaining_account.0.clone()) + }); + + if signers_seeds.is_empty() { + solana_cpi::invoke(&instruction, &account_infos) + } else { + solana_cpi::invoke_signed( + &instruction, + &account_infos, + signers_seeds, + ) + } + } +} + +/// Instruction builder for `DelegateCompressed` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[signer]` delegated_account +/// 2. `[]` compressed_delegation_program +/// 3. `[]` compressed_delegation_cpi_signer +/// 4. `[]` light_system_program +#[derive(Clone, Debug)] +pub struct DelegateCompressedCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> DelegateCompressedCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(DelegateCompressedCpiBuilderInstruction { + __program: program, + payer: None, + delegated_account: None, + compressed_delegation_program: None, + compressed_delegation_cpi_signer: None, + light_system_program: None, + args: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn payer( + &mut self, + payer: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.payer = Some(payer); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn compressed_delegation_program( + &mut self, + compressed_delegation_program: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.compressed_delegation_program = + Some(compressed_delegation_program); + self + } + #[inline(always)] + pub fn compressed_delegation_cpi_signer( + &mut self, + compressed_delegation_cpi_signer: &'b solana_account_info::AccountInfo< + 'a, + >, + ) -> &mut Self { + self.instruction.compressed_delegation_cpi_signer = + Some(compressed_delegation_cpi_signer); + self + } + #[inline(always)] + pub fn light_system_program( + &mut self, + light_system_program: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.light_system_program = Some(light_system_program); + self + } + #[inline(always)] + pub fn args(&mut self, args: DelegateCompressedArgs) -> &mut Self { + self.instruction.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction.__remaining_accounts.push(( + account, + is_writable, + is_signer, + )); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[(&'b solana_account_info::AccountInfo<'a>, bool, bool)], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + let args = DelegateCompressedInstructionArgs { + args: self.instruction.args.clone().expect("args is not set"), + }; + let instruction = DelegateCompressedCpi { + __program: self.instruction.__program, + + payer: self.instruction.payer.expect("payer is not set"), + + delegated_account: self + .instruction + .delegated_account + .expect("delegated_account is not set"), + + compressed_delegation_program: self + .instruction + .compressed_delegation_program + .expect("compressed_delegation_program is not set"), + + compressed_delegation_cpi_signer: self + .instruction + .compressed_delegation_cpi_signer + .expect("compressed_delegation_cpi_signer is not set"), + + light_system_program: self + .instruction + .light_system_program + .expect("light_system_program is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct DelegateCompressedCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_account_info::AccountInfo<'a>, + payer: Option<&'b solana_account_info::AccountInfo<'a>>, + delegated_account: Option<&'b solana_account_info::AccountInfo<'a>>, + compressed_delegation_program: + Option<&'b solana_account_info::AccountInfo<'a>>, + compressed_delegation_cpi_signer: + Option<&'b solana_account_info::AccountInfo<'a>>, + light_system_program: Option<&'b solana_account_info::AccountInfo<'a>>, + args: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: + Vec<(&'b solana_account_info::AccountInfo<'a>, bool, bool)>, +} diff --git a/compressed-delegation-client/src/generated/instructions/finalize.rs b/compressed-delegation-client/src/generated/instructions/finalize.rs new file mode 100644 index 000000000..f421fa003 --- /dev/null +++ b/compressed-delegation-client/src/generated/instructions/finalize.rs @@ -0,0 +1,380 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::FinalizeArgs; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +pub const FINALIZE_DISCRIMINATOR: u64 = 3; + +/// Accounts. +#[derive(Debug)] +pub struct Finalize { + pub validator: solana_pubkey::Pubkey, + + pub delegated_account: solana_pubkey::Pubkey, +} + +impl Finalize { + pub fn instruction( + &self, + args: FinalizeInstructionArgs, + ) -> solana_instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: FinalizeInstructionArgs, + remaining_accounts: &[solana_instruction::AccountMeta], + ) -> solana_instruction::Instruction { + let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); + accounts + .push(solana_instruction::AccountMeta::new(self.validator, true)); + accounts.push(solana_instruction::AccountMeta::new_readonly( + self.delegated_account, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = borsh::to_vec(&FinalizeInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&args).unwrap(); + data.append(&mut args); + + solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + } + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FinalizeInstructionData { + discriminator: u64, +} + +impl FinalizeInstructionData { + pub fn new() -> Self { + Self { discriminator: 3 } + } +} + +impl Default for FinalizeInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FinalizeInstructionArgs { + pub args: FinalizeArgs, +} + +/// Instruction builder for `Finalize`. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` validator +/// 1. `[]` delegated_account +#[derive(Clone, Debug, Default)] +pub struct FinalizeBuilder { + validator: Option, + delegated_account: Option, + args: Option, + __remaining_accounts: Vec, +} + +impl FinalizeBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn validator(&mut self, validator: solana_pubkey::Pubkey) -> &mut Self { + self.validator = Some(validator); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: solana_pubkey::Pubkey, + ) -> &mut Self { + self.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn args(&mut self, args: FinalizeArgs) -> &mut Self { + self.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_instruction::Instruction { + let accounts = Finalize { + validator: self.validator.expect("validator is not set"), + delegated_account: self + .delegated_account + .expect("delegated_account is not set"), + }; + let args = FinalizeInstructionArgs { + args: self.args.clone().expect("args is not set"), + }; + + accounts.instruction_with_remaining_accounts( + args, + &self.__remaining_accounts, + ) + } +} + +/// `finalize` CPI accounts. +pub struct FinalizeCpiAccounts<'a, 'b> { + pub validator: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, +} + +/// `finalize` CPI instruction. +pub struct FinalizeCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_account_info::AccountInfo<'a>, + + pub validator: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: FinalizeInstructionArgs, +} + +impl<'a, 'b> FinalizeCpi<'a, 'b> { + pub fn new( + program: &'b solana_account_info::AccountInfo<'a>, + accounts: FinalizeCpiAccounts<'a, 'b>, + args: FinalizeInstructionArgs, + ) -> Self { + Self { + __program: program, + validator: accounts.validator, + delegated_account: accounts.delegated_account, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); + accounts.push(solana_instruction::AccountMeta::new( + *self.validator.key, + true, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + *self.delegated_account.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = borsh::to_vec(&FinalizeInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&self.__args).unwrap(); + data.append(&mut args); + + let instruction = solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + }; + let mut account_infos = + Vec::with_capacity(3 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.validator.clone()); + account_infos.push(self.delegated_account.clone()); + remaining_accounts.iter().for_each(|remaining_account| { + account_infos.push(remaining_account.0.clone()) + }); + + if signers_seeds.is_empty() { + solana_cpi::invoke(&instruction, &account_infos) + } else { + solana_cpi::invoke_signed( + &instruction, + &account_infos, + signers_seeds, + ) + } + } +} + +/// Instruction builder for `Finalize` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` validator +/// 1. `[]` delegated_account +#[derive(Clone, Debug)] +pub struct FinalizeCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> FinalizeCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(FinalizeCpiBuilderInstruction { + __program: program, + validator: None, + delegated_account: None, + args: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn validator( + &mut self, + validator: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.validator = Some(validator); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn args(&mut self, args: FinalizeArgs) -> &mut Self { + self.instruction.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction.__remaining_accounts.push(( + account, + is_writable, + is_signer, + )); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[(&'b solana_account_info::AccountInfo<'a>, bool, bool)], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + let args = FinalizeInstructionArgs { + args: self.instruction.args.clone().expect("args is not set"), + }; + let instruction = FinalizeCpi { + __program: self.instruction.__program, + + validator: self + .instruction + .validator + .expect("validator is not set"), + + delegated_account: self + .instruction + .delegated_account + .expect("delegated_account is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct FinalizeCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_account_info::AccountInfo<'a>, + validator: Option<&'b solana_account_info::AccountInfo<'a>>, + delegated_account: Option<&'b solana_account_info::AccountInfo<'a>>, + args: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: + Vec<(&'b solana_account_info::AccountInfo<'a>, bool, bool)>, +} diff --git a/compressed-delegation-client/src/generated/instructions/mod.rs b/compressed-delegation-client/src/generated/instructions/mod.rs new file mode 100644 index 000000000..18606e53f --- /dev/null +++ b/compressed-delegation-client/src/generated/instructions/mod.rs @@ -0,0 +1,20 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +pub(crate) mod r#commit; +pub(crate) mod r#delegate; +pub(crate) mod r#delegate_compressed; +pub(crate) mod r#finalize; +pub(crate) mod r#undelegate; +pub(crate) mod r#undelegate_compressed; + +pub use self::r#commit::*; +pub use self::r#delegate::*; +pub use self::r#delegate_compressed::*; +pub use self::r#finalize::*; +pub use self::r#undelegate::*; +pub use self::r#undelegate_compressed::*; diff --git a/compressed-delegation-client/src/generated/instructions/undelegate.rs b/compressed-delegation-client/src/generated/instructions/undelegate.rs new file mode 100644 index 000000000..42105e09c --- /dev/null +++ b/compressed-delegation-client/src/generated/instructions/undelegate.rs @@ -0,0 +1,466 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::UndelegateArgs; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +pub const UNDELEGATE_DISCRIMINATOR: u64 = 4; + +/// Accounts. +#[derive(Debug)] +pub struct Undelegate { + pub payer: solana_pubkey::Pubkey, + + pub delegated_account: solana_pubkey::Pubkey, + + pub owner_program: solana_pubkey::Pubkey, + + pub system_program: solana_pubkey::Pubkey, +} + +impl Undelegate { + pub fn instruction( + &self, + args: UndelegateInstructionArgs, + ) -> solana_instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: UndelegateInstructionArgs, + remaining_accounts: &[solana_instruction::AccountMeta], + ) -> solana_instruction::Instruction { + let mut accounts = Vec::with_capacity(4 + remaining_accounts.len()); + accounts.push(solana_instruction::AccountMeta::new(self.payer, true)); + accounts.push(solana_instruction::AccountMeta::new( + self.delegated_account, + false, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + self.owner_program, + false, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + self.system_program, + false, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = + borsh::to_vec(&UndelegateInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&args).unwrap(); + data.append(&mut args); + + solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + } + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct UndelegateInstructionData { + discriminator: u64, +} + +impl UndelegateInstructionData { + pub fn new() -> Self { + Self { discriminator: 4 } + } +} + +impl Default for UndelegateInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct UndelegateInstructionArgs { + pub args: UndelegateArgs, +} + +/// Instruction builder for `Undelegate`. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[writable]` delegated_account +/// 2. `[]` owner_program +/// 3. `[]` system_program +#[derive(Clone, Debug, Default)] +pub struct UndelegateBuilder { + payer: Option, + delegated_account: Option, + owner_program: Option, + system_program: Option, + args: Option, + __remaining_accounts: Vec, +} + +impl UndelegateBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn payer(&mut self, payer: solana_pubkey::Pubkey) -> &mut Self { + self.payer = Some(payer); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: solana_pubkey::Pubkey, + ) -> &mut Self { + self.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn owner_program( + &mut self, + owner_program: solana_pubkey::Pubkey, + ) -> &mut Self { + self.owner_program = Some(owner_program); + self + } + #[inline(always)] + pub fn system_program( + &mut self, + system_program: solana_pubkey::Pubkey, + ) -> &mut Self { + self.system_program = Some(system_program); + self + } + #[inline(always)] + pub fn args(&mut self, args: UndelegateArgs) -> &mut Self { + self.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_instruction::Instruction { + let accounts = Undelegate { + payer: self.payer.expect("payer is not set"), + delegated_account: self + .delegated_account + .expect("delegated_account is not set"), + owner_program: self + .owner_program + .expect("owner_program is not set"), + system_program: self + .system_program + .expect("system_program is not set"), + }; + let args = UndelegateInstructionArgs { + args: self.args.clone().expect("args is not set"), + }; + + accounts.instruction_with_remaining_accounts( + args, + &self.__remaining_accounts, + ) + } +} + +/// `undelegate` CPI accounts. +pub struct UndelegateCpiAccounts<'a, 'b> { + pub payer: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, + + pub owner_program: &'b solana_account_info::AccountInfo<'a>, + + pub system_program: &'b solana_account_info::AccountInfo<'a>, +} + +/// `undelegate` CPI instruction. +pub struct UndelegateCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_account_info::AccountInfo<'a>, + + pub payer: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, + + pub owner_program: &'b solana_account_info::AccountInfo<'a>, + + pub system_program: &'b solana_account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: UndelegateInstructionArgs, +} + +impl<'a, 'b> UndelegateCpi<'a, 'b> { + pub fn new( + program: &'b solana_account_info::AccountInfo<'a>, + accounts: UndelegateCpiAccounts<'a, 'b>, + args: UndelegateInstructionArgs, + ) -> Self { + Self { + __program: program, + payer: accounts.payer, + delegated_account: accounts.delegated_account, + owner_program: accounts.owner_program, + system_program: accounts.system_program, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + let mut accounts = Vec::with_capacity(4 + remaining_accounts.len()); + accounts + .push(solana_instruction::AccountMeta::new(*self.payer.key, true)); + accounts.push(solana_instruction::AccountMeta::new( + *self.delegated_account.key, + false, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + *self.owner_program.key, + false, + )); + accounts.push(solana_instruction::AccountMeta::new_readonly( + *self.system_program.key, + false, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = + borsh::to_vec(&UndelegateInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&self.__args).unwrap(); + data.append(&mut args); + + let instruction = solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + }; + let mut account_infos = + Vec::with_capacity(5 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.payer.clone()); + account_infos.push(self.delegated_account.clone()); + account_infos.push(self.owner_program.clone()); + account_infos.push(self.system_program.clone()); + remaining_accounts.iter().for_each(|remaining_account| { + account_infos.push(remaining_account.0.clone()) + }); + + if signers_seeds.is_empty() { + solana_cpi::invoke(&instruction, &account_infos) + } else { + solana_cpi::invoke_signed( + &instruction, + &account_infos, + signers_seeds, + ) + } + } +} + +/// Instruction builder for `Undelegate` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[writable]` delegated_account +/// 2. `[]` owner_program +/// 3. `[]` system_program +#[derive(Clone, Debug)] +pub struct UndelegateCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> UndelegateCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(UndelegateCpiBuilderInstruction { + __program: program, + payer: None, + delegated_account: None, + owner_program: None, + system_program: None, + args: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn payer( + &mut self, + payer: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.payer = Some(payer); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn owner_program( + &mut self, + owner_program: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.owner_program = Some(owner_program); + self + } + #[inline(always)] + pub fn system_program( + &mut self, + system_program: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.system_program = Some(system_program); + self + } + #[inline(always)] + pub fn args(&mut self, args: UndelegateArgs) -> &mut Self { + self.instruction.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction.__remaining_accounts.push(( + account, + is_writable, + is_signer, + )); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[(&'b solana_account_info::AccountInfo<'a>, bool, bool)], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + let args = UndelegateInstructionArgs { + args: self.instruction.args.clone().expect("args is not set"), + }; + let instruction = UndelegateCpi { + __program: self.instruction.__program, + + payer: self.instruction.payer.expect("payer is not set"), + + delegated_account: self + .instruction + .delegated_account + .expect("delegated_account is not set"), + + owner_program: self + .instruction + .owner_program + .expect("owner_program is not set"), + + system_program: self + .instruction + .system_program + .expect("system_program is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct UndelegateCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_account_info::AccountInfo<'a>, + payer: Option<&'b solana_account_info::AccountInfo<'a>>, + delegated_account: Option<&'b solana_account_info::AccountInfo<'a>>, + owner_program: Option<&'b solana_account_info::AccountInfo<'a>>, + system_program: Option<&'b solana_account_info::AccountInfo<'a>>, + args: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: + Vec<(&'b solana_account_info::AccountInfo<'a>, bool, bool)>, +} diff --git a/compressed-delegation-client/src/generated/instructions/undelegate_compressed.rs b/compressed-delegation-client/src/generated/instructions/undelegate_compressed.rs new file mode 100644 index 000000000..9a29c1e09 --- /dev/null +++ b/compressed-delegation-client/src/generated/instructions/undelegate_compressed.rs @@ -0,0 +1,376 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::UndelegateCompressedArgs; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +pub const UNDELEGATE_COMPRESSED_DISCRIMINATOR: u64 = 5; + +/// Accounts. +#[derive(Debug)] +pub struct UndelegateCompressed { + pub payer: solana_pubkey::Pubkey, + + pub delegated_account: solana_pubkey::Pubkey, +} + +impl UndelegateCompressed { + pub fn instruction( + &self, + args: UndelegateCompressedInstructionArgs, + ) -> solana_instruction::Instruction { + self.instruction_with_remaining_accounts(args, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::vec_init_then_push)] + pub fn instruction_with_remaining_accounts( + &self, + args: UndelegateCompressedInstructionArgs, + remaining_accounts: &[solana_instruction::AccountMeta], + ) -> solana_instruction::Instruction { + let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); + accounts.push(solana_instruction::AccountMeta::new(self.payer, true)); + accounts.push(solana_instruction::AccountMeta::new( + self.delegated_account, + true, + )); + accounts.extend_from_slice(remaining_accounts); + let mut data = + borsh::to_vec(&UndelegateCompressedInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&args).unwrap(); + data.append(&mut args); + + solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + } + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct UndelegateCompressedInstructionData { + discriminator: u64, +} + +impl UndelegateCompressedInstructionData { + pub fn new() -> Self { + Self { discriminator: 5 } + } +} + +impl Default for UndelegateCompressedInstructionData { + fn default() -> Self { + Self::new() + } +} + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct UndelegateCompressedInstructionArgs { + pub args: UndelegateCompressedArgs, +} + +/// Instruction builder for `UndelegateCompressed`. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[writable, signer]` delegated_account +#[derive(Clone, Debug, Default)] +pub struct UndelegateCompressedBuilder { + payer: Option, + delegated_account: Option, + args: Option, + __remaining_accounts: Vec, +} + +impl UndelegateCompressedBuilder { + pub fn new() -> Self { + Self::default() + } + #[inline(always)] + pub fn payer(&mut self, payer: solana_pubkey::Pubkey) -> &mut Self { + self.payer = Some(payer); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: solana_pubkey::Pubkey, + ) -> &mut Self { + self.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn args(&mut self, args: UndelegateCompressedArgs) -> &mut Self { + self.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: solana_instruction::AccountMeta, + ) -> &mut Self { + self.__remaining_accounts.push(account); + self + } + /// Add additional accounts to the instruction. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[solana_instruction::AccountMeta], + ) -> &mut Self { + self.__remaining_accounts.extend_from_slice(accounts); + self + } + #[allow(clippy::clone_on_copy)] + pub fn instruction(&self) -> solana_instruction::Instruction { + let accounts = UndelegateCompressed { + payer: self.payer.expect("payer is not set"), + delegated_account: self + .delegated_account + .expect("delegated_account is not set"), + }; + let args = UndelegateCompressedInstructionArgs { + args: self.args.clone().expect("args is not set"), + }; + + accounts.instruction_with_remaining_accounts( + args, + &self.__remaining_accounts, + ) + } +} + +/// `undelegate_compressed` CPI accounts. +pub struct UndelegateCompressedCpiAccounts<'a, 'b> { + pub payer: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, +} + +/// `undelegate_compressed` CPI instruction. +pub struct UndelegateCompressedCpi<'a, 'b> { + /// The program to invoke. + pub __program: &'b solana_account_info::AccountInfo<'a>, + + pub payer: &'b solana_account_info::AccountInfo<'a>, + + pub delegated_account: &'b solana_account_info::AccountInfo<'a>, + /// The arguments for the instruction. + pub __args: UndelegateCompressedInstructionArgs, +} + +impl<'a, 'b> UndelegateCompressedCpi<'a, 'b> { + pub fn new( + program: &'b solana_account_info::AccountInfo<'a>, + accounts: UndelegateCompressedCpiAccounts<'a, 'b>, + args: UndelegateCompressedInstructionArgs, + ) -> Self { + Self { + __program: program, + payer: accounts.payer, + delegated_account: accounts.delegated_account, + __args: args, + } + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], &[]) + } + #[inline(always)] + pub fn invoke_with_remaining_accounts( + &self, + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(&[], remaining_accounts) + } + #[inline(always)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + self.invoke_signed_with_remaining_accounts(signers_seeds, &[]) + } + #[allow(clippy::arithmetic_side_effects)] + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed_with_remaining_accounts( + &self, + signers_seeds: &[&[&[u8]]], + remaining_accounts: &[( + &'b solana_account_info::AccountInfo<'a>, + bool, + bool, + )], + ) -> solana_program_error::ProgramResult { + let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); + accounts + .push(solana_instruction::AccountMeta::new(*self.payer.key, true)); + accounts.push(solana_instruction::AccountMeta::new( + *self.delegated_account.key, + true, + )); + remaining_accounts.iter().for_each(|remaining_account| { + accounts.push(solana_instruction::AccountMeta { + pubkey: *remaining_account.0.key, + is_signer: remaining_account.1, + is_writable: remaining_account.2, + }) + }); + let mut data = + borsh::to_vec(&UndelegateCompressedInstructionData::new()).unwrap(); + let mut args = borsh::to_vec(&self.__args).unwrap(); + data.append(&mut args); + + let instruction = solana_instruction::Instruction { + program_id: crate::COMPRESSED_DELEGATION_ID, + accounts, + data, + }; + let mut account_infos = + Vec::with_capacity(3 + remaining_accounts.len()); + account_infos.push(self.__program.clone()); + account_infos.push(self.payer.clone()); + account_infos.push(self.delegated_account.clone()); + remaining_accounts.iter().for_each(|remaining_account| { + account_infos.push(remaining_account.0.clone()) + }); + + if signers_seeds.is_empty() { + solana_cpi::invoke(&instruction, &account_infos) + } else { + solana_cpi::invoke_signed( + &instruction, + &account_infos, + signers_seeds, + ) + } + } +} + +/// Instruction builder for `UndelegateCompressed` via CPI. +/// +/// ### Accounts: +/// +/// 0. `[writable, signer]` payer +/// 1. `[writable, signer]` delegated_account +#[derive(Clone, Debug)] +pub struct UndelegateCompressedCpiBuilder<'a, 'b> { + instruction: Box>, +} + +impl<'a, 'b> UndelegateCompressedCpiBuilder<'a, 'b> { + pub fn new(program: &'b solana_account_info::AccountInfo<'a>) -> Self { + let instruction = Box::new(UndelegateCompressedCpiBuilderInstruction { + __program: program, + payer: None, + delegated_account: None, + args: None, + __remaining_accounts: Vec::new(), + }); + Self { instruction } + } + #[inline(always)] + pub fn payer( + &mut self, + payer: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.payer = Some(payer); + self + } + #[inline(always)] + pub fn delegated_account( + &mut self, + delegated_account: &'b solana_account_info::AccountInfo<'a>, + ) -> &mut Self { + self.instruction.delegated_account = Some(delegated_account); + self + } + #[inline(always)] + pub fn args(&mut self, args: UndelegateCompressedArgs) -> &mut Self { + self.instruction.args = Some(args); + self + } + /// Add an additional account to the instruction. + #[inline(always)] + pub fn add_remaining_account( + &mut self, + account: &'b solana_account_info::AccountInfo<'a>, + is_writable: bool, + is_signer: bool, + ) -> &mut Self { + self.instruction.__remaining_accounts.push(( + account, + is_writable, + is_signer, + )); + self + } + /// Add additional accounts to the instruction. + /// + /// Each account is represented by a tuple of the `AccountInfo`, a `bool` indicating whether the account is writable or not, + /// and a `bool` indicating whether the account is a signer or not. + #[inline(always)] + pub fn add_remaining_accounts( + &mut self, + accounts: &[(&'b solana_account_info::AccountInfo<'a>, bool, bool)], + ) -> &mut Self { + self.instruction + .__remaining_accounts + .extend_from_slice(accounts); + self + } + #[inline(always)] + pub fn invoke(&self) -> solana_program_error::ProgramResult { + self.invoke_signed(&[]) + } + #[allow(clippy::clone_on_copy)] + #[allow(clippy::vec_init_then_push)] + pub fn invoke_signed( + &self, + signers_seeds: &[&[&[u8]]], + ) -> solana_program_error::ProgramResult { + let args = UndelegateCompressedInstructionArgs { + args: self.instruction.args.clone().expect("args is not set"), + }; + let instruction = UndelegateCompressedCpi { + __program: self.instruction.__program, + + payer: self.instruction.payer.expect("payer is not set"), + + delegated_account: self + .instruction + .delegated_account + .expect("delegated_account is not set"), + __args: args, + }; + instruction.invoke_signed_with_remaining_accounts( + signers_seeds, + &self.instruction.__remaining_accounts, + ) + } +} + +#[derive(Clone, Debug)] +struct UndelegateCompressedCpiBuilderInstruction<'a, 'b> { + __program: &'b solana_account_info::AccountInfo<'a>, + payer: Option<&'b solana_account_info::AccountInfo<'a>>, + delegated_account: Option<&'b solana_account_info::AccountInfo<'a>>, + args: Option, + /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. + __remaining_accounts: + Vec<(&'b solana_account_info::AccountInfo<'a>, bool, bool)>, +} diff --git a/compressed-delegation-client/src/generated/mod.rs b/compressed-delegation-client/src/generated/mod.rs new file mode 100644 index 000000000..e0d740ad1 --- /dev/null +++ b/compressed-delegation-client/src/generated/mod.rs @@ -0,0 +1,15 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +pub mod accounts; +pub mod errors; +pub mod instructions; +pub mod programs; +pub mod shared; +pub mod types; + +pub(crate) use programs::*; diff --git a/compressed-delegation-client/src/generated/programs.rs b/compressed-delegation-client/src/generated/programs.rs new file mode 100644 index 000000000..19952fbd7 --- /dev/null +++ b/compressed-delegation-client/src/generated/programs.rs @@ -0,0 +1,12 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use solana_pubkey::{pubkey, Pubkey}; + +/// `compressed_delegation` program ID. +pub const COMPRESSED_DELEGATION_ID: Pubkey = + pubkey!("DEL2rPzhFaS5qzo8XY9ZNxSzuunWueySq3p2dxJfwPbT"); diff --git a/compressed-delegation-client/src/generated/shared.rs b/compressed-delegation-client/src/generated/shared.rs new file mode 100644 index 000000000..71b906d0f --- /dev/null +++ b/compressed-delegation-client/src/generated/shared.rs @@ -0,0 +1,21 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +#[cfg(feature = "fetch")] +#[derive(Debug, Clone)] +pub struct DecodedAccount { + pub address: solana_pubkey::Pubkey, + pub account: solana_account::Account, + pub data: T, +} + +#[cfg(feature = "fetch")] +#[derive(Debug, Clone)] +pub enum MaybeAccount { + Exists(DecodedAccount), + NotFound(solana_pubkey::Pubkey), +} diff --git a/compressed-delegation-client/src/generated/types/commit_args.rs b/compressed-delegation-client/src/generated/types/commit_args.rs new file mode 100644 index 000000000..ce4af5dfb --- /dev/null +++ b/compressed-delegation-client/src/generated/types/commit_args.rs @@ -0,0 +1,22 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::CompressedAccountMeta; +use crate::generated::types::ValidityProof; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CommitArgs { + pub current_compressed_delegated_account_data: Vec, + pub new_data: Vec, + pub account_meta: CompressedAccountMeta, + pub validity_proof: ValidityProof, + pub update_nonce: u64, + pub allow_undelegation: bool, +} diff --git a/compressed-delegation-client/src/generated/types/delegate_args.rs b/compressed-delegation-client/src/generated/types/delegate_args.rs new file mode 100644 index 000000000..3aecef440 --- /dev/null +++ b/compressed-delegation-client/src/generated/types/delegate_args.rs @@ -0,0 +1,32 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::CompressedAccountMeta; +use crate::generated::types::PackedAddressTreeInfo; +use crate::generated::types::ValidityProof; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; +use solana_pubkey::Pubkey; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DelegateArgs { + pub validity_proof: ValidityProof, + pub address_tree_info: Option, + pub output_state_tree_index: u8, + pub account_meta: Option, + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub owner_program_id: Pubkey, + pub validator: Option, + pub lamports: u64, + pub account_data: Vec, + pub pda_seeds: Vec>, + pub bump: u8, +} diff --git a/compressed-delegation-client/src/generated/types/delegate_compressed_args.rs b/compressed-delegation-client/src/generated/types/delegate_compressed_args.rs new file mode 100644 index 000000000..994060551 --- /dev/null +++ b/compressed-delegation-client/src/generated/types/delegate_compressed_args.rs @@ -0,0 +1,35 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::CompressedAccountMeta; +use crate::generated::types::InAccount; +use crate::generated::types::OutputCompressedAccountWithPackedContext; +use crate::generated::types::PackedAddressTreeInfo; +use crate::generated::types::ValidityProof; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; +use solana_pubkey::Pubkey; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DelegateCompressedArgs { + pub validity_proof: ValidityProof, + pub address_tree_info: PackedAddressTreeInfo, + pub account_meta: CompressedAccountMeta, + pub in_account: InAccount, + pub out_account: OutputCompressedAccountWithPackedContext, + pub delegated_account_data: Vec, + pub validator: Option, + pub lamports: u64, + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub owner_program_id: Pubkey, + pub pda_seeds: Vec>, + pub bump: u8, +} diff --git a/compressed-delegation-client/src/generated/types/external_undelegate_args.rs b/compressed-delegation-client/src/generated/types/external_undelegate_args.rs new file mode 100644 index 000000000..4886cb5c2 --- /dev/null +++ b/compressed-delegation-client/src/generated/types/external_undelegate_args.rs @@ -0,0 +1,16 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::CompressedDelegationRecord; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ExternalUndelegateArgs { + pub delegation_record: CompressedDelegationRecord, +} diff --git a/compressed-delegation-client/src/generated/types/finalize_args.rs b/compressed-delegation-client/src/generated/types/finalize_args.rs new file mode 100644 index 000000000..34922147e --- /dev/null +++ b/compressed-delegation-client/src/generated/types/finalize_args.rs @@ -0,0 +1,19 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::CompressedAccountMeta; +use crate::generated::types::ValidityProof; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FinalizeArgs { + pub current_compressed_delegated_account_data: Vec, + pub account_meta: CompressedAccountMeta, + pub validity_proof: ValidityProof, +} diff --git a/compressed-delegation-client/src/generated/types/mod.rs b/compressed-delegation-client/src/generated/types/mod.rs new file mode 100644 index 000000000..5be2af764 --- /dev/null +++ b/compressed-delegation-client/src/generated/types/mod.rs @@ -0,0 +1,22 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +pub(crate) mod r#commit_args; +pub(crate) mod r#delegate_args; +pub(crate) mod r#delegate_compressed_args; +pub(crate) mod r#external_undelegate_args; +pub(crate) mod r#finalize_args; +pub(crate) mod r#undelegate_args; +pub(crate) mod r#undelegate_compressed_args; + +pub use self::r#commit_args::*; +pub use self::r#delegate_args::*; +pub use self::r#delegate_compressed_args::*; +pub use self::r#external_undelegate_args::*; +pub use self::r#finalize_args::*; +pub use self::r#undelegate_args::*; +pub use self::r#undelegate_compressed_args::*; diff --git a/compressed-delegation-client/src/generated/types/undelegate_args.rs b/compressed-delegation-client/src/generated/types/undelegate_args.rs new file mode 100644 index 000000000..26851eb2e --- /dev/null +++ b/compressed-delegation-client/src/generated/types/undelegate_args.rs @@ -0,0 +1,20 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::CompressedAccountMeta; +use crate::generated::types::CompressedDelegationRecord; +use crate::generated::types::ValidityProof; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct UndelegateArgs { + pub validity_proof: ValidityProof, + pub delegation_record_account_meta: CompressedAccountMeta, + pub compressed_delegated_record: CompressedDelegationRecord, +} diff --git a/compressed-delegation-client/src/generated/types/undelegate_compressed_args.rs b/compressed-delegation-client/src/generated/types/undelegate_compressed_args.rs new file mode 100644 index 000000000..832671b35 --- /dev/null +++ b/compressed-delegation-client/src/generated/types/undelegate_compressed_args.rs @@ -0,0 +1,30 @@ +//! This code was AUTOGENERATED using the codama library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun codama to update it. +//! +//! +//! + +use crate::generated::types::CompressedAccountMeta; +use crate::generated::types::CompressedDelegationRecord; +use crate::generated::types::InAccount; +use crate::generated::types::OutputCompressedAccountWithPackedContext; +use crate::generated::types::ValidityProof; +use borsh::BorshDeserialize; +use borsh::BorshSerialize; +use solana_pubkey::Pubkey; + +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct UndelegateCompressedArgs { + pub validity_proof: ValidityProof, + pub account_meta: CompressedAccountMeta, + pub compressed_delegated_record: CompressedDelegationRecord, + pub in_account: InAccount, + pub out_account: OutputCompressedAccountWithPackedContext, + #[cfg_attr( + feature = "serde", + serde(with = "serde_with::As::") + )] + pub owner_program_id: Pubkey, +} diff --git a/compressed-delegation-client/src/lib.rs b/compressed-delegation-client/src/lib.rs new file mode 100644 index 000000000..5dedf3f45 --- /dev/null +++ b/compressed-delegation-client/src/lib.rs @@ -0,0 +1,63 @@ +#![allow(deprecated)] +#![allow(unused_imports)] + +#[rustfmt::skip] +#[path = "generated/mod.rs"] +mod generated_original; +mod utils; + +pub mod generated_impl { + pub mod types { + pub use light_compressed_account::instruction_data::{ + data::OutputCompressedAccountWithPackedContext, + with_readonly::InAccount, + }; + pub use light_sdk::instruction::{ + account_meta::CompressedAccountMeta, PackedAddressTreeInfo, + ValidityProof, + }; + + pub use super::super::generated_original::{ + accounts::CompressedDelegationRecord, types::*, + }; + } +} + +// This is the façade everyone should use: +pub mod generated { + pub use crate::{ + generated_impl::*, + generated_original::{ + accounts, errors, instructions, programs, shared, + }, + }; +} + +pub use generated::{ + accounts::*, + errors::*, + instructions::*, + programs::{COMPRESSED_DELEGATION_ID, COMPRESSED_DELEGATION_ID as ID}, + types::*, + *, +}; +use light_sdk::derive_light_cpi_signer; +use light_sdk_types::CpiSigner; +use solana_pubkey::{pubkey, Pubkey}; +pub use utils::*; + +pub const COMPRESSED_DELEGATION_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("DEL2rPzhFaS5qzo8XY9ZNxSzuunWueySq3p2dxJfwPbT"); + +pub const DEFAULT_VALIDATOR_IDENTITY: Pubkey = + pubkey!("MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57"); + +pub const EXTERNAL_UNDELEGATE_DISCRIMINATOR: [u8; 8] = + [0xD, 0x23, 0xB0, 0x7C, 0x70, 0x68, 0xFE, 0x73]; + +pub const EXTERNAL_UNDELEGATE_DISCRIMINATOR_U64: u64 = + u64::from_le_bytes(EXTERNAL_UNDELEGATE_DISCRIMINATOR); + +pub const fn id() -> Pubkey { + ID +} diff --git a/compressed-delegation-client/src/utils.rs b/compressed-delegation-client/src/utils.rs new file mode 100644 index 000000000..a510ae870 --- /dev/null +++ b/compressed-delegation-client/src/utils.rs @@ -0,0 +1,15 @@ +use light_hasher::{DataHasher, Hasher, HasherError, Sha256}; + +use crate::generated::accounts::CompressedDelegationRecord; + +pub const DCP_DISCRIMINATOR: [u8; 8] = + [0x4d, 0x41, 0x47, 0x49, 0x43, 0x42, 0x4c, 0x4b]; + +impl DataHasher for CompressedDelegationRecord { + fn hash(&self) -> Result<[u8; 32], HasherError> { + let bytes = borsh::to_vec(self).map_err(|_| HasherError::BorshError)?; + let mut hash = Sha256::hash(bytes.as_slice())?; + hash[0] = 0; + Ok(hash) + } +} diff --git a/config.example.toml b/config.example.toml index a410d84f4..836e55c4a 100644 --- a/config.example.toml +++ b/config.example.toml @@ -2,7 +2,7 @@ # MagicBlock Validator Configuration # ============================================================================== # This file serves as a reference for all available configuration options. -# ALL FIELDS ARE OPTIONAL. If removed, the "Default" value listed in the +# ALL FIELDS ARE OPTIONAL. If removed, the "Default" value listed in the # comments will be used. # # CONFIGURATION PRECEDENCE (Highest to Lowest): @@ -59,9 +59,9 @@ lifecycle = "ephemeral" # Example format: grpcs://.helius-rpc.com?api-key= # See Helius gRPC docs for available network endpoints. remotes = [ - "https://api.devnet.solana.com", - "wss://api.devnet.solana.com", - "grpcs://laserstream-devnet-ewr.helius-rpc.com?api-key=YOUR_API_KEY", + "https://api.devnet.solana.com", + "wss://api.devnet.solana.com", + "grpcs://laserstream-devnet-ewr.helius-rpc.com?api-key=YOUR_API_KEY", ] # Root directory for application storage (ledger, accountsdb, snapshots). @@ -253,6 +253,20 @@ reset = false # Env: MBV_TASK_SCHEDULER__MIN_INTERVAL min-interval = "10ms" +# ============================================================================== +# Compression Service +# ============================================================================== +[compression] +# The URL of the Photon indexer. +# Default: None +# Env: MBV_COMPRESSION__PHOTON_URL +# photon-url = "http://localhost:8784" + +# The API key for the Photon indexer. +# Default: None +# Env: MBV_COMPRESSION__API_KEY +# api-key = "your-api-key" + # ============================================================================== # Pre-loaded Programs # ============================================================================== diff --git a/docs/compression-documentation.md b/docs/compression-documentation.md new file mode 100644 index 000000000..180795d91 --- /dev/null +++ b/docs/compression-documentation.md @@ -0,0 +1,353 @@ +# MagicBlock Validator Compression Feature + +## Overview + +The MagicBlock Validator's compression feature enables efficient state management by leveraging Light Protocol's compressed account technology (zkCompression). This allows the validator to handle accounts that exist on the base chain in compressed form while using them in uncompressed form locally, providing significant rent savings and improved performance. + +### What is Account Compression? + +Account compression in Solana refers to storing account data off-chain in a Merkle tree structure while maintaining on-chain proofs of validity. The Light Protocol provides the infrastructure for compressed accounts, allowing: + +- **Reduced storage costs**: Account data is stored off-chain +- **Scalability**: Lower on-chain footprint for applications + +### Why Compression in MagicBlock Validator? + +The compression feature serves several key purposes in the MagicBlock Validator ecosystem: + +1. **Cost Efficiency**: Reduces the cost of delegating accounts to the validator +2. **Scalability**: Enables handling more accounts with less on-chain state +3. **State Synchronization**: Allows seamless synchronization between compressed on-chain state and uncompressed local state +4. **Cross-Program Compatibility**: Works with existing Solana programs without modification + +## Architecture Overview + +The compression feature consists of several interconnected components: + +```mermaid +graph TB + subgraph "User Layer" + U[User Applications] + end + + subgraph "Core Components" + CDP[Compressed Delegation Program] + + subgraph MBV ["MagicBlock Validator"] + FCL[Fetch & Clone Logic] + CS[Committor Service] + end + end + + subgraph "Infrastructure Layer" + LP[Light Protocol
Infrastructure] + CT[Compression Tasks] + end + + U --> CDP + U --> MBV + + CDP <--> CS + CDP <--> FCL + + CDP --> LP + CS --> CT + + style MBV fill:#212121 +``` + +## Core Components + +### 1. Compressed Delegation Program + +**Program ID**: `DEL2rPzhFaS5qzo8XY9ZNxSzuunWueySq3p2dxJfwPbT` + +This external program manages the delegation of compressed accounts to MagicBlock validators. It provides the interface between compressed accounts and the validator infrastructure. + +#### Key Data Structures + +**CompressedDelegationRecord**: + +```rust +pub struct CompressedDelegationRecord { + pub pda: Pubkey, // The uncompressed account PDA + pub authority: Pubkey, // Validator authority (who can modify) + pub last_update_nonce: u64, // Prevents replay attacks + pub is_undelegatable: bool, // Whether undelegation is allowed + pub owner: Pubkey, // Original account owner + pub delegation_slot: u64, // Slot when delegation occurred + pub lamports: u64, // Account lamports + pub data: Vec, // Account data +} +``` + +### 2. MagicBlock Validator (Chainlink Component) + +The validator's chainlink component handles the synchronization and local state management of compressed accounts. + +#### FetchCloner Integration + +When a compressed account is encountered, the `FetchCloner` performs special handling: + +1. **Detection**: Identifies accounts owned by the compressed delegation program +2. **Record Parsing**: Extracts the `CompressedDelegationRecord` from the account data +3. **State Transformation**: Converts compressed state to uncompressed form +4. **Local Storage**: Stores the account in uncompressed form locally +5. **Subscription Management**: Manages subscriptions for state updates + +#### Key Methods + +- `is_owned_by_compressed_delegation_program()`: Detects compressed accounts +- `apply_delegation_record_to_account()`: Transforms account state +- `fetch_and_parse_delegation_record()`: Retrieves delegation metadata + +### 3. Committor Service + +Handles the commitment of local state changes back to the compressed on-chain state. + +#### Compression Tasks + +- **CompressedCommitTask**: Commits local changes to compressed accounts +- **CompressedFinalizeTask**: Finalizes compressed account operations +- **CompressedUndelegateTask**: Handles undelegation of compressed accounts + +## Complete Compression Flow + +### 1. Account Delegation (Compression) + +```mermaid +sequenceDiagram + participant User + participant Program + participant CDP as Compressed Delegation Program + participant Light as Light Protocol + participant Validator + + User->>Program: Call delegate_compressed() + Program->>CDP: Invoke delegate_compressed instruction + CDP->>Light: Create compressed account + Light->>CDP: Return compressed account proof + CDP->>Validator: Store delegation record + Validator->>User: Account delegated successfully +``` + +#### Steps: + +1. User calls a program's `delegate_compressed` instruction +2. Program invokes the Compressed Delegation Program +3. CDP creates a compressed account via Light Protocol +4. CDP stores the delegation record with validator authority +5. Account is now accessible in uncompressed form locally + +### 2. Account Usage (Local Operations) + +```mermaid +sequenceDiagram + participant User + participant Validator + participant Chainlink + participant FetchCloner + + User->>Validator: Transaction with compressed account + Validator->>Chainlink: ensure_transaction_accounts() + Chainlink->>FetchCloner: fetch_and_clone_accounts() + FetchCloner->>FetchCloner: Detect compressed account + FetchCloner->>FetchCloner: Parse delegation record + FetchCloner->>FetchCloner: Transform to uncompressed + FetchCloner->>Validator: Return uncompressed account + Validator->>User: Execute transaction +``` + +#### Steps: + +1. Transaction references a compressed account +2. Chainlink ensures account availability +3. FetchCloner detects compressed ownership +4. Parses the CompressedDelegationRecord +5. Transforms account to uncompressed form +6. Stores locally for transaction execution + +### 3. State Commitment (Decompression) + +```mermaid +sequenceDiagram + participant Validator + participant Committor + participant CDP as Compressed Delegation Program + participant Light as Light Protocol + participant Chain as Base Chain + + Validator->>Committor: Schedule compressed commit + Committor->>Committor: Create CompressedCommitTask + Committor->>CDP: Execute commit instruction + CDP->>Light: Update compressed state + Light->>Chain: Verify and store proof + Chain->>Committor: Commit confirmed +``` + +#### Steps: + +1. Validator schedules compressed commit via Committor Service +2. Committor creates appropriate compression task +3. Task executes commit instruction on Compressed Delegation Program +4. CDP updates compressed state via Light Protocol +5. State changes are committed to base chain + +## Data Structures and Types + +### Address Derivation + +Compressed accounts use a deterministic address derivation: + +```rust +pub fn derive_cda_from_pda(pda: &Pubkey) -> Pubkey { + let seed = hashv_to_bn254_field_size_be_const_array::<3>(&[&pda.to_bytes()]); + let address = derive_address(&seed, &ADDRESS_TREE, &COMPRESSED_DELEGATION_ID); + Pubkey::new_from_array(address) +} +``` + +## Integration with Chainlink + +### Account Detection and Handling + +The `FetchCloner` automatically detects compressed accounts by checking ownership: + +```rust +let owned_by_compressed_delegation_program = + account.is_owned_by_compressed_delegation_program(); +``` + +### State Transformation + +When a compressed account is detected: + +1. Parse the `CompressedDelegationRecord` from account data +2. Set account as compressed: `account.set_compressed(true)` +3. Set proper owner: `account.set_owner(delegation_record.owner)` +4. Set account data: `account.set_data(delegation_record.data)` +5. Set lamports: `account.set_lamports(delegation_record.lamports)` +6. Mark as delegated: `account.set_delegated(is_delegated_to_us)` + +### Undelegation Details + +**Ownership Reassignment Flow**: + +During undelegation, ownership reassignment follows a two-phase process: + +1. **Primary Undelegation**: The Compressed Delegation Program itself handles the core ownership reassignment, transferring ownership back to the original account owner and restoring the account to its fully compressed state on-chain. + +2. **Cleanup Callback**: The external undelegate handler (referenced by the `EXTERNAL_UNDELEGATE_DISCRIMINATOR`) is a program-specific callback invoked for cleanup operations after ownership has already been reassigned. This typically involves: + - Restoring lamports and data to the uncompressed account + - Performing any program-specific state transitions + - Ensuring the account is ready for local validator operations + +This separation ensures that critical ownership reassignment is handled by the trusted delegation program, while allowing programs to perform their own cleanup logic through the external handler. + +### Subscription Management + +Compressed accounts require special subscription handling: + +- **Delegation records are never subscribed to by design** - no subscription occurs for compressed delegation records +- **Accounts are unsubscribed when delegated** to maintain synchronization with compressed state +- **Updates for delegated accounts must be retrieved via the Photon indexer** rather than subscriptions +- Undelegating accounts are kept until undelegation completes + +## Committor Service Integration + +### Task Types + +**CompressedCommitTask**: +- Commits local account changes to compressed state +- Updates the CompressedDelegationRecord +- Generates ZK proofs for state transitions + +**CompressedFinalizeTask**: +- Finalizes compressed account operations +- Handles cleanup after successful commits + +**CompressedUndelegateTask**: +- Removes delegation from compressed accounts +- Transitions accounts back to fully compressed state + +### Task Execution Flow + +1. **Preparation**: Gather compressed account data and current state +2. **Proof Generation**: Create ZK proofs for state transitions +3. **Instruction Building**: Construct compressed delegation program instructions +4. **Execution**: Submit transactions to base chain +5. **Confirmation**: Wait for confirmation and update local state + +## Configuration and Setup + +### Required Dependencies + +- `compressed-delegation-client`: Client library for compressed delegation +- `light-client`: Light Protocol client for compressed accounts +- `light-sdk`: Light Protocol SDK for ZK operations +- `solana-program`: Solana program dependencies + +### Environment Constants + +```rust +pub const COMPRESSED_DELEGATION_ID: Pubkey = + pubkey!("DEL2rPzhFaS5qzo8XY9ZNxSzuunWueySq3p2dxJfwPbT"); + +pub const ADDRESS_TREE: Pubkey = + pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"); + +pub const OUTPUT_QUEUE: Pubkey = + pubkey!("oq1na8gojfdUhsfCpyjNt6h4JaDWtHf1yQj4koBWfto"); +``` + +## Error Handling + +### Common Error Scenarios + +1. **Invalid Compressed Account**: Account data doesn't contain valid delegation record +2. **Proof Verification Failure**: ZK proofs fail verification +3. **Insufficient Balance**: Account lacks lamports for operations +4. **Unauthorized Access**: Non-validator attempts to modify delegated accounts + +### Recovery Mechanisms + +- **Fallback Fetching**: Retry account fetching with different methods +- **State Validation**: Verify account state integrity before operations +- **Graceful Degradation**: Fall back to uncompressed operations when possible + +## Testing and Validation + +### Integration Tests + +The compression feature includes comprehensive integration tests covering: + +- Compressed account delegation and undelegation +- State synchronization between compressed and uncompressed forms +- Commit operations and state settlement +- Error scenarios and edge cases + +### Test Infrastructure + +- **IxtestContext**: Test context providing compressed account utilities +- **Photon Indexer**: Mock indexer for compressed account proofs +- **Chainlink Integration**: Full chainlink testing with compression + +## Performance Considerations + +### Benefits + +- **Storage Efficiency**: ~10x reduction in on-chain storage costs +- **Network Efficiency**: Reduced data transfer for account synchronization + +### Trade-offs + +- **Computational Overhead**: ZK proof generation and verification +- **Latency**: Additional round trips for proof fetching +- **Complexity**: Increased system complexity for state management + +## Future Enhancements + +### Planned Features + +1. **Use BufferTasks**: Using `BufferTask` instead of `ArgTask` allows managing larger accounts. +2. **Work directly with compressed accounts**: The current implementation requires that an empty PDA exists to ensure the corresponding compressed account can be created only once. Using compressed account created directly by the user program would allow onboarding compressed accounts users simply. diff --git a/magicblock-account-cloner/src/bpf_loader_v1.rs b/magicblock-account-cloner/src/bpf_loader_v1.rs index b71b5c501..5908925ff 100644 --- a/magicblock-account-cloner/src/bpf_loader_v1.rs +++ b/magicblock-account-cloner/src/bpf_loader_v1.rs @@ -47,6 +47,7 @@ impl BpfUpgradableProgramModifications { owner: Some(bpf_loader_upgradeable::id()), executable: Some(false), delegated: Some(false), + compressed: Some(false), confined: Some(false), remote_slot: Some(loaded_program.remote_slot), } @@ -67,6 +68,7 @@ impl BpfUpgradableProgramModifications { owner: Some(bpf_loader_upgradeable::id()), executable: Some(true), delegated: Some(false), + compressed: Some(false), confined: Some(false), remote_slot: Some(loaded_program.remote_slot), } diff --git a/magicblock-account-cloner/src/lib.rs b/magicblock-account-cloner/src/lib.rs index a3baf1e00..e67ab6165 100644 --- a/magicblock-account-cloner/src/lib.rs +++ b/magicblock-account-cloner/src/lib.rs @@ -111,6 +111,7 @@ impl ChainlinkCloner { data: Some(request.account.data().to_owned()), executable: Some(request.account.executable()), delegated: Some(request.account.delegated()), + compressed: Some(request.account.compressed()), confined: Some(request.account.confined()), remote_slot: Some(request.account.remote_slot()), }; diff --git a/magicblock-accounts/src/scheduled_commits_processor.rs b/magicblock-accounts/src/scheduled_commits_processor.rs index 35eb21399..9883094f1 100644 --- a/magicblock-accounts/src/scheduled_commits_processor.rs +++ b/magicblock-accounts/src/scheduled_commits_processor.rs @@ -11,6 +11,7 @@ use magicblock_chainlink::{ remote_account_provider::{ chain_rpc_client::ChainRpcClientImpl, chain_updates_client::ChainUpdatesClient, + photon_client::PhotonClientImpl, }, submux::SubMuxClient, Chainlink, @@ -49,6 +50,7 @@ pub type ChainlinkImpl = Chainlink< SubMuxClient, AccountsDb, ChainlinkCloner, + PhotonClientImpl, >; pub struct ScheduledCommitsProcessorImpl { diff --git a/magicblock-aperture/src/state/mod.rs b/magicblock-aperture/src/state/mod.rs index 8e1263562..4d77ad7cb 100644 --- a/magicblock-aperture/src/state/mod.rs +++ b/magicblock-aperture/src/state/mod.rs @@ -8,6 +8,7 @@ use magicblock_chainlink::{ remote_account_provider::{ chain_rpc_client::ChainRpcClientImpl, chain_updates_client::ChainUpdatesClient, + photon_client::PhotonClientImpl, }, submux::SubMuxClient, Chainlink, @@ -24,6 +25,7 @@ pub type ChainlinkImpl = Chainlink< SubMuxClient, AccountsDb, ChainlinkCloner, + PhotonClientImpl, >; /// A container for the shared, global state of the RPC service. diff --git a/magicblock-api/Cargo.toml b/magicblock-api/Cargo.toml index 179b2f627..af90aca9b 100644 --- a/magicblock-api/Cargo.toml +++ b/magicblock-api/Cargo.toml @@ -11,6 +11,7 @@ edition.workspace = true anyhow = { workspace = true } borsh = "1.5.3" fd-lock = { workspace = true } +light-client = { workspace = true, features = ["v2"] } log = { workspace = true } magic-domain-program = { workspace = true } diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index 9422f7d7b..c6d911437 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -7,6 +7,7 @@ use std::{ thread, }; +use light_client::indexer::photon_indexer::PhotonIndexer; use log::*; use magicblock_account_cloner::{ map_committor_request_result, ChainlinkCloner, @@ -24,7 +25,8 @@ use magicblock_chainlink::{ config::ChainlinkConfig, remote_account_provider::{ chain_rpc_client::ChainRpcClientImpl, - chain_updates_client::ChainUpdatesClient, Endpoints, + chain_updates_client::ChainUpdatesClient, + photon_client::PhotonClientImpl, Endpoint, Endpoints, }, submux::SubMuxClient, Chainlink, @@ -98,6 +100,7 @@ type ChainlinkImpl = Chainlink< SubMuxClient, AccountsDb, ChainlinkCloner, + PhotonClientImpl, >; // ----------------- @@ -335,6 +338,13 @@ impl MagicValidator { async fn init_committor_service( config: &ValidatorParams, ) -> ApiResult>> { + let photon_client = config.compression.photon_url.as_ref().map(|url| { + Arc::new(PhotonIndexer::new( + url.clone(), + config.compression.api_key.clone(), + )) + }); + let committor_persist_path = config.storage.join("committor_service.sqlite"); debug!( @@ -352,6 +362,7 @@ impl MagicValidator { config.commit.compute_unit_price, ), }, + photon_client, )?)); if let Some(committor_service) = &committor_service { @@ -376,13 +387,20 @@ impl MagicValidator { accountsdb: &Arc, faucet_pubkey: Pubkey, ) -> ApiResult { - let endpoints = Endpoints::try_from(config.remotes.as_slice()) + let mut endpoints = Endpoints::try_from(config.remotes.as_slice()) .map_err(|e| { ApiError::from( magicblock_chainlink::errors::ChainlinkError::from(e), ) })?; + if let Some(url) = &config.compression.photon_url { + endpoints.push(Endpoint::Compression { + url: url.clone(), + api_key: config.compression.api_key.clone(), + }); + } + let cloner = ChainlinkCloner::new( committor_service, config.chainlink.clone(), diff --git a/magicblock-chainlink/Cargo.toml b/magicblock-chainlink/Cargo.toml index 836af492d..35dc7457e 100644 --- a/magicblock-chainlink/Cargo.toml +++ b/magicblock-chainlink/Cargo.toml @@ -4,11 +4,14 @@ version.workspace = true edition.workspace = true [dependencies] -arc-swap = "1.7" +arc-swap = { workspace = true } async-trait = { workspace = true } bincode = { workspace = true } +borsh = { workspace = true } +compressed-delegation-client = { workspace = true } env_logger = { workspace = true } futures-util = { workspace = true } +light-client = { workspace = true } helius-laserstream = { workspace = true } log = { workspace = true } lru = { workspace = true } diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/ata_projection.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/ata_projection.rs index a4368fa15..94b9727aa 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/ata_projection.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/ata_projection.rs @@ -11,15 +11,17 @@ use tokio::task::JoinSet; use super::{delegation, types::AccountWithCompanion, FetchCloner}; use crate::{ cloner::{AccountCloneRequest, Cloner}, - remote_account_provider::{ChainPubsubClient, ChainRpcClient}, + remote_account_provider::{ + photon_client::PhotonClient, ChainPubsubClient, ChainRpcClient, + }, }; /// Resolves ATAs with eATA projection. /// For each detected ATA, we derive the eATA PDA, subscribe to both, /// and, if the ATA is delegated to us and the eATA exists, we clone the eATA data /// into the ATA in the bank. -pub(crate) async fn resolve_ata_with_eata_projection( - this: &FetchCloner, +pub(crate) async fn resolve_ata_with_eata_projection( + this: &FetchCloner, atas: Vec<( Pubkey, AccountSharedData, @@ -34,6 +36,7 @@ where U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { if atas.is_empty() { return vec![]; diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/delegation.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/delegation.rs index a351f6147..f4f3ed00d 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/delegation.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/delegation.rs @@ -12,8 +12,8 @@ use crate::{ chainlink::errors::{ChainlinkError, ChainlinkResult}, cloner::Cloner, remote_account_provider::{ - ChainPubsubClient, ChainRpcClient, MatchSlotsConfig, - ResolvedAccountSharedData, + photon_client::PhotonClient, ChainPubsubClient, ChainRpcClient, + MatchSlotsConfig, ResolvedAccountSharedData, }, }; @@ -31,8 +31,8 @@ pub(crate) fn parse_delegation_record( }) } -pub(crate) fn apply_delegation_record_to_account( - this: &FetchCloner, +pub(crate) fn apply_delegation_record_to_account( + this: &FetchCloner, account: &mut ResolvedAccountSharedData, delegation_record: &DelegationRecord, ) -> Option @@ -41,6 +41,7 @@ where U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { let is_confined = delegation_record.authority.eq(&Pubkey::default()); let is_delegated_to_us = @@ -63,8 +64,8 @@ where } } -pub(crate) fn get_delegated_to_other( - this: &FetchCloner, +pub(crate) fn get_delegated_to_other( + this: &FetchCloner, delegation_record: &DelegationRecord, ) -> Option where @@ -72,6 +73,7 @@ where U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { let is_delegated_to_us = delegation_record.authority.eq(&this.validator_pubkey) @@ -80,8 +82,8 @@ where (!is_delegated_to_us).then_some(delegation_record.authority) } -pub(crate) async fn fetch_and_parse_delegation_record( - this: &FetchCloner, +pub(crate) async fn fetch_and_parse_delegation_record( + this: &FetchCloner, account_pubkey: Pubkey, min_context_slot: u64, fetch_origin: metrics::AccountFetchOrigin, @@ -91,6 +93,7 @@ where U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { let delegation_record_pubkey = delegation_record_pda_from_delegated_account(&account_pubkey); @@ -114,7 +117,7 @@ where if let Some(delegation_record_remote) = delegation_records.pop() { match delegation_record_remote.fresh_account() { Some(delegation_record_account) => { - FetchCloner::::parse_delegation_record( + FetchCloner::::parse_delegation_record( delegation_record_account.data(), delegation_record_pubkey, ) diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs index 327948599..f833975bd 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs @@ -7,6 +7,8 @@ use std::{ time::Duration, }; +use borsh::BorshDeserialize; +use compressed_delegation_client::CompressedDelegationRecord; use dlp::{ pda::delegation_record_pda_from_delegated_account, state::DelegationRecord, }; @@ -15,7 +17,7 @@ use magicblock_config::config::AllowedProgram; use magicblock_core::traits::AccountsBank; use magicblock_metrics::metrics::{self, AccountFetchOrigin}; use scc::{hash_map::Entry, HashMap}; -use solana_account::{AccountSharedData, ReadableAccount}; +use solana_account::{AccountSharedData, ReadableAccount, WritableAccount}; use solana_pubkey::Pubkey; use solana_sdk_ids::system_program; use tokio::{ @@ -48,6 +50,7 @@ use crate::{ }, cloner::{errors::ClonerResult, AccountCloneRequest, Cloner}, remote_account_provider::{ + photon_client::PhotonClient, program_account::get_loaderv3_get_program_data_address, ChainPubsubClient, ChainRpcClient, ForwardedSubscriptionUpdate, MatchSlotsConfig, RemoteAccount, RemoteAccountProvider, @@ -58,15 +61,16 @@ use crate::{ type RemoteAccountRequests = Vec>; #[derive(Clone)] -pub struct FetchCloner +pub struct FetchCloner where T: ChainRpcClient, U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { /// The RemoteAccountProvider to fetch accounts from - remote_account_provider: Arc>, + remote_account_provider: Arc>, /// Tracks pending account fetch requests to avoid duplicate fetches in parallel /// Once an account is fetched and cloned into the bank, it's removed from here pending_requests: Arc>, @@ -86,16 +90,17 @@ where allowed_programs: Option>, } -impl FetchCloner +impl FetchCloner where T: ChainRpcClient, U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { /// Create FetchCloner with subscription updates properly connected pub fn new( - remote_account_provider: &Arc>, + remote_account_provider: &Arc>, accounts_bank: &Arc, cloner: &Arc, validator_pubkey: Pubkey, @@ -183,9 +188,9 @@ where }); if let Some(in_bank_slot) = out_of_order_slot { trace!( - "Ignoring out-of-order subscription update for {pubkey}: bank slot {in_bank_slot}, update slot {}", - account.remote_slot() - ); + "Ignoring out-of-order subscription update for {pubkey}: bank slot {in_bank_slot}, update slot {}", + account.remote_slot() + ); return; } @@ -298,8 +303,10 @@ where let ForwardedSubscriptionUpdate { pubkey, account } = update; let owned_by_delegation_program = account.is_owned_by_delegation_program(); + let owned_by_compressed_delegation_program = + account.is_owned_by_compressed_delegation_program(); - if let Some(account) = account.fresh_account() { + if let Some(mut account) = account.fresh_account().cloned() { // If the account is owned by the delegation program we need to resolve // its true owner and determine if it is delegated to us if owned_by_delegation_program { @@ -425,6 +432,68 @@ where (None, None) } } + } else if owned_by_compressed_delegation_program { + // If the account is compressed, the delegation record is in the account itself + let delegation_record = + match CompressedDelegationRecord::try_from_slice( + account.data(), + ) { + Ok(delegation_record) => Some(delegation_record), + Err(parse_err) => { + debug!("The account's data did not contain a valid compressed delegation record for {pubkey}: {parse_err}, fetching..."); + match self + .remote_account_provider + .try_get(pubkey, AccountFetchOrigin::GetAccount) + .await + { + Ok(remote_acc) => { + if let Some(acc) = + remote_acc.fresh_account().cloned() + { + match CompressedDelegationRecord::try_from_slice(acc.data()) { + Ok(delegation_record) => Some(delegation_record), + Err(parse_err) => { + error!("fetched account parse failed for {pubkey}: {parse_err}"); + None + } + } + } else { + error!("remote fetch failed for {pubkey}: no fresh account returned"); + None + } + } + Err(fetch_err) => { + error!("remote fetch failed for {pubkey}: {fetch_err}"); + None + } + } + } + }; + + if let Some(delegation_record) = delegation_record { + account.set_compressed(true); + account.set_owner(delegation_record.owner); + account.set_data(delegation_record.data); + account.set_lamports(delegation_record.lamports); + + let is_delegated_to_us = + delegation_record.authority.eq(&self.validator_pubkey); + account.set_delegated(is_delegated_to_us); + + // TODO(dode): commit frequency ms is not supported for compressed delegation records + ( + Some(account), + Some(DelegationRecord { + authority: delegation_record.authority, + owner: delegation_record.owner, + delegation_slot: delegation_record.delegation_slot, + lamports: delegation_record.lamports, + commit_frequency_ms: 0, + }), + ) + } else { + (None, None) + } } else { // Accounts not owned by the delegation program can be cloned as is // No unsubscription needed for undelegated accounts @@ -549,6 +618,7 @@ where not_found, plain, owned_by_deleg, + owned_by_deleg_compressed, programs, atas, } = pipeline::classify_remote_accounts(accs, pubkeys); @@ -566,6 +636,10 @@ where .iter() .map(|(pubkey, _, slot)| (pubkey.to_string(), *slot)) .collect::>(); + let owned_by_deleg_compressed = owned_by_deleg_compressed + .iter() + .map(|(pubkey, _, slot)| (pubkey.to_string(), *slot)) + .collect::>(); let programs = programs .iter() .map(|(p, _, _)| p.to_string()) @@ -575,7 +649,7 @@ where .map(|(a, _, _, _)| a.to_string()) .collect::>(); trace!( - "Fetched accounts: \nnot_found: {not_found:?} \nplain: {plain:?} \nowned_by_deleg: {owned_by_deleg:?}\nprograms: {programs:?} \natas: {atas:?}", + "Fetched accounts: \nnot_found: {not_found:?} \nplain: {plain:?} \nowned_by_deleg: {owned_by_deleg:?} \nowned_by_deleg_compressed: {owned_by_deleg_compressed:?} \nprograms: {programs:?} \natas: {atas:?}", ); } @@ -663,6 +737,15 @@ where .await; accounts_to_clone.extend(ata_accounts); + // Handle compressed delegated accounts: for each compressed delegated account, we clone the account data into the bank. + let compressed_delegated_accounts = + pipeline::resolve_compressed_delegated_accounts( + self, + owned_by_deleg_compressed, + ) + .await?; + accounts_to_clone.extend(compressed_delegated_accounts); + // Compute sub cancellations now since we may potentially fail during a cloning step let cancel_strategy = pipeline::compute_cancel_strategy( pubkeys, @@ -701,8 +784,39 @@ where in_bank: &AccountSharedData, fetch_origin: AccountFetchOrigin, ) -> RefreshDecision { - if in_bank.undelegating() { - debug!("Fetching undelegating account {pubkey}. delegated={}, undelegating={}", in_bank.delegated(), in_bank.undelegating()); + if in_bank.compressed() { + let Some(record) = + CompressedDelegationRecord::from_bytes(in_bank.data()).ok() + else { + debug!("Account {pubkey} is compressed, but the delegation has already been processed (account is delegated)"); + return RefreshDecision::No; + }; + + if !account_still_undelegating_on_chain( + pubkey, + record.authority.eq(&self.validator_pubkey) + || record.authority.eq(&Pubkey::default()), + in_bank.remote_slot(), + Some(DelegationRecord { + authority: record.authority, + owner: record.owner, + delegation_slot: record.delegation_slot, + lamports: record.lamports, + commit_frequency_ms: 0, + }), + &self.validator_pubkey, + ) { + debug!("Account {pubkey} is compressed, but the compressed delegation record is not undelegating, refresh it"); + return RefreshDecision::Yes; + }; + debug!("Account {pubkey} is compressed, the compressed delegation record is undelegating, do not refresh it"); + } else if in_bank.undelegating() { + debug!( + "Fetching undelegating account {pubkey}. delegated={}, undelegating={}, compressed={}", + in_bank.delegated(), + in_bank.undelegating(), + in_bank.compressed() + ); let deleg_record = self .fetch_and_parse_delegation_record( *pubkey, diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs index 966b54e7d..a31d1e64f 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs @@ -1,10 +1,14 @@ use std::{collections::HashSet, sync::atomic::Ordering}; -use dlp::pda::delegation_record_pda_from_delegated_account; +use borsh::BorshDeserialize; +use compressed_delegation_client::CompressedDelegationRecord; +use dlp::{ + pda::delegation_record_pda_from_delegated_account, state::DelegationRecord, +}; use log::*; use magicblock_core::{token_programs::is_ata, traits::AccountsBank}; use magicblock_metrics::metrics::AccountFetchOrigin; -use solana_account::{AccountSharedData, ReadableAccount}; +use solana_account::{AccountSharedData, ReadableAccount, WritableAccount}; use solana_pubkey::Pubkey; use tokio::task::JoinSet; @@ -20,6 +24,7 @@ use crate::{ chainlink::errors::{ChainlinkError, ChainlinkResult}, cloner::{errors::ClonerResult, AccountCloneRequest, Cloner}, remote_account_provider::{ + photon_client::PhotonClient, program_account::{ get_loaderv3_get_program_data_address, ProgramAccountResolver, LOADER_V3, @@ -29,8 +34,8 @@ use crate::{ }, }; -pub(crate) fn build_existing_subs( - this: &FetchCloner, +pub(crate) fn build_existing_subs( + this: &FetchCloner, pubkeys: &[Pubkey], ) -> ExistingSubs where @@ -38,6 +43,7 @@ where U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { let delegation_records = pubkeys .iter() @@ -58,35 +64,40 @@ where ExistingSubs { existing_subs } } +/// Helper struct to hold classification buckets +#[derive(Default)] +struct ClassificationBuckets { + not_found: Vec<(Pubkey, u64)>, + plain: Vec, + owned_by_deleg: Vec<(Pubkey, AccountSharedData, u64)>, + owned_by_deleg_compressed: Vec<(Pubkey, AccountSharedData, u64)>, + programs: Vec<(Pubkey, AccountSharedData, u64)>, + atas: Vec<( + Pubkey, + AccountSharedData, + magicblock_core::token_programs::AtaInfo, + u64, + )>, +} + /// Classifies fetched remote accounts into categories pub(crate) fn classify_remote_accounts( accs: Vec, pubkeys: &[Pubkey], ) -> ClassifiedAccounts { - let mut not_found = Vec::new(); - let mut plain = Vec::new(); - let mut owned_by_deleg = Vec::new(); - let mut programs = Vec::new(); - let mut atas = Vec::new(); + let mut buckets = ClassificationBuckets::default(); for (acc, &pubkey) in accs.into_iter().zip(pubkeys) { - classify_single_account( - acc, - pubkey, - &mut not_found, - &mut plain, - &mut owned_by_deleg, - &mut programs, - &mut atas, - ); + classify_single_account(acc, pubkey, &mut buckets); } ClassifiedAccounts { - not_found, - plain, - owned_by_deleg, - programs, - atas, + not_found: buckets.not_found, + plain: buckets.plain, + owned_by_deleg: buckets.owned_by_deleg, + owned_by_deleg_compressed: buckets.owned_by_deleg_compressed, + programs: buckets.programs, + atas: buckets.atas, } } @@ -95,21 +106,12 @@ pub(crate) fn classify_remote_accounts( fn classify_single_account( acc: RemoteAccount, pubkey: Pubkey, - not_found: &mut Vec<(Pubkey, u64)>, - plain: &mut Vec, - owned_by_deleg: &mut Vec<(Pubkey, AccountSharedData, u64)>, - programs: &mut Vec<(Pubkey, AccountSharedData, u64)>, - atas: &mut Vec<( - Pubkey, - AccountSharedData, - magicblock_core::token_programs::AtaInfo, - u64, - )>, + buckets: &mut ClassificationBuckets, ) { use RemoteAccount::*; match acc { NotFound(slot) => { - not_found.push((pubkey, slot)); + buckets.not_found.push((pubkey, slot)); } Found(remote_account_state) => { match remote_account_state.account { @@ -118,7 +120,16 @@ fn classify_single_account( if account_shared_data.owner().eq(&dlp::id()) { // Account owned by delegation program - owned_by_deleg.push(( + buckets.owned_by_deleg.push(( + pubkey, + account_shared_data, + slot, + )); + } else if account_shared_data + .owner() + .eq(&compressed_delegation_client::id()) + { + buckets.owned_by_deleg_compressed.push(( pubkey, account_shared_data, slot, @@ -129,16 +140,21 @@ fn classify_single_account( pubkey, account_shared_data, slot, - programs, + &mut buckets.programs, ); } else if let Some(ata) = is_ata(&pubkey, &account_shared_data) { // Associated Token Account - atas.push((pubkey, account_shared_data, ata, slot)); + buckets.atas.push(( + pubkey, + account_shared_data, + ata, + slot, + )); } else { // Plain account - plain.push(AccountCloneRequest { + buckets.plain.push(AccountCloneRequest { pubkey, account: account_shared_data, commit_frequency_ms: None, @@ -199,8 +215,8 @@ pub(crate) fn partition_not_found( } /// Resolves delegated accounts by fetching their delegation records -pub(crate) async fn resolve_delegated_accounts( - this: &FetchCloner, +pub(crate) async fn resolve_delegated_accounts( + this: &FetchCloner, owned_by_deleg: Vec<(Pubkey, AccountSharedData, u64)>, plain: Vec, min_context_slot: Option, @@ -213,6 +229,7 @@ where U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { // For potentially delegated accounts we update the owner and delegation state first let mut fetch_with_delegation_record_join_set = JoinSet::new(); @@ -276,9 +293,6 @@ where let mut record_subs = Vec::with_capacity(accounts_fully_resolved.len()); let mut accounts_to_clone = plain; - // Collect unique owner programs to subscribe to concurrently - let mut owner_programs_to_subscribe: HashSet = HashSet::new(); - // Now process the accounts (this can fail without affecting unsubscription) for AccountWithCompanion { pubkey, @@ -300,7 +314,7 @@ where // However we may consider a different behavior when user is getting // multiple accounts. let delegation_record = - match FetchCloner::::parse_delegation_record( + match FetchCloner::::parse_delegation_record( delegation_record_data.data(), delegation_record_pubkey, ) { @@ -332,12 +346,6 @@ where &mut account, &delegation_record, ); - - // Collect unique owner programs to subscribe concurrently after the loop - if account.delegated() { - owner_programs_to_subscribe.insert(delegation_record.owner); - } - (commit_freq, delegated_to_other) } else { missing_delegation_record.push((pubkey, account.remote_slot())); @@ -351,32 +359,6 @@ where }); } - // Subscribe to owner programs concurrently in background (best-effort) - if !owner_programs_to_subscribe.is_empty() { - let remote_account_provider = this.remote_account_provider.clone(); - tokio::spawn(async move { - let subscribe_futures = - owner_programs_to_subscribe.into_iter().map(|owner| { - let provider = remote_account_provider.clone(); - async move { - let result = - provider.subscribe_program(owner).await; - (owner, result) - } - }); - let results = - futures_util::future::join_all(subscribe_futures).await; - for (owner, result) in results { - if let Err(err) = result { - warn!( - "Failed to subscribe to owner program {}: {}", - owner, err - ); - } - } - }); - } - (accounts_to_clone, record_subs) }; @@ -387,9 +369,65 @@ where }) } +pub(crate) async fn resolve_compressed_delegated_accounts( + this: &FetchCloner, + owned_by_deleg_compressed: Vec<(Pubkey, AccountSharedData, u64)>, +) -> ChainlinkResult> +where + T: ChainRpcClient, + U: ChainPubsubClient, + V: AccountsBank, + C: Cloner, + P: PhotonClient, +{ + Ok(owned_by_deleg_compressed.into_iter().filter_map( + |(pubkey, mut account, _)| { + let Ok(delegation_record) = + CompressedDelegationRecord::try_from_slice( + account.data(), + ) + .map_err(|err| { + error!("Failed to deserialize compressed delegation record for {pubkey}: {err}\nAccount: {:?}", account); + err + }) + else { + return None; + }; + + account.set_compressed(true); + account.set_lamports(delegation_record.lamports); + account.set_owner(delegation_record.owner); + account.set_data(delegation_record.data); + account.set_delegated( + delegation_record + .authority + .eq(&this.validator_pubkey), + ); + account.set_confined(delegation_record.authority.eq(&Pubkey::default())); + + let delegated_to_other = + this.get_delegated_to_other(&DelegationRecord { + authority: delegation_record.authority, + owner: delegation_record.owner, + delegation_slot: delegation_record.delegation_slot, + lamports: delegation_record.lamports, + commit_frequency_ms: 0, + }); + Some(AccountCloneRequest { + pubkey, + account, + commit_frequency_ms: None, + delegated_to_other, + }) + }, + ) + .collect::>() +) +} + /// Resolves program accounts, fetching program data accounts for LoaderV3 programs -pub(crate) async fn resolve_programs_with_program_data( - this: &FetchCloner, +pub(crate) async fn resolve_programs_with_program_data( + this: &FetchCloner, programs: Vec<(Pubkey, AccountSharedData, u64)>, min_context_slot: Option, fetch_origin: AccountFetchOrigin, @@ -401,6 +439,7 @@ where U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { // For LoaderV3 accounts we fetch the program data account let (loaderv3_programs, single_account_programs): (Vec<_>, Vec<_>) = @@ -479,7 +518,7 @@ where let account_program = account_pair[0].clone(); let account_data = account_pair[1].clone(); - let result = FetchCloner::::resolve_account_with_companion( + let result = FetchCloner::::resolve_account_with_companion( &this.accounts_bank, pubkey, program_data_pubkey, @@ -639,8 +678,8 @@ pub(crate) fn compute_cancel_strategy( } /// Clones accounts and programs into the bank -pub(crate) async fn clone_accounts_and_programs( - this: &FetchCloner, +pub(crate) async fn clone_accounts_and_programs( + this: &FetchCloner, accounts_to_clone: Vec, loaded_programs: Vec< crate::remote_account_provider::program_account::LoadedProgram, @@ -651,6 +690,7 @@ where U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { let mut join_set = JoinSet::new(); for request in accounts_to_clone { diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/program_loader.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/program_loader.rs index fdd376479..5d68eb3bd 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/program_loader.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/program_loader.rs @@ -8,13 +8,14 @@ use super::FetchCloner; use crate::{ cloner::Cloner, remote_account_provider::{ + photon_client::PhotonClient, program_account::{ProgramAccountResolver, LOADER_V1, LOADER_V3}, ChainPubsubClient, ChainRpcClient, }, }; -pub(crate) async fn handle_executable_sub_update( - this: &FetchCloner, +pub(crate) async fn handle_executable_sub_update( + this: &FetchCloner, pubkey: Pubkey, account: AccountSharedData, ) where @@ -22,6 +23,7 @@ pub(crate) async fn handle_executable_sub_update( U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, { if !this.is_program_allowed(&pubkey) { debug!("Skipping clone of program {pubkey}: not in allowed_programs"); diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/subscription.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/subscription.rs index 478de1691..f0b9b0924 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/subscription.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/subscription.rs @@ -5,7 +5,8 @@ use solana_pubkey::Pubkey; use tokio::task::JoinSet; use crate::remote_account_provider::{ - ChainPubsubClient, ChainRpcClient, RemoteAccountProvider, + photon_client::PhotonClient, ChainPubsubClient, ChainRpcClient, + RemoteAccountProvider, }; pub(crate) enum CancelStrategy { @@ -101,8 +102,12 @@ impl fmt::Display for CancelStrategy { } } -pub(crate) async fn cancel_subs( - provider: &Arc>, +pub(crate) async fn cancel_subs< + T: ChainRpcClient, + U: ChainPubsubClient, + P: PhotonClient, +>( + provider: &Arc>, strategy: CancelStrategy, ) { if strategy.is_empty() { diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs index 30b71c015..3d9b46d72 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/tests.rs @@ -20,6 +20,7 @@ use crate::{ cloner_stub::ClonerStub, deleg::{add_delegation_record_for, add_invalid_delegation_record_for}, init_logger, + photon_client_mock::PhotonClientMock, rpc_client_mock::{ChainRpcClientMock, ChainRpcClientMockBuilder}, utils::{create_test_lru_cache, random_pubkey}, }, @@ -32,6 +33,7 @@ type TestFetchClonerResult = ( ChainPubsubClientMock, AccountsBankStub, ClonerStub, + PhotonClientMock, >, >, mpsc::Sender, @@ -85,8 +87,13 @@ macro_rules! assert_cloned_undelegated_account { } struct FetcherTestCtx { - remote_account_provider: - Arc>, + remote_account_provider: Arc< + RemoteAccountProvider< + ChainRpcClientMock, + ChainPubsubClientMock, + PhotonClientMock, + >, + >, accounts_bank: Arc, rpc_client: crate::testing::rpc_client_mock::ChainRpcClientMock, #[allow(unused)] @@ -97,6 +104,7 @@ struct FetcherTestCtx { ChainPubsubClientMock, AccountsBankStub, ClonerStub, + PhotonClientMock, >, >, #[allow(unused)] @@ -138,6 +146,7 @@ where RemoteAccountProvider::new( rpc_client, pubsub_client, + None::, forward_tx, &config, subscribed_accounts, @@ -167,7 +176,11 @@ where /// Returns (FetchCloner, subscription_sender) for simulating subscription updates in tests fn init_fetch_cloner( remote_account_provider: Arc< - RemoteAccountProvider, + RemoteAccountProvider< + ChainRpcClientMock, + ChainPubsubClientMock, + PhotonClientMock, + >, >, bank: &Arc, validator_pubkey: Pubkey, @@ -911,7 +924,7 @@ async fn test_delegation_record_unsub_race_condition_prevention() { // Use a shared FetchCloner to test deduplication // Helper function to spawn a fetch_and_clone task with shared FetchCloner - let spawn_fetch_task = |fetch_cloner: &Arc>| { + let spawn_fetch_task = |fetch_cloner: &Arc>| { let fetch_cloner = fetch_cloner.clone(); tokio::spawn(async move { fetch_cloner @@ -1659,327 +1672,3 @@ async fn test_allowed_programs_empty_allows_all() { "Program 2 should be in the bank (empty allowed_programs allows all)" ); } - -// ----------------- -// Program Subscription Tests for Delegated Accounts -// ----------------- - -#[tokio::test] -async fn test_subscribe_to_original_owner_program_on_delegated_account_fetch() { - init_logger(); - let validator_pubkey = random_pubkey(); - let account_owner = random_pubkey(); - const CURRENT_SLOT: u64 = 100; - - let account_pubkey = random_pubkey(); - let account = Account { - lamports: 1_000_000, - data: vec![1, 2, 3, 4], - owner: dlp::id(), - executable: false, - rent_epoch: 0, - }; - - let FetcherTestCtx { - remote_account_provider, - accounts_bank, - rpc_client, - fetch_cloner, - .. - } = setup( - [(account_pubkey, account.clone())], - CURRENT_SLOT, - validator_pubkey, - ) - .await; - - // Add delegation record with original owner - add_delegation_record_for( - &rpc_client, - account_pubkey, - validator_pubkey, - account_owner, - ); - - // Fetch and clone the delegated account - let result = fetch_cloner - .fetch_and_clone_accounts( - &[account_pubkey], - None, - None, - AccountFetchOrigin::GetAccount, - None, - ) - .await; - - assert!(result.is_ok()); - - // Verify account was cloned and marked as delegated - assert_cloned_delegated_account!( - accounts_bank, - account_pubkey, - account, - CURRENT_SLOT, - account_owner - ); - - // Verify that we subscribed to the original owner program - let pubsub_client = remote_account_provider.pubsub_client(); - let subscribed_programs = pubsub_client.subscribed_program_ids(); - assert!( - subscribed_programs.contains(&account_owner), - "Should subscribe to original owner program {}, got: {:?}", - account_owner, - subscribed_programs - ); -} - -#[tokio::test] -async fn test_no_program_subscription_for_undelegated_account() { - init_logger(); - let validator_pubkey = random_pubkey(); - let account_owner = random_pubkey(); - const CURRENT_SLOT: u64 = 100; - - let account_pubkey = random_pubkey(); - let undelegated_account = Account { - lamports: 1_000_000, - data: vec![1, 2, 3, 4], - owner: account_owner, - executable: false, - rent_epoch: 0, - }; - - let FetcherTestCtx { - remote_account_provider, - accounts_bank, - fetch_cloner, - .. - } = setup( - [(account_pubkey, undelegated_account.clone())], - CURRENT_SLOT, - validator_pubkey, - ) - .await; - - // Verify that initially we don't subscribe to any program - let pubsub_client = remote_account_provider.pubsub_client(); - let initial_programs = pubsub_client.subscribed_program_ids(); - assert!( - initial_programs.is_empty(), - "Should have no program subscriptions initially" - ); - - // Fetch and clone the undelegated account - let result = fetch_cloner - .fetch_and_clone_accounts( - &[account_pubkey], - None, - None, - AccountFetchOrigin::GetAccount, - None, - ) - .await; - - assert!(result.is_ok()); - - // Verify account was cloned but not delegated - assert_cloned_undelegated_account!( - accounts_bank, - account_pubkey, - undelegated_account, - CURRENT_SLOT, - account_owner - ); - - // Still no program subscriptions since it wasn't delegated - let programs_after_fetch = pubsub_client.subscribed_program_ids(); - assert!( - programs_after_fetch.is_empty(), - "Should have no program subscriptions after fetching undelegated account" - ); -} - -#[allow(clippy::too_many_arguments)] -async fn send_subscription_update_and_get_subscribed_programs( - remote_account_provider: &Arc< - RemoteAccountProvider, - >, - accounts_bank: &Arc, - subscription_tx: &mpsc::Sender, - account_pubkey: Pubkey, - bank_account: Account, - update_account: Account, - slot: u64, - expected_program_id: Option, -) -> std::collections::HashSet { - use crate::remote_account_provider::{ - RemoteAccount, RemoteAccountUpdateSource, - }; - - accounts_bank.insert(account_pubkey, AccountSharedData::from(bank_account)); - - let pubsub_client = remote_account_provider.pubsub_client(); - let initial_programs = pubsub_client.subscribed_program_ids(); - assert!( - initial_programs.is_empty(), - "Should have no program subscriptions initially" - ); - - let remote_account = RemoteAccount::from_fresh_account( - update_account, - slot, - RemoteAccountUpdateSource::Subscription, - ); - let update = ForwardedSubscriptionUpdate { - pubkey: account_pubkey, - account: remote_account, - }; - subscription_tx.send(update).await.unwrap(); - - const POLL_INTERVAL: std::time::Duration = Duration::from_millis(10); - const TIMEOUT: std::time::Duration = Duration::from_millis(200); - - let result = tokio::time::timeout(TIMEOUT, async { - loop { - let subscribed = pubsub_client.subscribed_program_ids(); - match expected_program_id { - Some(expected) if subscribed.contains(&expected) => { - return subscribed; - } - None if !subscribed.is_empty() => { - return subscribed; - } - _ => {} - } - tokio::time::sleep(POLL_INTERVAL).await; - } - }) - .await; - - match result { - Ok(subscribed) => subscribed, - Err(_) if expected_program_id.is_some() => { - panic!( - "Timeout waiting for program subscription {:?}", - expected_program_id - ) - } - Err(_) => pubsub_client.subscribed_program_ids(), - } -} - -#[tokio::test] -async fn test_subscribe_to_original_owner_program_on_delegated_account_subscription_update( -) { - init_logger(); - let validator_pubkey = random_pubkey(); - let account_owner = random_pubkey(); - const CURRENT_SLOT: u64 = 100; - - let account_pubkey = random_pubkey(); - let delegated_account = Account { - lamports: 1_000_000, - data: vec![1, 2, 3, 4], - owner: dlp::id(), - executable: false, - rent_epoch: 0, - }; - - let FetcherTestCtx { - remote_account_provider, - accounts_bank, - rpc_client, - subscription_tx, - .. - } = setup( - [(account_pubkey, delegated_account.clone())], - CURRENT_SLOT, - validator_pubkey, - ) - .await; - - add_delegation_record_for( - &rpc_client, - account_pubkey, - validator_pubkey, - account_owner, - ); - - let bank_account = Account { - lamports: 500_000, - data: vec![0, 0, 0, 0], - owner: account_owner, - executable: false, - rent_epoch: 0, - }; - - let subscribed_programs = - send_subscription_update_and_get_subscribed_programs( - &remote_account_provider, - &accounts_bank, - &subscription_tx, - account_pubkey, - bank_account, - delegated_account, - CURRENT_SLOT, - Some(account_owner), - ) - .await; - - assert!( - subscribed_programs.contains(&account_owner), - "Should subscribe to original owner program {} via subscription update, got: {:?}", - account_owner, - subscribed_programs - ); -} - -#[tokio::test] -async fn test_no_program_subscription_for_undelegated_account_subscription_update( -) { - init_logger(); - let validator_pubkey = random_pubkey(); - let account_owner = random_pubkey(); - const CURRENT_SLOT: u64 = 100; - - let account_pubkey = random_pubkey(); - let undelegated_account = Account { - lamports: 1_000_000, - data: vec![1, 2, 3, 4], - owner: account_owner, - executable: false, - rent_epoch: 0, - }; - - let FetcherTestCtx { - remote_account_provider, - accounts_bank, - subscription_tx, - .. - } = setup( - [(account_pubkey, undelegated_account.clone())], - CURRENT_SLOT, - validator_pubkey, - ) - .await; - - let subscribed_programs = - send_subscription_update_and_get_subscribed_programs( - &remote_account_provider, - &accounts_bank, - &subscription_tx, - account_pubkey, - undelegated_account.clone(), - undelegated_account, - CURRENT_SLOT, - None, - ) - .await; - - assert!( - subscribed_programs.is_empty(), - "Should have no program subscriptions for undelegated account subscription update, got: {:?}", - subscribed_programs - ); -} diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/types.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/types.rs index e7ddbeb2b..984aa761a 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/types.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/types.rs @@ -32,6 +32,7 @@ pub(crate) struct ClassifiedAccounts { pub(crate) not_found: Vec<(Pubkey, u64)>, pub(crate) plain: Vec, pub(crate) owned_by_deleg: Vec<(Pubkey, AccountSharedData, u64)>, + pub(crate) owned_by_deleg_compressed: Vec<(Pubkey, AccountSharedData, u64)>, pub(crate) programs: Vec<(Pubkey, AccountSharedData, u64)>, pub(crate) atas: Vec<( Pubkey, diff --git a/magicblock-chainlink/src/chainlink/mod.rs b/magicblock-chainlink/src/chainlink/mod.rs index 144908ffc..d925215db 100644 --- a/magicblock-chainlink/src/chainlink/mod.rs +++ b/magicblock-chainlink/src/chainlink/mod.rs @@ -26,8 +26,10 @@ use crate::{ fetch_cloner::FetchAndCloneResult, filters::is_noop_system_transfer, remote_account_provider::{ - chain_updates_client::ChainUpdatesClient, ChainPubsubClient, - ChainRpcClient, ChainRpcClientImpl, Endpoints, RemoteAccountProvider, + chain_updates_client::ChainUpdatesClient, + photon_client::{PhotonClient, PhotonClientImpl}, + ChainPubsubClient, ChainRpcClient, ChainRpcClientImpl, Endpoints, + RemoteAccountProvider, }, submux::SubMuxClient, }; @@ -40,6 +42,8 @@ pub mod fetch_cloner; pub use blacklisted_accounts::*; +type ArcFetchCloner = Arc>; + // ----------------- // Chainlink // ----------------- @@ -48,9 +52,10 @@ pub struct Chainlink< U: ChainPubsubClient, V: AccountsBank, C: Cloner, + P: PhotonClient, > { accounts_bank: Arc, - fetch_cloner: Option>>, + fetch_cloner: Option>, /// The subscription to events for each account that is removed from /// the accounts tracked by the provider. /// In that case we also remove it from the bank since it is no longer @@ -68,12 +73,17 @@ pub struct Chainlink< remove_confined_accounts: bool, } -impl - Chainlink +impl< + T: ChainRpcClient, + U: ChainPubsubClient, + V: AccountsBank, + C: Cloner, + P: PhotonClient, + > Chainlink { pub fn try_new( accounts_bank: &Arc, - fetch_cloner: Option>>, + fetch_cloner: Option>, validator_pubkey: Pubkey, faucet_pubkey: Pubkey, config: &ChainLinkConfig, @@ -110,7 +120,13 @@ impl config: ChainlinkConfig, chainlink_config: &ChainLinkConfig, ) -> ChainlinkResult< - Chainlink, V, C>, + Chainlink< + ChainRpcClientImpl, + SubMuxClient, + V, + C, + PhotonClientImpl, + >, > { // Extract accounts provider and create fetch cloner while connecting // the subscription channel @@ -268,7 +284,7 @@ Kept: {} delegated, {} blacklisted", /// does nothing as only existing accounts are affected. /// See [lru::LruCache::promote] fn promote_accounts( - fetch_cloner: &FetchCloner, + fetch_cloner: &FetchCloner, pubkeys: &[&Pubkey], ) { fetch_cloner.promote_accounts(pubkeys); @@ -424,7 +440,7 @@ Kept: {} delegated, {} blacklisted", async fn fetch_accounts_common( &self, - fetch_cloner: &FetchCloner, + fetch_cloner: &FetchCloner, pubkeys: &[Pubkey], mark_empty_if_not_found: Option<&[Pubkey]>, fetch_origin: AccountFetchOrigin, @@ -490,7 +506,7 @@ Kept: {} delegated, {} blacklisted", Ok(()) } - pub fn fetch_cloner(&self) -> Option<&Arc>> { + pub fn fetch_cloner(&self) -> Option<&ArcFetchCloner> { self.fetch_cloner.as_ref() } diff --git a/magicblock-chainlink/src/remote_account_provider/chain_laser_actor.rs b/magicblock-chainlink/src/remote_account_provider/chain_laser_actor.rs index 3d88cfaf8..537d5ee91 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_laser_actor.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_laser_actor.rs @@ -120,7 +120,7 @@ pub struct ChainLaserActor { commitment: CommitmentLevel, /// Channel used to signal connection issues to the submux abort_sender: mpsc::Sender<()>, - /// Slot tracking for activation lookback + /// The current slot on chain, shared with RemoteAccountProvider /// This is only set when the gRPC provider supports backfilling subscription updates slots: Option, /// Unique client ID including the gRPC provider name for this actor instance used in logs @@ -566,7 +566,6 @@ impl ChainLaserActor { .as_ref() .map(|x| x.0.iter().cloned().collect::>()) .unwrap_or_default(); - subscribed_programs.insert(program_id); let mut accounts = HashMap::new(); diff --git a/magicblock-chainlink/src/remote_account_provider/chain_updates_client.rs b/magicblock-chainlink/src/remote_account_provider/chain_updates_client.rs index 17072094f..e932f0316 100644 --- a/magicblock-chainlink/src/remote_account_provider/chain_updates_client.rs +++ b/magicblock-chainlink/src/remote_account_provider/chain_updates_client.rs @@ -90,6 +90,11 @@ impl ChainUpdatesClient { "{endpoint:?}" ))) } + Compression { .. } => { + Err(RemoteAccountProviderError::InvalidPubsubEndpoint(format!( + "{endpoint:?}" + ))) + } } } } diff --git a/magicblock-chainlink/src/remote_account_provider/config.rs b/magicblock-chainlink/src/remote_account_provider/config.rs index 5ae74c504..9adfac5c7 100644 --- a/magicblock-chainlink/src/remote_account_provider/config.rs +++ b/magicblock-chainlink/src/remote_account_provider/config.rs @@ -53,7 +53,7 @@ impl RemoteAccountProviderConfig { subscribed_accounts_lru_capacity, lifecycle_mode, enable_subscription_metrics, - resubscription_delay: std::time::Duration::from_millis( + resubscription_delay: Duration::from_millis( DEFAULT_RESUBSCRIPTION_DELAY_MS, ), ..Default::default() @@ -101,7 +101,7 @@ impl Default for RemoteAccountProviderConfig { lifecycle_mode: LifecycleMode::default(), enable_subscription_metrics: true, program_subs: vec![dlp::id()].into_iter().collect(), - resubscription_delay: std::time::Duration::from_millis( + resubscription_delay: Duration::from_millis( DEFAULT_RESUBSCRIPTION_DELAY_MS, ), } diff --git a/magicblock-chainlink/src/remote_account_provider/endpoint.rs b/magicblock-chainlink/src/remote_account_provider/endpoint.rs index e301310aa..59817c2e0 100644 --- a/magicblock-chainlink/src/remote_account_provider/endpoint.rs +++ b/magicblock-chainlink/src/remote_account_provider/endpoint.rs @@ -1,9 +1,10 @@ -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; use magicblock_config::types::network::Remote; use url::Url; use super::errors::RemoteAccountProviderError; +use crate::remote_account_provider::RemoteAccountProviderResult; #[derive(Debug, Clone)] pub enum Endpoint { @@ -21,6 +22,10 @@ pub enum Endpoint { supports_backfill: bool, api_key: String, }, + Compression { + url: String, + api_key: Option, + }, } impl Endpoints { @@ -43,6 +48,19 @@ impl Endpoints { }) .collect() } + + pub fn photon(&self) -> RemoteAccountProviderResult> { + let photons = self + .iter() + .filter(|ep| matches!(ep, Endpoint::Compression { .. })) + .collect::>(); + if photons.len() > 1 { + return Err( + RemoteAccountProviderError::MultiplePhotonEndpointsProvided, + ); + } + Ok(photons.first().copied()) + } } impl TryFrom<&[Remote]> for Endpoints { @@ -121,6 +139,12 @@ impl Deref for Endpoints { } } +impl DerefMut for Endpoints { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl<'a> IntoIterator for &'a Endpoints { type Item = &'a Endpoint; type IntoIter = std::slice::Iter<'a, Endpoint>; diff --git a/magicblock-chainlink/src/remote_account_provider/errors.rs b/magicblock-chainlink/src/remote_account_provider/errors.rs index 860d38060..b28a456ac 100644 --- a/magicblock-chainlink/src/remote_account_provider/errors.rs +++ b/magicblock-chainlink/src/remote_account_provider/errors.rs @@ -14,6 +14,9 @@ pub enum RemoteAccountProviderError { #[error(transparent)] JoinError(#[from] tokio::task::JoinError), + #[error(transparent)] + IndexerError(#[from] light_client::indexer::IndexerError), + #[error("Receiver error: {0}")] RecvrError(#[from] tokio::sync::oneshot::error::RecvError), @@ -59,6 +62,9 @@ pub enum RemoteAccountProviderError { #[error("Accounts matched same slot ({0}), but it's less than min required context slot {2} ")] MatchingSlotsNotSatisfyingMinContextSlot(String, Vec, u64), + #[error("Failed to fetch accounts ({0})")] + FailedFetchingAccounts(String), + #[error("LRU capacity must be greater than 0, got {0}")] InvalidLruCapacity(usize), @@ -102,6 +108,9 @@ pub enum RemoteAccountProviderError { "The LoaderV4 program {0} account state deserialization failed: {1}" )] LoaderV4StateDeserializationFailed(Pubkey, String), + + #[error("Multiple photon endpoints provided")] + MultiplePhotonEndpointsProvided, } impl From diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index e2c197515..3d067d4f9 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -48,6 +48,7 @@ pub mod config; pub mod endpoint; pub mod errors; mod lru_cache; +pub mod photon_client; pub mod program_account; pub mod pubsub_common; mod remote_account; @@ -59,6 +60,10 @@ use magicblock_metrics::{ metrics::{ inc_account_fetches_failed, inc_account_fetches_found, inc_account_fetches_not_found, inc_account_fetches_success, + inc_compressed_account_fetches_failed, + inc_compressed_account_fetches_found, + inc_compressed_account_fetches_not_found, + inc_compressed_account_fetches_success, inc_per_program_account_fetch_stats, set_monitored_accounts_count, AccountFetchOrigin, ProgramFetchResult, }, @@ -69,7 +74,9 @@ use crate::{ errors::ChainlinkResult, remote_account_provider::{ chain_updates_client::ChainUpdatesClient, + photon_client::{PhotonClient, PhotonClientImpl}, pubsub_common::SubscriptionUpdate, + remote_account::FetchedRemoteAccounts, }, submux::SubMuxClient, }; @@ -91,13 +98,20 @@ unsafe impl Sync for ForwardedSubscriptionUpdate {} // Not sure why helius uses a different code for this error const HELIUS_CONTEXT_SLOT_NOT_REACHED: i64 = -32603; -pub struct RemoteAccountProvider { +pub struct RemoteAccountProvider< + T: ChainRpcClient, + U: ChainPubsubClient, + P: PhotonClient, +> { /// The RPC client to fetch accounts from chain the first time we receive /// a request for them rpc_client: T, /// The pubsub client to listen for updates on chain and keep the account /// states up to date pubsub_client: U, + /// The client to fetch compressed accounts from photon the first time we receive + /// a request for them + photon_client: Option

, /// Minimal tracking of accounts currently being fetched to handle race conditions /// between fetch and subscription updates. Only used during active fetch operations. fetching_accounts: Arc, @@ -147,7 +161,11 @@ impl Default for MatchSlotsConfig { } impl - RemoteAccountProvider> + RemoteAccountProvider< + ChainRpcClientImpl, + SubMuxClient, + PhotonClientImpl, + > { pub async fn try_from_urls_and_config( endpoints: &Endpoints, @@ -159,6 +177,7 @@ impl RemoteAccountProvider< ChainRpcClientImpl, SubMuxClient, + PhotonClientImpl, >, >, > { @@ -171,6 +190,7 @@ impl RemoteAccountProvider::< ChainRpcClientImpl, SubMuxClient, + PhotonClientImpl, >::try_new_from_endpoints( endpoints, commitment, @@ -185,20 +205,24 @@ impl } } -impl RemoteAccountProvider { +impl + RemoteAccountProvider +{ pub async fn try_from_clients_and_mode( rpc_client: T, pubsub_client: U, + photon_client: Option

, subscription_forwarder: mpsc::Sender, config: &RemoteAccountProviderConfig, lrucache_subscribed_accounts: Arc, chain_slot: Arc, - ) -> ChainlinkResult>> { + ) -> ChainlinkResult>> { if config.lifecycle_mode().needs_remote_account_provider() { Ok(Some( Self::new( rpc_client, pubsub_client, + photon_client, subscription_forwarder, config, lrucache_subscribed_accounts, @@ -309,6 +333,7 @@ impl RemoteAccountProvider { pub(crate) async fn new( rpc_client: T, pubsub_client: U, + photon_client: Option

, subscription_forwarder: mpsc::Sender, config: &RemoteAccountProviderConfig, lrucache_subscribed_accounts: Arc, @@ -332,6 +357,7 @@ impl RemoteAccountProvider { fetching_accounts: Arc::::default(), rpc_client, pubsub_client, + photon_client, chain_slot, last_update_slot: Arc::::default(), received_updates_count: Arc::::default(), @@ -370,6 +396,7 @@ impl RemoteAccountProvider { RemoteAccountProvider< ChainRpcClientImpl, SubMuxClient, + PhotonClientImpl, >, > { if endpoints.is_empty() { @@ -419,6 +446,11 @@ impl RemoteAccountProvider { let submux = SubMuxClient::new(pubsubs, subscribed_accounts.clone(), None); + let photon_client = endpoints + .photon()? + .map(PhotonClientImpl::new_from_endpoint) + .transpose()?; + if !config.program_subs().is_empty() { debug!( "Subscribing to program accounts: [{}]", @@ -436,9 +468,11 @@ impl RemoteAccountProvider { RemoteAccountProvider::< ChainRpcClientImpl, SubMuxClient, + PhotonClientImpl, >::new( rpc_client, submux, + photon_client, subscription_forwarder, config, subscribed_accounts, @@ -848,20 +882,16 @@ impl RemoteAccountProvider { } // 2. Inform upstream so it can remove it from the store - self.send_removal_update(evicted).await?; + self.send_removal_update(evicted).await; } Ok(()) } - async fn send_removal_update( - &self, - evicted: Pubkey, - ) -> RemoteAccountProviderResult<()> { - self.removed_account_tx.send(evicted).await.map_err( - RemoteAccountProviderError::FailedToSendAccountRemovalUpdate, - )?; - Ok(()) + async fn send_removal_update(&self, evicted: Pubkey) { + if let Err(err) = self.removed_account_tx.send(evicted).await { + warn!("Failed to send removal update for {evicted}: {err:?}"); + } } /// Check if an account is currently being watched (subscribed to) @@ -955,15 +985,247 @@ impl RemoteAccountProvider { program_ids: Option<&[Pubkey]>, ) { const MAX_RETRIES: u64 = 10; - const RPC_CALL_TIMEOUT: Duration = Duration::from_secs(2); let rpc_client = self.rpc_client.clone(); + let photon_client = Arc::new(self.photon_client.clone()); let fetching_accounts = self.fetching_accounts.clone(); - let commitment = self.rpc_client.commitment(); + let pubkeys = Arc::new(pubkeys); let mark_empty_if_not_found = mark_empty_if_not_found.unwrap_or(&[]).to_vec(); let program_ids = program_ids.map(|ids| ids.to_vec()); tokio::spawn(async move { + // Fetch accounts from RPC + // If any are owned by the compressed delegation program then we also fetch from Photon with retries + let (rpc_accounts, found_count, not_found_count) = + Self::fetch_from_rpc( + rpc_client, + pubkeys.clone(), + fetching_accounts.clone(), + mark_empty_if_not_found, + min_context_slot, + program_ids.clone(), + ) + .await?; + + let rpc_accounts_clone = rpc_accounts.clone(); + let pubkeys_clone = pubkeys.clone(); + let photon_client_clone = photon_client.clone(); + let compressed_accounts = async move || { + let FetchedRemoteAccounts::Rpc(accounts) = &rpc_accounts_clone + else { + return Err( + RemoteAccountProviderError::FailedFetchingAccounts( + "Failed to fetch RPC accounts for compressed accounts".to_string(), + ), + ); + }; + + let compressed_accounts_count = accounts + .iter() + .filter(|acc| { + acc.is_owned_by_compressed_delegation_program() + }) + .count() + as u64; + if compressed_accounts_count == 0 { + return Ok(None); + } + + let Some(photon_client) = &*photon_client_clone else { + return Err( + RemoteAccountProviderError::FailedFetchingAccounts( + "No photon client available".to_string(), + ), + ); + }; + + let mut remaining_retries: u64 = MAX_RETRIES; + loop { + let (compressed_accounts, found_count, not_found_count) = + Self::fetch_from_photon( + photon_client.clone(), + pubkeys_clone.clone(), + min_context_slot, + ) + .await?; + + let FetchedRemoteAccounts::Compressed(compressed_accounts) = + compressed_accounts + else { + let err_msg = + "Failed to fetch compressed accounts".to_string(); + error!("{err_msg}"); + return Err( + RemoteAccountProviderError::FailedFetchingAccounts( + err_msg, + ), + ); + }; + + if found_count == compressed_accounts_count { + return Ok(Some(( + FetchedRemoteAccounts::Compressed( + compressed_accounts, + ), + found_count, + not_found_count, + ))); + } + + remaining_retries -= 1; + if remaining_retries == 0 { + return Err( + RemoteAccountProviderError::FailedFetchingAccounts( + "Max photon retries reached".to_string(), + ), + ); + } + tokio::time::sleep(Duration::from_millis(400)).await; + } + }; + + let ( + remote_accounts_results, + found_count, + not_found_count, + compressed_found_count, + compressed_not_found_count, + ) = vec![ + Ok(Some((rpc_accounts, found_count, not_found_count))), + compressed_accounts().await, + ] + .into_iter() + .filter_map(|result| match result { + Ok(Some(result)) => Some(result), + Ok(None) => None, + Err(err) => { + error!("Failed to fetch accounts: {err:?}"); + None + } + }) + .fold( + (vec![], 0, 0, 0, 0), + |( + remote_accounts_results, + found_count, + not_found_count, + compressed_found_count, + compressed_not_found_count, + ), + (accs, found_cnt, not_found_cnt)| { + match &accs { + FetchedRemoteAccounts::Rpc(_) => ( + [remote_accounts_results, vec![accs]].concat(), + found_count + found_cnt, + not_found_count + not_found_cnt, + compressed_found_count, + compressed_not_found_count, + ), + FetchedRemoteAccounts::Compressed(_) => ( + [remote_accounts_results, vec![accs]].concat(), + found_count, + not_found_count, + compressed_found_count + found_cnt, + compressed_not_found_count + not_found_cnt, + ), + } + }, + ); + let remote_accounts = Self::consolidate_fetched_remote_accounts( + &pubkeys, + remote_accounts_results, + ); + + // Update metrics for successful RPC fetch + inc_account_fetches_success(pubkeys.len() as u64); + inc_account_fetches_found(fetch_origin, found_count); + inc_account_fetches_not_found(fetch_origin, not_found_count); + + if (*photon_client).is_some() { + // Update metrics for successful compressed fetch + inc_compressed_account_fetches_success(pubkeys.len() as u64); + inc_compressed_account_fetches_found( + fetch_origin, + compressed_found_count, + ); + inc_compressed_account_fetches_not_found( + fetch_origin, + compressed_not_found_count, + ); + } + + // Record per-program metrics if programs were provided + if let Some(program_ids) = &program_ids { + for program_id in program_ids { + if found_count > 0 { + inc_per_program_account_fetch_stats( + &program_id.to_string(), + ProgramFetchResult::Found, + found_count, + ); + } + if not_found_count > 0 { + inc_per_program_account_fetch_stats( + &program_id.to_string(), + ProgramFetchResult::NotFound, + not_found_count, + ); + } + } + } + + if log_enabled!(log::Level::Trace) { + trace!( + "Fetched({}) {remote_accounts:?}, notifying pending requests", + pubkeys_str(&pubkeys) + ); + } + + // Notify all pending requests with fetch results (unless subscription override occurred) + for (pubkey, remote_account) in + pubkeys.iter().zip(remote_accounts.iter()) + { + let requests = { + let mut fetching = fetching_accounts.lock().unwrap(); + // Remove from fetching and get pending requests + // Note: the account might have been resolved by subscription update already + if let Some((_, requests)) = fetching.remove(pubkey) { + requests + } else { + // Account was resolved by subscription update, skip + if log::log_enabled!(log::Level::Trace) { + trace!( + "Account {pubkey} was already resolved by subscription update" + ); + } + continue; + } + }; + + // Send the fetch result to all waiting requests + for request in requests { + let _ = request.send(Ok(remote_account.clone())); + } + } + Ok::<(), RemoteAccountProviderError>(()) + }); + } + + async fn fetch_from_rpc( + rpc_client: T, + pubkeys: Arc>, + fetching_accounts: Arc, + mark_empty_if_not_found: Vec, + min_context_slot: u64, + program_ids: Option>, + ) -> RemoteAccountProviderResult<(FetchedRemoteAccounts, u64, u64)> { + const MAX_RETRIES: u64 = 10; + const RPC_CALL_TIMEOUT: Duration = Duration::from_secs(2); + + let rpc_client = rpc_client.clone(); + let commitment = rpc_client.commitment(); + let pubkeys = pubkeys.clone(); + let (remote_accounts, found_count, not_found_count) = tokio::spawn(async move { use RemoteAccount::*; // Helper to notify all pending requests of fetch failure @@ -981,7 +1243,7 @@ impl RemoteAccountProvider { } } - for pubkey in &pubkeys { + for pubkey in &*pubkeys { // Update metrics // Remove pending requests and send error if let Some((_, requests)) = fetching.remove(pubkey) { @@ -1008,7 +1270,7 @@ impl RemoteAccountProvider { if remaining_retries <= 0 { let err_msg = format!("Max retries {MAX_RETRIES} reached, giving up on fetching accounts: {pubkeys:?}"); notify_error(&err_msg); - return; + return Err(RemoteAccountProviderError::FailedFetchingAccounts(err_msg)); } tokio::time::sleep(Duration::from_millis(400)).await; continue; @@ -1079,7 +1341,7 @@ impl RemoteAccountProvider { "RpcError fetching accounts {}: {err:?}", pubkeys_str(&pubkeys) ); notify_error(&err_msg); - return; + return Err(RemoteAccountProviderError::FailedFetchingAccounts(err_msg)); } } err => { @@ -1087,7 +1349,7 @@ impl RemoteAccountProvider { "RpcError fetching accounts {}: {err:?}", pubkeys_str(&pubkeys) ); notify_error(&err_msg); - return; + return Err(RemoteAccountProviderError::FailedFetchingAccounts(err_msg)); } } } @@ -1097,7 +1359,7 @@ impl RemoteAccountProvider { pubkeys_str(&pubkeys) ); notify_error(&err_msg); - return; + return Err(RemoteAccountProviderError::FailedFetchingAccounts(err_msg)); } }, Err(_) => { @@ -1106,7 +1368,7 @@ impl RemoteAccountProvider { if remaining_retries == 0 { let err_msg = format!("Max retries {MAX_RETRIES} reached, giving up on fetching accounts: {pubkeys:?}"); notify_error(&err_msg); - return; + return Err(RemoteAccountProviderError::FailedFetchingAccounts(err_msg)); } tokio::time::sleep(Duration::from_millis(400)).await; continue; @@ -1115,12 +1377,18 @@ impl RemoteAccountProvider { }; // TODO: should we retry if not or respond with an error? - assert!(response.context.slot >= min_context_slot); + if response.context.slot < min_context_slot { + let err_msg = format!( + "slot {} < min_context_slot {min_context_slot}", response.context.slot + ); + notify_error(&err_msg); + return Err(RemoteAccountProviderError::FailedFetchingAccounts(err_msg)); + } let mut found_count = 0u64; let mut not_found_count = 0u64; - let remote_accounts: Vec = pubkeys + Ok((pubkeys .iter() .zip(response.value) .map(|(pubkey, acc)| match acc { @@ -1151,75 +1419,155 @@ impl RemoteAccountProvider { NotFound(response.context.slot) } }) - .collect(); + .collect(), found_count, not_found_count)) + }).await??; + + Ok(( + FetchedRemoteAccounts::Rpc(remote_accounts), + found_count, + not_found_count, + )) + } - // Update metrics for successful RPC fetch - inc_account_fetches_success(pubkeys.len() as u64); - inc_account_fetches_found(fetch_origin, found_count); - inc_account_fetches_not_found(fetch_origin, not_found_count); + async fn fetch_from_photon( + photon_client: P, + pubkeys: Arc>, + min_context_slot: u64, + ) -> RemoteAccountProviderResult<(FetchedRemoteAccounts, u64, u64)> { + let (compressed_accounts, slot) = photon_client + .get_multiple_accounts(&pubkeys, Some(min_context_slot)) + .await + .inspect_err(|err| { + error!("Error fetching compressed accounts: {err:?}"); + inc_compressed_account_fetches_failed(pubkeys.len() as u64); + })?; - // Record per-program metrics if programs were provided - if let Some(program_ids) = &program_ids { - for program_id in program_ids { - if found_count > 0 { - inc_per_program_account_fetch_stats( - &program_id.to_string(), - ProgramFetchResult::Found, - found_count, - ); + if log_enabled!(log::Level::Trace) { + let compressed_accounts_str = compressed_accounts + .iter() + .zip(&*pubkeys) + .map(|(acc, pk)| format!("{}: {:?}", pk, acc)) + .collect::>() + .join(", "); + trace!("Fetched compressed accounts {compressed_accounts_str}"); + } + + let mut found_count = 0u64; + let mut not_found_count = 0u64; + let remote_accounts = compressed_accounts + .into_iter() + .map(|acc_opt| match acc_opt { + Some(acc) => { + found_count += 1; + RemoteAccount::from_fresh_account( + acc, + slot, + RemoteAccountUpdateSource::Compressed, + ) + } + None => { + not_found_count += 1; + RemoteAccount::NotFound(slot) + } + }) + .collect::>(); + Ok(( + FetchedRemoteAccounts::Compressed(remote_accounts), + found_count, + not_found_count, + )) + } + + fn consolidate_fetched_remote_accounts( + pubkeys: &[Pubkey], + remote_accounts_results: Vec, + ) -> Vec { + let (rpc_accounts, compressed_accounts) = { + if remote_accounts_results.is_empty() { + return vec![]; + } + if remote_accounts_results.len() == 1 { + match &remote_accounts_results[0] { + FetchedRemoteAccounts::Rpc(rpc_accounts) => { + return rpc_accounts.clone(); } - if not_found_count > 0 { - inc_per_program_account_fetch_stats( - &program_id.to_string(), - ProgramFetchResult::NotFound, - not_found_count, - ); + FetchedRemoteAccounts::Compressed(compressed_accounts) => { + return compressed_accounts.clone(); } } } - - if log_enabled!(log::Level::Trace) { - let pubkeys = pubkeys - .iter() - .map(|pk| pk.to_string()) - .collect::>() - .join(", "); - trace!( - "Fetched({pubkeys}) {remote_accounts:?}, notifying pending requests" - ); - } - - // Notify all pending requests with fetch results (unless subscription override occurred) - for (pubkey, remote_account) in - pubkeys.iter().zip(remote_accounts.iter()) - { - let requests = { - let mut fetching = fetching_accounts.lock().unwrap(); - // Remove from fetching and get pending requests - // Note: the account might have been resolved by subscription update already - if let Some((_, requests)) = fetching.remove(pubkey) { - requests - } else { - // Account was resolved by subscription update, skip - if log::log_enabled!(log::Level::Trace) { - trace!( - "Account {pubkey} was already resolved by subscription update" - ); + if remote_accounts_results.len() == 2 { + let mut rpc_accounts = None; + let mut compressed_accounts = None; + for res in remote_accounts_results { + match res { + FetchedRemoteAccounts::Rpc(rpc_accs) => { + rpc_accounts.replace(rpc_accs); + } + FetchedRemoteAccounts::Compressed(comp_accs) => { + compressed_accounts.replace(comp_accs); } - continue; } - }; - - // Send the fetch result to all waiting requests - for request in requests { - let _ = request.send(Ok(remote_account.clone())); } + (rpc_accounts.unwrap_or_default(), compressed_accounts) + } else { + error!("BUG: More than 2 fetch results found"); + return vec![]; } - }); + }; + + debug_assert_eq!(rpc_accounts.len(), pubkeys.len()); + debug_assert!(compressed_accounts + .as_ref() + .is_none_or(|comp_accs| comp_accs.len() == pubkeys.len())); + + let all_lens_match = pubkeys.len() == rpc_accounts.len() + && pubkeys.len() + == compressed_accounts + .as_ref() + .map_or(rpc_accounts.len(), |comp_accs| comp_accs.len()); + if !all_lens_match { + error!("BUG: Fetched accounts length mismatch: pubkeys {}, rpc {}, compressed {:?}", + pubkeys.len(), rpc_accounts.len(), + compressed_accounts.as_ref().map(|c| c.len())); + return rpc_accounts; + } + + use RemoteAccount::*; + match compressed_accounts { + Some(compressed_accounts) => + pubkeys.iter().zip( + rpc_accounts + .into_iter() + .zip(compressed_accounts)) + .map(|(pubkey, (rpc_acc, comp_acc))| match (rpc_acc, comp_acc) { + (Found(_), Found(comp_state)) => { + warn!("Both RPC and Compressed account found for pubkey {}. Using Compressed account.", pubkey); + Found(comp_state) + } + (Found(rpc_state), NotFound(_)) => Found(rpc_state), + (NotFound(_), Found(comp_state)) => Found(comp_state), + (NotFound(rpc_slot), NotFound(comp_slot)) => { + if rpc_slot >= comp_slot { + NotFound(rpc_slot) + } else { + NotFound(comp_slot) + } + } + }) + .collect(), + None => rpc_accounts, + } } } -impl RemoteAccountProvider { +impl + RemoteAccountProvider< + ChainRpcClientImpl, + ChainPubsubClientImpl, + PhotonClientImpl, + > +{ #[cfg(any(test, feature = "dev-context"))] pub fn rpc_client(&self) -> &RpcClient { &self.rpc_client.rpc_client @@ -1230,6 +1578,7 @@ impl RemoteAccountProvider< ChainRpcClientImpl, SubMuxClient, + PhotonClientImpl, > { #[cfg(any(test, feature = "dev-context"))] @@ -1239,7 +1588,11 @@ impl } impl - RemoteAccountProvider> + RemoteAccountProvider< + ChainRpcClientImpl, + SubMuxClient, + PhotonClientImpl, + > { #[cfg(any(test, feature = "dev-context"))] pub fn rpc_client(&self) -> &RpcClient { @@ -1307,6 +1660,7 @@ mod test { }; use crate::testing::{ init_logger, + photon_client_mock::PhotonClientMock, rpc_client_mock::{ AccountAtSlot, ChainRpcClientMock, ChainRpcClientMockBuilder, }, @@ -1331,6 +1685,7 @@ mod test { RemoteAccountProvider::new( rpc_client, pubsub_client, + None::, fwd_tx, &config, subscribed_accounts, @@ -1383,6 +1738,7 @@ mod test { RemoteAccountProvider::new( rpc_client.clone(), pubsub_client, + None::, fwd_tx, &config, subscribed_accounts, @@ -1422,7 +1778,11 @@ mod test { pubkey1: Pubkey, pubkey2: Pubkey, ) -> ( - RemoteAccountProvider, + RemoteAccountProvider< + ChainRpcClientMock, + ChainPubsubClientMock, + PhotonClientMock, + >, mpsc::Receiver, ) { init_logger(); @@ -1463,6 +1823,7 @@ mod test { RemoteAccountProvider::new( rpc_client, pubsub_client, + None::, forward_tx, &config, subscribed_accounts, @@ -1635,7 +1996,11 @@ mod test { pubkeys: &[Pubkey], accounts_capacity: usize, ) -> ( - RemoteAccountProvider, + RemoteAccountProvider< + ChainRpcClientMock, + ChainPubsubClientMock, + PhotonClientMock, + >, mpsc::Receiver, mpsc::Receiver, ) { @@ -1668,6 +2033,7 @@ mod test { let provider = RemoteAccountProvider::new( rpc_client, pubsub_client, + None::, forward_tx, &config, subscribed_accounts, @@ -1964,4 +2330,232 @@ mod test { assert_eq!(removed.len(), 1); assert!(removed.contains(&stale_pubkey)); } + + // ----------------- + // Compressed Accounts + // ----------------- + async fn setup_with_compressed_accounts( + pubkeys: &[Pubkey], + compressed_pubkeys: &[Pubkey], + accounts_capacity: usize, + ) -> ( + RemoteAccountProvider< + ChainRpcClientMock, + ChainPubsubClientMock, + PhotonClientMock, + >, + mpsc::Receiver, + mpsc::Receiver, + ) { + let rpc_client = { + let mut rpc_client_builder = + ChainRpcClientMockBuilder::new().slot(1); + for (idx, pubkey) in pubkeys.iter().enumerate() { + rpc_client_builder = rpc_client_builder.account( + *pubkey, + Account { + lamports: 555, + data: vec![5; idx + 1], + owner: if compressed_pubkeys.get(idx).is_some() { + compressed_delegation_client::id() + } else { + system_program::id() + }, + executable: false, + rent_epoch: 0, + }, + ); + } + rpc_client_builder.build() + }; + + let photon_client = PhotonClientMock::default(); + for (idx, pubkey) in compressed_pubkeys.iter().enumerate() { + photon_client + .add_account( + *pubkey, + Account { + lamports: 777, + data: vec![7; idx + 1], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + 1, + ) + .await; + } + + let (tx, rx) = mpsc::channel(1); + let pubsub_client = ChainPubsubClientMock::new(tx, rx); + + let (forward_tx, forward_rx) = mpsc::channel(100); + let (subscribed_accounts, config) = + create_test_lru_cache(accounts_capacity); + let chain_slot = Arc::new(AtomicU64::default()); + + let provider = RemoteAccountProvider::new( + rpc_client, + pubsub_client, + Some(photon_client), + forward_tx, + &config, + subscribed_accounts, + chain_slot, + ) + .await + .unwrap(); + + let removed_account_tx = provider.try_get_removed_account_rx().unwrap(); + (provider, forward_rx, removed_account_tx) + } + + macro_rules! assert_compressed_account { + ($acc:expr, $expected_lamports:expr, $expected_data_len:expr) => { + assert!($acc.is_found()); + assert_eq!( + $acc.source(), + Some(RemoteAccountUpdateSource::Compressed) + ); + assert_eq!($acc.fresh_lamports(), Some($expected_lamports)); + assert_eq!($acc.fresh_data_len(), Some($expected_data_len)); + }; + } + + macro_rules! assert_regular_account { + ($acc:expr, $expected_lamports:expr, $expected_data_len:expr) => { + assert!($acc.is_found()); + assert_eq!($acc.source(), Some(RemoteAccountUpdateSource::Fetch)); + assert_eq!($acc.fresh_lamports(), Some($expected_lamports)); + assert_eq!($acc.fresh_data_len(), Some($expected_data_len)); + }; + } + + // TODO(dode): Compressed accounts currently cannot exists with a corresponding RPC account. + #[ignore] + #[tokio::test] + async fn test_multiple_photon_accounts() { + init_logger(); + + let [cpk1, cpk2, cpk3] = [ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + let compressed_pubkeys = &[cpk1, cpk2, cpk3]; + + let (remote_account_provider, _, _) = + setup_with_compressed_accounts(&[], compressed_pubkeys, 3).await; + let accs = remote_account_provider + .try_get_multi_until_slots_match( + compressed_pubkeys, + Some(MatchSlotsConfig { + max_retries: 10, + retry_interval_ms: 50, + min_context_slot: Some(1), + }), + AccountFetchOrigin::GetAccount, + ) + .await + .unwrap(); + let [acc1, acc2, acc3] = accs.as_slice() else { + panic!("Expected 3 accounts"); + }; + assert_compressed_account!(acc1, 777, 1); + assert_compressed_account!(acc2, 777, 2); + assert_compressed_account!(acc3, 777, 3); + + let acc2 = remote_account_provider + .try_get(cpk2, AccountFetchOrigin::GetAccount) + .await + .unwrap(); + assert_compressed_account!(acc2, 777, 2); + } + + #[tokio::test] + async fn test_multiple_compressed_accounts() { + init_logger(); + let [pk1, pk2, pk3] = [ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + let pubkeys = &[pk1, pk2, pk3]; + let compressed_pubkeys = &pubkeys.clone(); + + let (remote_account_provider, _, _) = + setup_with_compressed_accounts(pubkeys, compressed_pubkeys, 3) + .await; + + let accs = remote_account_provider + .try_get_multi_until_slots_match( + pubkeys, + Some(MatchSlotsConfig { + max_retries: 10, + retry_interval_ms: 50, + min_context_slot: Some(1), + }), + AccountFetchOrigin::GetAccount, + ) + .await + .unwrap(); + let [acc1, acc2, acc3] = accs.as_slice() else { + panic!("Expected 3 accounts"); + }; + assert_compressed_account!(acc1, 777, 1); + assert_compressed_account!(acc2, 777, 2); + assert_compressed_account!(acc3, 777, 3); + + let cacc2 = remote_account_provider + .try_get(pk2, AccountFetchOrigin::GetAccount) + .await + .unwrap(); + assert_compressed_account!(cacc2, 777, 2); + } + + #[tokio::test] + async fn test_multiple_compressed_accounts_some_missing() { + init_logger(); + let [pk1, pk2, pk3] = [ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + let pubkeys = &[pk1, pk2, pk3]; + let compressed_pubkeys = &pubkeys[..2]; + + let (remote_account_provider, _, _) = + setup_with_compressed_accounts(pubkeys, compressed_pubkeys, 3) + .await; + + let accs = remote_account_provider + .try_get_multi_until_slots_match( + pubkeys, + Some(MatchSlotsConfig { + max_retries: 10, + retry_interval_ms: 50, + min_context_slot: Some(1), + }), + AccountFetchOrigin::GetAccount, + ) + .await + .unwrap(); + let [acc1, acc2, acc3] = accs.as_slice() else { + panic!("Expected 3 accounts"); + }; + assert_compressed_account!(acc1, 777, 1); + assert_compressed_account!(acc2, 777, 2); + assert_regular_account!(acc3, 555, 3); + + let cacc2 = remote_account_provider + .try_get(pk2, AccountFetchOrigin::GetAccount) + .await + .unwrap(); + assert_compressed_account!(cacc2, 777, 2); + let cacc3 = remote_account_provider + .try_get(pk3, AccountFetchOrigin::GetAccount) + .await + .unwrap(); + assert_regular_account!(cacc3, 555, 3); + } } diff --git a/magicblock-chainlink/src/remote_account_provider/photon_client.rs b/magicblock-chainlink/src/remote_account_provider/photon_client.rs new file mode 100644 index 000000000..f048283e1 --- /dev/null +++ b/magicblock-chainlink/src/remote_account_provider/photon_client.rs @@ -0,0 +1,160 @@ +use std::{ops::Deref, sync::Arc}; + +use async_trait::async_trait; +use light_client::indexer::{ + photon_indexer::PhotonIndexer, CompressedAccount, Context, Indexer, + IndexerError, IndexerRpcConfig, Response, +}; +use log::*; +use magicblock_core::compression::derive_cda_from_pda; +use solana_account::Account; +use solana_clock::Slot; +use solana_pubkey::Pubkey; + +use crate::remote_account_provider::{ + Endpoint, RemoteAccountProviderError, RemoteAccountProviderResult, +}; + +#[derive(Clone)] +pub struct PhotonClientImpl(Arc); + +impl Deref for PhotonClientImpl { + type Target = Arc; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PhotonClientImpl { + pub(crate) fn new(photon_indexer: Arc) -> Self { + Self(photon_indexer) + } + pub(crate) fn new_from_endpoint( + endpoint: &Endpoint, + ) -> RemoteAccountProviderResult { + let Endpoint::Compression { url, api_key } = endpoint else { + return Err( + RemoteAccountProviderError::AccountSubscriptionsTaskFailed( + format!( + "Endpoint is not a compression endpoint: {:?}", + endpoint + ), + ), + ); + }; + debug!("Creating PhotonClient with URL: {}", url); + Ok(Self::new(Arc::new(PhotonIndexer::new( + url.to_string(), + api_key.clone(), + )))) + } +} + +#[async_trait] +pub trait PhotonClient: Send + Sync + Clone + 'static { + async fn get_account( + &self, + pubkey: &Pubkey, + min_context_slot: Option, + ) -> RemoteAccountProviderResult>; + + async fn get_multiple_accounts( + &self, + pubkeys: &[Pubkey], + min_context_slot: Option, + ) -> RemoteAccountProviderResult<(Vec>, Slot)>; +} + +#[async_trait] +impl PhotonClient for PhotonClientImpl { + async fn get_account( + &self, + pubkey: &Pubkey, + min_context_slot: Option, + ) -> RemoteAccountProviderResult> { + let config = min_context_slot.map(|slot| IndexerRpcConfig { + slot, + ..Default::default() + }); + let cda = derive_cda_from_pda(pubkey); + let Response { + value: compressed_acc, + context: Context { slot, .. }, + } = match self.get_compressed_account(cda.to_bytes(), config).await { + Ok(res) => res, + // NOTE: @@@ this is broken, we actually are getting a `None` value + // when the account is not found + // We need to wait for the light-client to provide an `Option` for that + // value + Err(IndexerError::AccountNotFound) => { + return Ok(None); + } + Err(err) => { + return Err(err.into()); + } + }; + let account = account_from_compressed_account(Some(compressed_acc)); + Ok(account.map(|acc| (acc, slot))) + } + + async fn get_multiple_accounts( + &self, + pubkeys: &[Pubkey], + min_context_slot: Option, + ) -> RemoteAccountProviderResult<(Vec>, Slot)> { + let config = min_context_slot.map(|slot| IndexerRpcConfig { + slot, + ..Default::default() + }); + let cdas: Vec<_> = pubkeys + .iter() + .map(|pk| derive_cda_from_pda(pk).to_bytes()) + .collect(); + + if log::log_enabled!(log::Level::Debug) { + let pks_cdas: Vec<_> = pubkeys + .iter() + .zip(cdas.iter()) + .map(|(pk, cda)| { + (pk.to_string(), Pubkey::new_from_array(*cda).to_string()) + }) + .collect(); + debug!("Fetching multiple accounts: {pks_cdas:?}"); + } + + let Response { + value: compressed_accs, + context: Context { slot, .. }, + } = self + .get_multiple_compressed_accounts(Some(cdas), None, config) + .await?; + + let accounts = compressed_accs + .items + .into_iter() + .map(account_from_compressed_account) + // NOTE: the light-client API is incorrect currently. + // The server will return `None` for missing accounts, + .collect(); + Ok((accounts, slot)) + } +} + +// ----------------- +// Helpers +// ----------------- + +fn account_from_compressed_account( + compressed_acc: Option, +) -> Option { + let compressed_acc = compressed_acc?; + // NOTE: delegated compressed accounts are set to zero lamports when cloned + // Actual lamports have to be paid back when undelegating + Some(Account { + lamports: 0, + data: compressed_acc.data.unwrap_or_default().data, + owner: compressed_acc.owner, + executable: false, + rent_epoch: 0, + }) +} diff --git a/magicblock-chainlink/src/remote_account_provider/remote_account.rs b/magicblock-chainlink/src/remote_account_provider/remote_account.rs index 20f130868..f8a64b933 100644 --- a/magicblock-chainlink/src/remote_account_provider/remote_account.rs +++ b/magicblock-chainlink/src/remote_account_provider/remote_account.rs @@ -8,6 +8,7 @@ use solana_pubkey::Pubkey; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RemoteAccountUpdateSource { Fetch, + Compressed, Subscription, } @@ -166,6 +167,10 @@ impl ResolvedAccountSharedData { Bank(account) => account.remote_slot(), } } + + pub fn compressed(&self) -> bool { + self.account_shared_data().compressed() + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -188,6 +193,10 @@ impl RemoteAccount { ) -> Self { let mut account_shared_data = AccountSharedData::from(account); account_shared_data.set_remote_slot(slot); + account_shared_data.set_compressed(matches!( + source, + RemoteAccountUpdateSource::Compressed + )); RemoteAccount::Found(RemoteAccountState { account: ResolvedAccount::Fresh(account_shared_data), source, @@ -243,12 +252,12 @@ impl RemoteAccount { !matches!(self, RemoteAccount::NotFound(_)) } - pub fn fresh_account(&self) -> Option { + pub fn fresh_account(&self) -> Option<&AccountSharedData> { match self { RemoteAccount::Found(RemoteAccountState { account: ResolvedAccount::Fresh(account), .. - }) => Some(account.clone()), + }) => Some(account), _ => None, } } @@ -257,6 +266,10 @@ impl RemoteAccount { self.fresh_account().map(|acc| acc.lamports()) } + pub fn fresh_data_len(&self) -> Option { + self.fresh_account().map(|acc| acc.data().len()) + } + pub fn owner(&self) -> Option { self.fresh_account().map(|acc| *acc.owner()) } @@ -264,4 +277,15 @@ impl RemoteAccount { pub fn is_owned_by_delegation_program(&self) -> bool { self.owner().is_some_and(|owner| owner.eq(&dlp::id())) } + + pub fn is_owned_by_compressed_delegation_program(&self) -> bool { + self.owner() + .is_some_and(|owner| owner.eq(&compressed_delegation_client::id())) + } +} + +#[derive(Clone)] +pub enum FetchedRemoteAccounts { + Rpc(Vec), + Compressed(Vec), } diff --git a/magicblock-chainlink/src/testing/mod.rs b/magicblock-chainlink/src/testing/mod.rs index 29ecc776c..cc957da31 100644 --- a/magicblock-chainlink/src/testing/mod.rs +++ b/magicblock-chainlink/src/testing/mod.rs @@ -9,6 +9,8 @@ pub mod deleg; #[cfg(any(test, feature = "dev-context"))] pub mod eatas; #[cfg(any(test, feature = "dev-context"))] +pub mod photon_client_mock; +#[cfg(any(test, feature = "dev-context"))] pub mod rpc_client_mock; #[cfg(any(test, feature = "dev-context"))] pub mod utils; diff --git a/magicblock-chainlink/src/testing/photon_client_mock.rs b/magicblock-chainlink/src/testing/photon_client_mock.rs new file mode 100644 index 000000000..bdfeb01fe --- /dev/null +++ b/magicblock-chainlink/src/testing/photon_client_mock.rs @@ -0,0 +1,83 @@ +use std::{collections::HashMap, sync::Arc}; + +use async_trait::async_trait; +use magicblock_core::compression::derive_cda_from_pda; +use solana_account::Account; +use solana_clock::Slot; +use solana_pubkey::Pubkey; +use tokio::sync::RwLock; + +use crate::{ + remote_account_provider::{ + photon_client::PhotonClient, RemoteAccountProviderResult, + }, + testing::rpc_client_mock::AccountAtSlot, +}; + +#[derive(Clone, Default)] +pub struct PhotonClientMock { + accounts: Arc>>, +} + +impl PhotonClientMock { + pub fn new() -> Self { + Self { + accounts: Arc::new(RwLock::new(HashMap::new())), + } + } + + pub async fn add_account( + &self, + pubkey: Pubkey, + account: Account, + slot: Slot, + ) { + let cda = derive_cda_from_pda(&pubkey); + let mut accounts = self.accounts.write().await; + accounts.insert(cda, AccountAtSlot { account, slot }); + } +} + +#[async_trait] +impl PhotonClient for PhotonClientMock { + async fn get_account( + &self, + pubkey: &Pubkey, + min_context_slot: Option, + ) -> RemoteAccountProviderResult> { + let cda = derive_cda_from_pda(pubkey); + if let Some(account_at_slot) = self.accounts.read().await.get(&cda) { + if let Some(min_slot) = min_context_slot { + if account_at_slot.slot < min_slot { + return Ok(None); + } + } + return Ok(Some(( + account_at_slot.account.clone(), + account_at_slot.slot, + ))); + } + Ok(None) + } + + async fn get_multiple_accounts( + &self, + pubkeys: &[Pubkey], + min_context_slot: Option, + ) -> RemoteAccountProviderResult<(Vec>, Slot)> { + let mut accs = Vec::with_capacity(pubkeys.len()); + // Seed with min_context_slot if present to better approximate the + // "context slot" semantics of the real Photon client. + let mut slot = min_context_slot.unwrap_or(0); + for pubkey in pubkeys { + let account = self.get_account(pubkey, min_context_slot).await?; + if let Some((ref _acc, acc_slot)) = account { + if acc_slot > slot { + slot = acc_slot; + } + } + accs.push(account.map(|(acc, _)| acc)); + } + Ok((accs, slot)) + } +} diff --git a/magicblock-chainlink/src/testing/utils.rs b/magicblock-chainlink/src/testing/utils.rs index 7b55958e5..d2f95fb69 100644 --- a/magicblock-chainlink/src/testing/utils.rs +++ b/magicblock-chainlink/src/testing/utils.rs @@ -18,6 +18,7 @@ use crate::{ pub const PUBSUB_URL: &str = "ws://localhost:7800"; pub const RPC_URL: &str = "http://localhost:7799"; +pub const PHOTON_URL: &str = "http://localhost:8784"; pub fn random_pubkey() -> Pubkey { Keypair::new().pubkey() diff --git a/magicblock-chainlink/tests/01_ensure-accounts.rs b/magicblock-chainlink/tests/01_ensure-accounts.rs index 34012177f..24c3dc32e 100644 --- a/magicblock-chainlink/tests/01_ensure-accounts.rs +++ b/magicblock-chainlink/tests/01_ensure-accounts.rs @@ -16,6 +16,8 @@ use utils::test_context::TestContext; mod utils; use magicblock_chainlink::testing::init_logger; + +use crate::utils::accounts::compressed_account_shared_with_owner_and_slot; const CURRENT_SLOT: u64 = 11; async fn setup(slot: Slot) -> TestContext { @@ -53,7 +55,7 @@ async fn test_write_non_existing_account() { } // ----------------- -// BasicScenarios:Case 1 Account is initialized and never delegated +// BasicScenarios: Case 1 Account is initialized and never delegated // ----------------- #[tokio::test] async fn test_existing_account_undelegated() { @@ -357,3 +359,136 @@ async fn test_write_existing_account_invalid_delegation_record() { assert_not_subscribed!(chainlink, &[&deleg_record_pubkey, &pubkey]); } + +// ----------------- +// Compressed delegation record is initialized and delegated to us +// ----------------- +#[tokio::test] +async fn test_compressed_delegation_record_delegated() { + let TestContext { + chainlink, + photon_client, + rpc_client, + cloner, + validator_pubkey, + .. + } = setup(CURRENT_SLOT).await; + + let pubkey = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let compressed_account = compressed_account_shared_with_owner_and_slot( + pubkey, + validator_pubkey, + owner, + CURRENT_SLOT, + ); + photon_client + .add_account(pubkey, compressed_account.clone().into(), CURRENT_SLOT) + .await; + rpc_client.add_account( + pubkey, + Account { + owner: compressed_delegation_client::id(), + ..Account::default() + }, + ); + + let pubkeys = [pubkey]; + let res = chainlink + .ensure_accounts( + &pubkeys, + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + debug!("res: {res:?}"); + + assert_cloned_as_delegated!(cloner, &pubkeys, CURRENT_SLOT, owner); + assert_not_subscribed!(chainlink, &[&pubkey]); +} + +// ----------------- +// Compressed delegation record is initialized and delegated to another authority +// ----------------- +#[tokio::test] +async fn test_compressed_delegation_record_delegated_to_other() { + let TestContext { + chainlink, + photon_client, + rpc_client, + cloner, + .. + } = setup(CURRENT_SLOT).await; + + let pubkey = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let compressed_account = compressed_account_shared_with_owner_and_slot( + pubkey, + authority, + owner, + CURRENT_SLOT, + ); + photon_client + .add_account(pubkey, compressed_account.clone().into(), CURRENT_SLOT) + .await; + rpc_client.add_account( + pubkey, + Account { + owner: compressed_delegation_client::id(), + ..Account::default() + }, + ); + + let pubkeys = [pubkey]; + let res = chainlink + .ensure_accounts( + &pubkeys, + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + debug!("res: {res:?}"); + + assert_cloned_as_undelegated!(cloner, &pubkeys, CURRENT_SLOT, owner); + assert_subscribed_without_delegation_record!(chainlink, &[&pubkey]); +} + +// ----------------- +// Compressed delegation record is initialized and empty (undelegated) +// ----------------- +#[tokio::test] +async fn test_compressed_account_undelegated() { + let TestContext { + chainlink, + photon_client, + rpc_client, + cloner, + .. + } = setup(CURRENT_SLOT).await; + + let pubkey = Pubkey::new_unique(); + rpc_client.add_account(pubkey, Account::default()); + photon_client + .add_account(pubkey, Account::default(), CURRENT_SLOT) + .await; + + let pubkeys = [pubkey]; + let res = chainlink + .ensure_accounts( + &pubkeys, + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + debug!("res: {res:?}"); + + assert_cloned_as_undelegated!(cloner, &pubkeys, CURRENT_SLOT); + assert_subscribed_without_delegation_record!(chainlink, &[&pubkey]); +} diff --git a/magicblock-chainlink/tests/03_deleg_after_sub.rs b/magicblock-chainlink/tests/03_deleg_after_sub.rs index e733c954d..55a629831 100644 --- a/magicblock-chainlink/tests/03_deleg_after_sub.rs +++ b/magicblock-chainlink/tests/03_deleg_after_sub.rs @@ -13,6 +13,8 @@ use utils::{ accounts::account_shared_with_owner_and_slot, test_context::TestContext, }; +use crate::utils::accounts::compressed_account_shared_with_owner_and_slot; + mod utils; // Implements the following flow: @@ -119,3 +121,117 @@ async fn test_deleg_after_subscribe_case2() { assert_not_subscribed!(&chainlink, &[&pubkey, &delegation_record]); } } + +// NOTE: Flow "Account created then fetched, then delegated" +#[tokio::test] +async fn test_deleg_after_subscribe_case2_compressed() { + let mut slot: u64 = 11; + + let ctx = setup(slot).await; + let TestContext { + chainlink, + cloner, + rpc_client, + photon_client, + validator_pubkey, + .. + } = ctx.clone(); + + let pubkey = Pubkey::new_unique(); + let program_pubkey = Pubkey::new_unique(); + let acc = Account { + lamports: 1_000, + owner: program_pubkey, + ..Default::default() + }; + + // 1. Initially the account does not exist + // - readable: OK (non existing account) + // - writable: NO + { + info!("1. Initially the account does not exist"); + assert_not_cloned!(cloner, &[pubkey]); + + chainlink + .ensure_accounts( + &[pubkey], + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + assert_not_cloned!(cloner, &[pubkey]); + } + + // 2. Account created with original owner + // + // Now we can ensure it as readonly and it will be cloned + // - readable: OK + // - writable: NO + { + info!("2. Create account owned by program {program_pubkey}"); + + slot = rpc_client.set_slot(slot + 11); + let acc = + account_shared_with_owner_and_slot(&acc, program_pubkey, slot); + + // When the account is created we do not receive any update since we do not sub to a non-existing account + let updated = ctx + .send_and_receive_account_update(pubkey, acc.clone(), Some(400)) + .await; + assert!(!updated); + + chainlink + .ensure_accounts( + &[pubkey], + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + assert_cloned_as_undelegated!(cloner, &[pubkey], slot, program_pubkey); + assert_subscribed_without_delegation_record!(&chainlink, &[&pubkey]); + } + + // 3. Account delegated to us + // + // Delegate account to us and the sub update should be received + // even before the ensure_writable request + { + info!("3. Delegate account {pubkey} to us"); + + slot = rpc_client.set_slot(slot + 11); + let compressed_account = compressed_account_shared_with_owner_and_slot( + pubkey, + validator_pubkey, + program_pubkey, + slot, + ); + photon_client + .add_account(pubkey, compressed_account.clone().into(), slot) + .await; + let updated = ctx + .send_and_receive_account_update( + pubkey, + compressed_account.clone(), + Some(400), + ) + .await; + + // Needs to ensure accounts for compressed accounts + chainlink + .ensure_accounts( + &[pubkey], + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + + assert!(updated); + assert_cloned_as_delegated!(cloner, &[pubkey], slot, program_pubkey); + } +} diff --git a/magicblock-chainlink/tests/04_redeleg_other_separate_slots.rs b/magicblock-chainlink/tests/04_redeleg_other_separate_slots.rs index 1f6af6d87..198cc38eb 100644 --- a/magicblock-chainlink/tests/04_redeleg_other_separate_slots.rs +++ b/magicblock-chainlink/tests/04_redeleg_other_separate_slots.rs @@ -18,6 +18,8 @@ use utils::{ test_context::{DelegateResult, TestContext}, }; +use crate::utils::accounts::compressed_account_shared_with_owner_and_slot; + mod utils; async fn setup(slot: Slot) -> TestContext { @@ -127,3 +129,124 @@ async fn test_undelegate_redelegate_to_other_in_separate_slot() { assert_subscribed_without_delegation_record!(&chainlink, &[&pubkey]); } } + +#[tokio::test] +async fn test_undelegate_redelegate_to_other_in_separate_slot_compressed() { + let mut slot: u64 = 11; + + let ctx = setup(slot).await; + let TestContext { + chainlink, + cloner, + rpc_client, + photon_client, + validator_pubkey, + .. + } = ctx.clone(); + + let pubkey = Pubkey::new_unique(); + let program_pubkey = Pubkey::new_unique(); + let other_authority = Pubkey::new_unique(); + let acc = Account::default(); + + // 1. Account delegated to us + // Initial state: Account is delegated to us and we can read/write to it + { + info!("1. Account delegated to us"); + + slot = rpc_client.set_slot(slot + 11); + let mut compressed_account = + compressed_account_shared_with_owner_and_slot( + pubkey, + validator_pubkey, + program_pubkey, + slot, + ); + compressed_account.set_remote_slot(slot); + rpc_client.add_account( + pubkey, + Account { + owner: compressed_delegation_client::id(), + ..acc + }, + ); + photon_client + .add_account(pubkey, compressed_account.into(), slot) + .await; + + // Transaction to read + // Fetch account - see it's owned by DP, fetch compressed account, clone account as delegated + ctx.ensure_account(&pubkey).await.unwrap(); + assert_cloned_as_delegated!(cloner, &[pubkey], slot, program_pubkey); + assert_not_subscribed!(&chainlink, &[&pubkey]); + }; + + // 2. Account is undelegated + // Undelegation requested, setup subscription, writes refused + { + info!("2.1. Account is undelegated - Undelegation requested (account owner set to DP in Ephem)"); + + ctx.force_undelegation(&pubkey); + + info!("2.2. Would refuse write (account still owned by DP in Ephem)"); + ctx.ensure_account(&pubkey).await.unwrap(); + assert_remain_undelegating!(cloner, &[pubkey], slot); + + slot = rpc_client.set_slot(slot + 11); + + info!("2.3. Account is undelegated on chain"); + let undelegated_acc = ctx + .commit_and_undelegate(&pubkey, &program_pubkey) + .await + .unwrap(); + + // Account should be cloned as undelegated + assert_eq!(cloner.get_account(&pubkey).unwrap(), undelegated_acc); + + info!("2.4. Would refuse write (undelegated on chain)"); + ctx.ensure_account(&pubkey).await.unwrap(); + assert_cloned_as_undelegated!(cloner, &[pubkey], slot, program_pubkey); + assert_subscribed_without_delegation_record!(&chainlink, &[&pubkey]); + } + + // 4. Account redelegated to another authority + // Delegate to other, subscription update, writes refused + { + info!("4.1. Account redelegated to another authority - Delegate account to other"); + slot = rpc_client.set_slot(slot + 2); + + // Create compressed delegation record + let compressed_account = compressed_account_shared_with_owner_and_slot( + pubkey, + other_authority, + program_pubkey, + slot, + ); + photon_client + .add_account(pubkey, compressed_account.clone().into(), slot) + .await; + + let acc = rpc_client.get_account_at_slot(&pubkey).unwrap(); + let updated = ctx + .send_and_receive_account_update( + pubkey, + acc.account.clone(), + Some(400), + ) + .await; + assert!(updated, "Failed to receive delegation update"); + + // Account should remain owned by DP but delegated to other authority + let acc_redeleg_expected = account_shared_with_owner_and_slot( + &acc.account, + program_pubkey, + slot, + ); + assert_eq!(cloner.get_account(&pubkey).unwrap(), acc_redeleg_expected); + + info!("4.2. Would refuse write (delegated to other)"); + ctx.ensure_account(&pubkey).await.unwrap(); + assert_cloned_as_undelegated!(cloner, &[pubkey], slot, program_pubkey); + assert_subscribed_without_delegation_record!(&chainlink, &[&pubkey]); + } +} diff --git a/magicblock-chainlink/tests/05_redeleg_other_same_slot.rs b/magicblock-chainlink/tests/05_redeleg_other_same_slot.rs index 05e5b418a..c83a96286 100644 --- a/magicblock-chainlink/tests/05_redeleg_other_same_slot.rs +++ b/magicblock-chainlink/tests/05_redeleg_other_same_slot.rs @@ -8,13 +8,20 @@ use magicblock_chainlink::{ assert_cloned_as_delegated, assert_cloned_as_undelegated, assert_not_subscribed, assert_remain_undelegating, assert_subscribed_without_delegation_record, - testing::{deleg::add_delegation_record_for, init_logger}, + testing::{ + accounts::account_shared_with_owner, deleg::add_delegation_record_for, + init_logger, + }, }; use solana_account::Account; use solana_program::clock::Slot; use solana_pubkey::Pubkey; use utils::{ - accounts::account_shared_with_owner_and_slot, test_context::TestContext, + accounts::{ + account_shared_with_owner_and_slot, + compressed_account_shared_with_owner_and_slot, + }, + test_context::TestContext, }; mod utils; @@ -99,3 +106,100 @@ async fn test_undelegate_redelegate_to_other_in_same_slot() { assert_subscribed_without_delegation_record!(&chainlink, &[&pubkey]); } } + +#[tokio::test] +async fn test_undelegate_redelegate_to_other_in_same_slot_compressed() { + let mut slot: u64 = 11; + + let ctx = setup(slot).await; + let TestContext { + chainlink, + cloner, + rpc_client, + photon_client, + .. + } = ctx.clone(); + + let pubkey = Pubkey::new_unique(); + let program_pubkey = Pubkey::new_unique(); + let other_authority = Pubkey::new_unique(); + let acc = Account::default(); // Compressed accounts don't need lamports in the RPC mock + + // 1. Account delegated to us + // Initial state: Account is delegated to us and we can read/write to it + { + info!("1. Account delegated to us"); + + slot = rpc_client.set_slot(slot + 11); + let compressed_account = compressed_account_shared_with_owner_and_slot( + pubkey, + ctx.validator_pubkey, + program_pubkey, + slot, + ); + rpc_client.add_account( + pubkey, + Account { + owner: compressed_delegation_client::id(), + ..acc + }, + ); + photon_client + .add_account(pubkey, compressed_account.clone().into(), slot) + .await; + + // Transaction to read/write would be ok + // Fetch account - see it's owned by DP, fetch delegation record, clone account as delegated + ctx.ensure_account(&pubkey).await.unwrap(); + assert_cloned_as_delegated!(cloner, &[pubkey], slot, program_pubkey); + assert_not_subscribed!(&chainlink, &[&pubkey]); + }; + + // 2. Account is undelegated and redelegated to another authority (same slot) + // Undelegation requested, setup subscription, writes refused + { + info!("2.1. Account is undelegated - Undelegation requested (account owner set to DP in Ephem)"); + + ctx.force_undelegation(&pubkey); + + info!("2.2. Would refuse write (account still owned by DP in Ephem)"); + assert_remain_undelegating!(cloner, &[pubkey], slot); + + slot = rpc_client.set_slot(slot + 1); + + info!("2.3. Account is undelegated and redelegated to other authority in same slot"); + + // First trigger undelegation subscription + ctx.chainlink.undelegation_requested(pubkey).await.unwrap(); + + // Then immediately delegate to other authority (simulating same slot operation) + let compressed_account = compressed_account_shared_with_owner_and_slot( + pubkey, + other_authority, + program_pubkey, + slot, + ); + photon_client + .add_account(pubkey, compressed_account.clone().into(), slot) + .await; + // Update account to be delegated on chain and send a sub update + let acc = rpc_client.get_account_at_slot(&pubkey).unwrap(); + let delegated_acc = account_shared_with_owner( + &acc.account, + compressed_delegation_client::id(), + ); + let updated = ctx + .send_and_receive_account_update( + pubkey, + delegated_acc.clone(), + Some(400), + ) + .await; + assert!(updated, "Failed to receive delegation update"); + + // Account should be cloned as delegated to other (flagged as undelegated) + info!("2.4. Would refuse write (delegated to other)"); + assert_cloned_as_undelegated!(cloner, &[pubkey], slot, program_pubkey); + assert_subscribed_without_delegation_record!(&chainlink, &[&pubkey]); + } +} diff --git a/magicblock-chainlink/tests/06_redeleg_us_separate_slots.rs b/magicblock-chainlink/tests/06_redeleg_us_separate_slots.rs index 745eab7c7..23fb7ece9 100644 --- a/magicblock-chainlink/tests/06_redeleg_us_separate_slots.rs +++ b/magicblock-chainlink/tests/06_redeleg_us_separate_slots.rs @@ -9,6 +9,7 @@ use magicblock_chainlink::{ assert_not_subscribed, assert_remain_undelegating, assert_subscribed_without_delegation_record, testing::{deleg::add_delegation_record_for, init_logger}, + AccountFetchOrigin, }; use solana_account::Account; use solana_program::clock::Slot; @@ -17,6 +18,8 @@ use utils::{ accounts::account_shared_with_owner_and_slot, test_context::TestContext, }; +use crate::utils::accounts::compressed_account_shared_with_owner_and_slot; + mod utils; async fn setup(slot: Slot) -> TestContext { @@ -113,3 +116,141 @@ async fn test_undelegate_redelegate_to_us_in_separate_slots() { assert_not_subscribed!(chainlink, &[&pubkey, &deleg_record_pubkey]); } } + +#[tokio::test] +async fn test_undelegate_redelegate_to_us_in_separate_slots_compressed() { + let mut slot: u64 = 11; + + let ctx = setup(slot).await; + let TestContext { + chainlink, + cloner, + rpc_client, + photon_client, + .. + } = ctx.clone(); + + let pubkey = Pubkey::new_unique(); + let program_pubkey = Pubkey::new_unique(); + let acc = Account { + lamports: 1_000, + ..Default::default() + }; + + // 1. Account delegated to us + // Initial state: Account is delegated to us and we can read/write to it + { + info!("1. Account delegated to us"); + + slot = rpc_client.set_slot(slot + 11); + let compressed_account = compressed_account_shared_with_owner_and_slot( + pubkey, + ctx.validator_pubkey, + program_pubkey, + slot, + ); + photon_client + .add_account(pubkey, compressed_account.clone().into(), slot) + .await; + + let delegated_acc = account_shared_with_owner_and_slot( + &acc, + compressed_delegation_client::id(), + slot, + ); + rpc_client.add_account(pubkey, delegated_acc.clone().into()); + + // Fetch account - see it's owned by DP, fetch delegation record, clone account as delegated + ctx.ensure_account(&pubkey).await.unwrap(); + assert_cloned_as_delegated!(cloner, &[pubkey], slot, program_pubkey); + assert_not_subscribed!(&chainlink, &[&pubkey]); + }; + + // 2. Account is undelegated + // Undelegation requested, setup subscription, writes would be refused + { + info!("2.1. Account is undelegated - Undelegation requested (account owner set to DP in Ephem)"); + + ctx.force_undelegation(&pubkey); + + info!("2.2. Would refuse write (account still owned by DP in Ephem)"); + assert_remain_undelegating!(cloner, &[pubkey], slot); + + slot = rpc_client.set_slot(slot + 11); + + info!("2.3. Account is undelegated on chain"); + // Committor service calls this to trigger subscription + chainlink.undelegation_requested(pubkey).await.unwrap(); + + // Committor service then requests undelegation on chain + let acc = rpc_client.get_account_at_slot(&pubkey).unwrap(); + let undelegated_acc = account_shared_with_owner_and_slot( + &acc.account, + program_pubkey, + slot, + ); + photon_client + .add_account(pubkey, Account::default(), slot) + .await; + let updated = ctx + .send_and_receive_account_update( + pubkey, + undelegated_acc.clone(), + Some(400), + ) + .await; + assert!(updated, "Failed to receive undelegation update"); + + // Account should be cloned as undelegated + info!("2.4. Write would be refused (undelegated on chain)"); + assert_cloned_as_undelegated!(cloner, &[pubkey], slot, program_pubkey); + assert_subscribed_without_delegation_record!(&chainlink, &[&pubkey]); + } + + // 3. Account redelegated to us (separate slot) + // Delegate back to us, subscription update, writes allowed + { + info!("3.1. Account redelegated to us - Delegate account back to us"); + slot = rpc_client.set_slot(slot + 11); + + let compressed_account = compressed_account_shared_with_owner_and_slot( + pubkey, + ctx.validator_pubkey, + program_pubkey, + slot, + ); + photon_client + .add_account(pubkey, compressed_account.clone().into(), slot) + .await; + let delegated_acc = account_shared_with_owner_and_slot( + &Account::default(), + compressed_delegation_client::id(), + slot, + ); + let updated = ctx + .send_and_receive_account_update( + pubkey, + delegated_acc.clone(), + Some(400), + ) + .await; + assert!(updated, "Failed to receive delegation update"); + + chainlink + .ensure_accounts( + &[pubkey], + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + + // Account should be cloned as delegated back to us + info!("3.2. Would allow write (delegated to us again)"); + assert_cloned_as_delegated!(cloner, &[pubkey], slot, program_pubkey); + + // Account is delegated to us, so we don't subscribe to it nor its delegation record + assert_not_subscribed!(chainlink, &[&pubkey]); + } +} diff --git a/magicblock-chainlink/tests/07_redeleg_us_same_slot.rs b/magicblock-chainlink/tests/07_redeleg_us_same_slot.rs index 5c32d2adf..a78c8cae5 100644 --- a/magicblock-chainlink/tests/07_redeleg_us_same_slot.rs +++ b/magicblock-chainlink/tests/07_redeleg_us_same_slot.rs @@ -8,12 +8,18 @@ use magicblock_chainlink::{ assert_cloned_as_delegated, assert_not_subscribed, assert_remain_undelegating, testing::{deleg::add_delegation_record_for, init_logger}, + AccountFetchOrigin, }; use solana_account::Account; use solana_program::clock::Slot; use solana_pubkey::Pubkey; -use utils::{ - accounts::account_shared_with_owner_and_slot, test_context::TestContext, + +use crate::utils::{ + accounts::{ + account_shared_with_owner_and_slot, + compressed_account_shared_with_owner_and_slot, + }, + test_context::TestContext, }; mod utils; @@ -108,3 +114,116 @@ async fn test_undelegate_redelegate_to_us_in_same_slot() { assert_not_subscribed!(chainlink, &[&pubkey, &deleg_record_pubkey]); } } + +#[tokio::test] +async fn test_undelegate_redelegate_to_us_in_same_slot_compressed() { + let mut slot: u64 = 11; + + let ctx = setup(slot).await; + let TestContext { + chainlink, + cloner, + rpc_client, + photon_client, + .. + } = ctx.clone(); + + let pubkey = Pubkey::new_unique(); + let program_pubkey = Pubkey::new_unique(); + let acc = Account { + lamports: 1_000, + ..Default::default() + }; + + // 1. Account delegated to us + // Initial state: Account is delegated to us and we can read/write to it + { + info!("1. Account delegated to us"); + + slot = rpc_client.set_slot(slot + 11); + let delegated_acc = account_shared_with_owner_and_slot( + &acc, + compressed_delegation_client::id(), + slot, + ); + rpc_client.add_account(pubkey, delegated_acc.clone().into()); + + let compressed_account = compressed_account_shared_with_owner_and_slot( + pubkey, + ctx.validator_pubkey, + program_pubkey, + slot, + ); + photon_client + .add_account(pubkey, compressed_account.clone().into(), slot) + .await; + + // Transaction to read + // Fetch account - see it's owned by DP, fetch delegation record, clone account as delegated + ctx.ensure_account(&pubkey).await.unwrap(); + assert_cloned_as_delegated!(cloner, &[pubkey], slot, program_pubkey); + assert_not_subscribed!(&chainlink, &[&pubkey]); + } + + // 2. Account is undelegated and redelegated to us (same slot) + // Undelegation requested, setup subscription, writes refused until redelegation + { + info!("2.1. Account is undelegated - Undelegation requested (account owner set to DP in Ephem)"); + + ctx.force_undelegation(&pubkey); + + info!("2.2. Would refuse write (account still owned by DP in Ephem)"); + assert_remain_undelegating!(cloner, &[pubkey], slot); + + slot = rpc_client.set_slot(slot + 1); + + info!("2.3. Account is undelegated and redelegated to us in same slot"); + + // First trigger undelegation subscription + ctx.chainlink.undelegation_requested(pubkey).await.unwrap(); + + // Update account to be delegated on chain and send a sub update + let compressed_account = compressed_account_shared_with_owner_and_slot( + pubkey, + ctx.validator_pubkey, + program_pubkey, + slot, + ); + photon_client + .add_account(pubkey, compressed_account.clone().into(), slot) + .await; + + let acc = rpc_client.get_account_at_slot(&pubkey).unwrap(); + let delegated_acc = account_shared_with_owner_and_slot( + &acc.account, + compressed_delegation_client::id(), + slot, + ); + let updated = ctx + .send_and_receive_account_update( + pubkey, + delegated_acc.clone(), + Some(400), + ) + .await; + assert!(updated, "Failed to receive delegation update"); + + // Then immediately delegate back to us (simulating same slot operation) + chainlink + .ensure_accounts( + &[pubkey], + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + + // Account should be cloned as delegated back to us + info!("2.4. Would allow write (delegated to us again)"); + assert_cloned_as_delegated!(cloner, &[pubkey], slot, program_pubkey); + + // Account is delegated to us, so we don't subscribe to it nor its delegation record + assert_not_subscribed!(chainlink, &[&pubkey]); + } +} diff --git a/magicblock-chainlink/tests/utils/accounts.rs b/magicblock-chainlink/tests/utils/accounts.rs index 5b167626c..cf5b0e3ed 100644 --- a/magicblock-chainlink/tests/utils/accounts.rs +++ b/magicblock-chainlink/tests/utils/accounts.rs @@ -1,4 +1,6 @@ #![allow(dead_code)] +use borsh::BorshSerialize; +use compressed_delegation_client::CompressedDelegationRecord; use magicblock_chainlink::testing::accounts::account_shared_with_owner; use solana_account::{Account, AccountSharedData}; use solana_instruction::{AccountMeta, Instruction}; @@ -16,6 +18,35 @@ pub fn account_shared_with_owner_and_slot( acc } +pub fn compressed_account_shared_with_owner_and_slot( + pda: Pubkey, + authority: Pubkey, + owner: Pubkey, + slot: u64, +) -> AccountSharedData { + let delegation_record_bytes = CompressedDelegationRecord { + pda, + authority, + last_update_nonce: 0, + is_undelegatable: false, + owner, + delegation_slot: slot, + lamports: 1000, + data: vec![], + } + .try_to_vec() + .unwrap(); + let mut acc = Account::new( + 0, + delegation_record_bytes.len(), + &compressed_delegation_client::ID, + ); + acc.data = delegation_record_bytes; + let mut acc = AccountSharedData::from(acc); + acc.set_remote_slot(slot); + acc +} + #[derive(Debug, Clone)] pub struct TransactionAccounts { pub readonly_accounts: Vec, diff --git a/magicblock-chainlink/tests/utils/test_context.rs b/magicblock-chainlink/tests/utils/test_context.rs index a59b9800b..564b24f4e 100644 --- a/magicblock-chainlink/tests/utils/test_context.rs +++ b/magicblock-chainlink/tests/utils/test_context.rs @@ -18,6 +18,7 @@ use magicblock_chainlink::{ accounts::account_shared_with_owner, cloner_stub::ClonerStub, deleg::add_delegation_record_for, + photon_client_mock::PhotonClientMock, rpc_client_mock::{ChainRpcClientMock, ChainRpcClientMockBuilder}, utils::{create_test_lru_cache, create_test_lru_cache_with_config}, }, @@ -35,16 +36,24 @@ pub type TestChainlink = Chainlink< ChainPubsubClientMock, AccountsBankStub, ClonerStub, + PhotonClientMock, >; #[derive(Clone)] pub struct TestContext { pub rpc_client: ChainRpcClientMock, pub pubsub_client: ChainPubsubClientMock, + pub photon_client: PhotonClientMock, pub chainlink: Arc, pub bank: Arc, pub remote_account_provider: Option< - Arc>, + Arc< + RemoteAccountProvider< + ChainRpcClientMock, + ChainPubsubClientMock, + PhotonClientMock, + >, + >, >, pub cloner: Arc, pub validator_pubkey: Pubkey, @@ -52,13 +61,14 @@ pub struct TestContext { impl TestContext { pub async fn init(slot: Slot) -> Self { - let (rpc_client, pubsub_client) = { + let (rpc_client, pubsub_client, photon_client) = { let rpc_client = ChainRpcClientMockBuilder::new().slot(slot).build(); let (updates_sndr, updates_rcvr) = mpsc::channel(100); let pubsub_client = ChainPubsubClientMock::new(updates_sndr, updates_rcvr); - (rpc_client, pubsub_client) + let photon_client = PhotonClientMock::new(); + (rpc_client, pubsub_client, photon_client) }; let lifecycle_mode = LifecycleMode::Ephemeral; @@ -79,6 +89,7 @@ impl TestContext { RemoteAccountProvider::try_from_clients_and_mode( rpc_client.clone(), pubsub_client.clone(), + Some(photon_client.clone()), tx, &config, subscribed_accounts, @@ -120,6 +131,7 @@ impl TestContext { Self { rpc_client, pubsub_client, + photon_client, chainlink: Arc::new(chainlink), bank, cloner, diff --git a/magicblock-committor-program/bin/magicblock_committor_program.so b/magicblock-committor-program/bin/magicblock_committor_program.so index 8e6eece3818b04ba304a5ac99de6ffcb8a67eda7..0f98c9cd189468bbd5053d9725684de7344b5119 100755 GIT binary patch literal 128696 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%0Ab&!9=7w*j-S^{??p!; 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%0AJXWjsO8kLo_rr7CZ<*FVbp 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 Instruction { commit_id.to_le_bytes().as_slice(), ); - let program_id = crate::id(); let ix = CommittorInstruction::Close { pubkey, commit_id, @@ -41,5 +43,5 @@ pub fn create_close_ix(args: CreateCloseIxArgs) -> Instruction { AccountMeta::new(chunks_pda, false), AccountMeta::new(buffer_pda, false), ]; - Instruction::new_with_borsh(program_id, &ix, accounts) + build_instruction(ix, accounts) } diff --git a/magicblock-committor-program/src/instruction_builder/init_buffer.rs b/magicblock-committor-program/src/instruction_builder/init_buffer.rs index 04c5a1c08..4fe28abf7 100644 --- a/magicblock-committor-program/src/instruction_builder/init_buffer.rs +++ b/magicblock-committor-program/src/instruction_builder/init_buffer.rs @@ -4,7 +4,10 @@ use solana_program::{ }; use solana_pubkey::Pubkey; -use crate::{instruction::CommittorInstruction, pdas}; +use crate::{ + instruction::CommittorInstruction, instruction_builder::build_instruction, + pdas, +}; // ----------------- // create_init_ix @@ -48,7 +51,6 @@ pub fn create_init_ix(args: CreateInitIxArgs) -> (Instruction, Pubkey, Pubkey) { &pubkey, commit_id.to_le_bytes().as_slice(), ); - let program_id = crate::id(); let ix = CommittorInstruction::Init { pubkey, commit_id, @@ -65,9 +67,5 @@ pub fn create_init_ix(args: CreateInitIxArgs) -> (Instruction, Pubkey, Pubkey) { AccountMeta::new(buffer_pda, false), AccountMeta::new_readonly(system_program::id(), false), ]; - ( - Instruction::new_with_borsh(program_id, &ix, accounts), - chunks_pda, - buffer_pda, - ) + (build_instruction(ix, accounts), chunks_pda, buffer_pda) } diff --git a/magicblock-committor-program/src/instruction_builder/mod.rs b/magicblock-committor-program/src/instruction_builder/mod.rs index ec76233ec..29814df86 100644 --- a/magicblock-committor-program/src/instruction_builder/mod.rs +++ b/magicblock-committor-program/src/instruction_builder/mod.rs @@ -1,4 +1,21 @@ +use borsh::BorshSerialize; +use solana_program::instruction::{AccountMeta, Instruction}; + +use crate::instruction::CommittorInstruction; + pub mod close_buffer; pub mod init_buffer; pub mod realloc_buffer; pub mod write_buffer; + +fn build_instruction( + ix: CommittorInstruction, + accounts: Vec, +) -> Instruction { + Instruction::new_with_bytes( + crate::id(), + &ix.try_to_vec() + .expect("Serialization of instruction should never fail"), + accounts, + ) +} diff --git a/magicblock-committor-program/src/instruction_builder/realloc_buffer.rs b/magicblock-committor-program/src/instruction_builder/realloc_buffer.rs index d5529b7ea..f38286765 100644 --- a/magicblock-committor-program/src/instruction_builder/realloc_buffer.rs +++ b/magicblock-committor-program/src/instruction_builder/realloc_buffer.rs @@ -3,7 +3,8 @@ use solana_pubkey::Pubkey; use crate::{ consts::MAX_ACCOUNT_ALLOC_PER_INSTRUCTION_SIZE, - instruction::CommittorInstruction, pdas, + instruction::CommittorInstruction, instruction_builder::build_instruction, + pdas, }; // ----------------- @@ -70,7 +71,6 @@ fn create_realloc_buffer_ix( commit_id.to_le_bytes().as_slice(), ); - let program_id = crate::id(); let ix = CommittorInstruction::ReallocBuffer { pubkey, buffer_account_size, @@ -82,5 +82,5 @@ fn create_realloc_buffer_ix( AccountMeta::new(authority, true), AccountMeta::new(buffer_pda, false), ]; - Instruction::new_with_borsh(program_id, &ix, accounts) + build_instruction(ix, accounts) } diff --git a/magicblock-committor-program/src/instruction_builder/write_buffer.rs b/magicblock-committor-program/src/instruction_builder/write_buffer.rs index 9cbba078c..b6d3ad6e8 100644 --- a/magicblock-committor-program/src/instruction_builder/write_buffer.rs +++ b/magicblock-committor-program/src/instruction_builder/write_buffer.rs @@ -1,7 +1,10 @@ use solana_program::instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; -use crate::{instruction::CommittorInstruction, pdas}; +use crate::{ + instruction::CommittorInstruction, instruction_builder::build_instruction, + pdas, +}; // ----------------- // create_write_ix @@ -33,7 +36,6 @@ pub fn create_write_ix(args: CreateWriteIxArgs) -> Instruction { commit_id.to_le_bytes().as_slice(), ); - let program_id = crate::id(); let ix = CommittorInstruction::Write { pubkey, commit_id, @@ -47,5 +49,5 @@ pub fn create_write_ix(args: CreateWriteIxArgs) -> Instruction { AccountMeta::new(chunks_pda, false), AccountMeta::new(buffer_pda, false), ]; - Instruction::new_with_borsh(program_id, &ix, accounts) + build_instruction(ix, accounts) } diff --git a/magicblock-committor-program/src/processor.rs b/magicblock-committor-program/src/processor.rs index bc3e6637b..bbb97968b 100644 --- a/magicblock-committor-program/src/processor.rs +++ b/magicblock-committor-program/src/processor.rs @@ -1,4 +1,4 @@ -use borsh::{to_vec, BorshDeserialize}; +use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, log::sol_log_64, msg, program::invoke_signed, program_error::ProgramError, system_instruction, @@ -191,7 +191,7 @@ fn process_init( chunks_account_info .data .borrow_mut() - .copy_from_slice(&to_vec(&chunks)?); + .copy_from_slice(&chunks.try_to_vec()?); Ok(()) } @@ -337,7 +337,7 @@ fn process_write( chunks .set_offset_delivered(offset as usize) .map_err(CommittorError::from)?; - chunks_data.copy_from_slice(&to_vec(&chunks)?); + chunks_data.copy_from_slice(&chunks.try_to_vec()?); Ok(()) } diff --git a/magicblock-committor-program/src/state/changeset_chunks.rs b/magicblock-committor-program/src/state/changeset_chunks.rs index b03c6427f..000dc44a8 100644 --- a/magicblock-committor-program/src/state/changeset_chunks.rs +++ b/magicblock-committor-program/src/state/changeset_chunks.rs @@ -15,7 +15,7 @@ pub struct ChangesetChunk { pub offset: u32, pub data_chunk: Vec, // chunk size can never exceed the ix max size which is well below u16::MAX (65_535) - #[borsh(skip)] + #[borsh_skip] chunk_size: u16, } diff --git a/magicblock-committor-program/src/state/chunks.rs b/magicblock-committor-program/src/state/chunks.rs index 0fa245d85..9f6f11c8e 100644 --- a/magicblock-committor-program/src/state/chunks.rs +++ b/magicblock-committor-program/src/state/chunks.rs @@ -61,7 +61,7 @@ impl Chunks { /// Returns how many bytes [`Chunks`] will occupy certain count pub fn struct_size(count: usize) -> usize { // bits: Vec, - Self::count_to_bitfield_bytes(count) + 4 + Self::count_to_bitfield_bytes(count) // count: usize, + std::mem::size_of::() // chunk_size: u16, diff --git a/magicblock-committor-service/Cargo.toml b/magicblock-committor-service/Cargo.toml index e6bde7973..f0c74f872 100644 --- a/magicblock-committor-service/Cargo.toml +++ b/magicblock-committor-service/Cargo.toml @@ -15,10 +15,15 @@ async-trait = { workspace = true } base64 = { workspace = true } bincode = { workspace = true } borsh = { workspace = true } +compressed-delegation-client = { workspace = true } dyn-clone = { workspace = true } futures-util = { workspace = true } +light-client = { workspace = true, features = ["v2"] } +light-sdk = { workspace = true, features = ["v2"] } log = { workspace = true } lru = { workspace = true } +magicblock-core = { workspace = true } +magicblock-config = { workspace = true } magicblock-committor-program = { workspace = true, features = [ "no-entrypoint", ] } diff --git a/magicblock-committor-service/src/committor_processor.rs b/magicblock-committor-service/src/committor_processor.rs index 7c2075e47..5a56c1d6b 100644 --- a/magicblock-committor-service/src/committor_processor.rs +++ b/magicblock-committor-service/src/committor_processor.rs @@ -1,5 +1,6 @@ use std::{collections::HashSet, path::Path, sync::Arc}; +use light_client::indexer::photon_indexer::PhotonIndexer; use log::*; use magicblock_rpc_client::MagicblockRpcClient; use magicblock_table_mania::{GarbageCollectorConfig, TableMania}; @@ -35,6 +36,7 @@ impl CommittorProcessor { authority: Keypair, persist_file: P, chain_config: ChainConfig, + photon_client: Option>, ) -> CommittorServiceResult where P: AsRef, @@ -60,6 +62,7 @@ impl CommittorProcessor { // Create commit scheduler let commits_scheduler = IntentExecutionManager::new( magic_block_rpc_client.clone(), + photon_client.clone(), DummyDB::new(), Some(persister.clone()), table_mania.clone(), diff --git a/magicblock-committor-service/src/intent_execution_manager.rs b/magicblock-committor-service/src/intent_execution_manager.rs index 5c85354ed..9a8934436 100644 --- a/magicblock-committor-service/src/intent_execution_manager.rs +++ b/magicblock-committor-service/src/intent_execution_manager.rs @@ -5,6 +5,7 @@ pub mod intent_scheduler; use std::sync::Arc; pub use intent_execution_engine::BroadcastedIntentExecutionResult; +use light_client::indexer::photon_indexer::PhotonIndexer; use magicblock_rpc_client::MagicblockRpcClient; use magicblock_table_mania::TableMania; use tokio::sync::{broadcast, mpsc, mpsc::error::TrySendError}; @@ -32,6 +33,7 @@ pub struct IntentExecutionManager { impl IntentExecutionManager { pub fn new( rpc_client: MagicblockRpcClient, + photon_client: Option>, db: D, intent_persister: Option

, table_mania: TableMania, @@ -39,10 +41,13 @@ impl IntentExecutionManager { ) -> Self { let db = Arc::new(db); - let commit_id_tracker = - Arc::new(CacheTaskInfoFetcher::new(rpc_client.clone())); + let commit_id_tracker = Arc::new(CacheTaskInfoFetcher::new( + rpc_client.clone(), + photon_client.clone(), + )); let executor_factory = IntentExecutorFactoryImpl { rpc_client, + photon_client, table_mania, compute_budget_config, commit_id_tracker, diff --git a/magicblock-committor-service/src/intent_executor/error.rs b/magicblock-committor-service/src/intent_executor/error.rs index a72a0a9f4..a6642251e 100644 --- a/magicblock-committor-service/src/intent_executor/error.rs +++ b/magicblock-committor-service/src/intent_executor/error.rs @@ -47,6 +47,8 @@ pub enum IntentExecutorError { FailedToFitError, #[error("SignerError: {0}")] SignerError(#[from] SignerError), + #[error("Inconsistent tasks compression used in strategy")] + InconsistentTaskCompression, // TODO(edwin): remove once proper retries introduced #[error("TaskBuilderError: {0}")] TaskBuilderError(#[from] TaskBuilderError), @@ -111,7 +113,8 @@ impl IntentExecutorError { } => commit_signature.map(|el| (el, *finalize_signature)), IntentExecutorError::EmptyIntentError | IntentExecutorError::FailedToFitError - | IntentExecutorError::SignerError(_) => None, + | IntentExecutorError::SignerError(_) + | IntentExecutorError::InconsistentTaskCompression => None, } } } @@ -339,6 +342,9 @@ impl From for IntentExecutorError { match value { TaskStrategistError::FailedToFitError => Self::FailedToFitError, TaskStrategistError::SignerError(err) => Self::SignerError(err), + TaskStrategistError::InconsistentTaskCompression => { + Self::InconsistentTaskCompression + } } } } diff --git a/magicblock-committor-service/src/intent_executor/intent_executor_factory.rs b/magicblock-committor-service/src/intent_executor/intent_executor_factory.rs index 24ac3d33c..e027bc815 100644 --- a/magicblock-committor-service/src/intent_executor/intent_executor_factory.rs +++ b/magicblock-committor-service/src/intent_executor/intent_executor_factory.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use light_client::indexer::photon_indexer::PhotonIndexer; use magicblock_rpc_client::MagicblockRpcClient; use magicblock_table_mania::TableMania; @@ -21,6 +22,7 @@ pub trait IntentExecutorFactory { /// Dummy struct to simplify signature of CommitSchedulerWorker pub struct IntentExecutorFactoryImpl { pub rpc_client: MagicblockRpcClient, + pub photon_client: Option>, pub table_mania: TableMania, pub compute_budget_config: ComputeBudgetConfig, pub commit_id_tracker: Arc, @@ -38,6 +40,7 @@ impl IntentExecutorFactory for IntentExecutorFactoryImpl { ); IntentExecutorImpl::::new( self.rpc_client.clone(), + self.photon_client.clone(), transaction_preparator, self.commit_id_tracker.clone(), ) diff --git a/magicblock-committor-service/src/intent_executor/mod.rs b/magicblock-committor-service/src/intent_executor/mod.rs index 28cdde0b5..2f7d9487c 100644 --- a/magicblock-committor-service/src/intent_executor/mod.rs +++ b/magicblock-committor-service/src/intent_executor/mod.rs @@ -4,10 +4,13 @@ pub mod single_stage_executor; pub mod task_info_fetcher; pub mod two_stage_executor; -use std::{mem, ops::ControlFlow, sync::Arc, time::Duration}; +use std::{ + future::Future, mem, ops::ControlFlow, pin::Pin, sync::Arc, time::Duration, +}; use async_trait::async_trait; use futures_util::future::{join, try_join_all}; +use light_client::indexer::photon_indexer::PhotonIndexer; use log::{trace, warn}; use magicblock_metrics::metrics; use magicblock_program::{ @@ -57,6 +60,10 @@ use crate::{ utils::persist_status_update_by_message_set, }; +pub type BoxFut<'a, T> = Pin + Send + 'a>>; +pub type CommitSlotFn<'a> = + Arc BoxFut<'a, Option> + Send + Sync + 'a>; + #[derive(Clone, Copy, Debug)] pub enum ExecutionOutput { // TODO: with arrival of challenge window remove SingleStage @@ -107,6 +114,7 @@ pub trait IntentExecutor: Send + Sync + 'static { pub struct IntentExecutorImpl { authority: Keypair, rpc_client: MagicblockRpcClient, + photon_client: Option>, transaction_preparator: T, task_info_fetcher: Arc, @@ -123,6 +131,7 @@ where { pub fn new( rpc_client: MagicblockRpcClient, + photon_client: Option>, transaction_preparator: T, task_info_fetcher: Arc, ) -> Self { @@ -130,6 +139,7 @@ where Self { authority, rpc_client, + photon_client, transaction_preparator, task_info_fetcher, junk: vec![], @@ -165,6 +175,7 @@ where &self.task_info_fetcher, &base_intent, persister, + &self.photon_client, ) .await?; @@ -190,10 +201,12 @@ where &self.task_info_fetcher, &base_intent, persister, + &self.photon_client, ); let finalize_tasks_fut = TaskBuilderImpl::finalize_tasks( &self.task_info_fetcher, &base_intent, + &self.photon_client, ); let (commit_tasks, finalize_tasks) = join(commit_tasks_fut, finalize_tasks_fut).await; @@ -307,12 +320,18 @@ where /// Handles out of sync commit id error, fixes current strategy /// Returns strategy to be cleaned up - /// TODO(edwin): TransactionStrategy -> CleanuoStrategy or something, naming it confusing for something that is cleaned up + /// TODO(edwin): TransactionStrategy -> CleanupStrategy or something, naming it confusing for something that is cleaned up async fn handle_commit_id_error( &self, committed_pubkeys: &[Pubkey], strategy: &mut TransactionStrategy, ) -> Result { + // TODO(dode): Handle cases where some tasks are compressed and some are not + let compressed = strategy + .optimized_tasks + .iter() + .any(|task| task.is_compressed()); + let tasks_and_metas: Vec<_> = strategy .optimized_tasks .iter_mut() @@ -340,7 +359,11 @@ where .reset(ResetType::Specific(committed_pubkeys)); let commit_ids = self .task_info_fetcher - .fetch_next_commit_ids(committed_pubkeys, min_context_slot) + .fetch_next_commit_ids( + committed_pubkeys, + min_context_slot, + compressed, + ) .await .map_err(TaskBuilderError::CommitTasksBuildError)?; @@ -365,6 +388,7 @@ where Ok(TransactionStrategy { optimized_tasks: to_cleanup, lookup_tables_keys: old_alts, + compressed: strategy.compressed, }) } @@ -386,6 +410,7 @@ where TransactionStrategy { optimized_tasks: action_tasks, lookup_tables_keys: old_alts, + compressed: strategy.compressed, } } @@ -419,6 +444,7 @@ where let commit_strategy = TransactionStrategy { optimized_tasks: commit_stage_tasks, lookup_tables_keys: commit_alt_pubkeys, + compressed: strategy.compressed, }; let finalize_alt_pubkeys = if strategy.lookup_tables_keys.is_empty() { @@ -432,12 +458,14 @@ where let finalize_strategy = TransactionStrategy { optimized_tasks: finalize_stage_tasks, lookup_tables_keys: finalize_alt_pubkeys, + compressed: strategy.compressed, }; // We clean up only ALTs let to_cleanup = TransactionStrategy { optimized_tasks: vec![], lookup_tables_keys: strategy.lookup_tables_keys, + compressed: strategy.compressed, }; (commit_strategy, finalize_strategy, to_cleanup) @@ -463,11 +491,13 @@ where TransactionStrategy { optimized_tasks: removed_task, lookup_tables_keys: old_alts, + compressed: strategy.compressed, } } else { TransactionStrategy { optimized_tasks: vec![], lookup_tables_keys: vec![], + compressed: strategy.compressed, } } } @@ -547,10 +577,14 @@ where } Err(IntentExecutorError::EmptyIntentError) | Err(IntentExecutorError::FailedToFitError) + | Err(IntentExecutorError::InconsistentTaskCompression) | Err(IntentExecutorError::TaskBuilderError(_)) | Err(IntentExecutorError::FailedCommitPreparationError( TransactionPreparatorError::SignerError(_), )) + | Err(IntentExecutorError::FailedCommitPreparationError( + TransactionPreparatorError::InconsistentTaskCompression, + )) | Err(IntentExecutorError::FailedFinalizePreparationError( TransactionPreparatorError::SignerError(_), )) => Some(CommitStatus::Failed), @@ -613,10 +647,11 @@ where } } - pub async fn prepare_and_execute_strategy( + pub async fn prepare_and_execute_strategy<'a, P: IntentPersister>( &self, transaction_strategy: &mut TransactionStrategy, persister: &Option

, + commit_slot_fn: Option>, ) -> IntentExecutorResult< IntentExecutorResult, TransactionPreparatorError, @@ -628,6 +663,8 @@ where &self.authority, transaction_strategy, persister, + &self.photon_client, + commit_slot_fn, ) .await?; diff --git a/magicblock-committor-service/src/intent_executor/single_stage_executor.rs b/magicblock-committor-service/src/intent_executor/single_stage_executor.rs index d57ea52cf..c70675aa9 100644 --- a/magicblock-committor-service/src/intent_executor/single_stage_executor.rs +++ b/magicblock-committor-service/src/intent_executor/single_stage_executor.rs @@ -60,6 +60,7 @@ where .prepare_and_execute_strategy( &mut self.transaction_strategy, persister, + None, ) .await .map_err(IntentExecutorError::FailedFinalizePreparationError)?; @@ -207,8 +208,10 @@ where &mut TransactionStrategy { optimized_tasks: vec![finalize_task], lookup_tables_keys: vec![], + compressed: task.is_compressed(), }, &None::, + None, ) .await .map_err(IntentExecutorError::FailedFinalizePreparationError)? @@ -221,6 +224,7 @@ where Ok(ControlFlow::Continue(TransactionStrategy { optimized_tasks: vec![], lookup_tables_keys: vec![], + compressed: task.is_compressed(), })) } } diff --git a/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs b/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs index 4eb390454..80440de5c 100644 --- a/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs +++ b/magicblock-committor-service/src/intent_executor/task_info_fetcher.rs @@ -1,13 +1,26 @@ use std::{ - collections::HashMap, num::NonZeroUsize, sync::Mutex, time::Duration, + collections::HashMap, + num::NonZeroUsize, + sync::{Arc, Mutex}, + time::Duration, }; use async_trait::async_trait; +use borsh::BorshDeserialize; +use compressed_delegation_client::CompressedDelegationRecord; use dlp::{ delegation_metadata_seeds_from_delegated_account, state::DelegationMetadata, }; +use light_client::{ + indexer::{ + photon_indexer::PhotonIndexer, Indexer, IndexerError, IndexerRpcConfig, + RetryConfig, + }, + rpc::RpcError as LightRpcError, +}; use log::{error, info, warn}; use lru::LruCache; +use magicblock_core::compression::derive_cda_from_pda; use magicblock_metrics::metrics; use magicblock_rpc_client::{MagicBlockRpcClientError, MagicblockRpcClient}; use solana_account::Account; @@ -31,6 +44,7 @@ pub trait TaskInfoFetcher: Send + Sync + 'static { &self, pubkeys: &[Pubkey], min_context_slot: u64, + compressed: bool, ) -> TaskInfoFetcherResult>; /// Fetches rent reimbursement address for pubkeys @@ -60,15 +74,20 @@ pub enum ResetType<'a> { pub struct CacheTaskInfoFetcher { rpc_client: MagicblockRpcClient, + photon_client: Option>, cache: Mutex>, } impl CacheTaskInfoFetcher { - pub fn new(rpc_client: MagicblockRpcClient) -> Self { + pub fn new( + rpc_client: MagicblockRpcClient, + photon_client: Option>, + ) -> Self { const CACHE_SIZE: NonZeroUsize = NonZeroUsize::new(1000).unwrap(); Self { rpc_client, + photon_client, cache: Mutex::new(LruCache::new(CACHE_SIZE)), } } @@ -161,6 +180,18 @@ impl CacheTaskInfoFetcher { TaskInfoFetcherError::MagicBlockRpcClientError(ref err) => { warn!("Fetch account error: {}, attempt: {}", err, i); } + TaskInfoFetcherError::IndexerError(ref err) => { + warn!("Fetch compressed delegation records error: {:?}, attempt: {}", err, i); + } + TaskInfoFetcherError::NoCompressedAccount(_) => break Err(err), + TaskInfoFetcherError::NoCompressedData(_) => break Err(err), + TaskInfoFetcherError::DeserializeError(ref err) => { + warn!("Deserialize compressed delegation record error: {:?}, attempt: {}", err, i); + } + TaskInfoFetcherError::LightRpcError(ref err) => { + warn!("Fetch account error: {:?}, attempt: {}", err, i); + } + TaskInfoFetcherError::PhotonClientNotFound => break Err(err), } if i >= max_retries.get() { @@ -221,6 +252,64 @@ impl CacheTaskInfoFetcher { Ok(accounts) } + + /// Fetches delegation records using Photon Indexer + /// Photon + pub async fn fetch_compressed_delegation_records_with_retries( + photon_client: &PhotonIndexer, + pubkeys: &[Pubkey], + min_context_slot: u64, + max_retries: NonZeroUsize, + ) -> TaskInfoFetcherResult> { + // Early return if no pubkeys to process + if pubkeys.is_empty() { + return Ok(Vec::new()); + } + + let cdas = pubkeys + .iter() + .map(|pubkey| derive_cda_from_pda(pubkey).to_bytes()) + .collect::>(); + let compressed_accounts = photon_client + .get_multiple_compressed_accounts( + Some(cdas), + None, + Some(IndexerRpcConfig { + slot: min_context_slot, + retry_config: RetryConfig { + num_retries: max_retries.get() as u32, + ..Default::default() + }, + }), + ) + .await + .map_err(TaskInfoFetcherError::IndexerError)? + .value; + + metrics::inc_task_info_fetcher_compressed_count(); + + let compressed_delegation_records = compressed_accounts + .items + .into_iter() + .zip(pubkeys.iter()) + .map(|(acc, pubkey)| { + let delegation_record = + CompressedDelegationRecord::try_from_slice( + &acc.ok_or(TaskInfoFetcherError::NoCompressedAccount( + *pubkey, + ))? + .data + .ok_or(TaskInfoFetcherError::NoCompressedData(*pubkey))? + .data, + ) + .map_err(TaskInfoFetcherError::DeserializeError)?; + + Ok::<_, TaskInfoFetcherError>(delegation_record) + }) + .collect::, _>>()?; + + Ok(compressed_delegation_records) + } } /// TaskInfoFetcher implementation that also caches most used 1000 keys @@ -232,6 +321,7 @@ impl TaskInfoFetcher for CacheTaskInfoFetcher { &self, pubkeys: &[Pubkey], min_context_slot: u64, + compressed: bool, ) -> TaskInfoFetcherResult> { if pubkeys.is_empty() { return Ok(HashMap::new()); @@ -270,15 +360,32 @@ impl TaskInfoFetcher for CacheTaskInfoFetcher { to_request.sort(); to_request.dedup(); - let remaining_ids = Self::fetch_metadata_with_retries( - &self.rpc_client, - &to_request, - min_context_slot, - NUM_FETCH_RETRIES, - ) - .await? - .into_iter() - .map(|metadata| metadata.last_update_nonce); + let remaining_ids = if compressed { + let Some(photon_client) = self.photon_client.as_ref() else { + return Err(TaskInfoFetcherError::PhotonClientNotFound); + }; + Self::fetch_compressed_delegation_records_with_retries( + photon_client, + &to_request, + min_context_slot, + NUM_FETCH_RETRIES, + ) + .await? + .into_iter() + .map(|metadata| metadata.last_update_nonce) + .collect::>() + } else { + Self::fetch_metadata_with_retries( + &self.rpc_client, + &to_request, + min_context_slot, + NUM_FETCH_RETRIES, + ) + .await? + .into_iter() + .map(|metadata| metadata.last_update_nonce) + .collect::>() + }; // We don't care if anything changed in between with cache - just update and return our ids. { @@ -358,6 +465,8 @@ impl TaskInfoFetcher for CacheTaskInfoFetcher { #[derive(thiserror::Error, Debug)] pub enum TaskInfoFetcherError { + #[error("LightRpcError: {0}")] + LightRpcError(#[from] LightRpcError), #[error("Metadata not found for: {0}")] AccountNotFoundError(Pubkey), #[error("InvalidAccountDataError for: {0}")] @@ -366,6 +475,16 @@ pub enum TaskInfoFetcherError { MinContextSlotNotReachedError(u64, Box), #[error("MagicBlockRpcClientError: {0}")] MagicBlockRpcClientError(Box), + #[error("IndexerError: {0}")] + IndexerError(#[from] IndexerError), + #[error("NoCompressedAccount: {0}")] + NoCompressedAccount(Pubkey), + #[error("CompressedAccountDataNotFound: {0}")] + NoCompressedData(Pubkey), + #[error("CompressedAccountDataDeserializeError: {0}")] + DeserializeError(#[from] std::io::Error), + #[error("PhotonClientNotFound")] + PhotonClientNotFound, } impl TaskInfoFetcherError { @@ -416,6 +535,12 @@ impl TaskInfoFetcherError { Self::InvalidAccountDataError(_) => None, Self::MinContextSlotNotReachedError(_, err) => err.signature(), Self::MagicBlockRpcClientError(err) => err.signature(), + Self::IndexerError(_) => None, + Self::NoCompressedAccount(_) => None, + Self::NoCompressedData(_) => None, + Self::DeserializeError(_) => None, + Self::LightRpcError(_) => None, + Self::PhotonClientNotFound => None, } } } diff --git a/magicblock-committor-service/src/intent_executor/two_stage_executor.rs b/magicblock-committor-service/src/intent_executor/two_stage_executor.rs index 41791e2f4..c41c322ca 100644 --- a/magicblock-committor-service/src/intent_executor/two_stage_executor.rs +++ b/magicblock-committor-service/src/intent_executor/two_stage_executor.rs @@ -1,7 +1,9 @@ -use std::ops::ControlFlow; +use std::{ops::ControlFlow, sync::Arc}; use log::{error, warn}; +use solana_commitment_config::CommitmentConfig; use solana_pubkey::Pubkey; +use solana_rpc_client_api::config::RpcTransactionConfig; use solana_signature::Signature; use crate::{ @@ -12,7 +14,7 @@ use crate::{ }, task_info_fetcher::TaskInfoFetcher, two_stage_executor::sealed::Sealed, - IntentExecutorImpl, + CommitSlotFn, IntentExecutorImpl, }, persist::{IntentPersister, IntentPersisterImpl}, tasks::{ @@ -86,6 +88,7 @@ where .prepare_and_execute_strategy( &mut self.state.commit_strategy, persister, + None::, ) .await .map_err(IntentExecutorError::FailedCommitPreparationError)?; @@ -222,8 +225,10 @@ where &mut TransactionStrategy { optimized_tasks: vec![finalize_task], lookup_tables_keys: vec![], + compressed: task.is_compressed(), }, &None::, + None, ) .await .map_err(IntentExecutorError::FailedCommitPreparationError)? @@ -235,6 +240,7 @@ where Ok(ControlFlow::Continue(TransactionStrategy { optimized_tasks: vec![], lookup_tables_keys: vec![], + compressed: task.is_compressed(), })) } } @@ -250,6 +256,29 @@ where mut self, persister: &Option

, ) -> IntentExecutorResult> { + // Fetching the slot at which the transaction was executed + // Task preparations requiring fresh data can use that info + // Using a future to avoid fetching when unnecessary + let rpc_client = self.inner.rpc_client.clone(); + let sig = self.state.commit_signature; + let commit_slot_fn: CommitSlotFn = Arc::new(move || { + let rpc_client = rpc_client.clone(); + Box::pin(async move { + rpc_client + .get_transaction( + &sig, + Some(RpcTransactionConfig { + commitment: Some(CommitmentConfig::confirmed()), + max_supported_transaction_version: Some(0), + ..Default::default() + }), + ) + .await + .map(|tx| tx.slot) + .ok() + }) + }); + let mut i = 0; let finalize_result = loop { i += 1; @@ -260,6 +289,7 @@ where .prepare_and_execute_strategy( &mut self.state.finalize_strategy, persister, + Some(commit_slot_fn.clone()), ) .await .map_err(IntentExecutorError::FailedFinalizePreparationError)?; diff --git a/magicblock-committor-service/src/pubkeys_provider.rs b/magicblock-committor-service/src/pubkeys_provider.rs index 82e1829b2..4e20458b9 100644 --- a/magicblock-committor-service/src/pubkeys_provider.rs +++ b/magicblock-committor-service/src/pubkeys_provider.rs @@ -45,6 +45,7 @@ pub fn provide_common_pubkeys(validator: &Pubkey) -> HashSet { let mut set = HashSet::new(); let deleg_program = dlp::id(); + let compressed_delegation_program = compressed_delegation_client::id(); let protocol_fees_vault = pda::fees_vault_pda(); let validator_fees_vault = pda::validator_fees_vault_pda_from_validator(validator); @@ -52,13 +53,15 @@ pub fn provide_common_pubkeys(validator: &Pubkey) -> HashSet { trace!( "Common pubkeys: - validator: {} - delegation program: {} - protocol fees vault: {} - validator fees vault: {} - committor program: {}", + validator: {} + delegation program: {} + compressed delegation program: {} + protocol fees vault: {} + validator fees vault: {} + committor program: {}", validator, deleg_program, + compressed_delegation_program, protocol_fees_vault, validator_fees_vault, committor_program @@ -67,6 +70,7 @@ pub fn provide_common_pubkeys(validator: &Pubkey) -> HashSet { set.insert(*validator); set.insert(system_program_id()); set.insert(deleg_program); + set.insert(compressed_delegation_program); set.insert(protocol_fees_vault); set.insert(validator_fees_vault); set.insert(committor_program); diff --git a/magicblock-committor-service/src/service.rs b/magicblock-committor-service/src/service.rs index 1e42761e5..4f292e6a2 100644 --- a/magicblock-committor-service/src/service.rs +++ b/magicblock-committor-service/src/service.rs @@ -1,5 +1,6 @@ use std::{path::Path, sync::Arc, time::Instant}; +use light_client::indexer::photon_indexer::PhotonIndexer; use log::*; use solana_keypair::Keypair; use solana_pubkey::Pubkey; @@ -99,6 +100,7 @@ impl CommittorActor { authority: Keypair, persist_file: P, chain_config: ChainConfig, + photon_client: Option>, ) -> CommittorServiceResult where P: AsRef, @@ -107,6 +109,7 @@ impl CommittorActor { authority, persist_file, chain_config, + photon_client, )?); Ok(Self { @@ -263,6 +266,7 @@ impl CommittorService { authority: Keypair, persist_file: P, chain_config: ChainConfig, + photon_client: Option>, ) -> CommittorServiceResult where P: AsRef, @@ -277,6 +281,7 @@ impl CommittorService { authority, persist_file, chain_config, + photon_client, )?; tokio::spawn(async move { actor.run(cancel_token).await; diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index 6ce9eadb7..3ce579972 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -1,3 +1,4 @@ +use compressed_delegation_client::types::{CommitArgs, FinalizeArgs}; use dlp::{ args::{CallHandlerArgs, CommitDiffArgs, CommitStateArgs}, compute_diff, @@ -5,29 +6,36 @@ use dlp::{ call_handler_size_budget, commit_diff_size_budget, commit_size_budget, finalize_size_budget, undelegate_size_budget, }, - AccountSizeClass, + total_size_budget, AccountSizeClass, }; use magicblock_metrics::metrics::LabelValue; use solana_account::ReadableAccount; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; +use solana_system_program::id as system_program_id; #[cfg(test)] use crate::tasks::TaskStrategy; use crate::tasks::{ buffer_task::{BufferTask, BufferTaskType}, + task_builder::CompressedData, visitor::Visitor, BaseActionTask, BaseTask, BaseTaskError, BaseTaskResult, CommitDiffTask, - CommitTask, FinalizeTask, PreparationState, TaskType, UndelegateTask, + CommitTask, CompressedCommitTask, CompressedFinalizeTask, + CompressedUndelegateTask, FinalizeTask, PreparationState, PreparationTask, + TaskType, UndelegateTask, }; /// Task that will be executed on Base layer via arguments #[derive(Clone)] pub enum ArgsTaskType { Commit(CommitTask), + CompressedCommit(CompressedCommitTask), CommitDiff(CommitDiffTask), Finalize(FinalizeTask), + CompressedFinalize(CompressedFinalizeTask), Undelegate(UndelegateTask), // Special action really + CompressedUndelegate(CompressedUndelegateTask), BaseAction(BaseActionTask), } @@ -45,8 +53,21 @@ impl From for ArgsTask { impl ArgsTask { pub fn new(task_type: ArgsTaskType) -> Self { + // Only prepare compressed tasks [`ArgsTaskType`] type + let preparation_state = match task_type { + ArgsTaskType::Commit(_) + | ArgsTaskType::CommitDiff(_) + | ArgsTaskType::Finalize(_) + | ArgsTaskType::Undelegate(_) + | ArgsTaskType::BaseAction(_) => PreparationState::NotNeeded, + ArgsTaskType::CompressedCommit(_) + | ArgsTaskType::CompressedFinalize(_) + | ArgsTaskType::CompressedUndelegate(_) => { + PreparationState::Required(PreparationTask::Compressed) + } + }; Self { - preparation_state: PreparationState::NotNeeded, + preparation_state, task_type, } } @@ -69,6 +90,26 @@ impl BaseTask for ArgsTask { args, ) } + ArgsTaskType::CompressedCommit(value) => { + compressed_delegation_client::CommitBuilder::new() + .validator(*validator) + .delegated_account(value.committed_account.pubkey) + .args(CommitArgs { + current_compressed_delegated_account_data: value + .compressed_data + .compressed_delegation_record_bytes + .clone(), + new_data: value.committed_account.account.data.clone(), + account_meta: value.compressed_data.account_meta, + validity_proof: value.compressed_data.proof, + update_nonce: value.commit_id, + allow_undelegation: value.allow_undelegation, + }) + .add_remaining_accounts( + &value.compressed_data.remaining_accounts, + ) + .instruction() + } ArgsTaskType::CommitDiff(value) => { let args = CommitDiffArgs { nonce: value.commit_id, @@ -94,6 +135,34 @@ impl BaseTask for ArgsTask { value.delegated_account, ) } + ArgsTaskType::CompressedFinalize(value) => { + compressed_delegation_client::FinalizeBuilder::new() + .validator(*validator) + .delegated_account(value.delegated_account) + .args(FinalizeArgs { + current_compressed_delegated_account_data: value + .compressed_data + .compressed_delegation_record_bytes + .clone(), + account_meta: value.compressed_data.account_meta, + validity_proof: value.compressed_data.proof, + }) + .add_remaining_accounts( + &value.compressed_data.remaining_accounts, + ) + .instruction() + } + ArgsTaskType::CompressedUndelegate(value) => { + compressed_delegation_client::UndelegateBuilder::new() + .payer(*validator) + .delegated_account(value.delegated_account) + .owner_program(value.owner_program) + .system_program(system_program_id()) + .add_remaining_accounts( + &value.compressed_data.remaining_accounts, + ) + .instruction() + } ArgsTaskType::Undelegate(value) => { dlp::instruction_builder::undelegate( *validator, @@ -143,11 +212,13 @@ impl BaseTask for ArgsTask { } ArgsTaskType::BaseAction(_) | ArgsTaskType::Finalize(_) - | ArgsTaskType::Undelegate(_) => Err(self), + | ArgsTaskType::Undelegate(_) + | ArgsTaskType::CompressedCommit(_) + | ArgsTaskType::CompressedFinalize(_) + | ArgsTaskType::CompressedUndelegate(_) => Err(self), } } - /// Nothing to prepare for [`ArgsTaskType`] type fn preparation_state(&self) -> &PreparationState { &self.preparation_state } @@ -159,7 +230,7 @@ impl BaseTask for ArgsTask { if !matches!(new_state, PreparationState::NotNeeded) { Err(BaseTaskError::PreparationStateTransitionError) } else { - // Do nothing + self.preparation_state = new_state; Ok(()) } } @@ -171,6 +242,9 @@ impl BaseTask for ArgsTask { ArgsTaskType::BaseAction(task) => task.action.compute_units, ArgsTaskType::Undelegate(_) => 70_000, ArgsTaskType::Finalize(_) => 70_000, + ArgsTaskType::CompressedCommit(_) => 250_000, + ArgsTaskType::CompressedUndelegate(_) => 250_000, + ArgsTaskType::CompressedFinalize(_) => 250_000, } } @@ -186,6 +260,45 @@ impl BaseTask for ArgsTask { task.committed_account.account.data.len() as u32, )) } + ArgsTaskType::CompressedCommit(_task) => { + total_size_budget(&[ + AccountSizeClass::Tiny, // validator + AccountSizeClass::ExtraLarge, // Light System Program + AccountSizeClass::Tiny, // CPI Signer + AccountSizeClass::Tiny, // Registered Program PDA + AccountSizeClass::Tiny, // Account Compression Authority + AccountSizeClass::Tiny, // Account Compression Program + AccountSizeClass::Tiny, // System Program + AccountSizeClass::Huge, // Batch Merkle Tree + AccountSizeClass::ExtraLarge, // Output Queue + ]) + } + ArgsTaskType::CompressedFinalize(_task) => { + total_size_budget(&[ + AccountSizeClass::Tiny, // validator + AccountSizeClass::ExtraLarge, // Light System Program + AccountSizeClass::Tiny, // CPI Signer + AccountSizeClass::Tiny, // Registered Program PDA + AccountSizeClass::Tiny, // Account Compression Authority + AccountSizeClass::Tiny, // Account Compression Program + AccountSizeClass::Tiny, // System Program + AccountSizeClass::Huge, // Batch Merkle Tree + AccountSizeClass::ExtraLarge, // Output Queue + ]) + } + ArgsTaskType::CompressedUndelegate(_task) => { + total_size_budget(&[ + AccountSizeClass::Tiny, // validator + AccountSizeClass::ExtraLarge, // Light System Program + AccountSizeClass::Tiny, // CPI Signer + AccountSizeClass::Tiny, // Registered Program PDA + AccountSizeClass::Tiny, // Account Compression Authority + AccountSizeClass::Tiny, // Account Compression Program + AccountSizeClass::Tiny, // System Program + AccountSizeClass::Huge, // Batch Merkle Tree + AccountSizeClass::ExtraLarge, // Output Queue + ]) + } ArgsTaskType::BaseAction(task) => { // assume all other accounts are Small accounts. let other_accounts_budget = @@ -214,10 +327,15 @@ impl BaseTask for ArgsTask { fn task_type(&self) -> TaskType { match &self.task_type { ArgsTaskType::Commit(_) => TaskType::Commit, + ArgsTaskType::CompressedCommit(_) => TaskType::CompressedCommit, ArgsTaskType::CommitDiff(_) => TaskType::Commit, ArgsTaskType::BaseAction(_) => TaskType::Action, ArgsTaskType::Undelegate(_) => TaskType::Undelegate, + ArgsTaskType::CompressedUndelegate(_) => { + TaskType::CompressedUndelegate + } ArgsTaskType::Finalize(_) => TaskType::Finalize, + ArgsTaskType::CompressedFinalize(_) => TaskType::CompressedFinalize, } } @@ -231,14 +349,79 @@ impl BaseTask for ArgsTask { ArgsTaskType::Commit(task) => { task.commit_id = commit_id; } + ArgsTaskType::CompressedCommit(task) => { + task.commit_id = commit_id; + } ArgsTaskType::CommitDiff(task) => { task.commit_id = commit_id; } ArgsTaskType::BaseAction(_) | ArgsTaskType::Finalize(_) - | ArgsTaskType::Undelegate(_) => {} + | ArgsTaskType::Undelegate(_) + | ArgsTaskType::CompressedFinalize(_) + | ArgsTaskType::CompressedUndelegate(_) => {} }; } + + fn is_compressed(&self) -> bool { + matches!( + &self.task_type, + ArgsTaskType::CompressedCommit(_) + | ArgsTaskType::CompressedFinalize(_) + | ArgsTaskType::CompressedUndelegate(_) + ) + } + + fn set_compressed_data(&mut self, compressed_data: CompressedData) { + match &mut self.task_type { + ArgsTaskType::CompressedCommit(value) => { + value.compressed_data = compressed_data; + } + ArgsTaskType::CompressedFinalize(value) => { + value.compressed_data = compressed_data; + } + ArgsTaskType::CompressedUndelegate(value) => { + value.compressed_data = compressed_data; + } + _ => {} + } + } + + fn get_compressed_data(&self) -> Option<&CompressedData> { + match &self.task_type { + ArgsTaskType::CompressedCommit(value) => { + Some(&value.compressed_data) + } + ArgsTaskType::CompressedFinalize(value) => { + Some(&value.compressed_data) + } + ArgsTaskType::CompressedUndelegate(value) => { + Some(&value.compressed_data) + } + _ => None, + } + } + + fn delegated_account(&self) -> Option { + match &self.task_type { + ArgsTaskType::Commit(value) => Some(value.committed_account.pubkey), + ArgsTaskType::CompressedCommit(value) => { + Some(value.committed_account.pubkey) + } + ArgsTaskType::CommitDiff(value) => { + Some(value.committed_account.pubkey) + } + ArgsTaskType::Finalize(value) => Some(value.delegated_account), + ArgsTaskType::CompressedFinalize(value) => { + Some(value.delegated_account) + } + ArgsTaskType::Undelegate(value) => Some(value.delegated_account), + ArgsTaskType::CompressedUndelegate(value) => { + Some(value.delegated_account) + } + ArgsTaskType::BaseAction(_) => None, + } + } } impl LabelValue for ArgsTask { @@ -249,6 +432,11 @@ impl LabelValue for ArgsTask { ArgsTaskType::BaseAction(_) => "args_action", ArgsTaskType::Finalize(_) => "args_finalize", ArgsTaskType::Undelegate(_) => "args_undelegate", + ArgsTaskType::CompressedCommit(_) => "args_compressed_commit", + ArgsTaskType::CompressedFinalize(_) => "args_compressed_finalize", + ArgsTaskType::CompressedUndelegate(_) => { + "args_compressed_undelegate" + } } } } diff --git a/magicblock-committor-service/src/tasks/buffer_task.rs b/magicblock-committor-service/src/tasks/buffer_task.rs index 700d7f8e5..56090ad3b 100644 --- a/magicblock-committor-service/src/tasks/buffer_task.rs +++ b/magicblock-committor-service/src/tasks/buffer_task.rs @@ -17,8 +17,8 @@ use crate::{ consts::MAX_WRITE_CHUNK_SIZE, tasks::{ visitor::Visitor, BaseTask, BaseTaskError, BaseTaskResult, - CommitDiffTask, CommitTask, PreparationState, PreparationTask, - TaskType, + BufferPreparationTask, CommitDiffTask, CommitTask, PreparationState, + PreparationTask, TaskType, }, }; @@ -61,12 +61,14 @@ impl BufferTask { let chunks = Chunks::from_data_length(data.len(), MAX_WRITE_CHUNK_SIZE); - PreparationState::Required(PreparationTask { - commit_id: task.commit_id, - pubkey: task.committed_account.pubkey, - committed_data: data, - chunks, - }) + PreparationState::Required(PreparationTask::Buffer( + BufferPreparationTask { + commit_id: task.commit_id, + pubkey: task.committed_account.pubkey, + committed_data: data, + chunks, + }, + )) } BufferTaskType::CommitDiff(task) => { @@ -78,12 +80,14 @@ impl BufferTask { let chunks = Chunks::from_data_length(diff.len(), MAX_WRITE_CHUNK_SIZE); - PreparationState::Required(PreparationTask { - commit_id: task.commit_id, - pubkey: task.committed_account.pubkey, - committed_data: diff, - chunks, - }) + PreparationState::Required(PreparationTask::Buffer( + BufferPreparationTask { + commit_id: task.commit_id, + pubkey: task.committed_account.pubkey, + committed_data: diff, + chunks, + }, + )) } } } @@ -220,6 +224,34 @@ impl BaseTask for BufferTask { self.preparation_state = Self::preparation_required(&self.task_type) } + + fn is_compressed(&self) -> bool { + false + } + + fn set_compressed_data( + &mut self, + _compressed_data: super::task_builder::CompressedData, + ) { + // No-op + } + + fn get_compressed_data( + &self, + ) -> Option<&super::task_builder::CompressedData> { + None + } + + fn delegated_account(&self) -> Option { + match &self.task_type { + BufferTaskType::Commit(value) => { + Some(value.committed_account.pubkey) + } + BufferTaskType::CommitDiff(value) => { + Some(value.committed_account.pubkey) + } + } + } } impl LabelValue for BufferTask { diff --git a/magicblock-committor-service/src/tasks/mod.rs b/magicblock-committor-service/src/tasks/mod.rs index 2792a0c6c..6444342cc 100644 --- a/magicblock-committor-service/src/tasks/mod.rs +++ b/magicblock-committor-service/src/tasks/mod.rs @@ -19,7 +19,7 @@ use solana_instruction::Instruction; use solana_pubkey::Pubkey; use thiserror::Error; -use crate::tasks::visitor::Visitor; +use crate::tasks::{task_builder::CompressedData, visitor::Visitor}; pub mod args_task; pub mod buffer_task; @@ -34,8 +34,11 @@ pub use task_builder::TaskBuilderImpl; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum TaskType { Commit, + CompressedCommit, Finalize, + CompressedFinalize, Undelegate, + CompressedUndelegate, Action, } @@ -98,6 +101,18 @@ pub trait BaseTask: Send + Sync + DynClone + LabelValue { /// Calls [`Visitor`] with specific task type fn visit(&self, visitor: &mut dyn Visitor); + /// Returns true if task is compressed + fn is_compressed(&self) -> bool; + + /// Sets compressed data for task + fn set_compressed_data(&mut self, compressed_data: CompressedData); + + /// Gets compressed data for task + fn get_compressed_data(&self) -> Option<&CompressedData>; + + /// Delegated account for task + fn delegated_account(&self) -> Option; + /// Resets commit id fn reset_commit_id(&mut self, commit_id: u64); } @@ -119,6 +134,14 @@ pub struct CommitDiffTask { pub base_account: Account, } +#[derive(Clone)] +pub struct CompressedCommitTask { + pub commit_id: u64, + pub allow_undelegation: bool, + pub committed_account: CommittedAccount, + pub compressed_data: CompressedData, +} + #[derive(Clone)] pub struct UndelegateTask { pub delegated_account: Pubkey, @@ -126,18 +149,43 @@ pub struct UndelegateTask { pub rent_reimbursement: Pubkey, } +#[derive(Clone)] +pub struct CompressedUndelegateTask { + pub delegated_account: Pubkey, + pub owner_program: Pubkey, + pub compressed_data: CompressedData, +} + #[derive(Clone)] pub struct FinalizeTask { pub delegated_account: Pubkey, } +#[derive(Clone)] +pub struct CompressedFinalizeTask { + pub delegated_account: Pubkey, + pub compressed_data: CompressedData, +} + #[derive(Clone)] pub struct BaseActionTask { pub action: BaseAction, } +/// Task that will be executed on Base layer via arguments +#[derive(Clone)] +pub enum ArgsTask { + Commit(CommitTask), + CompressedCommit(CompressedCommitTask), + Finalize(FinalizeTask), + CompressedFinalize(CompressedFinalizeTask), + Undelegate(UndelegateTask), // Special action really + CompressedUndelegate(CompressedUndelegateTask), + BaseAction(BaseActionTask), +} + #[derive(Clone, Debug)] -pub struct PreparationTask { +pub struct BufferPreparationTask { pub commit_id: u64, pub pubkey: Pubkey, pub chunks: Chunks, @@ -146,16 +194,17 @@ pub struct PreparationTask { pub committed_data: Vec, } -impl PreparationTask { +#[derive(Clone, Debug)] +pub enum PreparationTask { + Buffer(BufferPreparationTask), + Compressed, +} + +impl BufferPreparationTask { /// Returns initialization [`Instruction`] pub fn init_instruction(&self, authority: &Pubkey) -> Instruction { - // // SAFETY: as object_length internally uses only already allocated or static buffers, - // // and we don't use any fs writers, so the only error that may occur here is of kind - // // OutOfMemory or WriteZero. This is impossible due to: - // // Chunks::new panics if its size exceeds MAX_ACCOUNT_ALLOC_PER_INSTRUCTION_SIZE or 10_240 - // // https://github.com/near/borsh-rs/blob/f1b75a6b50740bfb6231b7d0b1bd93ea58ca5452/borsh/src/ser/helpers.rs#L59 let chunks_account_size = - borsh::object_length(&self.chunks).unwrap() as u64; + Chunks::struct_size(self.chunks.count()) as u64; let buffer_account_size = self.committed_data.len() as u64; let (instruction, _, _) = create_init_ix(CreateInitIxArgs { @@ -433,6 +482,9 @@ mod serialization_safety_test { else { panic!("invalid preparation state on creation!"); }; + let PreparationTask::Buffer(preparation_task) = preparation_task else { + panic!("invalid preparation task on creation!"); + }; assert_serializable(&preparation_task.init_instruction(&authority)); for ix in preparation_task.realloc_instructions(&authority) { assert_serializable(&ix); diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index b0db22f12..0ce98b8de 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -1,12 +1,25 @@ use std::sync::Arc; use async_trait::async_trait; -use log::error; +use futures_util::{stream::FuturesUnordered, TryStreamExt}; +use light_client::indexer::{ + photon_indexer::PhotonIndexer, Indexer, IndexerError, IndexerRpcConfig, +}; +use light_sdk::{ + error::LightSdkError, + instruction::{ + account_meta::CompressedAccountMeta, PackedAccounts, + SystemAccountMetaConfig, ValidityProof, + }, +}; +use log::*; +use magicblock_core::compression::derive_cda_from_pda; use magicblock_program::magic_scheduled_base_intent::{ CommitType, CommittedAccount, MagicBaseIntent, ScheduledBaseIntent, UndelegateType, }; use solana_account::Account; +use solana_instruction::AccountMeta; use solana_pubkey::Pubkey; use solana_signature::Signature; @@ -18,10 +31,21 @@ use crate::{ persist::IntentPersister, tasks::{ args_task::{ArgsTask, ArgsTaskType}, - BaseActionTask, BaseTask, FinalizeTask, UndelegateTask, + task_strategist::TaskStrategistError, + BaseActionTask, BaseTask, CompressedCommitTask, CompressedFinalizeTask, + CompressedUndelegateTask, FinalizeTask, UndelegateTask, }, }; +#[derive(Clone, Debug, Default, PartialEq)] +pub struct CompressedData { + pub hash: [u8; 32], + pub compressed_delegation_record_bytes: Vec, + pub remaining_accounts: Vec, + pub account_meta: CompressedAccountMeta, + pub proof: ValidityProof, +} + #[async_trait] pub trait TasksBuilder { // Creates tasks for commit stage @@ -29,12 +53,14 @@ pub trait TasksBuilder { commit_id_fetcher: &Arc, base_intent: &ScheduledBaseIntent, persister: &Option

, + photon_client: &Option>, ) -> TaskBuilderResult>>; // Create tasks for finalize stage async fn finalize_tasks( info_fetcher: &Arc, base_intent: &ScheduledBaseIntent, + photon_client: &Option>, ) -> TaskBuilderResult>>; } @@ -89,26 +115,36 @@ impl TasksBuilder for TaskBuilderImpl { commit_id_fetcher: &Arc, base_intent: &ScheduledBaseIntent, persister: &Option

, + photon_client: &Option>, ) -> TaskBuilderResult>> { - let (accounts, allow_undelegation) = match &base_intent.base_intent { - MagicBaseIntent::BaseActions(actions) => { - let tasks = actions - .iter() - .map(|el| { - let task = BaseActionTask { action: el.clone() }; - let task = - ArgsTask::new(ArgsTaskType::BaseAction(task)); - Box::new(task) as Box - }) - .collect(); + let (accounts, allow_undelegation, compressed) = + match &base_intent.base_intent { + MagicBaseIntent::BaseActions(actions) => { + let tasks = actions + .iter() + .map(|el| { + let task = BaseActionTask { action: el.clone() }; + let task = + ArgsTask::new(ArgsTaskType::BaseAction(task)); + Box::new(task) as Box + }) + .collect(); - return Ok(tasks); - } - MagicBaseIntent::Commit(t) => (t.get_committed_accounts(), false), - MagicBaseIntent::CommitAndUndelegate(t) => { - (t.commit_action.get_committed_accounts(), true) - } - }; + return Ok(tasks); + } + MagicBaseIntent::Commit(t) => { + (t.get_committed_accounts(), false, false) + } + MagicBaseIntent::CommitAndUndelegate(t) => { + (t.commit_action.get_committed_accounts(), true, false) + } + MagicBaseIntent::CompressedCommit(t) => { + (t.get_committed_accounts(), false, true) + } + MagicBaseIntent::CompressedCommitAndUndelegate(t) => { + (t.commit_action.get_committed_accounts(), true, true) + } + }; let (commit_ids, base_accounts) = { let mut min_context_slot = 0; @@ -132,7 +168,8 @@ impl TasksBuilder for TaskBuilderImpl { tokio::join!( commit_id_fetcher.fetch_next_commit_ids( &committed_pubkeys, - min_context_slot + min_context_slot, + compressed ), commit_id_fetcher.get_base_accounts( diffable_pubkeys.as_slice(), @@ -155,23 +192,75 @@ impl TasksBuilder for TaskBuilderImpl { // Persist commit ids for commitees commit_ids .iter() - .for_each(|(pubkey, commit_id) | { + .for_each(|(pubkey, commit_id)| { if let Err(err) = persister.set_commit_id(base_intent.id, pubkey, *commit_id) { error!("Failed to persist commit id: {}, for message id: {} with pubkey {}: {}", commit_id, base_intent.id, pubkey, err); } }); - let tasks = accounts - .iter() - .map(|account| { - let commit_id = *commit_ids.get(&account.pubkey).expect("CommitIdFetcher provide commit ids for all listed pubkeys, or errors!"); - // TODO (snawaz): if accounts do not have duplicate, then we can use remove - // instead: - // let base_account = base_accounts.remove(&account.pubkey); - let base_account = base_accounts.get(&account.pubkey).cloned(); - let task = Self::create_commit_task(commit_id, allow_undelegation, account.clone(), base_account); - Box::new(task) as Box - }).collect(); + let tasks = if compressed { + // For compressed accounts, prepare compression data + let photon_client = photon_client + .as_ref() + .ok_or(TaskBuilderError::PhotonClientNotFound)?; + let commit_ids = commit_ids.clone(); + + accounts + .iter() + .map(|account| { + let commit_ids = commit_ids.clone(); + async move { + let commit_id = *commit_ids + .get(&account.pubkey) + .ok_or(TaskBuilderError::MissingCommitId( + account.pubkey, + ))?; + let compressed_data = get_compressed_data( + &account.pubkey, + photon_client, + None, + ) + .await?; + let task = ArgsTaskType::CompressedCommit( + CompressedCommitTask { + commit_id, + allow_undelegation, + committed_account: account.clone(), + compressed_data, + }, + ); + Ok::<_, TaskBuilderError>( + Box::new(ArgsTask::new(task)) as Box + ) + } + }) + .collect::>() + .try_collect() + .await? + } else { + accounts + .iter() + .map(|account| { + let commit_id = *commit_ids.get(&account.pubkey).ok_or( + TaskBuilderError::MissingCommitId(account.pubkey), + )?; + // TODO (snawaz): if accounts do not have duplicate, then we can use remove + // instead: + // let base_account = base_accounts.remove(&account.pubkey); + let base_account = + base_accounts.get(&account.pubkey).cloned(); + let task = Self::create_commit_task( + commit_id, + allow_undelegation, + account.clone(), + base_account, + ); + Ok::<_, TaskBuilderError>( + Box::new(task) as Box + ) + }) + .collect::>()? + }; Ok(tasks) } @@ -180,41 +269,124 @@ impl TasksBuilder for TaskBuilderImpl { async fn finalize_tasks( info_fetcher: &Arc, base_intent: &ScheduledBaseIntent, + photon_client: &Option>, ) -> TaskBuilderResult>> { // Helper to create a finalize task - fn finalize_task(account: &CommittedAccount) -> Box { - let task_type = ArgsTaskType::Finalize(FinalizeTask { - delegated_account: account.pubkey, - }); - Box::new(ArgsTask::new(task_type)) + fn finalize_task( + account: &CommittedAccount, + compressed_data: Option, + ) -> Box { + if let Some(compressed_data) = compressed_data { + let task_type = + ArgsTaskType::CompressedFinalize(CompressedFinalizeTask { + delegated_account: account.pubkey, + compressed_data, + }); + Box::new(ArgsTask::new(task_type)) + } else { + let task_type = ArgsTaskType::Finalize(FinalizeTask { + delegated_account: account.pubkey, + }); + Box::new(ArgsTask::new(task_type)) + } } // Helper to create an undelegate task fn undelegate_task( account: &CommittedAccount, rent_reimbursement: &Pubkey, + compressed_data: Option, ) -> Box { - let task_type = ArgsTaskType::Undelegate(UndelegateTask { - delegated_account: account.pubkey, - owner_program: account.account.owner, - rent_reimbursement: *rent_reimbursement, - }); - Box::new(ArgsTask::new(task_type)) + if let Some(compressed_data) = compressed_data { + let task_type = ArgsTaskType::CompressedUndelegate( + CompressedUndelegateTask { + delegated_account: account.pubkey, + owner_program: account.account.owner, + compressed_data, + }, + ); + Box::new(ArgsTask::new(task_type)) + } else { + let task_type = ArgsTaskType::Undelegate(UndelegateTask { + delegated_account: account.pubkey, + owner_program: account.account.owner, + rent_reimbursement: *rent_reimbursement, + }); + Box::new(ArgsTask::new(task_type)) + } + } + + // Helper to get compressed data + async fn get_compressed_data_for_accounts( + is_compressed: bool, + committed_accounts: &[CommittedAccount], + photon_client: &Option>, + ) -> TaskBuilderResult>> { + if is_compressed { + let photon_client = photon_client + .as_ref() + .ok_or(TaskBuilderError::PhotonClientNotFound)?; + committed_accounts + .iter() + .map(|account| async { + Ok(Some( + get_compressed_data( + &account.pubkey, + photon_client, + None, + ) + .await?, + )) + }) + .collect::>() + .try_collect() + .await + } else { + Ok(vec![None; committed_accounts.len()]) + } } // Helper to process commit types - fn process_commit(commit: &CommitType) -> Vec> { + async fn process_commit( + commit: &CommitType, + photon_client: &Option>, + is_compressed: bool, + ) -> TaskBuilderResult>> { match commit { - CommitType::Standalone(accounts) => { - accounts.iter().map(finalize_task).collect() + CommitType::Standalone(committed_accounts) => { + Ok(committed_accounts + .iter() + .zip( + get_compressed_data_for_accounts( + is_compressed, + committed_accounts, + photon_client, + ) + .await?, + ) + .map(|(account, compressed_data)| { + finalize_task(account, compressed_data) + }) + .collect()) } CommitType::WithBaseActions { committed_accounts, base_actions, + .. } => { let mut tasks = committed_accounts .iter() - .map(finalize_task) + .zip( + get_compressed_data_for_accounts( + is_compressed, + committed_accounts, + photon_client, + ) + .await?, + ) + .map(|(account, compressed_data)| { + finalize_task(account, compressed_data) + }) .collect::>(); tasks.extend(base_actions.iter().map(|action| { let task = BaseActionTask { @@ -224,16 +396,25 @@ impl TasksBuilder for TaskBuilderImpl { ArgsTask::new(ArgsTaskType::BaseAction(task)); Box::new(task) as Box })); - tasks + Ok(tasks) } } } + let is_compressed = base_intent.is_compressed(); match &base_intent.base_intent { MagicBaseIntent::BaseActions(_) => Ok(vec![]), - MagicBaseIntent::Commit(commit) => Ok(process_commit(commit)), + MagicBaseIntent::Commit(commit) + | MagicBaseIntent::CompressedCommit(commit) => { + Ok(process_commit(commit, photon_client, is_compressed).await?) + } MagicBaseIntent::CommitAndUndelegate(t) => { - let mut tasks = process_commit(&t.commit_action); + let mut tasks = process_commit( + &t.commit_action, + photon_client, + is_compressed, + ) + .await?; // Get rent reimbursments for undelegated accounts let accounts = t.get_committed_accounts(); @@ -255,10 +436,45 @@ impl TasksBuilder for TaskBuilderImpl { tasks.extend(accounts.iter().zip(rent_reimbursements).map( |(account, rent_reimbursement)| { - undelegate_task(account, &rent_reimbursement) + undelegate_task(account, &rent_reimbursement, None) }, )); + match &t.undelegate_action { + UndelegateType::Standalone => Ok(tasks), + UndelegateType::WithBaseActions(actions) => { + tasks.extend(actions.iter().map(|action| { + let task = BaseActionTask { + action: action.clone(), + }; + let task = + ArgsTask::new(ArgsTaskType::BaseAction(task)); + Box::new(task) as Box + })); + + Ok(tasks) + } + } + } + MagicBaseIntent::CompressedCommitAndUndelegate(t) => { + let mut tasks = process_commit( + &t.commit_action, + photon_client, + is_compressed, + ) + .await?; + + // TODO: Compressed undelegate is not supported yet + // This is because the validator would have to pay rent out of pocket. + // This could be solved by using the ephemeral payer to ensure the user can pay the rent. + // https://github.com/magicblock-labs/magicblock-validator/issues/651 + + // tasks.extend(accounts.iter().zip(rent_reimbursements).map( + // |(account, rent_reimbursement)| { + // undelegate_task(account, &rent_reimbursement, None) + // }, + // )); + match &t.undelegate_action { UndelegateType::Standalone => Ok(tasks), UndelegateType::WithBaseActions(actions) => { @@ -285,6 +501,22 @@ pub enum TaskBuilderError { CommitTasksBuildError(#[source] TaskInfoFetcherError), #[error("FinalizedTasksBuildError: {0}")] FinalizedTasksBuildError(#[source] TaskInfoFetcherError), + #[error("CompressedDataFetchError: {0}")] + CompressedDataFetchError(#[source] IndexerError), + #[error("LightSdkError: {0}")] + LightSdkError(#[source] LightSdkError), + #[error("MissingStateTrees")] + MissingStateTrees, + #[error("MissingAddress")] + MissingAddress, + #[error("MissingCompressedData")] + MissingCompressedData, + #[error("Photon client not found")] + PhotonClientNotFound, + #[error("TaskStrategistError: {0}")] + TaskStrategistError(#[from] TaskStrategistError), + #[error("MissingCommitId: {0}")] + MissingCommitId(Pubkey), } impl TaskBuilderError { @@ -292,8 +524,74 @@ impl TaskBuilderError { match self { Self::CommitTasksBuildError(err) => err.signature(), Self::FinalizedTasksBuildError(err) => err.signature(), + Self::CompressedDataFetchError(_) => None, + Self::LightSdkError(_) => None, + Self::MissingStateTrees => None, + Self::MissingAddress => None, + Self::MissingCompressedData => None, + Self::PhotonClientNotFound => None, + Self::TaskStrategistError(_) => None, + Self::MissingCommitId(_) => None, } } } pub type TaskBuilderResult = Result; + +pub(crate) async fn get_compressed_data( + pubkey: &Pubkey, + photon_client: &PhotonIndexer, + photon_config: Option, +) -> Result { + let cda = derive_cda_from_pda(pubkey); + let compressed_delegation_record = photon_client + .get_compressed_account(cda.to_bytes(), photon_config.clone()) + .await + .map_err(TaskBuilderError::CompressedDataFetchError)? + .value; + let proof_result = photon_client + .get_validity_proof( + vec![compressed_delegation_record.hash], + vec![], + photon_config, + ) + .await + .map_err(TaskBuilderError::CompressedDataFetchError)? + .value; + + let system_account_meta_config = + SystemAccountMetaConfig::new(compressed_delegation_client::ID); + let mut remaining_accounts = PackedAccounts::default(); + remaining_accounts + .add_system_accounts_v2(system_account_meta_config) + .map_err(TaskBuilderError::LightSdkError)?; + let packed_tree_accounts = proof_result + .pack_tree_infos(&mut remaining_accounts) + .state_trees + .ok_or(TaskBuilderError::MissingStateTrees)?; + + let tree_info = packed_tree_accounts + .packed_tree_infos + .first() + .copied() + .ok_or(TaskBuilderError::MissingStateTrees)?; + + let account_meta = CompressedAccountMeta { + tree_info, + address: compressed_delegation_record + .address + .ok_or(TaskBuilderError::MissingAddress)?, + output_state_tree_index: packed_tree_accounts.output_tree_index, + }; + + Ok(CompressedData { + hash: compressed_delegation_record.hash, + compressed_delegation_record_bytes: compressed_delegation_record + .data + .ok_or(TaskBuilderError::MissingCompressedData)? + .data, + remaining_accounts: remaining_accounts.to_account_metas().0, + account_meta, + proof: proof_result.proof, + }) +} diff --git a/magicblock-committor-service/src/tasks/task_strategist.rs b/magicblock-committor-service/src/tasks/task_strategist.rs index 0486e88a3..ebee2b8ed 100644 --- a/magicblock-committor-service/src/tasks/task_strategist.rs +++ b/magicblock-committor-service/src/tasks/task_strategist.rs @@ -20,9 +20,31 @@ use crate::{ pub struct TransactionStrategy { pub optimized_tasks: Vec>, pub lookup_tables_keys: Vec, + pub compressed: bool, } impl TransactionStrategy { + pub fn try_new( + optimized_tasks: Vec>, + lookup_tables_keys: Vec, + ) -> Result { + let compressed = optimized_tasks + .iter() + .fold(None, |state, task| match state { + None => Some(Ok(task.is_compressed())), + Some(Ok(state)) if state != task.is_compressed() => { + Some(Err(TaskStrategistError::InconsistentTaskCompression)) + } + Some(Ok(state)) => Some(Ok(state)), + Some(Err(err)) => Some(Err(err)), + }) + .unwrap_or(Ok(false))?; + Ok(Self { + optimized_tasks, + lookup_tables_keys, + compressed, + }) + } /// In case old strategy used ALTs recalculate old value /// NOTE: this can be used when full revaluation is unnecessary, like: /// some tasks were reset, number of tasks didn't increase @@ -80,6 +102,18 @@ impl TaskStrategist { ) -> TaskStrategistResult { const MAX_UNITED_TASKS_LEN: usize = 22; + // Compressed commits and finalize must be executed in two stages + if commit_tasks.iter().any(|t| t.is_compressed()) + || finalize_tasks.iter().any(|t| t.is_compressed()) + { + return Self::build_two_stage( + commit_tasks, + finalize_tasks, + authority, + persister, + ); + } + // We can unite in 1 tx a lot of commits // but then there's a possibility of hitting CPI limit, aka // MaxInstructionTraceLengthExceeded error. @@ -117,6 +151,9 @@ impl TaskStrategist { Err(TaskStrategistError::SignerError(err)) => { return Err(err.into()) } + Err(TaskStrategistError::InconsistentTaskCompression) => { + return Err(TaskStrategistError::InconsistentTaskCompression); + } }; // If ALTs aren't used then we sure this will be optimal - return @@ -187,10 +224,7 @@ impl TaskStrategist { .for_each(|task| task.visit(&mut persistor_visitor)); } - Ok(TransactionStrategy { - optimized_tasks: tasks, - lookup_tables_keys: vec![], - }) + TransactionStrategy::try_new(tasks, vec![]) } // In case task optimization didn't work // attempt using lookup tables for all keys involved in tasks @@ -209,10 +243,7 @@ impl TaskStrategist { // Get lookup table keys let lookup_tables_keys = Self::collect_lookup_table_keys(validator, &tasks); - Ok(TransactionStrategy { - optimized_tasks: tasks, - lookup_tables_keys, - }) + TransactionStrategy::try_new(tasks, lookup_tables_keys) } else { Err(TaskStrategistError::FailedToFitError) } @@ -283,7 +314,7 @@ impl TaskStrategist { /// the limit MAX_ENCODED_TRANSACTION_SIZE. The caller needs to check and make decision accordingly. fn try_optimize_tx_size_if_needed( tasks: &mut [Box], - ) -> Result { + ) -> Result { // Get initial transaction size let calculate_tx_length = |tasks: &[Box]| { match TransactionUtils::assemble_tasks_tx( @@ -294,7 +325,7 @@ impl TaskStrategist { ) { Ok(tx) => Ok(serialize_and_encode_base64(&tx).len()), Err(TaskStrategistError::FailedToFitError) => Ok(usize::MAX), - Err(TaskStrategistError::SignerError(err)) => Err(err), + Err(err) => Err(err), } }; @@ -367,12 +398,14 @@ impl TaskStrategist { } } -#[derive(thiserror::Error, Debug)] +#[derive(thiserror::Error, Debug, PartialEq)] pub enum TaskStrategistError { #[error("Failed to fit in single TX")] FailedToFitError, #[error("SignerError: {0}")] SignerError(#[from] SignerError), + #[error("Inconsistent task compression")] + InconsistentTaskCompression, } pub type TaskStrategistResult = Result; @@ -381,6 +414,7 @@ pub type TaskStrategistResult = Result; mod tests { use std::{collections::HashMap, sync::Arc}; + use light_client::indexer::photon_indexer::PhotonIndexer; use magicblock_program::magic_scheduled_base_intent::{ BaseAction, CommittedAccount, ProgramArgs, }; @@ -398,9 +432,10 @@ mod tests { persist::IntentPersisterImpl, tasks::{ task_builder::{ - TaskBuilderImpl, TasksBuilder, COMMIT_STATE_SIZE_THRESHOLD, + CompressedData, TaskBuilderImpl, TasksBuilder, + COMMIT_STATE_SIZE_THRESHOLD, }, - BaseActionTask, TaskStrategy, UndelegateTask, + BaseActionTask, CompressedCommitTask, TaskStrategy, UndelegateTask, }, }; @@ -412,6 +447,7 @@ mod tests { &self, pubkeys: &[Pubkey], _: u64, + _compressed: bool, ) -> TaskInfoFetcherResult> { Ok(pubkeys.iter().map(|pubkey| (*pubkey, 0)).collect()) } @@ -482,6 +518,29 @@ mod tests { } } + // Helper to create a simple compressed commit task + fn create_test_compressed_commit_task( + commit_id: u64, + data_size: usize, + ) -> ArgsTask { + ArgsTask::new(ArgsTaskType::CompressedCommit(CompressedCommitTask { + commit_id, + allow_undelegation: false, + committed_account: CommittedAccount { + pubkey: Pubkey::new_unique(), + account: Account { + lamports: 1000, + data: vec![1; data_size], + owner: system_program_id(), + executable: false, + rent_epoch: 0, + }, + remote_slot: Default::default(), + }, + compressed_data: CompressedData::default(), + })) + } + // Helper to create a Base action task fn create_test_base_action_task(len: usize) -> ArgsTask { ArgsTask::new(ArgsTaskType::BaseAction(BaseActionTask { @@ -765,6 +824,26 @@ mod tests { assert!(!strategy.lookup_tables_keys.is_empty()); } + #[test] + fn test_mixed_task_types_compressed() { + let validator = Pubkey::new_unique(); + let tasks = vec![ + Box::new(create_test_commit_task(1, 100, 0)) as Box, + Box::new(create_test_compressed_commit_task(2, 100)) + as Box, + ]; + + let Err(err) = TaskStrategist::build_strategy( + tasks, + &validator, + &None::, + ) else { + panic!("Should not build invalid strategy"); + }; + + assert_eq!(err, TaskStrategistError::InconsistentTaskCompression); + } + #[tokio::test] async fn test_build_single_stage_mode() { let pubkey = [Pubkey::new_unique()]; @@ -775,13 +854,17 @@ mod tests { &info_fetcher, &intent, &None::, + &None::>, + ) + .await + .unwrap(); + let finalize_task = TaskBuilderImpl::finalize_tasks( + &info_fetcher, + &intent, + &None::>, ) .await .unwrap(); - let finalize_task = - TaskBuilderImpl::finalize_tasks(&info_fetcher, &intent) - .await - .unwrap(); let execution_mode = TaskStrategist::build_execution_strategy( commit_task, @@ -807,13 +890,17 @@ mod tests { &info_fetcher, &intent, &None::, + &None::>, + ) + .await + .unwrap(); + let finalize_task = TaskBuilderImpl::finalize_tasks( + &info_fetcher, + &intent, + &None::>, ) .await .unwrap(); - let finalize_task = - TaskBuilderImpl::finalize_tasks(&info_fetcher, &intent) - .await - .unwrap(); let execution_mode = TaskStrategist::build_execution_strategy( commit_task, @@ -844,13 +931,17 @@ mod tests { &info_fetcher, &intent, &None::, + &None::>, + ) + .await + .unwrap(); + let finalize_task = TaskBuilderImpl::finalize_tasks( + &info_fetcher, + &intent, + &None::>, ) .await .unwrap(); - let finalize_task = - TaskBuilderImpl::finalize_tasks(&info_fetcher, &intent) - .await - .unwrap(); let execution_mode = TaskStrategist::build_execution_strategy( commit_task, diff --git a/magicblock-committor-service/src/transaction_preparator/delivery_preparator.rs b/magicblock-committor-service/src/transaction_preparator/delivery_preparator.rs index 4b927af36..8d2b8657b 100644 --- a/magicblock-committor-service/src/transaction_preparator/delivery_preparator.rs +++ b/magicblock-committor-service/src/transaction_preparator/delivery_preparator.rs @@ -1,6 +1,7 @@ -use std::{collections::HashSet, ops::ControlFlow, time::Duration}; +use std::{collections::HashSet, ops::ControlFlow, sync::Arc, time::Duration}; use futures_util::future::{join, join_all, try_join_all}; +use light_client::indexer::{photon_indexer::PhotonIndexer, IndexerRpcConfig}; use log::{error, info}; use magicblock_committor_program::{ instruction_chunks::chunk_realloc_ixs, Chunks, @@ -28,10 +29,13 @@ use solana_transaction::versioned::VersionedTransaction; use solana_transaction_error::TransactionError; use crate::{ + intent_executor::CommitSlotFn, persist::{CommitStatus, IntentPersister}, tasks::{ - task_strategist::TransactionStrategy, BaseTask, BaseTaskError, - CleanupTask, PreparationState, PreparationTask, + task_builder::{get_compressed_data, TaskBuilderError}, + task_strategist::TransactionStrategy, + BaseTask, BaseTaskError, BufferPreparationTask, CleanupTask, + PreparationState, PreparationTask, }, utils::persist_status_update, ComputeBudgetConfig, @@ -57,20 +61,31 @@ impl DeliveryPreparator { } /// Prepares buffers and necessary pieces for optimized TX - pub async fn prepare_for_delivery( + pub async fn prepare_for_delivery<'a, P: IntentPersister>( &self, authority: &Keypair, strategy: &mut TransactionStrategy, persister: &Option

, + photon_client: &Option>, + commit_slot_fn: Option>, ) -> DeliveryPreparatorResult> { let preparation_futures = - strategy.optimized_tasks.iter_mut().map(|task| async move { - let _timer = - metrics::observe_committor_intent_task_preparation_time( - task.as_ref(), - ); - self.prepare_task_handling_errors(authority, task, persister) + strategy.optimized_tasks.iter_mut().map(|task| { + let commit_slot_fn_clone = commit_slot_fn.clone(); + async move { + let _timer = + metrics::observe_committor_intent_task_preparation_time( + task.as_ref(), + ); + self.prepare_task_handling_errors( + authority, + task, + persister, + photon_client, + commit_slot_fn_clone, + ) .await + } }); let task_preparations = join_all(preparation_futures); @@ -92,11 +107,13 @@ impl DeliveryPreparator { } /// Prepares necessary parts for TX if needed, otherwise returns immediately - pub async fn prepare_task( + pub async fn prepare_task<'a, P: IntentPersister>( &self, authority: &Keypair, task: &mut dyn BaseTask, persister: &Option

, + photon_client: &Option>, + commit_slot_fn: Option>, ) -> DeliveryPreparatorResult<(), InternalError> { let PreparationState::Required(preparation_task) = task.preparation_state() @@ -104,54 +121,107 @@ impl DeliveryPreparator { return Ok(()); }; - // Persist as failed until rewritten - let update_status = CommitStatus::BufferAndChunkPartiallyInitialized; - persist_status_update( - persister, - &preparation_task.pubkey, - preparation_task.commit_id, - update_status, - ); + match preparation_task { + PreparationTask::Buffer(buffer_info) => { + // Persist as failed until rewritten + let update_status = + CommitStatus::BufferAndChunkPartiallyInitialized; + persist_status_update( + persister, + &buffer_info.pubkey, + buffer_info.commit_id, + update_status, + ); - // Initialize buffer account. Init + reallocs - self.initialize_buffer_account(authority, preparation_task) - .await?; + // Initialize buffer account. Init + reallocs + self.initialize_buffer_account(authority, buffer_info) + .await?; + + // Persist initialization success + let update_status = CommitStatus::BufferAndChunkInitialized; + persist_status_update( + persister, + &buffer_info.pubkey, + buffer_info.commit_id, + update_status, + ); - // Persist initialization success - let update_status = CommitStatus::BufferAndChunkInitialized; - persist_status_update( - persister, - &preparation_task.pubkey, - preparation_task.commit_id, - update_status, - ); + // Writing chunks with some retries + self.write_buffer_with_retries(authority, buffer_info) + .await?; + // Persist that buffer account initiated successfully + let update_status = + CommitStatus::BufferAndChunkFullyInitialized; + persist_status_update( + persister, + &buffer_info.pubkey, + buffer_info.commit_id, + update_status, + ); - // Writing chunks with some retries - self.write_buffer_with_retries(authority, preparation_task) - .await?; - // Persist that buffer account initiated successfully - let update_status = CommitStatus::BufferAndChunkFullyInitialized; - persist_status_update( - persister, - &preparation_task.pubkey, - preparation_task.commit_id, - update_status, - ); + let cleanup_task = buffer_info.cleanup_task(); + task.switch_preparation_state(PreparationState::Cleanup( + cleanup_task, + ))?; + } + PreparationTask::Compressed => { + // Trying to fetch fresh data from the indexer + let commit_slot = if let Some(commit_slot_fn) = commit_slot_fn { + commit_slot_fn().await + } else { + None + }; + let photon_config = commit_slot.map(|slot| IndexerRpcConfig { + slot, + ..Default::default() + }); + + let delegated_account = task + .delegated_account() + .ok_or(InternalError::DelegatedAccountNotFound)?; + let photon_client = photon_client + .as_ref() + .ok_or(InternalError::PhotonClientNotFound)?; + + let compressed_data = get_compressed_data( + &delegated_account, + photon_client, + photon_config, + ) + .await + .map_err(|e| { + error!( + "Failed to get compressed data for delegated_account={} commit_slot={:?}: {:?}", + delegated_account, commit_slot, e + ); + InternalError::TaskBuilderError(e) + })?; + task.set_compressed_data(compressed_data); + } + } - let cleanup_task = preparation_task.cleanup_task(); - task.switch_preparation_state(PreparationState::Cleanup(cleanup_task))?; Ok(()) } /// Runs `prepare_task` and, if the buffer was already initialized, /// performs cleanup and retries once. - pub async fn prepare_task_handling_errors( + pub async fn prepare_task_handling_errors<'a, P: IntentPersister>( &self, authority: &Keypair, task: &mut Box, persister: &Option

, + photon_client: &Option>, + commit_slot_fn: Option>, ) -> Result<(), InternalError> { - let res = self.prepare_task(authority, task.as_mut(), persister).await; + let res = self + .prepare_task( + authority, + task.as_mut(), + persister, + photon_client, + commit_slot_fn.clone(), + ) + .await; match res { Err(InternalError::BufferExecutionError( BufferExecutionError::AccountAlreadyInitializedError( @@ -168,9 +238,10 @@ impl DeliveryPreparator { res => return res, } - // Prepare cleanup task - let PreparationState::Required(preparation_task) = - task.preparation_state().clone() + // Prepare buffer cleanup task + let PreparationState::Required(PreparationTask::Buffer( + preparation_task, + )) = task.preparation_state().clone() else { return Ok(()); }; @@ -180,10 +251,17 @@ impl DeliveryPreparator { self.cleanup(authority, std::slice::from_ref(task), &[]) .await?; task.switch_preparation_state(PreparationState::Required( - preparation_task, + PreparationTask::Buffer(preparation_task), ))?; - self.prepare_task(authority, task.as_mut(), persister).await + self.prepare_task( + authority, + task.as_mut(), + persister, + photon_client, + commit_slot_fn, + ) + .await } /// Initializes buffer account for future writes @@ -191,8 +269,8 @@ impl DeliveryPreparator { async fn initialize_buffer_account( &self, authority: &Keypair, - preparation_task: &PreparationTask, - ) -> DeliveryPreparatorResult<(), BufferExecutionError> { + preparation_task: &BufferPreparationTask, + ) -> DeliveryPreparatorResult<(), InternalError> { let authority_pubkey = authority.pubkey(); let init_instruction = preparation_task.init_instruction(&authority_pubkey); @@ -241,7 +319,7 @@ impl DeliveryPreparator { async fn write_buffer_with_retries( &self, authority: &Keypair, - preparation_task: &PreparationTask, + preparation_task: &BufferPreparationTask, ) -> DeliveryPreparatorResult<(), InternalError> { let authority_pubkey = authority.pubkey(); let write_instructions = @@ -540,6 +618,8 @@ impl From for BufferExecutionError { #[derive(thiserror::Error, Debug)] pub enum InternalError { + #[error("Compressed data not found")] + CompressedDataNotFound, #[error("0 retries was requested")] ZeroRetriesRequestedError, #[error("Chunks PDA does not exist for writing. pda: {0}")] @@ -548,8 +628,18 @@ pub enum InternalError { BorshError(#[from] std::io::Error), #[error("TableManiaError: {0}")] TableManiaError(#[from] TableManiaError), + #[error("TransactionCreationError: {0}")] + TransactionCreationError(#[from] CompileError), + #[error("TransactionSigningError: {0}")] + TransactionSigningError(#[from] SignerError), #[error("MagicBlockRpcClientError: {0}")] MagicBlockRpcClientError(Box), + #[error("Delegated account not found")] + DelegatedAccountNotFound, + #[error("PhotonClientNotFound")] + PhotonClientNotFound, + #[error("TaskBuilderError: {0}")] + TaskBuilderError(#[from] TaskBuilderError), #[error("BufferExecutionError: {0}")] BufferExecutionError(#[from] BufferExecutionError), #[error("BaseTaskError: {0}")] diff --git a/magicblock-committor-service/src/transaction_preparator/error.rs b/magicblock-committor-service/src/transaction_preparator/error.rs index f95c4dbbe..0bbe1400c 100644 --- a/magicblock-committor-service/src/transaction_preparator/error.rs +++ b/magicblock-committor-service/src/transaction_preparator/error.rs @@ -11,6 +11,8 @@ use crate::{ pub enum TransactionPreparatorError { #[error("Failed to fit in single TX")] FailedToFitError, + #[error("Inconsistent tasks compression used in strategy")] + InconsistentTaskCompression, #[error("SignerError: {0}")] SignerError(#[from] SignerError), #[error("DeliveryPreparationError: {0}")] @@ -37,6 +39,9 @@ impl From for TransactionPreparatorError { match value { TaskStrategistError::FailedToFitError => Self::FailedToFitError, TaskStrategistError::SignerError(err) => Self::SignerError(err), + TaskStrategistError::InconsistentTaskCompression => { + Self::InconsistentTaskCompression + } } } } diff --git a/magicblock-committor-service/src/transaction_preparator/mod.rs b/magicblock-committor-service/src/transaction_preparator/mod.rs index 4eef17995..e059f1274 100644 --- a/magicblock-committor-service/src/transaction_preparator/mod.rs +++ b/magicblock-committor-service/src/transaction_preparator/mod.rs @@ -1,4 +1,7 @@ +use std::sync::Arc; + use async_trait::async_trait; +use light_client::indexer::photon_indexer::PhotonIndexer; use magicblock_rpc_client::MagicblockRpcClient; use magicblock_table_mania::TableMania; use solana_keypair::Keypair; @@ -6,6 +9,7 @@ use solana_message::VersionedMessage; use solana_pubkey::Pubkey; use crate::{ + intent_executor::CommitSlotFn, persist::IntentPersister, tasks::{ task_strategist::TransactionStrategy, utils::TransactionUtils, BaseTask, @@ -26,11 +30,13 @@ pub mod error; pub trait TransactionPreparator: Send + Sync + 'static { /// Return [`VersionedMessage`] corresponding to [`TransactionStrategy`] /// Handles all necessary preparation needed for successful [`BaseTask`] execution - async fn prepare_for_strategy( + async fn prepare_for_strategy<'a, P: IntentPersister>( &self, authority: &Keypair, transaction_strategy: &mut TransactionStrategy, intent_persister: &Option

, + photon_client: &Option>, + commit_slot_fn: Option>, ) -> PreparatorResult; /// Cleans up after strategy @@ -71,11 +77,13 @@ impl TransactionPreparatorImpl { #[async_trait] impl TransactionPreparator for TransactionPreparatorImpl { - async fn prepare_for_strategy( + async fn prepare_for_strategy<'a, P: IntentPersister>( &self, authority: &Keypair, tx_strategy: &mut TransactionStrategy, intent_persister: &Option

, + photon_client: &Option>, + commit_slot_fn: Option>, ) -> PreparatorResult { // If message won't fit, there's no reason to prepare anything // Fail early @@ -94,7 +102,13 @@ impl TransactionPreparator for TransactionPreparatorImpl { // Pre tx preparations. Create buffer accs + lookup tables let lookup_tables = self .delivery_preparator - .prepare_for_delivery(authority, tx_strategy, intent_persister) + .prepare_for_delivery( + authority, + tx_strategy, + intent_persister, + photon_client, + commit_slot_fn, + ) .await?; let message = TransactionUtils::assemble_tasks_tx( diff --git a/magicblock-committor-service/src/types.rs b/magicblock-committor-service/src/types.rs index d2076df46..a6b1f8451 100644 --- a/magicblock-committor-service/src/types.rs +++ b/magicblock-committor-service/src/types.rs @@ -25,6 +25,10 @@ impl metrics::LabelValue for ScheduledBaseIntentWrapper { MagicBaseIntent::BaseActions(_) => "actions", MagicBaseIntent::Commit(_) => "commit", MagicBaseIntent::CommitAndUndelegate(_) => "commit_and_undelegate", + MagicBaseIntent::CompressedCommit(_) => "compressed_commit", + MagicBaseIntent::CompressedCommitAndUndelegate(_) => { + "compressed_commit_and_undelegate" + } } } } diff --git a/magicblock-config/README.md b/magicblock-config/README.md index 79b88c99d..53b3b8a86 100644 --- a/magicblock-config/README.md +++ b/magicblock-config/README.md @@ -86,13 +86,14 @@ block-size = "block256" The configuration is split into domain-specific structs available in `src/config/`: - * **`ValidatorConfig`**: Identity keypair, base fees. - * **`LedgerConfig`**: Block production timing, verification settings. - * **`AccountsDbConfig`**: Snapshotting, indexing, and storage size tuning. - * **`ChainOperationConfig`**: On-chain registration details (Country code, FQDN). - * **`ChainLinkConfig`**: Account cloning settings. - * **`CommitStrategy`**: Compute unit pricing for base chain commits. - * **`TaskSchedulerConfig`**: Task scheduling settings. +- **`ValidatorConfig`**: Identity keypair, base fees. +- **`LedgerConfig`**: Block production timing, verification settings. +- **`AccountsDbConfig`**: Snapshotting, indexing, and storage size tuning. +- **`ChainOperationConfig`**: On-chain registration details (Country code, FQDN). +- **`ChainLinkConfig`**: Account cloning settings. +- **`CommitStrategy`**: Compute unit pricing for base chain commits. +- **`TaskSchedulerConfig`**: Task scheduling settings. +- **`CompressionConfig`**: Compressed accounts and interactions settings. ## Testing @@ -101,4 +102,3 @@ This crate includes a comprehensive test suite verifying the precedence logic, o ```bash cargo test -p magicblock-config ``` - diff --git a/magicblock-config/src/config/compression.rs b/magicblock-config/src/config/compression.rs new file mode 100644 index 000000000..7e7490648 --- /dev/null +++ b/magicblock-config/src/config/compression.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +/// Configuration for the compression service. +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case", deny_unknown_fields, default)] +pub struct CompressionConfig { + /// The URL of the Photon indexer. + pub photon_url: Option, + /// The API key for the Photon indexer. + pub api_key: Option, +} diff --git a/magicblock-config/src/config/mod.rs b/magicblock-config/src/config/mod.rs index 1b3d7fba1..81b6bd8b8 100644 --- a/magicblock-config/src/config/mod.rs +++ b/magicblock-config/src/config/mod.rs @@ -2,6 +2,7 @@ pub mod accounts; pub mod aperture; pub mod chain; pub mod cli; +pub mod compression; pub mod ledger; pub mod lifecycle; pub mod metrics; @@ -15,6 +16,7 @@ pub use aperture::ApertureConfig; pub use chain::{ AllowedProgram, ChainLinkConfig, ChainOperationConfig, CommittorConfig, }; +pub use compression::CompressionConfig; pub use ledger::LedgerConfig; pub use lifecycle::LifecycleMode; pub use program::LoadableProgram; diff --git a/magicblock-config/src/lib.rs b/magicblock-config/src/lib.rs index c698b872a..04a8b1f9b 100644 --- a/magicblock-config/src/lib.rs +++ b/magicblock-config/src/lib.rs @@ -22,8 +22,8 @@ pub mod types; use crate::{ config::{ AccountsDbConfig, ChainLinkConfig, ChainOperationConfig, - CommittorConfig, LedgerConfig, LoadableProgram, TaskSchedulerConfig, - ValidatorConfig, + CommittorConfig, CompressionConfig, LedgerConfig, LoadableProgram, + TaskSchedulerConfig, ValidatorConfig, }, types::Remote, }; @@ -61,6 +61,7 @@ pub struct ValidatorParams { pub chainlink: ChainLinkConfig, pub chain_operation: Option, pub task_scheduler: TaskSchedulerConfig, + pub compression: CompressionConfig, pub programs: Vec, } diff --git a/magicblock-config/src/tests.rs b/magicblock-config/src/tests.rs index 63c259a55..04a771dab 100644 --- a/magicblock-config/src/tests.rs +++ b/magicblock-config/src/tests.rs @@ -558,7 +558,7 @@ fn test_env_vars_full_coverage() { assert_eq!(config.chainlink.max_monitored_accounts, 123); assert_eq!( config.chainlink.resubscription_delay, - Duration::from_millis(150) + std::time::Duration::from_millis(150) ); // Task Scheduler diff --git a/magicblock-core/Cargo.toml b/magicblock-core/Cargo.toml index 7ce049e79..4096e81e5 100644 --- a/magicblock-core/Cargo.toml +++ b/magicblock-core/Cargo.toml @@ -8,10 +8,11 @@ license.workspace = true edition.workspace = true [dependencies] - -tokio = { workspace = true, features = ["sync"] } +compressed-delegation-client = { workspace = true } flume = { workspace = true } - +light-sdk = { workspace = true } +light-compressed-account = { workspace = true } +magicblock-magic-program-api = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } solana-hash = { workspace = true } @@ -22,6 +23,6 @@ solana-transaction = { workspace = true, features = ["blake3", "verify"] } solana-transaction-context = { workspace = true } solana-transaction-error = { workspace = true } solana-transaction-status-client-types = { workspace = true } -magicblock-magic-program-api = { workspace = true } spl-token = { workspace = true } spl-token-2022 = { workspace = true } +tokio = { workspace = true, features = ["sync"] } diff --git a/magicblock-core/src/compression/mod.rs b/magicblock-core/src/compression/mod.rs new file mode 100644 index 000000000..8ee734a70 --- /dev/null +++ b/magicblock-core/src/compression/mod.rs @@ -0,0 +1,43 @@ +use light_compressed_account::address::derive_address; +use light_sdk::light_hasher::hash_to_field_size::hashv_to_bn254_field_size_be_const_array; +use solana_pubkey::Pubkey; + +// Light protocol V2 accounts: +// https://www.zkcompression.com/resources/addresses-and-urls#v2-2 +pub const ADDRESS_TREE: Pubkey = + Pubkey::from_str_const("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"); +pub const OUTPUT_QUEUE: Pubkey = + Pubkey::from_str_const("oq1na8gojfdUhsfCpyjNt6h4JaDWtHf1yQj4koBWfto"); + +/// Derives a CDA (Compressed derived Address) from a PDA (Program derived Address) +/// of a compressed account we want to use in our validator in uncompressed form. +pub fn derive_cda_from_pda(pda: &Pubkey) -> Pubkey { + // Since the PDA is already unique we use the delegation program's id + // as a program id. + let seed = + hashv_to_bn254_field_size_be_const_array::<3>(&[&pda.to_bytes()]) + .expect("BN254 hash of PDA must succeed for a 32-byte PDA seed"); + let address = derive_address( + &seed, + &ADDRESS_TREE.to_bytes(), + &compressed_delegation_client::ID.to_bytes(), + ); + Pubkey::new_from_array(address) +} + +#[cfg(test)] +mod tests { + use solana_pubkey::pubkey; + + use super::*; + + #[test] + fn test_derive_cda_from_pda() { + let pda = pubkey!("6pyGAQnqveUcHJ4iT1B6N72iJSBWcb6KRht315Fo7mLX"); + let cda = derive_cda_from_pda(&pda); + assert_eq!( + cda, + pubkey!("13CJjg6sMzZ8Lsn1oyQggzcyq5nFHYt97i7bhMu7BNu9") + ); + } +} diff --git a/magicblock-core/src/lib.rs b/magicblock-core/src/lib.rs index f9bfa285c..b47dd63e6 100644 --- a/magicblock-core/src/lib.rs +++ b/magicblock-core/src/lib.rs @@ -13,6 +13,7 @@ macro_rules! debug_panic { ) } +pub mod compression; pub mod link; pub mod tls; pub mod token_programs; diff --git a/magicblock-magic-program-api/src/instruction.rs b/magicblock-magic-program-api/src/instruction.rs index a4050e4e7..3fb1c6b44 100644 --- a/magicblock-magic-program-api/src/instruction.rs +++ b/magicblock-magic-program-api/src/instruction.rs @@ -106,6 +106,38 @@ pub enum MagicBlockInstruction { /// Noop instruction Noop(u64), + + /// Schedules the compressed accounts provided at end of accounts Vec to be committed. + /// It should be invoked from the program whose PDA accounts are to be + /// committed. + /// + /// This is the first part of scheduling a commit. + /// A second transaction [MagicBlockInstruction::AcceptScheduleCommits] has to run in order + /// to finish scheduling the commit. + /// + /// # Account references + /// - **0.** `[WRITE, SIGNER]` Payer requesting the commit to be scheduled + /// - **1.** `[WRITE]` Magic Context Account containing to which we store + /// the scheduled commits + /// - **2..n** `[]` Accounts to be committed + ScheduleCompressedCommit, + + /// This is the exact same instruction as [MagicBlockInstruction::ScheduleCompressedCommit] except + /// that the [ScheduledCommit] is flagged such that when accounts are committed, a request + /// to undelegate them is included with the same transaction. + /// Additionally the validator will refuse anymore transactions for the specific account + /// since they are no longer considered delegated to it. + /// + /// This is the first part of scheduling a commit. + /// A second transaction [MagicBlockInstruction::AcceptScheduleCommits] has to run in order + /// to finish scheduling the commit. + /// + /// # Account references + /// - **0.** `[WRITE, SIGNER]` Payer requesting the commit to be scheduled + /// - **1.** `[WRITE]` Magic Context Account containing to which we store + /// the scheduled commits + /// - **2..n** `[]` Accounts to be committed and undelegated + ScheduleCompressedCommitAndUndelegate, } impl MagicBlockInstruction { @@ -122,6 +154,7 @@ pub struct AccountModification { pub executable: Option, pub data: Option>, pub delegated: Option, + pub compressed: Option, pub confined: Option, pub remote_slot: Option, } @@ -133,6 +166,7 @@ pub struct AccountModificationForInstruction { pub executable: Option, pub data_key: Option, pub delegated: Option, + pub compressed: Option, pub confined: Option, pub remote_slot: Option, } diff --git a/magicblock-metrics/src/metrics/mod.rs b/magicblock-metrics/src/metrics/mod.rs index f09e5c24c..5a0fe243a 100644 --- a/magicblock-metrics/src/metrics/mod.rs +++ b/magicblock-metrics/src/metrics/mod.rs @@ -45,10 +45,12 @@ lazy_static::lazy_static! { "Number of cloned accounts in the RemoteAccountClonerWorker" ) .unwrap(); +} - // ----------------- - // Ledger - // ----------------- +// ----------------- +// Ledger +// ----------------- +lazy_static::lazy_static! { static ref LEDGER_SIZE_GAUGE: IntGauge = IntGauge::new( "ledger_size_gauge", "Ledger size in Bytes", ).unwrap(); @@ -137,10 +139,12 @@ lazy_static::lazy_static! { vec![0.1, 1.0, 2.0, 3.0, 10.0, 60.0] ), ).unwrap(); +} - // ----------------- - // Accounts - // ----------------- +// ----------------- +// Accounts +// ----------------- +lazy_static::lazy_static! { static ref ACCOUNTS_SIZE_GAUGE: IntGauge = IntGauge::new( "accounts_size_gauge", "Size of persisted accounts (in bytes) currently on disk", ).unwrap(); @@ -181,10 +185,12 @@ lazy_static::lazy_static! { &["client_id"], ) .unwrap(); +} - // ----------------- - // RPC/Aperture - // ----------------- +// ----------------- +// RPC/Aperture +// ----------------- +lazy_static::lazy_static! { pub static ref ENSURE_ACCOUNTS_TIME: HistogramVec = HistogramVec::new( HistogramOpts::new("ensure_accounts_time", "Time spent in ensuring account presence") .buckets( @@ -271,6 +277,39 @@ lazy_static::lazy_static! { ) .unwrap(); + // Account fetch results from Photon (Compressed) + pub static ref COMPRESSED_ACCOUNT_FETCHES_SUCCESS_COUNT: IntCounter = + IntCounter::new( + "compressed_account_fetches_success_count", + "Total number of successful network compressed account fetches", + ) + .unwrap(); + + pub static ref COMPRESSED_ACCOUNT_FETCHES_FAILED_COUNT: IntCounter = + IntCounter::new( + "compressed_account_fetches_failed_count", + "Total number of failed network compressed account fetches \ + (RPC errors)", + ) + .unwrap(); + + pub static ref COMPRESSED_ACCOUNT_FETCHES_FOUND_COUNT: IntCounterVec = IntCounterVec::new( + Opts::new( + "compressed_account_fetches_found_count", + "Total number of network compressed account fetches that found an account", + ), + &["origin"], + ) + .unwrap(); + + pub static ref COMPRESSED_ACCOUNT_FETCHES_NOT_FOUND_COUNT: IntCounterVec = IntCounterVec::new( + Opts::new( + "compressed_account_fetches_not_found_count", + "Total number of network compressed account fetches where account was not found", + ), + &["origin"], + ).unwrap(); + pub static ref PER_PROGRAM_ACCOUNT_FETCH_STATS: IntCounterVec = IntCounterVec::new( Opts::new( "per_program_account_fetch_stats", @@ -300,11 +339,12 @@ lazy_static::lazy_static! { "Total number of undelegating accounts found to be already undelegated on chain", ) .unwrap(); +} - - // ----------------- - // Transaction Execution - // ----------------- +// ----------------- +// Transaction Execution +// ----------------- +lazy_static::lazy_static! { pub static ref TRANSACTION_COUNT: IntCounter = IntCounter::new( "transaction_count", "Total number of executed transactions" ).unwrap(); @@ -316,14 +356,12 @@ lazy_static::lazy_static! { "max_lock_contention_queue_size", "Maximum observed queue size for an account lock contention" ).unwrap(); +} - - - - - // ----------------- - // CommittorService - // ----------------- +// ----------------- +// CommittorService +// ----------------- +lazy_static::lazy_static! { static ref COMMITTOR_INTENTS_COUNT: IntCounter = IntCounter::new( "committor_intents_count", "Total number of scheduled committor intents" ).unwrap(); @@ -365,6 +403,10 @@ lazy_static::lazy_static! { "task_info_fetcher_a_count", "Get mupltiple account count" ).unwrap(); + static ref TASK_INFO_FETCHER_COMPRESSED_COUNT: IntCounter = IntCounter::new( + "task_info_fetcher_compressed_count", "Get multiple compressed delegation records count" + ).unwrap(); + static ref TABLE_MANIA_A_COUNT: IntCounter = IntCounter::new( "table_mania_a_count", "Get mupltiple account count" ).unwrap(); @@ -394,10 +436,12 @@ lazy_static::lazy_static! { vec![1.0, 3.0, 5.0, 10.0, 15.0, 17.0, 20.0] ), ).unwrap(); +} - // ----------------- - // Pubsub Clients - // ----------------- +// ----------------- +// Pubsub Clients +// ----------------- +lazy_static::lazy_static! { static ref CONNECTED_PUBSUB_CLIENTS_GAUGE: IntGauge = IntGauge::new( "connected_pubsub_clients_gauge", "Total number of connected pubsub clients" @@ -474,6 +518,10 @@ pub(crate) fn register() { register!(ACCOUNT_FETCHES_FAILED_COUNT); register!(ACCOUNT_FETCHES_FOUND_COUNT); register!(ACCOUNT_FETCHES_NOT_FOUND_COUNT); + register!(COMPRESSED_ACCOUNT_FETCHES_SUCCESS_COUNT); + register!(COMPRESSED_ACCOUNT_FETCHES_FAILED_COUNT); + register!(COMPRESSED_ACCOUNT_FETCHES_FOUND_COUNT); + register!(COMPRESSED_ACCOUNT_FETCHES_NOT_FOUND_COUNT); register!(PER_PROGRAM_ACCOUNT_FETCH_STATS); register!(UNDELEGATION_REQUESTED_COUNT); register!(UNDELEGATION_COMPLETED_COUNT); @@ -482,6 +530,7 @@ pub(crate) fn register() { register!(MAX_LOCK_CONTENTION_QUEUE_SIZE); register!(REMOTE_ACCOUNT_PROVIDER_A_COUNT); register!(TASK_INFO_FETCHER_A_COUNT); + register!(TASK_INFO_FETCHER_COMPRESSED_COUNT); register!(TABLE_MANIA_A_COUNT); register!(TABLE_MANIA_CLOSED_A_COUNT); register!(CONNECTED_PUBSUB_CLIENTS_GAUGE); @@ -676,6 +725,32 @@ pub fn inc_account_fetches_not_found( .inc_by(count); } +pub fn inc_compressed_account_fetches_success(count: u64) { + COMPRESSED_ACCOUNT_FETCHES_SUCCESS_COUNT.inc_by(count); +} + +pub fn inc_compressed_account_fetches_failed(count: u64) { + COMPRESSED_ACCOUNT_FETCHES_FAILED_COUNT.inc_by(count); +} + +pub fn inc_compressed_account_fetches_found( + fetch_origin: AccountFetchOrigin, + count: u64, +) { + COMPRESSED_ACCOUNT_FETCHES_FOUND_COUNT + .with_label_values(&[fetch_origin.value()]) + .inc_by(count); +} + +pub fn inc_compressed_account_fetches_not_found( + fetch_origin: AccountFetchOrigin, + count: u64, +) { + COMPRESSED_ACCOUNT_FETCHES_NOT_FOUND_COUNT + .with_label_values(&[fetch_origin.value()]) + .inc_by(count); +} + pub fn inc_program_subscription_account_updates_count( client_id: &impl LabelValue, ) { @@ -722,6 +797,10 @@ pub fn inc_task_info_fetcher_a_count() { TASK_INFO_FETCHER_A_COUNT.inc() } +pub fn inc_task_info_fetcher_compressed_count() { + TASK_INFO_FETCHER_COMPRESSED_COUNT.inc() +} + pub fn inc_table_mania_a_count() { TABLE_MANIA_A_COUNT.inc() } diff --git a/magicblock-rpc-client/src/lib.rs b/magicblock-rpc-client/src/lib.rs index a7571c45a..de9fefdcc 100644 --- a/magicblock-rpc-client/src/lib.rs +++ b/magicblock-rpc-client/src/lib.rs @@ -247,6 +247,10 @@ impl MagicblockRpcClient { Self { client } } + pub fn url(&self) -> String { + self.client.url() + } + pub async fn get_latest_blockhash( &self, ) -> MagicBlockRpcClientResult { diff --git a/programs/elfs/guinea.so b/programs/elfs/guinea.so index 62c6cedb4b653ad5f28a2473eb54d35b33c51701..2528e2e6d4877d3229f0834ff4ea12e5746d624d 100755 GIT binary patch delta 40321 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

%T_7PSbn3_`gURrUVb59V* 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 z zbghL>V`DG9ZEjp7PHgBf*sx-fPwCv`0rMfxQx_$^$_Qm?F&RbQnVU3vI!X(y_VHIw zXaT3OjYTkw)o6O`ANySGG-`V3mWteXzHIUG2w@b%GbUE-G%9=P_IZV*gASaZP0Hz| z^XHK3Xw&>`-zMe}3*w*VX?O!my-(Ef^x9gUPOjmp_#r?rqE@kdIkpzWFA|vBaY4LC zU=cj@i0jKJ6AUsW3lFL1bAx=yoF87v*S_`89JsJU*^+jRz|aegFhukvFm@z%5ccCB z2t1xOJjjYAtN4Tx#z;a88VXG{oOP(~+9=@bA7QlweTSD^xH*k8=-CTjh%VFiZKlvY z3sXp*_3#djA7g=p6}(_20Y6KiXr^Ehc&mD zETEU(okad0z4h)Fe7cqPoQ`3T`wE6XdEpe{y^tesJl9QH-aDDcaai~>T^5CN-Egen z<$b7%)hs_LhTl+f!w)y;A1gD%y)nEC7u-)jsvH@o9&`E{fF$mxsLwRGbdvz zYaPyhC^EP+v4zzzZ*3Xx2?(il;i7Rd+=wpY&x{Pex&E1MS#*bpcI%eGk@olWd-iK($ zJy-kcU*%pP?1bwltT-?sY;0h7Mv9kJ=#3YBRF7PIc;j$JPyZQ4os2I5IbzxG5x-1^ z1~EDKEQ4yDBj;l26t2F!#jqq~6??Dc3W4*{bM771_bYH7*pH_-+?!3>D7|;ofDXv< z)OB;U>$_N5OrD~j-v&zqIp7($GCn8# zo)E7YyyFe$nFjw>&a;MFS}HJa2;Jm!r*dgC(z;j+>@f-h5O%|8=!WH0@nI#5!)bKV zv#UlX$1&v1DC6C|*3I$ga)xhSmrlJ^7n5}Qde!)7L5t@!&zd;e|GsP=x20!&CGVGT z?iikid?rc8>lx|XsCLbAKrTJd|5zx`%SFB z*dMv^V{a_qq2=1xcT~|?u3Hpde%$Pc!{_Bp>=TU%XU?H>f`9^ zzgsa|u6S|TVGnmQBzx9~(Ug(S%m#*F>DAC_Siuyr1>jxeX8B6PL_f0V3An_@JeF@4BAxWlP)^2fr*ZTr`s@R7j-1oj5h4jbVkRPWF?<+o}7KOJn`l9fb{9ZKgBKRE_ zrgrtw8SH(9eTRLr6`Nz*5P#;uv1dt}I}Oo?A|f~qZ!g_Hy^xHg=g&wAt!1rS5JkUU z;x@}UWiWkbNg}C%ti+_Cm5P~ufi8yrZ3~0UN~Q|OL|4v`V`m4}nehAz7M;ViQY zPkFhC%%pcdl18p;TJ^}qgnUQ$JbE>mNXIUnLnhPpOM4-zu77O&=uW6(1&GH>zK~^I z$+PfpzBj&`sWd;lrxzE-i^98lYfrbf_9wAEFm$hdqXVb;Vz%UlQL?6omGdTp?r_O( zH@wMWF|dY5OK&nG2ln#i6R!RF@k20!d<^j7>oK@MZ@TX>4{_1iHRH`A$LNNdEOY;{ zrd>4=#H8u$)nd%@w@C&4Wcl6Z>~uPft|b4{bbu}~9gT2?&fP0lk-^kuCBcjS5mqTV zgRZdBNJrBvkmG2>Lw0H}kFK)CA=XE9>>nPBuGMn$=qG=;!+bD}7C!MJ*+89t%#QBV zSZ-*V_{RZ+cf`d7$mHFHn%$dz8}{j$Oefoa{}Kr@Sn#E;#vhA&3mEfrNJ1b zK+Z)hF#@~m!-YJkQoPAwU!+vYw`cN|jO=b)kJ_KHWH)nnqZ?L_bENk(oA$0g(1%QI z`e1!_2zh}zHzhgp`2CHArCcq)iIvVsfUHRO&6uvZ{9J0eLT>bgi9+vvtTE0U>LiMM}>wYy7K=mHb?HG5nE&F(57@p z!Rh=vf@}7%?+DTX;&%iS_u+R0X5l{iebY+wwtY=Y|1`}sQ}^T7Km9vwmE*hR9w-#q zoatzT_Ka`d0qiuQ<)EV)vU^(+98p-sO9_rnB)^p6=z(<3OB1EEI!#(Duz&c{?T)Aj zlV1FCv7-#qPan>NQ>f_9mvhbZy|m9?ra4+gZp${h^e;2brv3EbU&fga?Wb8=?s8NC zapRUu)7VLOZ7DRv*yk5?%qwx`RABffmfi-pukEJ~zLM+67lQKcp$A_{G*|8i)8qy! z&3TjFwKdUP2Ms?GOP}2uXI60uAH(gt$Qw)h`Q45>A*HpN&h$TMZbUs|=}-Pz?EDQ| zvdq?(>6@(&A}V~9p~bIGV@>?<)e+_=`rGz*fq4$^6mtUN#$LsGPtZrGk>(t~u@_N@5ULqP@PXL&d_Upqh(cf`h5 z+w^l9eDfU17lD2|5>5X>Iu{b}0lIWYtl9Sv-LNAu{^$X_NI0ZKwCSEhbT@Eac#wV% zT$_}NdALEzA0D7tI}^>RhiKu>*!Vo9e%K}yvf8@n;+?T(^#QsPn9n#wUxehD19a;@ zW6ktK^dO*32WiB+iRLtr_ik)_=RvztJjh{nVF&3<;97EsE{0^$LAn}}KOdr7-@VIG z9%<5%?U@7j+Ej&S_->{IhzkX}uYF?tXEr*%H^qku46HxEr#bC&&F|o=k@s>P$i3vf z%gv7u(OvK5nm~kaf>>_a`w8X;hv=mD6CDM>H0S-<=HCv{o$u$GhdEOh_*&B6M%c^T z@wbUs=Ec7yI?^M}rVsz-B4*uodhqXA=y8c3eDA0RQAr;p(DQahnT>w<5FyK~c%R<2 zYj=Fk-k>d9WF0n#IbBN^o%>;~nFkK?VU~GIH{AtE#bMf~Bg_1AH_e6Q)x-3rj#P70 zH(d%z-eLM8BoBAf-H@yX&VQtuaoscvlGwv^8YCUiQUAy?3-;2@fcAk*Nd9@49{WdZ z{KD>_9!}%ZfFAc8rdc1w#`oCF>NGA0kj=w%=0~ySWT@xeSo5O8%=c=$>4uLI&8H8e z+nC!xBVgIiY4>&0h<_%U4}(}p!n$c8B&QvwbHQ1rb<+p`nHXPm*scofo4|$cQQFO1 z>iWZU7nJ#TH~k)xv~C*vaiW=gnC3#VuA5GSpstVvM#fW52C zquYFG+E@qv834 zkg)-^svu(yApyN=0dGVs6>E$(oA(lOzyS?v-sL1KosLe(y5ltRC~@=vcW*y3!;v0i zTBWCxdmT}5^XcitWpce7C4eI%01HL(0gsO$u;7QHr4i(E@}%`#1Q}`8ertUhL8dt> zV$A08k+7E)P_Y_|L8c(-1!5ySr^DCjUms4IE?;gbL0SWCkfcI z9hjer15?EHH?3`PBq2VhziQch2v#=BmTBZnK^6@v05KPzNixl&KUi5qz|@scY0Ea<^rr$xWs8|&i!i+UP zTAMMW77Gs{x5FpMR&EA)(0t=ZYikDCig@uk`1$xJl^ZPZjM;0ViY_|%>^14@#Be~X=u_WKo%K0XmK;8N%8Qupu zyJuO8$3Yi?mcaR-!-!?dvC~f-729D+{>Tt^{3$Tt@u${V<4C{e#_{Ay({$}^F1(Oz zCU9dNn?QEOud!)b%#J?eGsaU#t%Dbl)#fT_(L}P?TzAxpm_%N5xS_IblgPb5pb@V@ zcU?@%%#BB_trwG><|be+fHpi0_%af2brz7vnH~)rFW56h@%Sbk}8Qa%q;x5IrToWI?Pdvf*%5313&rxA(AMb ztrD#B9ws@CmLRox<-;VAn1#oz7at*W9c@5KdldW&x@_*Fq!7wCLw_d{2keD3f4Y>c zFdd--OzZAtoCka`*Q~nG`hFS7GSeTlQkO&f_|sT|wQxBpbCdx0hs!y4A_bQd)aHeh z)S8YKAV;nwDFfj~&Zqo6COmqhlhpykl$FrW-}YKNS3)h@k6Fi7lGuTR*(M?{;}p-n zXrRj+rj>1xOvgtA<_C+o92utBe76Ov$Ao?b62*nHcIa1D;uB<2ygfhiV$OgoPM|M- z0_H}AzEaR({~+|M=FLx#K5%4UoM|<$A|vpo`OqqIy2-M7pMq%uv(oCw?PMTZ0*UHz z27dV{Z{x!uruBS1Xo_OC)eAAb^(29?tfaNjob@5F{yfBZn&+&AV9rQOHjq164ZR!4 zY@Rh|69{+>L_W1ih-}6jrr9Rq=rU8_|^zs9?fE!uZ!<*6xIx4__m1IJ&@fpL!iUpdH4}w%19t z(}%BoaB~5j4|nrW0`3#}VJ``<(kpoy$6uguV08O3Z{Q66!91(ql#eHyur;}BTjCkEHwg+Xwgk)Z`+-H(}JCadt*cOEUaSSCS^O1xY z?ZEe$_#VmU&?3JWXt=#48wzR~>|A7Q_H6J&T6SrYPDu2c+ zzpV+<&ti1%Eqw2G4p=;P~0ZUm~tbp&|e>{xEq>=Pt|zI7mjGTe$eX3 zB|JaU;;E;eZz>1e^9GT>hHp~iVb*xDpBX^?#vH(yB~5=fR5Tv&lwt4(E^GnP*_%T+ zE7@s8v-~*mX(|HH&nI75m5RPpTMXmD#rr^hi(6VFJ|fHve3Zi()PkYL6fw|bL8tfy zBPwL#;msSksb}|%F^)mr8+~J$!nnRud_pz|c(9A%r4hCtT0V~@U=laGIZgrLjBo@l zqbIn~9%#Z;mf8mn%V2+cd@hpA?o@f*zA3_K{bjs<5Ofw}l;1yNUt<+X3s|<1;`p%_ zy98T|=^RMikfIl3>Q8|`aNThyPm7&c8hk}~zYpI^z$JfMC;6uMuHb|z7xL66OeBZj zlro;LzqiXA&MT1XQ_zDcEF-}T{*4QZ9LO0dEGUP${0_?}V;0|1=as>#+~;!g!+qd` z?Ox1vIYkObq9K;UPc?0n@m!#5c$0Co z_8C)zKzrb3E_;G}=CUWqXD)k!eCDzz$ah2Cyk2PkMyL&Y5~G9SvV)n1#@$dWc3de& zTey-gD?d~8&WFxBL!WatBmvzw7(?aGlb%@q5$S&U!hn`vqiq_ zD)|pT%yIA;LH_I+E5J$H@5Yj#cn(ixgK^57h<$7C8|>p!Q9+U|T4276f9O=ft9?SG z_1@d$9CHagAH7X7*isY8sCV${9{*f2_c1Mh!gFHf$J!Yv(CvFptn{4B(YQ-tM>)j@ z(K5Wxfii4y!t1+4im006=C?Q~u=ARMVH~7MF+Bfjz}POIn`7y_=*!oFFI(<+NP+eI zb~3~@Yp#3N{Q0vy?mKRB-!^;hoQ37?bB9}xyh8>>ESP^o_KmZ%Z@#_awgp-9o1c4! zn54O5J9%W#h>kA^`*#(%c*EEW7&VpGkrUCaSznL=R>dLGFFbA<=ZxKBy?BVEj&9t+ zaUT5Am;XlIFdhOo93%wV^B;wLJ|Dv;pVO?>-6VE+#|@luK9rXqmG6P9Oi}(G$d|G( zYxN#03(7fjpq+4rQs74j2oPBj!;$Bx4OOYK!WO&4j@l^jg|36`Z-( zT6`EjCn(uV#K!|XzZ|gW6PB?Euzf*U1sDQj;CcWs|4@u)oemh98@PG^3uk9}aq#qQ zo0|t3IglX@P!HZAi8)_cSx4Zp+fIKD=whh@<*b=V9}VTjL|{}y6$3@pqoF()EPkm? zKKdhncV-w%zp}PMd9#-DnXi0p9Xvu3o!h>KL-?)3N60|uTX1_OyNxM>W?*QOSH?bT z<~JZcV;>Qxr^0C*pdE%eynwzG=q98azJU#kf>tzt67ln1=-SlS-;%8G&ei~) zhPW|^mjd?nyvyn7@A0&A8&9v>PuM?(!4dpe1u$q4xMrV%-UF`!rOH@HFFzN`!zei? zAW&!U00XYoy=kHQ!B?#MZqh%a`#UbQ;d`ET?&ImeBSbt}a6I&gwe)-F zl>8&shVQ}3c_0F>^e$^Re2K&U8T>| z=@_W5hC1+q`j_hTn}BZ9Z>vs^ROKH;dczMyJm46{y&%`cq#H)V4_4w28hsPcr2arJ z)ajd5`eL0P4)sR|v?{eYfzRHK*M^aJ`#oqjFIx0PQC zbf*J0&*ND<9FYLGAR%z+xC7#)J3s|j0{$3lSvNWR_KtUW!2&JzECiMTwjvJ$mKMTn zoxe*f|G37_$}?^8y}^c`Nb2yK%Z1KUdD=FMr#pTM4*W4N+a(D#cKl==`w1pVJNwojhXKNBNE$in2xW9no{w8^qWYtk&>2Mv7z5!XY8Jt*D|m~R<^2Kpf1g$E-r6*V_6Y z-8fy)Yhl>|^vQR^+Z|caCTJq>2gCRT@@0s{BHsg+ODDvupp(Hf16SiwYd6Tdv=Q@AZ<42Zh%1I@?6m5s=1!lJZ|ltUhPOXtR)vS#g0mU`3r7N9N!iS-}4K zR=j6Uh@3NfVd?zYa~GB_@GQ7%)_i#XZcb&6;TCO`%ac3}7iqXk!;Knt3;RlWMH;RO zV(yr6!ho`X+s#`kan4{BSBmwZq~~3#(u*`)rr|oVgbkGgqdSlx1c~*ate{21T^fe( zXW1qB^EF(eVfQq_F9&AHwW@%QnJTU+Q*qw)Dy|a`15$ya8&!I_hU*0O$pYOPL+ET( zK%R!nG~A%!Rt<;Vq{?%1>Sj&gZ7MF(aJh!vcOXC5Us@n^o+_a7 zE*0l_RNP*rV)yS=+_O~0>5r+nV;NxVf2ly+a+M)p!$ps)^a>5vXt-6w?HZ1Pkiz^= zDv&Q?jKmdd)e1Y-t2ljwirX}t)1cC8a7hN2lwb3V7=JRO`dO8sO>8_#deuuRy-~xR z8jgBd<*(Fm$IDYxhS0yLI8Vcs8g9^Vhjl@inKZQo*G_Ory<0TgrQx_&RQ>`DS6lUA zX0|VHtIAfc;aUy1X}HL*mao%ri;i0rexK2!W*F(ODhx)xhO0DOtKpp2)biDTRdJny z!B-oV4ET&e!;Nof0^U?{tA;x@94elQWyclSG1PCA+cN-HYPd_o`E9BK6&h~Pu<@44 zpJT&Vzfoh$K-{k3g10d@=z5hJZqsm&hST3s%a;Z5O^i|#%wV`t!(AGV+pY@C({QL(eT*KWOc3a7(n@O`8#a@T(#f}eEG2I%@=}_tA8m`rFn>8=e z^bK==q!ugCaHWPDtf^7v_~@vAs>O1wddMcV)@P9TLO)iiZVl&YxJ1L18m`fBqlVj6 z>@&L53|LTW3TZfB!(|%o&~WH(O`d|m$IF!r_?fzfyEUBtsVcBi!;Knl({Ps^`#@G` zrz)^Q!wnj4)o_o7qdqg(CEc+^!<9CS^&2(+QVX=du>=?bJ?vTpR@~w)izf*CehTAmU zq2ZQ)tL0mN3S#E19kvW4@n2I1XW%HYl@Q9y!B4OWyOX#^!|fW5A}W7L5cAQ}5X|6$ z;h%8FOIG04aDj%)G+eFWIt|z2&M%WclBGshAcJEgLNaiiuHk&^g)?CARA|}N8gA5Z zw}wM;hZru|&KwOFXt+YfKBHRAFe>}20;A4Uab3KMje#nz(r~SY8x;(l*QR8^a#q8} zS!xB<8t&9^TcXO}so~Hh8y%g;ZO;H)py4tNcWKyg+XO=S#=$C1PgQZg4KwF0w`D+i zqgumt8g9{WyN0_o9G9jlkQ2mw@U&&944oQ|I!C3aYq&teWg0dz)$#?|67%|vj$twb z+YcWaX*m5{m0qsl{PR?LJO5YO1EZo#!%-sze*v%0NS7G`7iqX$!yOtfv!9<-0W;Wi5kheAVZxY#kbIAEVL>#;UkX!|CHRx`spZ6}rzTS2K({ z4R>ic?gB-CQKaD(4YzB!e1g3^I8E6__6);l)o{0ldnT&<`IA&!rQtRW8yDO7F9Ij2 zux9{nEU+;E?$L1dB`Uqmnl%u5vShNFU7_Iy4R>oe{Zh4jorb$K?7mFl_Zj(WhS8?s z9u239cRzBz>7JqpH0s1VAxW>iLZ#Oz7nxLw2L zcdPUU4R==t=&XLD;GRGR)^C()*!aClZ+$?;ZA(>LUaR6-4Yvd^uips$gIXX@!zE9s z^fnDg{ZXZtX}Ic1l^*w$z@mPmOlGk5!@qy7;WiETXt=UYt)NxIJsNIY%lQLu9y&yZ z95&q=u2`p5P^aM*4YzC9Sg)3k<2d-{p>dW>)AcDo=riYqqNNIKPTpT2dulXw(?s z1Smy;P7TY`n*uEohcQW?#q1j1Q73hvP`oY|w$ z^EnPqUS%3Xm4@N)A4NcmhFdk+p;5!F8gAFH5vjJ&tzkIGMX^9WVCH{PfijJuO2f4pZqP6s5u#MkuHg<1 zcf!jjr2rgMqTn74!=WPzJzv9c>WD&z!$%YhhmR<@Cd#K~z?mWn0~{)%U^rAn!Ely{ zg5{Ya(xPxWh(d?6NEBS;(=z0#BT@jICZZI8(?k>uhlwZ{4gpaxoGhYXIA27;J~&=P z$$)c56b#3UC>V|(Q81h?qF^{%M8R;jh=Sp05j*x7aD0e8!+@hk6buKBC>Tx|QLsF0 zL|P1v4^imyv=K>%(?o1^@IN?A#Fhb{m}(e~5>e=Ij);QcG!X^Ep&|-~qeT=92a5zT z`ac{w63l=*IA=t`aLkB;;gk^t!(k!{hOWO7jrcAPaR~h1xCZ{fC2^%q0i;)Hxco`R&iI{hTd*mJ`@pUHnIP^9x8=_p#wg6f z3eXCT;z>o~Y8ws%dZ!JCTc2c^&zO~;TMuQMS>d%`@Q0q-FRa(H&D_)ELk^^tBgY(Z zT8pqIq}Bw;h>@QU%QTc&59OFy(ekrlnLcdIIz$FqU*wo)1inO;UnJL9VZ+RkVKoK3 L=L#4rn#TD5bvg`j delta 32301 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�L&iyc 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 bool { // Currently we only store a vec of scheduled commits in the MagicContext - // The first 8 bytes contain the length of the vec + // The first bytes 8..16 contain the length of the vec // This works even if the length is actually stored as a u32 // since we zero out the entire context whenever we update the vec !is_zeroed(&data[8..16]) diff --git a/programs/magicblock/src/magic_scheduled_base_intent.rs b/programs/magicblock/src/magic_scheduled_base_intent.rs index 4f2b8cc18..d5b9003e8 100644 --- a/programs/magicblock/src/magic_scheduled_base_intent.rs +++ b/programs/magicblock/src/magic_scheduled_base_intent.rs @@ -110,6 +110,14 @@ impl ScheduledBaseIntent { pub fn is_empty(&self) -> bool { self.base_intent.is_empty() } + + pub fn is_compressed(&self) -> bool { + matches!( + &self.base_intent, + MagicBaseIntent::CompressedCommit(_) + | MagicBaseIntent::CompressedCommitAndUndelegate(_) + ) + } } // BaseIntent user wants to send to base layer @@ -119,6 +127,8 @@ pub enum MagicBaseIntent { BaseActions(Vec), Commit(CommitType), CommitAndUndelegate(CommitAndUndelegate), + CompressedCommit(CommitType), + CompressedCommitAndUndelegate(CommitAndUndelegate), } impl MagicBaseIntent { @@ -151,6 +161,8 @@ impl MagicBaseIntent { MagicBaseIntent::BaseActions(_) => false, MagicBaseIntent::Commit(_) => false, MagicBaseIntent::CommitAndUndelegate(_) => true, + MagicBaseIntent::CompressedCommit(_) => false, + MagicBaseIntent::CompressedCommitAndUndelegate(_) => true, } } @@ -161,6 +173,12 @@ impl MagicBaseIntent { MagicBaseIntent::CommitAndUndelegate(t) => { Some(t.get_committed_accounts()) } + MagicBaseIntent::CompressedCommit(t) => { + Some(t.get_committed_accounts()) + } + MagicBaseIntent::CompressedCommitAndUndelegate(t) => { + Some(t.get_committed_accounts()) + } } } @@ -173,6 +191,12 @@ impl MagicBaseIntent { MagicBaseIntent::CommitAndUndelegate(t) => { Some(t.get_committed_accounts_mut()) } + MagicBaseIntent::CompressedCommit(t) => { + Some(t.get_committed_accounts_mut()) + } + MagicBaseIntent::CompressedCommitAndUndelegate(t) => { + Some(t.get_committed_accounts_mut()) + } } } @@ -187,6 +211,8 @@ impl MagicBaseIntent { MagicBaseIntent::BaseActions(actions) => actions.is_empty(), MagicBaseIntent::Commit(t) => t.is_empty(), MagicBaseIntent::CommitAndUndelegate(t) => t.is_empty(), + MagicBaseIntent::CompressedCommit(t) => t.is_empty(), + MagicBaseIntent::CompressedCommitAndUndelegate(t) => t.is_empty(), } } } diff --git a/programs/magicblock/src/magicblock_processor.rs b/programs/magicblock/src/magicblock_processor.rs index a555da4dd..2c5543fac 100644 --- a/programs/magicblock/src/magicblock_processor.rs +++ b/programs/magicblock/src/magicblock_processor.rs @@ -1,5 +1,11 @@ +use std::collections::HashSet; + use magicblock_magic_program_api::instruction::MagicBlockInstruction; -use solana_program_runtime::declare_process_instruction; +use solana_instruction::error::InstructionError; +use solana_program_runtime::{ + declare_process_instruction, invoke_context::InvokeContext, +}; +use solana_pubkey::Pubkey; use crate::{ mutate_accounts::process_mutate_accounts, @@ -7,13 +13,39 @@ use crate::{ schedule_task::{process_cancel_task, process_schedule_task}, schedule_transactions::{ process_accept_scheduled_commits, process_schedule_base_intent, - process_schedule_commit, ProcessScheduleCommitOptions, + process_schedule_commit, process_schedule_compressed_commit, + ProcessScheduleCommitOptions, }, toggle_executable_check::process_toggle_executable_check, }; pub const DEFAULT_COMPUTE_UNITS: u64 = 150; +pub enum CommitKind { + Commit, + CommitAndUndelegate, + CompressedCommit, + CompressedCommitAndUndelegate, +} + +impl CommitKind { + pub fn request_undelegation(&self) -> bool { + matches!( + self, + CommitKind::CommitAndUndelegate + | CommitKind::CompressedCommitAndUndelegate + ) + } + + pub fn compressed(&self) -> bool { + matches!( + self, + CommitKind::CompressedCommit + | CommitKind::CompressedCommitAndUndelegate + ) + } +} + declare_process_instruction!( Entrypoint, DEFAULT_COMPUTE_UNITS, @@ -45,19 +77,23 @@ declare_process_instruction!( &mut accounts, message, ), - ScheduleCommit => process_schedule_commit( + ScheduleCommit => { + dispatch_commit(signers, invoke_context, CommitKind::Commit) + } + ScheduleCompressedCommit => dispatch_commit( signers, invoke_context, - ProcessScheduleCommitOptions { - request_undelegation: false, - }, + CommitKind::CompressedCommit, ), - ScheduleCommitAndUndelegate => process_schedule_commit( + ScheduleCommitAndUndelegate => dispatch_commit( signers, invoke_context, - ProcessScheduleCommitOptions { - request_undelegation: true, - }, + CommitKind::CommitAndUndelegate, + ), + ScheduleCompressedCommitAndUndelegate => dispatch_commit( + signers, + invoke_context, + CommitKind::CompressedCommitAndUndelegate, ), AcceptScheduleCommits => { process_accept_scheduled_commits(signers, invoke_context) @@ -87,3 +123,18 @@ declare_process_instruction!( } } ); + +fn dispatch_commit( + signers: HashSet, + invoke_context: &mut InvokeContext, + commit_kind: CommitKind, +) -> Result<(), InstructionError> { + let opts = ProcessScheduleCommitOptions { + request_undelegation: commit_kind.request_undelegation(), + }; + if commit_kind.compressed() { + process_schedule_compressed_commit(signers, invoke_context, opts) + } else { + process_schedule_commit(signers, invoke_context, opts) + } +} diff --git a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs index 8deb32296..b7e42797e 100644 --- a/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs +++ b/programs/magicblock/src/mutate_accounts/process_mutate_accounts.rs @@ -212,6 +212,14 @@ pub(crate) fn process_mutate_accounts( ); account.borrow_mut().set_delegated(delegated); } + if let Some(compressed) = modification.compressed { + ic_msg!( + invoke_context, + "MutateAccounts: setting compressed to {}", + compressed + ); + account.borrow_mut().set_compressed(compressed); + } if let Some(confined) = modification.confined { ic_msg!( invoke_context, @@ -329,6 +337,7 @@ mod tests { executable: Some(true), data: Some(vec![1, 2, 3, 4, 5]), delegated: Some(true), + compressed: Some(true), confined: Some(true), remote_slot: None, }; @@ -376,6 +385,7 @@ mod tests { let modified_account: AccountSharedData = accounts.drain(0..1).next().unwrap(); assert!(modified_account.delegated()); + assert!(modified_account.compressed()); assert!(modified_account.confined()); assert_matches!( modified_account.into(), diff --git a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs index fd26282c9..5bf7b09c9 100644 --- a/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs +++ b/programs/magicblock/src/schedule_transactions/process_schedule_commit.rs @@ -33,6 +33,23 @@ pub(crate) fn process_schedule_commit( signers: HashSet, invoke_context: &mut InvokeContext, opts: ProcessScheduleCommitOptions, +) -> Result<(), InstructionError> { + schedule_commit(signers, invoke_context, opts, false) +} + +pub(crate) fn process_schedule_compressed_commit( + signers: HashSet, + invoke_context: &mut InvokeContext, + opts: ProcessScheduleCommitOptions, +) -> Result<(), InstructionError> { + schedule_commit(signers, invoke_context, opts, true) +} + +fn schedule_commit( + signers: HashSet, + invoke_context: &mut InvokeContext, + opts: ProcessScheduleCommitOptions, + compressed: bool, ) -> Result<(), InstructionError> { const PAYER_IDX: u16 = 0; const MAGIC_CONTEXT_IDX: u16 = PAYER_IDX + 1; @@ -251,13 +268,25 @@ pub(crate) fn process_schedule_commit( InstructionUtils::scheduled_commit_sent(intent_id, blockhash); let commit_sent_sig = action_sent_transaction.signatures[0]; - let base_intent = if opts.request_undelegation { - MagicBaseIntent::CommitAndUndelegate(CommitAndUndelegate { - commit_action: CommitType::Standalone(committed_accounts), - undelegate_action: UndelegateType::Standalone, - }) - } else { - MagicBaseIntent::Commit(CommitType::Standalone(committed_accounts)) + let base_intent = match (opts.request_undelegation, compressed) { + (true, true) => MagicBaseIntent::CompressedCommitAndUndelegate( + CommitAndUndelegate { + commit_action: CommitType::Standalone(committed_accounts), + undelegate_action: UndelegateType::Standalone, + }, + ), + (true, false) => { + MagicBaseIntent::CommitAndUndelegate(CommitAndUndelegate { + commit_action: CommitType::Standalone(committed_accounts), + undelegate_action: UndelegateType::Standalone, + }) + } + (false, true) => MagicBaseIntent::CompressedCommit( + CommitType::Standalone(committed_accounts), + ), + (false, false) => { + MagicBaseIntent::Commit(CommitType::Standalone(committed_accounts)) + } }; let scheduled_base_intent = ScheduledBaseIntent { id: intent_id, diff --git a/programs/magicblock/src/utils/instruction_utils.rs b/programs/magicblock/src/utils/instruction_utils.rs index eb42ab0ba..fb9727b8e 100644 --- a/programs/magicblock/src/utils/instruction_utils.rs +++ b/programs/magicblock/src/utils/instruction_utils.rs @@ -43,17 +43,38 @@ impl InstructionUtils { payer: &Pubkey, pdas: Vec, ) -> Instruction { - let mut account_metas = vec![ - AccountMeta::new(*payer, true), - AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), - ]; - for pubkey in &pdas { - account_metas.push(AccountMeta::new_readonly(*pubkey, true)); - } - Instruction::new_with_bincode( - crate::id(), - &MagicBlockInstruction::ScheduleCommit, - account_metas, + schedule_commit_instruction_helper( + payer, + pdas, + MagicBlockInstruction::ScheduleCommit, + ) + } + + // ----------------- + // Schedule Compressed Commit + // ----------------- + #[cfg(test)] + pub fn schedule_compressed_commit( + payer: &Keypair, + pubkeys: Vec, + recent_blockhash: Hash, + ) -> Transaction { + let ix = Self::schedule_compressed_commit_instruction( + &payer.pubkey(), + pubkeys, + ); + Self::into_transaction(payer, ix, recent_blockhash) + } + + #[cfg(test)] + pub(crate) fn schedule_compressed_commit_instruction( + payer: &Pubkey, + pdas: Vec, + ) -> Instruction { + schedule_commit_instruction_helper( + payer, + pdas, + MagicBlockInstruction::ScheduleCompressedCommit, ) } @@ -78,17 +99,37 @@ impl InstructionUtils { payer: &Pubkey, pdas: Vec, ) -> Instruction { - let mut account_metas = vec![ - AccountMeta::new(*payer, true), - AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), - ]; - for pubkey in &pdas { - account_metas.push(AccountMeta::new(*pubkey, true)); - } - Instruction::new_with_bincode( - crate::id(), - &MagicBlockInstruction::ScheduleCommitAndUndelegate, - account_metas, + schedule_commit_instruction_helper( + payer, + pdas, + MagicBlockInstruction::ScheduleCommitAndUndelegate, + ) + } + // ----------------- + // Schedule Compressed Commit and Undelegate + // ----------------- + #[cfg(test)] + pub fn schedule_compressed_commit_and_undelegate( + payer: &Keypair, + pubkeys: Vec, + recent_blockhash: Hash, + ) -> Transaction { + let ix = Self::schedule_compressed_commit_and_undelegate_instruction( + &payer.pubkey(), + pubkeys, + ); + Self::into_transaction(payer, ix, recent_blockhash) + } + + #[cfg(test)] + pub(crate) fn schedule_compressed_commit_and_undelegate_instruction( + payer: &Pubkey, + pdas: Vec, + ) -> Instruction { + schedule_commit_instruction_helper( + payer, + pdas, + MagicBlockInstruction::ScheduleCompressedCommitAndUndelegate, ) } @@ -182,6 +223,7 @@ impl InstructionUtils { .data .map(set_account_mod_data), delegated: account_modification.delegated, + compressed: account_modification.compressed, confined: account_modification.confined, remote_slot: account_modification.remote_slot, }; @@ -311,3 +353,29 @@ impl InstructionUtils { ) } } + +/// Schedule commit instructions use exactly the same accounts +#[cfg(test)] +fn schedule_commit_instruction_helper( + payer: &Pubkey, + pdas: Vec, + instruction: MagicBlockInstruction, +) -> Instruction { + let mut account_metas = vec![ + AccountMeta::new(*payer, true), + AccountMeta::new(MAGIC_CONTEXT_PUBKEY, false), + ]; + let is_undelegation = matches!( + instruction, + MagicBlockInstruction::ScheduleCommitAndUndelegate + | MagicBlockInstruction::ScheduleCompressedCommitAndUndelegate + ); + for pubkey in &pdas { + if is_undelegation { + account_metas.push(AccountMeta::new(*pubkey, false)); + } else { + account_metas.push(AccountMeta::new_readonly(*pubkey, true)); + } + } + Instruction::new_with_bincode(crate::id(), &instruction, account_metas) +} diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 490f94946..2eb06a629 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -86,7 +86,7 @@ dependencies = [ "solana-hash", "solana-message", "solana-packet", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-short-vec", "solana-signature", @@ -126,6 +126,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-sized" +version = "1.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.112", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -223,14 +233,17 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] [[package]] name = "ark-bn254" @@ -238,9 +251,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" dependencies = [ - "ark-ec", - "ark-ff", - "ark-std", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-std 0.4.0", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-std 0.5.0", ] [[package]] @@ -249,10 +273,10 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" dependencies = [ - "ark-ff", - "ark-poly", - "ark-serialize", - "ark-std", + "ark-ff 0.4.2", + "ark-poly 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "derivative", "hashbrown 0.13.2", "itertools 0.10.5", @@ -260,16 +284,37 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash 0.8.12", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe 0.6.0", + "fnv", + "hashbrown 0.15.2", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "zeroize", +] + [[package]] name = "ark-ff" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" dependencies = [ - "ark-ff-asm", - "ark-ff-macros", - "ark-serialize", - "ark-std", + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "derivative", "digest 0.10.7", "itertools 0.10.5", @@ -280,6 +325,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe 0.6.0", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-traits", + "paste", + "zeroize", +] + [[package]] name = "ark-ff-asm" version = "0.4.2" @@ -290,6 +355,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.112", +] + [[package]] name = "ark-ff-macros" version = "0.4.2" @@ -303,27 +378,68 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.112", +] + [[package]] name = "ark-poly" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" dependencies = [ - "ark-ff", - "ark-serialize", - "ark-std", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", "derivative", "hashbrown 0.13.2", ] +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash 0.8.12", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe 0.6.0", + "fnv", + "hashbrown 0.15.2", +] + [[package]] name = "ark-serialize" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ - "ark-serialize-derive", - "ark-std", + "ark-serialize-derive 0.4.2", + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint 0.4.6", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive 0.5.0", + "ark-std 0.5.0", + "arrayvec", "digest 0.10.7", "num-bigint 0.4.6", ] @@ -339,6 +455,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.112", +] + [[package]] name = "ark-std" version = "0.4.0" @@ -349,6 +476,17 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", + "rayon", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -438,9 +576,9 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener 5.4.1", "event-listener-strategy", @@ -466,7 +604,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -477,7 +615,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -639,7 +777,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -657,7 +795,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -780,7 +918,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -875,22 +1013,22 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.8.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -936,9 +1074,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -981,7 +1119,7 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -1059,7 +1197,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -1113,6 +1251,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "compressed-delegation-client" +version = "0.6.1" +dependencies = [ + "borsh 0.10.4", + "light-compressed-account", + "light-hasher", + "light-sdk", + "light-sdk-types", + "solana-account-info", + "solana-cpi", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", +] + [[package]] name = "compression-codecs" version = "0.4.35" @@ -1352,7 +1506,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -1376,7 +1530,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -1387,7 +1541,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -1453,24 +1607,24 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 2.0.111", + "syn 2.0.112", "unicode-xid", ] @@ -1529,7 +1683,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -1552,7 +1706,7 @@ checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -1614,12 +1768,24 @@ version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" dependencies = [ - "enum-ordinalize", + "enum-ordinalize 3.1.15", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize 4.3.2", + "proc-macro2", + "quote", + "syn 2.0.112", +] + [[package]] name = "either" version = "1.15.0" @@ -1658,7 +1824,7 @@ checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -1671,7 +1837,27 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.112", ] [[package]] @@ -1727,11 +1913,11 @@ dependencies = [ "solana-account", "solana-account-info", "solana-cpi", - "solana-instruction", - "solana-program-error", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", "solana-program-memory", - "solana-pubkey", - "solana-system-interface", + "solana-pubkey 2.2.1", + "solana-system-interface 1.0.0", "solana-sysvar", ] @@ -1885,7 +2071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.59.0", ] @@ -1929,9 +2115,18 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "five8" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "23f76610e969fa1784327ded240f1e28a3fd9520c9cec93b636fcf62dd37f772" +dependencies = [ + "five8_core 1.0.0", +] [[package]] name = "five8_const" @@ -1939,7 +2134,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" dependencies = [ - "five8_core", + "five8_core 0.1.2", +] + +[[package]] +name = "five8_const" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0f1728185f277989ca573a402716ae0beaaea3f76a8ff87ef9dd8fb19436c5" +dependencies = [ + "five8_core 1.0.0", ] [[package]] @@ -1948,12 +2152,24 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" +[[package]] +name = "five8_core" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" + [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.5" @@ -2111,7 +2327,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -2233,7 +2449,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -2262,12 +2478,27 @@ dependencies = [ "spinning_top", ] +[[package]] +name = "groth16-solana" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6d1ffb18dbf5cfc60b11bd7da88474c672870247c1e5b498619bcb6ba3d8f5" +dependencies = [ + "ark-bn254 0.5.0", + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "num-bigint 0.4.6", + "solana-bn254", + "thiserror 1.0.69", +] + [[package]] name = "guinea" -version = "0.6.0" +version = "0.6.1" dependencies = [ "bincode", - "magicblock-magic-program-api 0.6.0", + "magicblock-magic-program-api 0.6.1", "serde", "solana-program", ] @@ -2400,7 +2631,7 @@ dependencies = [ "prost 0.12.6", "prost-types 0.12.6", "rand 0.8.5", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "sha2 0.10.9", @@ -2627,6 +2858,22 @@ dependencies = [ "tokio-rustls 0.24.1", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.35", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -2653,12 +2900,29 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -2666,12 +2930,16 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "hyper 1.8.1", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.6.1", + "system-configuration 0.6.1", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2903,7 +3171,7 @@ name = "integration-test-tools" version = "0.0.0" dependencies = [ "anyhow", - "borsh 1.6.0", + "borsh 0.10.4", "color-backtrace", "log", "magicblock-config", @@ -2912,7 +3180,8 @@ dependencies = [ "random-port", "rayon", "serde", - "solana-pubkey", + "shlex", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-rpc-client-api", "solana-sdk", @@ -2929,6 +3198,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2963,17 +3242,35 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" dependencies = [ "jiff-static", "log", @@ -2984,13 +3281,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -3095,13 +3392,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.10.0", "libc", - "redox_syscall 0.6.0", + "redox_syscall 0.7.0", ] [[package]] @@ -3189,18 +3486,384 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "light-account-checks" +version = "0.3.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "solana-account-info", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "solana-sysvar", + "thiserror 2.0.17", +] + +[[package]] +name = "light-batched-merkle-tree" +version = "0.3.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "aligned-sized", + "borsh 0.10.4", + "light-account-checks", + "light-bloom-filter", + "light-compressed-account", + "light-hasher", + "light-macros", + "light-merkle-tree-metadata", + "light-verifier", + "light-zero-copy", + "solana-account-info", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "solana-sysvar", + "thiserror 2.0.17", + "zerocopy", +] + +[[package]] +name = "light-bloom-filter" +version = "0.3.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "bitvec", + "num-bigint 0.4.6", + "solana-nostd-keccak", + "solana-program-error 2.2.2", + "thiserror 2.0.17", +] + +[[package]] +name = "light-bounded-vec" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cfa375d028164719e3ffef93d2e5c27855cc8a5bb5bf257b868d17c12a3e66" +dependencies = [ + "bytemuck", + "memoffset", + "solana-program-error 2.2.2", + "thiserror 1.0.69", +] + +[[package]] +name = "light-client" +version = "0.13.1" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "async-trait", + "base64 0.13.1", + "borsh 0.10.4", + "bs58", + "bytemuck", + "lazy_static", + "light-compressed-account", + "light-concurrent-merkle-tree", + "light-hasher", + "light-indexed-merkle-tree", + "light-merkle-tree-metadata", + "light-prover-client", + "light-sdk", + "num-bigint 0.4.6", + "num-traits", + "photon-api", + "rand 0.8.5", + "solana-account", + "solana-account-decoder-client-types", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-commitment-config", + "solana-compute-budget-interface", + "solana-epoch-info", + "solana-hash", + "solana-instruction 2.2.1", + "solana-keypair", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-signature", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "light-compressed-account" +version = "0.3.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "borsh 0.10.4", + "bytemuck", + "light-hasher", + "light-macros", + "light-program-profiler", + "light-zero-copy", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "thiserror 2.0.17", + "zerocopy", +] + +[[package]] +name = "light-concurrent-merkle-tree" +version = "2.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "borsh 0.10.4", + "light-bounded-vec", + "light-hasher", + "memoffset", + "solana-program-error 2.2.2", + "thiserror 2.0.17", +] + +[[package]] +name = "light-hasher" +version = "3.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "ark-bn254 0.5.0", + "ark-ff 0.5.0", + "arrayvec", + "borsh 0.10.4", + "light-poseidon 0.3.0", + "num-bigint 0.4.6", + "sha2 0.10.9", + "sha3", + "solana-nostd-keccak", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "thiserror 2.0.17", +] + +[[package]] +name = "light-indexed-array" +version = "0.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "light-hasher", + "num-bigint 0.4.6", + "num-traits", + "thiserror 2.0.17", +] + +[[package]] +name = "light-indexed-merkle-tree" +version = "2.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "light-bounded-vec", + "light-concurrent-merkle-tree", + "light-hasher", + "light-merkle-tree-reference", + "num-bigint 0.4.6", + "num-traits", + "solana-program-error 2.2.2", + "thiserror 2.0.17", +] + +[[package]] +name = "light-macros" +version = "2.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.112", +] + +[[package]] +name = "light-merkle-tree-metadata" +version = "0.3.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "borsh 0.10.4", + "bytemuck", + "light-compressed-account", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-sysvar", + "thiserror 2.0.17", + "zerocopy", +] + +[[package]] +name = "light-merkle-tree-reference" +version = "2.0.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "light-hasher", + "light-indexed-array", + "num-bigint 0.4.6", + "num-traits", + "thiserror 2.0.17", +] + [[package]] name = "light-poseidon" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee" dependencies = [ - "ark-bn254", - "ark-ff", + "ark-bn254 0.4.0", + "ark-ff 0.4.2", + "num-bigint 0.4.6", + "thiserror 1.0.69", +] + +[[package]] +name = "light-poseidon" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3d87542063daaccbfecd78b60f988079b6ec4e089249658b9455075c78d42" +dependencies = [ + "ark-bn254 0.5.0", + "ark-ff 0.5.0", "num-bigint 0.4.6", "thiserror 1.0.69", ] +[[package]] +name = "light-profiler-macro" +version = "0.1.0" +source = "git+https://github.com/Lightprotocol/light-program-profiler?rev=36a75e14f54dd862bf2f338c97435ffc7e3e8de9#36a75e14f54dd862bf2f338c97435ffc7e3e8de9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.112", +] + +[[package]] +name = "light-program-profiler" +version = "0.1.0" +source = "git+https://github.com/Lightprotocol/light-program-profiler?rev=36a75e14f54dd862bf2f338c97435ffc7e3e8de9#36a75e14f54dd862bf2f338c97435ffc7e3e8de9" +dependencies = [ + "light-profiler-macro", +] + +[[package]] +name = "light-prover-client" +version = "2.0.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "ark-bn254 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "light-hasher", + "light-indexed-array", + "light-sparse-merkle-tree", + "num-bigint 0.4.6", + "num-traits", + "reqwest 0.11.27", + "serde", + "serde_json", + "solana-bn254", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "light-sdk" +version = "0.13.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "borsh 0.10.4", + "light-account-checks", + "light-compressed-account", + "light-hasher", + "light-macros", + "light-sdk-macros", + "light-sdk-types", + "light-zero-copy", + "num-bigint 0.4.6", + "solana-account-info", + "solana-cpi", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "thiserror 2.0.17", +] + +[[package]] +name = "light-sdk-macros" +version = "0.13.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "light-hasher", + "light-poseidon 0.3.0", + "proc-macro2", + "quote", + "solana-pubkey 2.2.1", + "syn 2.0.112", +] + +[[package]] +name = "light-sdk-types" +version = "0.13.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "borsh 0.10.4", + "light-account-checks", + "light-compressed-account", + "light-hasher", + "light-macros", + "light-zero-copy", + "solana-msg 2.2.1", + "thiserror 2.0.17", +] + +[[package]] +name = "light-sparse-merkle-tree" +version = "0.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "light-hasher", + "light-indexed-array", + "num-bigint 0.4.6", + "num-traits", + "thiserror 2.0.17", +] + +[[package]] +name = "light-verifier" +version = "2.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "groth16-solana", + "light-compressed-account", + "thiserror 2.0.17", +] + +[[package]] +name = "light-zero-copy" +version = "0.2.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "light-zero-copy-derive", + "solana-program-error 2.2.2", + "zerocopy", +] + +[[package]] +name = "light-zero-copy-derive" +version = "0.1.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 2.0.112", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -3299,12 +3962,12 @@ dependencies = [ "borsh 1.6.0", "bytemuck_derive", "solana-program", - "solana-system-interface", + "solana-system-interface 3.0.0", ] [[package]] name = "magicblock-account-cloner" -version = "0.6.0" +version = "0.6.1" dependencies = [ "async-trait", "bincode", @@ -3315,16 +3978,16 @@ dependencies = [ "magicblock-config", "magicblock-core", "magicblock-ledger", - "magicblock-magic-program-api 0.6.0", + "magicblock-magic-program-api 0.6.1", "magicblock-program", "magicblock-rpc-client", "rand 0.9.2", "solana-account", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-loader-v3-interface 3.0.0", "solana-loader-v4-interface", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-signature", "solana-signer", @@ -3336,7 +3999,7 @@ dependencies = [ [[package]] name = "magicblock-accounts" -version = "0.6.0" +version = "0.6.1" dependencies = [ "async-trait", "log", @@ -3347,7 +4010,7 @@ dependencies = [ "magicblock-core", "magicblock-program", "solana-hash", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-transaction", "solana-transaction-error", "thiserror 1.0.69", @@ -3358,7 +4021,7 @@ dependencies = [ [[package]] name = "magicblock-accounts-db" -version = "0.6.0" +version = "0.6.1" dependencies = [ "lmdb-rkv", "log", @@ -3368,13 +4031,13 @@ dependencies = [ "parking_lot", "reflink-copy", "solana-account", - "solana-pubkey", + "solana-pubkey 2.2.1", "thiserror 1.0.69", ] [[package]] name = "magicblock-aperture" -version = "0.6.0" +version = "0.6.1" dependencies = [ "agave-geyser-plugin-interface", "arc-swap", @@ -3407,7 +4070,7 @@ dependencies = [ "solana-fee-structure", "solana-keypair", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client-api", "solana-signature", "solana-system-transaction", @@ -3422,11 +4085,12 @@ dependencies = [ [[package]] name = "magicblock-api" -version = "0.6.0" +version = "0.6.1" dependencies = [ "anyhow", "borsh 1.6.0", "fd-lock", + "light-client", "log", "magic-domain-program", "magicblock-account-cloner", @@ -3438,7 +4102,7 @@ dependencies = [ "magicblock-config", "magicblock-core", "magicblock-ledger", - "magicblock-magic-program-api 0.6.0", + "magicblock-magic-program-api 0.6.1", "magicblock-metrics", "magicblock-processor", "magicblock-program", @@ -3455,12 +4119,12 @@ dependencies = [ "solana-genesis-config", "solana-hash", "solana-inline-spl", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", "solana-native-token", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-rpc-client", "solana-sha256-hasher", @@ -3477,20 +4141,23 @@ dependencies = [ [[package]] name = "magicblock-chainlink" -version = "0.6.0" +version = "0.6.1" dependencies = [ "arc-swap", "async-trait", "bincode", + "borsh 0.10.4", + "compressed-delegation-client", "env_logger 0.11.8", "futures-util", "helius-laserstream", + "light-client", "log", "lru", "magicblock-config", "magicblock-core", "magicblock-delegation-program 1.1.3 (git+https://github.com/magicblock-labs/delegation-program.git?rev=1874b4f5f5f55cb9ab54b64de2cc0d41107d1435)", - "magicblock-magic-program-api 0.6.0", + "magicblock-magic-program-api 0.6.1", "magicblock-metrics", "parking_lot", "scc", @@ -3502,20 +4169,20 @@ dependencies = [ "solana-commitment-config", "solana-feature-set", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-loader-v3-interface 3.0.0", "solana-loader-v4-interface", "solana-message", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-pubsub-client", "solana-rpc-client", "solana-rpc-client-api", "solana-sdk-ids", "solana-signature", "solana-signer", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-sysvar", "solana-transaction", "solana-transaction-error", @@ -3530,29 +4197,34 @@ dependencies = [ [[package]] name = "magicblock-committor-program" -version = "0.6.0" +version = "0.6.1" dependencies = [ - "borsh 1.6.0", + "borsh 0.10.4", "paste", "solana-account", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "thiserror 1.0.69", ] [[package]] name = "magicblock-committor-service" -version = "0.6.0" +version = "0.6.1" dependencies = [ "async-trait", "base64 0.21.7", "bincode", - "borsh 1.6.0", + "borsh 0.10.4", + "compressed-delegation-client", "dyn-clone", "futures-util", + "light-client", + "light-sdk", "log", "lru", "magicblock-committor-program", + "magicblock-config", + "magicblock-core", "magicblock-delegation-program 1.1.3 (git+https://github.com/magicblock-labs/delegation-program.git?rev=1874b4f5f5f55cb9ab54b64de2cc0d41107d1435)", "magicblock-metrics", "magicblock-program", @@ -3565,11 +4237,11 @@ dependencies = [ "solana-commitment-config", "solana-compute-budget-interface", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", @@ -3586,7 +4258,7 @@ dependencies = [ [[package]] name = "magicblock-config" -version = "0.6.0" +version = "0.6.1" dependencies = [ "clap", "derive_more", @@ -3596,7 +4268,7 @@ dependencies = [ "serde", "serde_with", "solana-keypair", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signer", "toml 0.8.23", "url", @@ -3604,15 +4276,18 @@ dependencies = [ [[package]] name = "magicblock-core" -version = "0.6.0" +version = "0.6.1" dependencies = [ + "compressed-delegation-client", "flume", - "magicblock-magic-program-api 0.6.0", + "light-compressed-account", + "light-sdk", + "magicblock-magic-program-api 0.6.1", "solana-account", "solana-account-decoder", "solana-hash", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-transaction", "solana-transaction-context", @@ -3650,7 +4325,7 @@ dependencies = [ "pinocchio-pubkey", "pinocchio-system", "rkyv 0.7.45", - "solana-curve25519", + "solana-curve25519 3.1.5", "solana-program", "solana-security-txt", "static_assertions", @@ -3660,7 +4335,7 @@ dependencies = [ [[package]] name = "magicblock-ledger" -version = "0.6.0" +version = "0.6.1" dependencies = [ "arc-swap", "bincode", @@ -3679,12 +4354,12 @@ dependencies = [ "solana-account-decoder", "solana-clock", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-measure", "solana-message", "solana-metrics", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-signer", "solana-storage-proto", @@ -3710,7 +4385,7 @@ dependencies = [ [[package]] name = "magicblock-magic-program-api" -version = "0.6.0" +version = "0.6.1" dependencies = [ "bincode", "serde", @@ -3719,7 +4394,7 @@ dependencies = [ [[package]] name = "magicblock-metrics" -version = "0.6.0" +version = "0.6.1" dependencies = [ "http-body-util", "hyper 1.8.1", @@ -3733,7 +4408,7 @@ dependencies = [ [[package]] name = "magicblock-processor" -version = "0.6.0" +version = "0.6.1" dependencies = [ "bincode", "log", @@ -3744,6 +4419,7 @@ dependencies = [ "magicblock-program", "parking_lot", "rustc-hash 2.1.1", + "serde", "solana-account", "solana-bpf-loader-program", "solana-compute-budget-program", @@ -3753,7 +4429,7 @@ dependencies = [ "solana-loader-v4-program", "solana-program", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent-collector", "solana-sdk-ids", "solana-svm 2.2.1 (git+https://github.com/magicblock-labs/magicblock-svm.git?rev=3e9456ec4)", @@ -3768,12 +4444,12 @@ dependencies = [ [[package]] name = "magicblock-program" -version = "0.6.0" +version = "0.6.1" dependencies = [ "bincode", "lazy_static", "magicblock-core", - "magicblock-magic-program-api 0.6.0", + "magicblock-magic-program-api 0.6.1", "num-derive", "num-traits", "parking_lot", @@ -3783,11 +4459,11 @@ dependencies = [ "solana-clock", "solana-fee-calculator", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-log-collector", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-seed-derivable", "solana-signature", @@ -3800,7 +4476,7 @@ dependencies = [ [[package]] name = "magicblock-rpc-client" -version = "0.6.0" +version = "0.6.1" dependencies = [ "log", "solana-account", @@ -3809,8 +4485,8 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-hash", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", @@ -3822,7 +4498,7 @@ dependencies = [ [[package]] name = "magicblock-table-mania" -version = "0.6.0" +version = "0.6.1" dependencies = [ "ed25519-dalek", "log", @@ -3834,10 +4510,10 @@ dependencies = [ "solana-clock", "solana-commitment-config", "solana-compute-budget-interface", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-signer", "solana-slot-hashes", @@ -3848,7 +4524,7 @@ dependencies = [ [[package]] name = "magicblock-task-scheduler" -version = "0.6.0" +version = "0.6.1" dependencies = [ "bincode", "chrono", @@ -3860,9 +4536,9 @@ dependencies = [ "magicblock-ledger", "magicblock-program", "rusqlite", - "solana-instruction", + "solana-instruction 2.2.1", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", @@ -3875,7 +4551,7 @@ dependencies = [ [[package]] name = "magicblock-validator-admin" -version = "0.6.0" +version = "0.6.1" dependencies = [ "log", "magicblock-delegation-program 1.1.3 (git+https://github.com/magicblock-labs/delegation-program.git?rev=1874b4f5f5f55cb9ab54b64de2cc0d41107d1435)", @@ -3892,7 +4568,7 @@ dependencies = [ [[package]] name = "magicblock-version" -version = "0.6.0" +version = "0.6.1" dependencies = [ "git-version", "rustc_version", @@ -3901,7 +4577,7 @@ dependencies = [ "solana-feature-set", "solana-frozen-abi-macro", "solana-rpc-client-api", - "solana-sanitize", + "solana-sanitize 2.2.1", ] [[package]] @@ -4061,6 +4737,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "munge" version = "0.4.7" @@ -4078,7 +4760,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -4099,7 +4781,7 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", "security-framework 2.11.1", @@ -4214,6 +4896,7 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "serde", ] [[package]] @@ -4240,7 +4923,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -4323,7 +5006,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -4406,7 +5089,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -4415,6 +5098,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" + [[package]] name = "openssl-sys" version = "0.9.111" @@ -4510,7 +5199,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -4543,10 +5232,34 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset", + "fixedbitset 0.4.2", + "indexmap 2.12.1", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset 0.5.7", "indexmap 2.12.1", ] +[[package]] +name = "photon-api" +version = "0.51.0" +source = "git+https://github.com/magicblock-labs/light-protocol?rev=849e6aa#849e6aae59b6afc6fa03872c2a8c6fb312be12f1" +dependencies = [ + "reqwest 0.12.28", + "serde", + "serde_derive", + "serde_json", + "serde_with", + "url", + "uuid", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -4564,7 +5277,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -4611,7 +5324,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0225638cadcbebae8932cb7f49cb5da7c15c21beb19f048f05a5ca7d93f065" dependencies = [ - "five8_const", + "five8_const 0.1.4", "pinocchio", "sha2-const-stable", ] @@ -4646,9 +5359,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "portable-atomic-util" @@ -4725,12 +5438,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -4748,7 +5461,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.9", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -4774,9 +5487,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -4789,7 +5502,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", "version_check", "yansi", ] @@ -4799,9 +5512,14 @@ name = "program-flexi-counter" version = "0.0.0" dependencies = [ "bincode", - "borsh 1.6.0", + "borsh 0.10.4", + "compressed-delegation-client", "ephemeral-rollups-sdk", - "magicblock-magic-program-api 0.6.0", + "light-batched-merkle-tree", + "light-hasher", + "light-sdk", + "light-sdk-types", + "magicblock-magic-program-api 0.6.1", "serde", "solana-program", ] @@ -4814,7 +5532,7 @@ dependencies = [ "solana-program-test", "solana-sdk", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", "tokio", ] @@ -4822,10 +5540,10 @@ dependencies = [ name = "program-schedulecommit" version = "0.0.0" dependencies = [ - "borsh 1.6.0", + "borsh 0.10.4", "ephemeral-rollups-sdk", "magicblock-delegation-program 1.1.3 (git+https://github.com/magicblock-labs/delegation-program.git?rev=1874b4f5f5f55cb9ab54b64de2cc0d41107d1435)", - "magicblock-magic-program-api 0.6.0", + "magicblock-magic-program-api 0.6.1", "rkyv 0.7.45", "solana-program", "static_assertions", @@ -4835,7 +5553,7 @@ dependencies = [ name = "program-schedulecommit-security" version = "0.0.0" dependencies = [ - "borsh 1.6.0", + "borsh 0.10.4", "ephemeral-rollups-sdk", "program-schedulecommit", "solana-program", @@ -4897,8 +5615,8 @@ dependencies = [ "itertools 0.10.5", "lazy_static", "log", - "multimap", - "petgraph", + "multimap 0.8.3", + "petgraph 0.6.5", "prettyplease 0.1.25", "prost 0.11.9", "prost-types 0.11.9", @@ -4918,14 +5636,14 @@ dependencies = [ "heck 0.5.0", "itertools 0.12.1", "log", - "multimap", + "multimap 0.10.1", "once_cell", - "petgraph", - "prettyplease 0.2.36", + "petgraph 0.6.5", + "prettyplease 0.2.37", "prost 0.12.6", "prost-types 0.12.6", "regex", - "syn 2.0.111", + "syn 2.0.112", "tempfile", ] @@ -4936,16 +5654,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ "heck 0.5.0", - "itertools 0.12.1", + "itertools 0.14.0", "log", - "multimap", + "multimap 0.10.1", "once_cell", - "petgraph", - "prettyplease 0.2.36", + "petgraph 0.7.1", + "prettyplease 0.2.37", "prost 0.13.5", "prost-types 0.13.5", "regex", - "syn 2.0.111", + "syn 2.0.112", "tempfile", ] @@ -4972,7 +5690,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -4982,10 +5700,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -5067,7 +5785,7 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -5087,7 +5805,7 @@ checksum = "9e2e25ee72f5b24d773cae88422baddefff7714f97aab68d96fe2b6fc4a28fb2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -5352,9 +6070,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ "bitflags 2.10.0", ] @@ -5376,7 +6094,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -5387,7 +6105,7 @@ checksum = "23bbed272e39c47a095a5242218a67412a220006842558b03fe2935e8f3d7b92" dependencies = [ "cfg-if", "libc", - "rustix 1.1.2", + "rustix 1.1.3", "windows", ] @@ -5451,8 +6169,8 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-rustls", - "hyper-tls", + "hyper-rustls 0.24.2", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -5468,7 +6186,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 0.1.2", - "system-configuration", + "system-configuration 0.5.1", "tokio", "tokio-native-tls", "tokio-rustls 0.24.1", @@ -5482,6 +6200,48 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.12", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.7", + "hyper-tls 0.6.0", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-native-tls", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reqwest-middleware" version = "0.2.5" @@ -5491,7 +6251,7 @@ dependencies = [ "anyhow", "async-trait", "http 0.2.12", - "reqwest", + "reqwest 0.11.27", "serde", "task-local-extensions", "thiserror 1.0.69", @@ -5566,7 +6326,7 @@ checksum = "bd83f5f173ff41e00337d97f6572e416d022ef8a19f371817259ae960324c482" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -5643,9 +6403,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags 2.10.0", "errno", @@ -5683,11 +6443,11 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.0", "rustls-pki-types", "schannel", "security-framework 3.5.1", @@ -5777,9 +6537,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -5813,7 +6573,7 @@ name = "schedulecommit-client" version = "0.0.0" dependencies = [ "anyhow", - "borsh 1.6.0", + "borsh 0.10.4", "integration-test-tools", "log", "magicblock-core", @@ -5830,11 +6590,18 @@ name = "schedulecommit-committor-service" version = "0.0.0" dependencies = [ "async-trait", - "borsh 1.6.0", + "borsh 0.10.4", + "compressed-delegation-client", "futures", + "light-client", + "light-compressed-account", + "light-sdk", + "light-sdk-types", "log", + "magicblock-chainlink", "magicblock-committor-program", "magicblock-committor-service", + "magicblock-core", "magicblock-delegation-program 1.1.3 (git+https://github.com/magicblock-labs/delegation-program.git?rev=1874b4f5f5f55cb9ab54b64de2cc0d41107d1435)", "magicblock-program", "magicblock-rpc-client", @@ -5843,7 +6610,7 @@ dependencies = [ "program-schedulecommit", "rand 0.8.5", "solana-account", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-rpc-client-api", "solana-sdk", @@ -5855,12 +6622,12 @@ dependencies = [ name = "schedulecommit-test-scenarios" version = "0.0.0" dependencies = [ - "borsh 1.6.0", + "borsh 0.10.4", "ephemeral-rollups-sdk", "integration-test-tools", "log", "magicblock-core", - "magicblock-magic-program-api 0.6.0", + "magicblock-magic-program-api 0.6.1", "program-schedulecommit", "rand 0.8.5", "schedulecommit-client", @@ -5875,9 +6642,10 @@ dependencies = [ name = "schedulecommit-test-security" version = "0.0.0" dependencies = [ + "borsh 0.10.4", "integration-test-tools", "magicblock-core", - "magicblock-magic-program-api 0.6.0", + "magicblock-magic-program-api 0.6.1", "program-schedulecommit", "program-schedulecommit-security", "schedulecommit-client", @@ -5899,9 +6667,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "ref-cast", @@ -6034,20 +6802,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -6083,7 +6851,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.0", "serde_core", "serde_json", "serde_with_macros", @@ -6099,7 +6867,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -6125,7 +6893,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -6196,10 +6964,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -6287,8 +7056,8 @@ dependencies = [ "serde_derive", "solana-account-info", "solana-clock", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-sysvar", ] @@ -6314,11 +7083,11 @@ dependencies = [ "solana-config-program", "solana-epoch-schedule", "solana-fee-calculator", - "solana-instruction", + "solana-instruction 2.2.1", "solana-nonce", "solana-program", "solana-program-pack", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", "solana-slot-hashes", @@ -6344,7 +7113,7 @@ dependencies = [ "serde_derive", "serde_json", "solana-account", - "solana-pubkey", + "solana-pubkey 2.2.1", "zstd", ] @@ -6356,9 +7125,9 @@ checksum = "e0c17d606a298a205fae325489fbed88ee6dc4463c111672172327e741c8905d" dependencies = [ "bincode", "serde", - "solana-program-error", + "solana-program-error 2.2.2", "solana-program-memory", - "solana-pubkey", + "solana-pubkey 2.2.1", ] [[package]] @@ -6400,7 +7169,7 @@ dependencies = [ "solana-measure", "solana-metrics", "solana-nohash-hasher", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rayon-threadlimit", "solana-sdk", "solana-svm-transaction", @@ -6410,6 +7179,21 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "solana-address" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37320fd2945c5d654b2c6210624a52d66c3f1f73b653ed211ab91a703b35bdd" +dependencies = [ + "five8", + "five8_const 1.0.0", + "serde", + "serde_derive", + "solana-define-syscall 4.0.1", + "solana-program-error 3.0.0", + "solana-sanitize 3.0.1", +] + [[package]] name = "solana-address-lookup-table-interface" version = "2.2.2" @@ -6421,8 +7205,8 @@ dependencies = [ "serde", "serde_derive", "solana-clock", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-slot-hashes", ] @@ -6442,12 +7226,12 @@ dependencies = [ "solana-bincode", "solana-clock", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-packet", "solana-program-runtime", - "solana-pubkey", - "solana-system-interface", + "solana-pubkey 2.2.1", + "solana-system-interface 1.0.0", "solana-transaction-context", "thiserror 2.0.17", ] @@ -6520,7 +7304,7 @@ checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" dependencies = [ "num-bigint 0.4.6", "num-traits", - "solana-define-syscall", + "solana-define-syscall 2.2.1", ] [[package]] @@ -6531,7 +7315,7 @@ checksum = "19a3787b8cf9c9fe3dd360800e8b70982b9e5a8af9e11c354b6665dd4a003adc" dependencies = [ "bincode", "serde", - "solana-instruction", + "solana-instruction 2.2.1", ] [[package]] @@ -6541,9 +7325,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" dependencies = [ "blake3", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "solana-hash", - "solana-sanitize", + "solana-sanitize 2.2.1", ] [[package]] @@ -6552,12 +7336,12 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9abc69625158faaab02347370b91c0d8e0fe347bf9287239f0fbe8f5864d91da" dependencies = [ - "ark-bn254", - "ark-ec", - "ark-ff", - "ark-serialize", + "ark-bn254 0.4.0", + "ark-ec 0.4.2", + "ark-ff 0.4.2", + "ark-serialize 0.4.2", "bytemuck", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "thiserror 2.0.17", ] @@ -6590,10 +7374,10 @@ dependencies = [ "solana-clock", "solana-compute-budget", "solana-cpi", - "solana-curve25519", + "solana-curve25519 2.2.1", "solana-feature-set", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keccak-hasher", "solana-loader-v3-interface 3.0.0", "solana-loader-v4-interface", @@ -6605,13 +7389,13 @@ dependencies = [ "solana-program-entrypoint", "solana-program-memory", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sbpf", "solana-sdk-ids", "solana-secp256k1-recover", "solana-sha256-hasher", "solana-stable-layout", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-sysvar", "solana-sysvar-id", "solana-timings", @@ -6636,7 +7420,7 @@ dependencies = [ "rand 0.8.5", "solana-clock", "solana-measure", - "solana-pubkey", + "solana-pubkey 2.2.1", "tempfile", ] @@ -6653,7 +7437,7 @@ dependencies = [ "solana-feature-set", "solana-loader-v4-program", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-stake-program", "solana-system-program", @@ -6678,7 +7462,7 @@ dependencies = [ "solana-config-program", "solana-feature-set", "solana-loader-v4-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-stake-program", "solana-system-program", @@ -6707,11 +7491,11 @@ dependencies = [ "solana-connection-cache", "solana-epoch-info", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-measure", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-pubsub-client", "solana-quic-client", "solana-quic-definitions", @@ -6741,13 +7525,13 @@ dependencies = [ "solana-commitment-config", "solana-epoch-info", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-signer", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction", "solana-transaction-error", ] @@ -6808,9 +7592,9 @@ dependencies = [ "solana-compute-budget", "solana-compute-budget-interface", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-packet", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-svm-transaction", "solana-transaction-error", @@ -6826,7 +7610,7 @@ dependencies = [ "borsh 1.6.0", "serde", "serde_derive", - "solana-instruction", + "solana-instruction 2.2.1", "solana-sdk-ids", ] @@ -6852,15 +7636,15 @@ dependencies = [ "serde_derive", "solana-account", "solana-bincode", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-packet", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-short-vec", "solana-stake-interface", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction-context", ] @@ -6908,11 +7692,11 @@ dependencies = [ "solana-fee-structure", "solana-metrics", "solana-packet", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-runtime-transaction", "solana-sdk-ids", "solana-svm-transaction", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction-error", "solana-vote-program", ] @@ -6924,10 +7708,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8dc71126edddc2ba014622fc32d0f5e2e78ec6c5a1e0eb511b85618c09e9ea11" dependencies = [ "solana-account-info", - "solana-define-syscall", - "solana-instruction", - "solana-program-error", - "solana-pubkey", + "solana-define-syscall 2.2.1", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "solana-stable-layout", ] @@ -6940,7 +7724,21 @@ dependencies = [ "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", - "solana-define-syscall", + "solana-define-syscall 2.2.1", + "subtle", + "thiserror 2.0.17", +] + +[[package]] +name = "solana-curve25519" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebca352e7716ff1a0877272f87c772c958489c1d568a92d318dc0c75939d2884" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "solana-define-syscall 3.0.0", "subtle", "thiserror 2.0.17", ] @@ -6960,6 +7758,18 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf784bb2cb3e02cac9801813c30187344228d2ae952534902108f6150573a33d" +[[package]] +name = "solana-define-syscall" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9697086a4e102d28a156b8d6b521730335d6951bd39a5e766512bbe09007cee" + +[[package]] +name = "solana-define-syscall" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" + [[package]] name = "solana-derivation-path" version = "2.2.1" @@ -6981,7 +7791,7 @@ dependencies = [ "bytemuck_derive", "ed25519-dalek", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-precompile-error", "solana-sdk-ids", ] @@ -7018,7 +7828,7 @@ checksum = "96c5fd2662ae7574810904585fd443545ed2b568dbd304b25a31e79ccc76e81b" dependencies = [ "siphasher 0.3.11", "solana-hash", - "solana-pubkey", + "solana-pubkey 2.2.1", ] [[package]] @@ -7045,13 +7855,13 @@ dependencies = [ "solana-address-lookup-table-interface", "solana-clock", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keccak-hasher", "solana-message", "solana-nonce", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", "thiserror 2.0.17", ] @@ -7066,12 +7876,12 @@ dependencies = [ "serde_derive", "solana-account", "solana-account-info", - "solana-instruction", - "solana-program-error", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", ] [[package]] @@ -7084,7 +7894,7 @@ dependencies = [ "lazy_static", "solana-epoch-schedule", "solana-hash", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sha256-hasher", ] @@ -7130,7 +7940,7 @@ checksum = "b83f88a126213cbcb57672c5e70ddb9791eff9b480e9f39fe9285fd2abca66fa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -7155,7 +7965,7 @@ dependencies = [ "solana-logger", "solana-native-token", "solana-poh-config", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", "solana-sha256-hasher", @@ -7188,7 +7998,7 @@ dependencies = [ "serde", "serde_derive", "solana-atomic-u64", - "solana-sanitize", + "solana-sanitize 2.2.1", "wasm-bindgen", ] @@ -7209,7 +8019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "951545bd7d0ab4a878cfc7375ac9f1a475cb6936626677b2ba1d25e7b9f3910b" dependencies = [ "bytemuck", - "solana-pubkey", + "solana-pubkey 2.2.1", ] [[package]] @@ -7225,11 +8035,35 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-define-syscall", - "solana-pubkey", + "solana-define-syscall 2.2.1", + "solana-pubkey 2.2.1", "wasm-bindgen", ] +[[package]] +name = "solana-instruction" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1b699a2c1518028a9982e255e0eca10c44d90006542d9d7f9f40dbce3f7c78" +dependencies = [ + "bincode", + "borsh 1.6.0", + "serde", + "solana-define-syscall 4.0.1", + "solana-instruction-error", + "solana-pubkey 4.0.0", +] + +[[package]] +name = "solana-instruction-error" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04259e03c05faf38a8c24217b5cfe4c90572ae6184ab49cddb1584fdd756d3f" +dependencies = [ + "num-traits", + "solana-program-error 3.0.0", +] + [[package]] name = "solana-instructions-sysvar" version = "2.2.1" @@ -7238,10 +8072,10 @@ checksum = "427f2d0d6dc0bb49f16cef5e7f975180d2e80aab9bdd3b2af68e2d029ec63f43" dependencies = [ "bitflags 2.10.0", "solana-account-info", - "solana-instruction", - "solana-program-error", - "solana-pubkey", - "solana-sanitize", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-serialize-utils", "solana-sysvar-id", @@ -7254,9 +8088,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" dependencies = [ "sha3", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "solana-hash", - "solana-sanitize", + "solana-sanitize 2.2.1", ] [[package]] @@ -7270,7 +8104,7 @@ dependencies = [ "ed25519-dalek-bip32", "rand 0.7.3", "solana-derivation-path", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-seed-derivable", "solana-seed-phrase", "solana-signature", @@ -7312,8 +8146,8 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", ] @@ -7326,10 +8160,10 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", ] [[package]] @@ -7341,8 +8175,8 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", ] @@ -7355,10 +8189,10 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", ] [[package]] @@ -7373,14 +8207,14 @@ dependencies = [ "solana-bincode", "solana-bpf-loader-program", "solana-compute-budget", - "solana-instruction", + "solana-instruction 2.2.1", "solana-loader-v3-interface 3.0.0", "solana-loader-v4-interface", "solana-log-collector", "solana-measure", "solana-packet", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sbpf", "solana-sdk-ids", "solana-transaction-context", @@ -7426,12 +8260,12 @@ dependencies = [ "serde_derive", "solana-bincode", "solana-hash", - "solana-instruction", - "solana-pubkey", - "solana-sanitize", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-short-vec", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction-error", "wasm-bindgen", ] @@ -7446,7 +8280,7 @@ dependencies = [ "gethostname", "lazy_static", "log", - "reqwest", + "reqwest 0.11.27", "solana-clock", "solana-cluster-type", "solana-sha256-hasher", @@ -7460,7 +8294,16 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36a1a14399afaabc2781a1db09cb14ee4cc4ee5c7a5a3cfcc601811379a8092" dependencies = [ - "solana-define-syscall", + "solana-define-syscall 2.2.1", +] + +[[package]] +name = "solana-msg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "264275c556ea7e22b9d3f87d56305546a38d4eee8ec884f3b126236cb7dcbbb4" +dependencies = [ + "solana-define-syscall 3.0.0", ] [[package]] @@ -7507,7 +8350,7 @@ dependencies = [ "serde_derive", "solana-fee-calculator", "solana-hash", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sha256-hasher", ] @@ -7523,6 +8366,15 @@ dependencies = [ "solana-sdk-ids", ] +[[package]] +name = "solana-nostd-keccak" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ced70920435b1baa58f76e6f84bbc1110ddd1d6161ec76b6d731ae8431e9c4" +dependencies = [ + "sha3", +] + [[package]] name = "solana-offchain-message" version = "2.2.1" @@ -7532,8 +8384,8 @@ dependencies = [ "num_enum", "solana-hash", "solana-packet", - "solana-pubkey", - "solana-sanitize", + "solana-pubkey 2.2.1", + "solana-sanitize 2.2.1", "solana-sha256-hasher", "solana-signature", "solana-signer", @@ -7577,7 +8429,7 @@ dependencies = [ "solana-message", "solana-metrics", "solana-packet", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rayon-threadlimit", "solana-sdk-ids", "solana-short-vec", @@ -7601,9 +8453,9 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ad1ea160d08dc423c35021fa3e437a5783eb256f5ab8bc3024e27db913acf42" dependencies = [ - "ark-bn254", - "light-poseidon", - "solana-define-syscall", + "ark-bn254 0.4.0", + "light-poseidon 0.2.0", + "solana-define-syscall 2.2.1", "thiserror 2.0.17", ] @@ -7628,7 +8480,7 @@ dependencies = [ "solana-feature-set", "solana-message", "solana-precompile-error", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-secp256k1-program", "solana-secp256r1-program", @@ -7640,7 +8492,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81a57a24e6a4125fc69510b6774cd93402b943191b6cddad05de7281491c90fe" dependencies = [ - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-signer", ] @@ -7680,14 +8532,14 @@ dependencies = [ "solana-clock", "solana-cpi", "solana-decode-error", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "solana-epoch-rewards", "solana-epoch-schedule", "solana-example-mocks", "solana-feature-gate-interface", "solana-fee-calculator", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-instructions-sysvar", "solana-keccak-hasher", "solana-last-restart-slot", @@ -7695,17 +8547,17 @@ dependencies = [ "solana-loader-v3-interface 3.0.0", "solana-loader-v4-interface", "solana-message", - "solana-msg", + "solana-msg 2.2.1", "solana-native-token", "solana-nonce", "solana-program-entrypoint", - "solana-program-error", + "solana-program-error 2.2.2", "solana-program-memory", "solana-program-option", "solana-program-pack", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-sdk-macro", "solana-secp256k1-recover", @@ -7717,7 +8569,7 @@ dependencies = [ "solana-slot-history", "solana-stable-layout", "solana-stake-interface", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-sysvar", "solana-sysvar-id", "solana-vote-interface", @@ -7732,9 +8584,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "473ffe73c68d93e9f2aa726ad2985fe52760052709aaab188100a42c618060ec" dependencies = [ "solana-account-info", - "solana-msg", - "solana-program-error", - "solana-pubkey", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", ] [[package]] @@ -7748,11 +8600,17 @@ dependencies = [ "serde", "serde_derive", "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-pubkey 2.2.1", ] +[[package]] +name = "solana-program-error" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" + [[package]] name = "solana-program-memory" version = "2.2.1" @@ -7760,7 +8618,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b0268f6c89825fb634a34bd0c3b8fdaeaecfc3728be1d622a8ee6dd577b60d4" dependencies = [ "num-traits", - "solana-define-syscall", + "solana-define-syscall 2.2.1", ] [[package]] @@ -7775,7 +8633,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" dependencies = [ - "solana-program-error", + "solana-program-error 2.2.2", ] [[package]] @@ -7799,13 +8657,13 @@ dependencies = [ "solana-epoch-schedule", "solana-feature-set", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-last-restart-slot", "solana-log-collector", "solana-measure", "solana-metrics", "solana-precompiles", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sbpf", "solana-sdk-ids", @@ -7841,7 +8699,7 @@ dependencies = [ "solana-compute-budget", "solana-feature-set", "solana-inline-spl", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-logger", "solana-program-runtime", @@ -7868,7 +8726,7 @@ dependencies = [ "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", - "five8_const", + "five8_const 0.1.4", "getrandom 0.2.16", "js-sys", "num-traits", @@ -7877,12 +8735,21 @@ dependencies = [ "serde_derive", "solana-atomic-u64", "solana-decode-error", - "solana-define-syscall", - "solana-sanitize", + "solana-define-syscall 2.2.1", + "solana-sanitize 2.2.1", "solana-sha256-hasher", "wasm-bindgen", ] +[[package]] +name = "solana-pubkey" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f7104d456b58e1418c21a8581e89810278d1190f70f27ece7fc0b2c9282a57" +dependencies = [ + "solana-address", +] + [[package]] name = "solana-pubsub-client" version = "2.2.1" @@ -7892,14 +8759,14 @@ dependencies = [ "crossbeam-channel", "futures-util", "log", - "reqwest", + "reqwest 0.11.27", "semver", "serde", "serde_derive", "serde_json", "solana-account-decoder-client-types", "solana-clock", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client-api", "solana-signature", "thiserror 2.0.17", @@ -7930,7 +8797,7 @@ dependencies = [ "solana-measure", "solana-metrics", "solana-net-utils", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-quic-definitions", "solana-rpc-client-api", "solana-signer", @@ -7985,7 +8852,7 @@ dependencies = [ "solana-clock", "solana-epoch-schedule", "solana-genesis-config", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", ] @@ -7996,7 +8863,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f6f9113c6003492e74438d1288e30cffa8ccfdc2ef7b49b9e816d8034da18cd" dependencies = [ - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-reward-info", ] @@ -8008,7 +8875,7 @@ checksum = "2b293f4246626c0e0a991531f08848a713ada965612e99dc510963f04d12cae7" dependencies = [ "lazy_static", "solana-feature-set", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", ] @@ -8034,7 +8901,7 @@ dependencies = [ "bs58", "indicatif", "log", - "reqwest", + "reqwest 0.11.27", "reqwest-middleware", "semver", "serde", @@ -8048,9 +8915,9 @@ dependencies = [ "solana-epoch-schedule", "solana-feature-gate-interface", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client-api", "solana-signature", "solana-transaction", @@ -8070,7 +8937,7 @@ dependencies = [ "base64 0.22.1", "bs58", "jsonrpc-core", - "reqwest", + "reqwest 0.11.27", "reqwest-middleware", "semver", "serde", @@ -8083,7 +8950,7 @@ dependencies = [ "solana-fee-calculator", "solana-inflation", "solana-inline-spl", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signer", "solana-transaction-error", "solana-transaction-status-client-types", @@ -8102,7 +8969,7 @@ dependencies = [ "solana-hash", "solana-message", "solana-nonce", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-sdk-ids", "thiserror 2.0.17", @@ -8170,7 +9037,7 @@ dependencies = [ "solana-perf", "solana-program", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rayon-threadlimit", "solana-runtime-transaction", "solana-sdk", @@ -8206,7 +9073,7 @@ dependencies = [ "solana-compute-budget-instruction", "solana-hash", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-signature", "solana-svm-transaction", @@ -8221,6 +9088,12 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" +[[package]] +name = "solana-sanitize" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09694a0fc14e5ffb18f9b7b7c0f15ecb6eac5b5610bf76a1853459d19daf9" + [[package]] name = "solana-sbpf" version = "0.10.0" @@ -8266,7 +9139,7 @@ dependencies = [ "solana-genesis-config", "solana-hard-forks", "solana-inflation", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", "solana-native-token", @@ -8279,13 +9152,13 @@ dependencies = [ "solana-presigner", "solana-program", "solana-program-memory", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-quic-definitions", "solana-rent-collector", "solana-rent-debits", "solana-reserved-account-keys", "solana-reward-info", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-sdk-macro", "solana-secp256k1-program", @@ -8315,7 +9188,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" dependencies = [ - "solana-pubkey", + "solana-pubkey 2.2.1", ] [[package]] @@ -8327,7 +9200,7 @@ dependencies = [ "bs58", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -8343,7 +9216,7 @@ dependencies = [ "serde_derive", "sha3", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-precompile-error", "solana-sdk-ids", ] @@ -8356,7 +9229,7 @@ checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" dependencies = [ "borsh 1.6.0", "libsecp256k1", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "thiserror 2.0.17", ] @@ -8369,7 +9242,7 @@ dependencies = [ "bytemuck", "openssl", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-precompile-error", "solana-sdk-ids", ] @@ -8446,9 +9319,9 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" dependencies = [ - "solana-instruction", - "solana-pubkey", - "solana-sanitize", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", + "solana-sanitize 2.2.1", ] [[package]] @@ -8458,7 +9331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0037386961c0d633421f53560ad7c80675c0447cba4d1bb66d60974dd486c7ea" dependencies = [ "sha2 0.10.9", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "solana-hash", ] @@ -8494,7 +9367,7 @@ dependencies = [ "serde", "serde-big-array", "serde_derive", - "solana-sanitize", + "solana-sanitize 2.2.1", ] [[package]] @@ -8503,7 +9376,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c41991508a4b02f021c1342ba00bcfa098630b213726ceadc7cb032e051975b" dependencies = [ - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-transaction-error", ] @@ -8540,8 +9413,8 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" dependencies = [ - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", ] [[package]] @@ -8558,10 +9431,10 @@ dependencies = [ "solana-clock", "solana-cpi", "solana-decode-error", - "solana-instruction", - "solana-program-error", - "solana-pubkey", - "solana-system-interface", + "solana-instruction 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", + "solana-system-interface 1.0.0", "solana-sysvar-id", ] @@ -8579,12 +9452,12 @@ dependencies = [ "solana-config-program", "solana-feature-set", "solana-genesis-config", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-native-token", "solana-packet", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", "solana-stake-interface", @@ -8596,7 +9469,7 @@ dependencies = [ [[package]] name = "solana-storage-proto" -version = "0.6.0" +version = "0.6.1" dependencies = [ "bincode", "bs58", @@ -8605,9 +9478,9 @@ dependencies = [ "serde", "solana-account-decoder", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-transaction", "solana-transaction-context", @@ -8649,7 +9522,7 @@ dependencies = [ "solana-net-utils", "solana-packet", "solana-perf", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-quic-definitions", "solana-signature", "solana-signer", @@ -8683,7 +9556,7 @@ dependencies = [ "solana-feature-set", "solana-fee-structure", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-instructions-sysvar", "solana-loader-v4-program", "solana-log-collector", @@ -8694,7 +9567,7 @@ dependencies = [ "solana-precompiles", "solana-program", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-rent-debits", "solana-sdk", @@ -8727,7 +9600,7 @@ dependencies = [ "solana-feature-set", "solana-fee-structure", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-instructions-sysvar", "solana-loader-v4-program", "solana-log-collector", @@ -8738,7 +9611,7 @@ dependencies = [ "solana-precompiles", "solana-program", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-rent-debits", "solana-reserved-account-keys", @@ -8770,7 +9643,7 @@ checksum = "4fc4392f0eed412141a376e99dfb052069b96f13697a9abb335504babe29387a" dependencies = [ "solana-hash", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-signature", "solana-transaction", @@ -8787,11 +9660,26 @@ dependencies = [ "serde", "serde_derive", "solana-decode-error", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "wasm-bindgen", ] +[[package]] +name = "solana-system-interface" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14591d6508042ebefb110305d3ba761615927146a26917ade45dc332d8e1ecde" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-address", + "solana-instruction 3.1.0", + "solana-msg 3.0.0", + "solana-program-error 3.0.0", +] + [[package]] name = "solana-system-program" version = "2.2.1" @@ -8804,15 +9692,15 @@ dependencies = [ "serde_derive", "solana-account", "solana-bincode", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-nonce", "solana-nonce-account", "solana-packet", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-sysvar", "solana-transaction-context", "solana-type-overrides", @@ -8827,9 +9715,9 @@ dependencies = [ "solana-hash", "solana-keypair", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signer", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction", ] @@ -8848,20 +9736,20 @@ dependencies = [ "serde_derive", "solana-account-info", "solana-clock", - "solana-define-syscall", + "solana-define-syscall 2.2.1", "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-instructions-sysvar", "solana-last-restart-slot", "solana-program-entrypoint", - "solana-program-error", + "solana-program-error 2.2.2", "solana-program-memory", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-sdk-macro", "solana-slot-hashes", @@ -8876,7 +9764,7 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" dependencies = [ - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", ] @@ -8896,15 +9784,15 @@ dependencies = [ "solana-connection-cache", "solana-epoch-info", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", "solana-signer", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction", "solana-transaction-error", ] @@ -8923,7 +9811,7 @@ checksum = "49d9eabdce318cb07c60a23f1cc367b43e177c79225b5c2a081869ad182172ad" dependencies = [ "eager", "enum-iterator", - "solana-pubkey", + "solana-pubkey 2.2.1", ] [[package]] @@ -8934,7 +9822,7 @@ checksum = "a228df037e560a02aac132193f492bdd761e2f90188cd16a440f149882f589b1" dependencies = [ "rustls 0.23.35", "solana-keypair", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signer", "x509-parser", ] @@ -8960,7 +9848,7 @@ dependencies = [ "solana-measure", "solana-message", "solana-net-utils", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-pubsub-client", "solana-quic-definitions", "solana-rpc-client", @@ -8985,18 +9873,18 @@ dependencies = [ "solana-bincode", "solana-feature-set", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-message", "solana-precompiles", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-reserved-account-keys", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-sdk-ids", "solana-short-vec", "solana-signature", "solana-signer", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction-error", "wasm-bindgen", ] @@ -9011,8 +9899,8 @@ dependencies = [ "serde", "serde_derive", "solana-account", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-rent", "solana-signature", ] @@ -9025,8 +9913,8 @@ checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" dependencies = [ "serde", "serde_derive", - "solana-instruction", - "solana-sanitize", + "solana-instruction 2.2.1", + "solana-sanitize 2.2.1", ] [[package]] @@ -9065,16 +9953,16 @@ dependencies = [ "solana-account-decoder", "solana-clock", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-loader-v2-interface", "solana-message", "solana-program", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-reserved-account-keys", "solana-reward-info", "solana-sdk-ids", "solana-signature", - "solana-system-interface", + "solana-system-interface 1.0.0", "solana-transaction", "solana-transaction-error", "solana-transaction-status-client-types", @@ -9143,7 +10031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe7e48cbf4e70c05199f50d5f14aafc58331ad39229747c795320bcb362ed063" dependencies = [ "assert_matches", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-runtime-transaction", "solana-transaction", "static_assertions", @@ -9165,7 +10053,7 @@ dependencies = [ "serde", "serde_derive", "solana-feature-set", - "solana-sanitize", + "solana-sanitize 2.2.1", "solana-serde-varint", ] @@ -9183,9 +10071,9 @@ dependencies = [ "solana-bincode", "solana-clock", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-packet", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-signature", "solana-svm-transaction", @@ -9208,14 +10096,14 @@ dependencies = [ "solana-clock", "solana-decode-error", "solana-hash", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", "solana-serde-varint", "solana-serialize-utils", "solana-short-vec", - "solana-system-interface", + "solana-system-interface 1.0.0", ] [[package]] @@ -9236,11 +10124,11 @@ dependencies = [ "solana-epoch-schedule", "solana-feature-set", "solana-hash", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-packet", "solana-program-runtime", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rent", "solana-sdk-ids", "solana-signer", @@ -9260,7 +10148,7 @@ dependencies = [ "bytemuck", "num-derive", "num-traits", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-program-runtime", "solana-sdk-ids", @@ -9291,8 +10179,8 @@ dependencies = [ "serde_json", "sha3", "solana-derivation-path", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-seed-derivable", "solana-seed-phrase", @@ -9314,7 +10202,7 @@ dependencies = [ "num-derive", "num-traits", "solana-feature-set", - "solana-instruction", + "solana-instruction 2.2.1", "solana-log-collector", "solana-program-runtime", "solana-sdk-ids", @@ -9343,10 +10231,10 @@ dependencies = [ "serde_derive", "serde_json", "sha3", - "solana-curve25519", + "solana-curve25519 2.2.1", "solana-derivation-path", - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", "solana-sdk-ids", "solana-seed-derivable", "solana-seed-phrase", @@ -9436,8 +10324,8 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f8349dbcbe575f354f9a533a21f272f3eb3808a49e2fdc1c34393b88ba76cb" dependencies = [ - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", ] [[package]] @@ -9447,7 +10335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7398da23554a31660f17718164e31d31900956054f54f52d5ec1be51cb4f4b3" dependencies = [ "bytemuck", - "solana-program-error", + "solana-program-error 2.2.2", "solana-sha256-hasher", "spl-discriminator-derive", ] @@ -9460,7 +10348,7 @@ checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" dependencies = [ "quote", "spl-discriminator-syn", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -9472,7 +10360,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.9", - "syn 2.0.111", + "syn 2.0.112", "thiserror 1.0.69", ] @@ -9496,11 +10384,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f09647c0974e33366efeb83b8e2daebb329f0420149e74d3a4bd2c08cf9f7cb" dependencies = [ "solana-account-info", - "solana-instruction", - "solana-msg", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", "solana-program-entrypoint", - "solana-program-error", - "solana-pubkey", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", ] [[package]] @@ -9509,8 +10397,8 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24af0730130fea732616be9425fe8eb77782e2aab2f0e76837b6a66aaba96c6b" dependencies = [ - "solana-instruction", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-pubkey 2.2.1", ] [[package]] @@ -9525,10 +10413,10 @@ dependencies = [ "num-derive", "num-traits", "solana-decode-error", - "solana-msg", - "solana-program-error", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", "solana-program-option", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-zk-sdk", "thiserror 2.0.17", ] @@ -9555,7 +10443,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.9", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -9569,10 +10457,10 @@ dependencies = [ "num-traits", "solana-account-info", "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "spl-discriminator", "spl-pod", "spl-program-error", @@ -9659,7 +10547,7 @@ checksum = "170378693c5516090f6d37ae9bad2b9b6125069be68d9acd4865bbe9fc8499fd" dependencies = [ "base64 0.22.1", "bytemuck", - "solana-curve25519", + "solana-curve25519 2.2.1", "solana-zk-sdk", ] @@ -9670,7 +10558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eff2d6a445a147c9d6dd77b8301b1e116c8299601794b558eafa409b342faf96" dependencies = [ "bytemuck", - "solana-curve25519", + "solana-curve25519 2.2.1", "solana-program", "solana-zk-sdk", "spl-pod", @@ -9709,10 +10597,10 @@ dependencies = [ "num-derive", "num-traits", "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "spl-discriminator", "spl-pod", "thiserror 1.0.69", @@ -9729,10 +10617,10 @@ dependencies = [ "num-traits", "solana-borsh", "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "spl-discriminator", "spl-pod", "spl-type-length-value", @@ -9752,10 +10640,10 @@ dependencies = [ "solana-account-info", "solana-cpi", "solana-decode-error", - "solana-instruction", - "solana-msg", - "solana-program-error", - "solana-pubkey", + "solana-instruction 2.2.1", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.2.1", "spl-discriminator", "spl-pod", "spl-program-error", @@ -9775,8 +10663,8 @@ dependencies = [ "num-traits", "solana-account-info", "solana-decode-error", - "solana-msg", - "solana-program-error", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", "spl-discriminator", "spl-pod", "thiserror 1.0.69", @@ -9840,7 +10728,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -9868,9 +10756,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" dependencies = [ "proc-macro2", "quote", @@ -9888,6 +10776,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -9909,7 +10800,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -9920,7 +10811,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys 0.6.0", ] [[package]] @@ -9933,6 +10835,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "sysvars" version = "0.0.0" @@ -10009,14 +10921,14 @@ checksum = "28768569381ae187ca3e46026f5c0786c4120e146fc74bda02ab930dc1b94bd3" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -10040,24 +10952,33 @@ name = "test-chainlink" version = "0.0.0" dependencies = [ "bincode", + "borsh 0.10.4", + "compressed-delegation-client", "futures", "integration-test-tools", + "light-client", + "light-compressed-account", + "light-hasher", + "light-sdk", "log", "magicblock-chainlink", "magicblock-config", + "magicblock-core", "magicblock-delegation-program 1.1.3 (git+https://github.com/magicblock-labs/delegation-program.git?rev=1874b4f5f5f55cb9ab54b64de2cc0d41107d1435)", "program-flexi-counter", "program-mini", "solana-account", + "solana-compute-budget-interface", "solana-loader-v2-interface", "solana-loader-v3-interface 4.0.1", "solana-loader-v4-interface", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-rpc-client-api", "solana-sdk", "solana-sdk-ids", - "solana-system-interface", + "solana-system-interface 1.0.0", + "solana-transaction-status", "spl-token", "tokio", ] @@ -10097,7 +11018,7 @@ dependencies = [ [[package]] name = "test-kit" -version = "0.6.0" +version = "0.6.1" dependencies = [ "env_logger 0.11.8", "guinea", @@ -10107,7 +11028,7 @@ dependencies = [ "magicblock-ledger", "magicblock-processor", "solana-account", - "solana-instruction", + "solana-instruction 2.2.1", "solana-keypair", "solana-program", "solana-rpc-client", @@ -10204,7 +11125,7 @@ dependencies = [ "magicblock-rpc-client", "magicblock-table-mania", "paste", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-rpc-client", "solana-sdk", "test-kit", @@ -10256,7 +11177,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -10267,7 +11188,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -10360,7 +11281,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -10401,7 +11322,7 @@ checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" dependencies = [ "bincode", "bytes", - "educe", + "educe 0.4.23", "futures-core", "futures-sink", "pin-project", @@ -10499,9 +11420,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -10522,21 +11443,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.9" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap 2.12.1", - "toml_datetime 0.7.3", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -10601,11 +11522,11 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" dependencies = [ - "prettyplease 0.2.36", + "prettyplease 0.2.37", "proc-macro2", "prost-build 0.12.6", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -10614,12 +11535,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" dependencies = [ - "prettyplease 0.2.36", + "prettyplease 0.2.37", "proc-macro2", "prost-build 0.13.5", "prost-types 0.13.5", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -10665,6 +11586,25 @@ dependencies = [ "futures-util", "pin-project-lite", "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.2", "tower-layer", "tower-service", ] @@ -10683,9 +11623,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -10701,14 +11641,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -10917,6 +11857,7 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -11029,7 +11970,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", "wasm-bindgen-shared", ] @@ -11200,7 +12141,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -11211,7 +12152,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -11230,6 +12171,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -11619,7 +12571,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.2", + "rustix 1.1.3", ] [[package]] @@ -11658,7 +12610,7 @@ dependencies = [ "solana-clock", "solana-hash", "solana-message", - "solana-pubkey", + "solana-pubkey 2.2.1", "solana-signature", "solana-transaction", "solana-transaction-context", @@ -11687,7 +12639,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", "synstructure 0.13.2", ] @@ -11708,7 +12660,7 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -11728,7 +12680,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", "synstructure 0.13.2", ] @@ -11743,13 +12695,13 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] [[package]] @@ -11782,9 +12734,15 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.112", ] +[[package]] +name = "zmij" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9747e91771f56fd7893e1164abd78febd14a670ceec257caad15e051de35f06" + [[package]] name = "zstd" version = "0.13.3" diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index 6a528b2a0..776fc25d8 100644 --- a/test-integration/Cargo.toml +++ b/test-integration/Cargo.toml @@ -31,10 +31,11 @@ edition = "2021" anyhow = "1.0.86" async-trait = "0.1.77" bincode = "1.3.3" -borsh = { version = "1.2.1", features = ["derive", "unstable__schema"] } +borsh = "0.10.4" chrono = "0.4" cleanass = "0.0.1" color-backtrace = { version = "0.7" } +compressed-delegation-client = { path = "../compressed-delegation-client" } ctrlc = "3.4.7" ephemeral-rollups-sdk = { git = "https://github.com/magicblock-labs/ephemeral-rollups-sdk.git", rev = "36f9485", default-features = false, features = [ "modular-sdk", @@ -43,6 +44,12 @@ futures = "0.3.31" integration-test-tools = { path = "test-tools" } isocountry = "0.3.2" lazy_static = "1.4.0" +light-batched-merkle-tree = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } +light-client = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } +light-compressed-account = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } +light-hasher = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } +light-sdk = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } +light-sdk-types = { git = "https://github.com/magicblock-labs/light-protocol", rev = "849e6aa" } log = "0.4.20" magicblock-api = { path = "../magicblock-api" } magicblock-chainlink = { path = "../magicblock-chainlink", features = [ @@ -79,7 +86,9 @@ rkyv = "0.7.45" schedulecommit-client = { path = "schedulecommit/client" } serde = "1.0.217" serial_test = "3.2.0" +shlex = "1.3.0" solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "2246929" } +solana-compute-budget-interface = "2.2" solana-loader-v2-interface = "2.2" solana-loader-v3-interface = "4.0" solana-loader-v4-interface = "2.1" diff --git a/test-integration/configs/chainlink-conf.devnet.toml b/test-integration/configs/chainlink-conf.devnet.toml index 16da71f89..e4d40198e 100644 --- a/test-integration/configs/chainlink-conf.devnet.toml +++ b/test-integration/configs/chainlink-conf.devnet.toml @@ -36,6 +36,10 @@ path = "../programs/redline/redline.so" id = "DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh" path = "../schedulecommit/elfs/dlp.so" +[[programs]] +id = "DEL2rPzhFaS5qzo8XY9ZNxSzuunWueySq3p2dxJfwPbT" +path = "../programs/compressed_delegation/compressed_delegation.so" + [[programs]] id = "DmnRGfyyftzacFb1XadYhWF6vWqXwtQk5tbr6XgR3BA1" path = "../schedulecommit/elfs/mdp.so" diff --git a/test-integration/configs/committor-conf.devnet.toml b/test-integration/configs/committor-conf.devnet.toml index 62fc3ef10..31192cd9a 100644 --- a/test-integration/configs/committor-conf.devnet.toml +++ b/test-integration/configs/committor-conf.devnet.toml @@ -32,6 +32,10 @@ block-time = "50ms" id = "DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh" path = "../schedulecommit/elfs/dlp.so" +[[programs]] +id = "DEL2rPzhFaS5qzo8XY9ZNxSzuunWueySq3p2dxJfwPbT" +path = "../programs/compressed_delegation/compressed_delegation.so" + # NOTE: `cargo build-sbf` needs to run from the root to build the program [[programs]] id = "ComtrB2KEaWgXsW1dhr1xYL4Ht4Bjj3gXnnL6KMdABq" diff --git a/test-integration/programs/compressed_delegation/compressed_delegation.so b/test-integration/programs/compressed_delegation/compressed_delegation.so new file mode 100755 index 0000000000000000000000000000000000000000..58aeba7f0cecd20e14ff60ee7f9c4e5ce15683b9 GIT binary patch literal 219624 zcmdSC3!K&0RWJS_B5FVKpGo&dcV{3<`O(?AmNi%^`u?!RB)xUD) zqzQX|^>%O%_QhS_^*z>4bS5(uI{i4)2JFGHHJzR% z8Az5V)8+4F$*csq3i7XPDDv^CznG@wJWl2i4YJRWf9L*^&-3xh?_oZV<8ssG(b*E| z$;Ufi%W@ujIn;vsmRq@e{1l5N9?vp3T+bh2J_%oc^NTg1(arQmkHw2DzKQYELLS=q zv=FPYFbVO)g66+{SCTY_EdL1Od_5IbmxKkRq;W6v&0fa1ipEHoY4xNxIemg$!j$yg zs*jV4(YNhl^cg+QkH%`^o88B_(v4OoNp*upZS>8%ndw8_kYbW7nEv7fl5AY9V3P53 zvB42PQDQQF-UE6omVY;6=bO_x`_)Vz-ozMu13u?lrJH;XJ$$}?>1FyOqvtuu_g2v3 zay7jPJ;XnIDe+fL{(1R*fRB@g*JpYbcJe`}5YLg0n-l1C;|iwF6yVzzE?n5Uhj6-x z`HIunpvF~}Pa6F#$yeEp($fArn6L3ZdoIm}Jkk#luJ#_ifL}+Zy?E8?Bog&YiN z>4ZLS)CkX%=jQ}}S~{ia$Up0MSr>AorSon1k81vNnm*cf#^RF~q-ZkQbz0-bQNuqo zNsdYWM%`dReFCt_AN}hcZ)xfS;EzcY3kex8Q+|UQ&^2~hvmMBH@(!WzQQ|EmFVP>x z>GD8A%pd4YvE#c`e`3CVL+Cg%y+hNz{Oq|}eA4=UC8}=}gh@!PUysaKtaQ3QOh)g| zqrAzfkbI4NjpY0_rRSe7lm&0)VYaohAGIXO&_l4=QhwFAmJ1KfR=Dy$cEswzlbW8c zqI`=oD%>DB3QCpO9U+(Smo(pIVQN{P-)a4Iv!Dm(?z~(6NZ2g@z=}J;d$)rLjvv8J&`(SJMe^y$jMvlI>m~ZPY))RJ9A1iyQ1 z{2q=^+xzcVIPJCmX^I4XpO$*lH7w_R508)^>9)06E<7?%2}rkXP`L8QB898lwkTXI zKx)#SM>I-i3Ym~tvCyI8Tp@XpKA22z-vPaaLP#hii#3Km;n!sO2a*3#%_jNh>yJj% z9_Y)FLP`5w+CM;gA12;(-EzV|$ntTyKag@md!S8%zrRoOIbVu`Pt!|_nZB*8aa{iQ zrF>=AcQc>yGv>#Qz-#TSmi7}*X|Bdcq(7l66cW?R8tXYC{9`)xZ}zyNpOh~*MSOAl z5o#-0kMt9Sk&Xew_mc{P&!D)F{DI~Jzh7v3z90Hkdg1JO?aiWRk$wNSnV|oRN`EYm z-%~yKvkTJ1Wb3dtH)(v3G4%7dY=`V%$Hc4gYUK-g|Cg zewX*F6-*j?7-Kx|m!E0>`EMg7eGSvA>o|^vCn%pVK|L)N=4*y#zxg4B!vyI$()s;K z5Adub93CFf=aolRYFyoRQt1p6tZ!(UCMUz&4r%(Asz;XLL;C>bAWad%M$v2NZr# z>R*m}4xUp!g`?I#M$Zy9{-FDm<=>rbM$%X-fP_YoF~*rb`FZCX2|p}Q)r zSIU47_i4R%-obp~P+4K%*ZhZRC+-~J^Ts`lAC!85-|c;zUY7Ljx0A5pdzpXe@D9a0 zd^`EN^)T)5_Sg|gZu0ArLaX387@_}n;&m4uf z-+r^g=>r_MhG#4$yo`3Gx{%{WwObe=VW@UU%MEw0PELG{^Bf7Xo|CZe7{yR z9O;;&_9@&i#7L;@Fum(KspZ^%Y`__mfLwni^&b(hZ$N&htEks*XTtp?xU%C$c`E%b zhi8q@e@4o~UT%=*g=8VJC9~O`+T`9~chGtFTOfaH_vkFPyO6w8%TJ~+w@dy)Ld4$* z{rGWd$7JK=rRvA8t!f=7=LoaY8Eog%Vi)CqY0nR6d(%}3^=5~HDi?^TalgT0)_UY; zx{BjUILvV%T}8VS?%%8#EARecjjQV@@1b|IpAWAaQ2blpy-A;MU-t&nhjNQvz@L9F z_;7{kp^fuM--Y^Zej935zxhvFm(uQTFa4O}8{)8@43}u1f|N%{&F=~w#GjVNHGf6) z7NJ@?q;RO)_}Qb$$=2(LKTJ?RtNpZV+k1~_{^3$h*`XDt=4S@$m}!aQRi!ef&#R^7T91#L;V|i`KEj3$nOz?4hW3d6 zQzKutch6w?53;?fjZ2}npY(TMuFrj*lXhQY@JH-<*^iH{R*FGso_ynqTrk7i54b*6_CgkcXQ$Az@t_|;4ON=su(=V;fs zu1{?2+Iaz-kL8YeR?Lqv(*xv#9{iB3RaR=J70;mQW2pUaw!idS8i(578+^iG`n_rC zIN|!|`Mmcj!V@f)E;Bh!uw1%~9VzYZ(uSoAsVCd09%2_{(Mm$7ohQE1=M4Wj!~dIx z|19DD|3Uch8N#c6iE(#X@uUmqGG5lIcOz0CM3TvY=001m3>0J1T<6-60Au(739US4 z{!H<#9ZtttJ`nU9Ire$ILGD($7fE-_7Q^k8ylTd#NYkG1~XEcRG5>*cgQ)sA5@CHSw#IQPP>orsxzDi^qN)U!%{P>jlI=+&@Ns z?J<7!Kcuja*XYkr3w_!8OGFnuf2S)PviZSdD;06tds6ESj}Iu^xLYHU-*Wv?UA2St ztXoU|(oK>1(guY?o#m^&TL@3E9cfoZ;Rgi^Fr{55SWn&X^&W5GZ*ltVT3@=3_?#c1 zMtL-D)yE0sC(V?39buPWyxs?W^>sG*0Z|yM+PX`zbGIfp<$_tDteZ~1OV_uy?vlY+ zLRxxC=^WZcL}}@m!du@bj*Wzwq6RWG%J@4|#Pc=C!S$o2*>wHrgrxhp1ADSxx<)v} zei3RXlwWhDeNh;@w#VinJDD0rsCP%EbtGj;DkLlP!R*C+UeMXB_*XP1ZT=PN?RjI9 zKklD{Px8NqF`pl}-|qb%^H5Z!{#9;07E==Edm;HZwnvbTeFyw>Ux%5Dj)KrpNdD$K zR{svjcarrIi0)G638{3@c)jHJM70XZXO;fhD=Cu6+IjX3O}#55wD+4uK4mEqCa9-( zKF;!KKjCng`rHs6Av`E`L8CT{-u6ixCQfO+Y5yVi*Y}wn-(z+>TgOD{BpL2Mr1`gI zc8T-T^x(LbPY+VB(Qh#in5@5+1&`~^Tp7Qc;X?9GPM4eQ$zby@y7kAg5yxN|1}B^O^j(g+&@d>trN7{+xuT^`L}3X?N2lg6B{)4^{6mGyPNi(qdX=! zE~WifTYlQhYJa!FL*|bS_m>TRsm8k*kf9XCGMc!+)ud*E^j^*$}{ zenO}(HkkfGsI$IwA+$;S#yZ=#ec=*?hw5zq@WLAuuGHDy>Ov~G`}66-0ev3oY;U>{ zQUX5e)%Jy)@C?-tv0V#y=<`aQa;+}>afL&DztQ)o!FvsUufo+w_FFppMY<3pmFRz+ zdRtxifWhns>B21vr!zJ%&el(;H>s_kh#glsRl7NkhZ@JZw4396sGYLsi;3UHx3s%V zyc`B)-RH}iZ@Bw_rVrJ=V(|MFE+pTl?U~FjY=T|*kY;OJe?fl8)k0?H3(0MYuQH$O z=ub-qQGsMo{1ybjkZff>2bQDKPw}L3{apE*BuB-50G@6AbB)00%JT-~kA$#@dh*ek z^A&EqTcc#2^g{&f6C*4VxKHBOSloDmw4^rg9Tl`MldL}O`aEZmq=7%P%} zno=FbPd=yZ9PK)L0UKVG3UgJjWDGq<(b2B+Ixm&=Tg^WxdVutJetK5;3_6q^sYGMf zPazjD=vgef8IGM-JhDEl@$6l!uQGyMGQaL2+%UbxehXL&e-=5?eO{jQ`21D~zuWn-;?xS9% zw=oX)v0Qo^W3;C%KfOKkb}>CXuvmXA=(tSfezA4{^JMM#hCGmQihMSH&$jlb8(9zP zM|rQm@<5+H@b<5_esQt-A&<%G{|D4Rg`BhVDv}4~oApyMUx@nu{F3Tt$9y5`|I8)T z|0?poPQ6~gn}+;S`2VY@-|*z+(6IjPdR!gWYTNo(3FvF zuiq_5B;?zFv3fs@dUN$P?aQkbpWBzbJ*iZd>GO2-g!Y@k4*Kmcgg(3vbWPD8%VC#Ghy5kA_ZMYtJe)+u6?bd>h z7hlxwHppH*FIX0Qli3ZJO!p6h(V3^;*NL6(@R-@*9<#%bnH}yiJN%d+m5}x@4v#4) zFk|<7FyB}sNqM_GSvy}Z`SN<%SZMg7ab`f0CTr&%Xs6lXtbN{I)02EVq4&UtakTed z(h1n@=e$P+kgz}mcB$jv%|chkcON$%A>MG5{UTI8rU_}c@w;-=VB>e?BL*A4D<3jg zqc;AAa8e(@Pl3P2TuHLznm-na2t7U?KX5`HHT7+l`b}+ei1;rq$fW$g8tt5-KMo(z zYLrftcm5VH;{d)$`U6DW*vx#h**x(ZM>N6hPu|YG(Eh@u*yZ!$NM_?g&Q9j(t=w1E z_EaN(1@^)DaVhnmeNOeWgHBby`(yL=D4EIDt=H*~1)W`L$1bK9P^`)NvHc zoAA%kAJ#u`h2{%&vlBO*QMkQduLHZ9{d&=9&DZSLS6_0!{xI5M^!vG4*LOdE-K=Nd zg-f-QKOV~|t8Ywgm+Oy@PgC0y`n#|vTZ98X&dfIZla@{@zC!Z7${%mX`rX`j%gZ;O z-#{NG)9;&pmdg3$@|r1WE2KW>TNufW>mwXrQXkiCzabmf zX@0YDJwJ}aj~NjB4YSvwM*gn9Ca1TH&Py)Nn_I@Ct2GQ(ns3#})P}S-(dHBBK?1z}1+PWF|3CrSk?uF2Ug`mgnZO-n5k+Kqy zx?h*+gZUYmKG5Fz{!`jRy7TgH1WOk^SS&x|{dBBf_<3shSNr}$_F;2;%FAJ^&5tlo zM?bk(e}v*p)*oj~CJ*CB+jui<^h&d%Jzo`Do|lKOuRP=W zf%)Ns)3(tL8vmvm2cM0-`r;(?xP54!H|=J%{_FZ(+%LVDo`B@ZW}m$#-U0(>hG+Mu&Hv4rN&DTJ=>rJJySm_$9 zOZV$u2fZ}A=lEPt^Lj8MdI1*Z`>spJ}Rbz}LwdHou#Uz6&xIW$R=v_bK@8>syYmA#Fo|pYw&c37!oKX6NR; z|2Ti0Zs^54$vPF?;AdH$Pe$*Th2D&hQ_vZnpg$fqnw@gLZ==~M_xm?OTcn@+dfr9_ z+x&hXAM@j8A(_WLVua8U7wS7TxoLg@#Avh^K;kkj~9~9>v;k9Gc*4zvg2{T zDPG122E2D>u&AqPGznC`K&uVjQ-*Cjof@F-=3+~oi1g+ zw%B7q6JUe&c^F+GEnLQca$b4<{oEaSW z771N_lEvr4v$ryTAz7q9T>dZg`b)f@Uq=0P){A`?9iN8?V_Z5Y2UT63t}m`9TOZ{6 zX$v|gKZg*{D+TqWcKhl`UtIs)k8r!ZeZ=S!y$7CH?_v4e4qf{}JMV7&x9R`?$tCMq z=Oq2Iko+#kE#KF-aV0)yBV>(TyOjEf@e0BH7@Sjqh5odiqZ#aEhUPs9ydPn->y*}0 zNXjVQc77z7UDwfIOtYwd;=O(Ze?O}sZ9tgE>9w9$0i2DaqK7E4#Lm6=xu(GxhUYHi z=J?7UV&|6bBI3!;Ep5r2TYB*uq`dI~xU?Ukb4rkN+}cjBH!Yo3*!gi}){FI=e(V>V z!nnAQ<+JlX#@DP}LLOQGJ^mf3JKN{8^Q*z$(+E${56;%x?jv6K6ZhUY@dTjaUw!|q z^d6?e-a>xqHkO+=%=FjoY4<0UPIl+4e%FJ^===oeG=0z0H+zWnczNxgcZ>h>2-BUO zP$NIxzWI57$CqA>UIRNuKLUIQiG}1o%E9FH>CRVazNba#;3M)OF53CJygajWbSO23 zKa_KJj&4Ack~V!5ya*XTY<$b~5zLk_$mN5?@6CaH?2+ozNAKnPi+Q?IlV^j?ZQj#k z`|99hMfiw$jtA(cj{}XROoczD=c|lAOO~<#_=7Nr3=&+AFbDPI0AVh__?knH<3Sp5eEI7-Svd?{Cf6$LaU^n zN%*wuwyE16o}=Fk{4iOQ@#pA3lRph>kK5;p+38IkU3xxsQ^y<~$I#xbU&e^WfcXQm z7CWeL#g6&f-;nRJFSO<>O1?tE@pqG~_dq|29hd8USjCQcsz)gIkz6_GS0Py-f02K- z>RGYlTAd#w|HfQ?=$p*bEdO`udEa73DZvT}^8aX#exw%?p8v)3_vY|*>vK@g@;Bw4 zvpgAw@=xTRV}$qj+VFfds=uIA5$^|b<%pMt2IXEGJ@@->kp2r%I^=PM)aU&S{cicH zNY3-o3b7L=-*5%(YPg(M3Hkpn%Ae)ywtP7JAR!*tU|&$~52ajj2FH_h^t{^Te7@Lk zgB^ihj_3OS_uL^iMTCx?hjB}?T^O9f_ipm|Q1300Kl5v?T|@J4CY_8D*e$eRa5_8k z8mYG|Fy50wD5y5kPJMheV96i>_>bHPnE3!76*ylX(*Hl9HJ|SGcvI(0#ki^S6^g0Y zd8^`g{~mIjT(2I{e49FF2o0cjOeu}aO|CaI;>FHB_OH(SxxQ-i1o(48GVTXaIYAEP z7qQK#Q0}2Qhqy6;zaj^D1bLaCoOp9XgY=&Xii$1rSNE1`J7D2 z<$Q2*4()0jkw1ug7*D0w7$=uR^@8u6qIZR4qZTZ7evjIzW_{K^sed4*t3wwBLmM<*WWb&PCd;lL`p5x;em5&PrODlg!|E25iT~R%kqQ5XT z&|hKor0t9CwcBO1@2ye2(BGfY_I06oEqXHfI3fsg`6e5$fHw%NAY#->CcCbZ{_It-#I$|brL$BPkld@tMAWq_5DR$pY<=l2V#?~ zGssYuY%;%PQ^(6_|2z0zZfqCs_&XT{EVy4)NN@`h%D+T!GTS76%w+->J6@yqG%kN8 zSDt={OcJ#GH5@-WUfn)^$;O$71^wm0yyh2XCPHx=2j-@8t5)-{g8{oNrGq zALWGHt$hB+C|^On^Va8UTb^_MDX!bJfw55Hq|^j0$^nS6$*J+d#r;)ee^5*(1Qu6rVzu4Jy)6fc$_mD=q4~i1t$Dc?XhU5q0=TIIcQ2ui$H^hGY zT*{*a%KxU8ACSM#t$vh1`Tv1(LtEtUb1RP$DE~_+H?#xApRxUzzaXdc5#@hQ;7op3 zOM-+VGD-+`|1Us1@p%d0K7lbWf}DKaCc`sCKj^ve6ZQ`Dd>HkboTo1T&z#R$K9ob< zh2$%mKH4FG1gFPi^rs(vaGeff8-C+GpY?rLq@Nx5O`UXK_2I$gu6JI}-=EC%btmyR z?_=SOZN82g#`qlik9J}I0{U9eQ9{-m=kht$%kuZ_e*U#wr$Q63UV`B3kAA<1-(Qt3 zQqPEobD91=vH(YrvEcKNo--8w(>6n|03B0lEo66TC9SgJ} zQ|G@ya(A@M$Dto?&mI&y&_(Z%_?*Pme&UUvL-W?i^VeIxG0pdF%6q;55`YKeKzqJ# zX+Eqc!t(pQsA01-T!QP>`m0e(vmf|-UIMF^DO_QUz&_vB^k&!TM7WYxF-DLO*O{+|*0 z8;|jM6MwE9RvvWk2i;qkKYes^x_z83$~Y;_z7Vb;zUH_`*!y*OBjIA<6^b~#k$49S zkfMyoo!_T%qpXqW&#n5y%#8j3T#ES{*T%{oh|zxM4`{q4TjI;oax_UOJ{Qvf6vd~ zXNNp~K;)5~7jk*K9vEKVXZL#;3Ub>vFk^n~LHYMaLYh9t-$>8icfW^ee(y`RU&!Qa z|7Xb?jeo{BU+2&E3muP`TZBiYKOwx+(%bfJ?jfN__W-(5qqi}@)O4xoqdWu!MU(r} z$Q;3b4$Q|#f6w0a@xKmVzMyg}B>zj}Y`@dw8j9BL!u8q@{a!BgL-5D-XtVKa$$ZMq z?QG*UEWbe3ZGqCq9rQc&Ydn=`_7dBtcKZf=$d7#R4=mq%mvnN%?tVnEK!Lh_z

    zm#(9KmG<(yF7l&4`Tktmdyeg`0kimNLbmRs9yVBz=suj4LqDks9lr0Go#&`?e>NOG zO?)d@-p}iy+${rQS89Cj_lTx?5lCdGyIIcdMc7KXc-bq+_XC7Mr~IG2*Romc1mGoC z(!3XDeJkFkwcKH6U>oxk<7UZ}70lMdkU4VZD%yPe)wr^1G`$BNPYmGf0 z>|g`@ePZ}+Epoe(6Av#}yv*cg^BR9IDm`d&E1BE`59H?eT*d8pCV9QCMP4(k9VV}1 zCa+!E9Nlj_lbc6Q8;Im)b~!bF-}|}yhyOR_HZDzw<@N=cC;w;44fVf}a+~oS<#s+N zx6i!(ndO%D9%p~G{^IxO1e0@dhV{E4X`F`Mk|9Fh2PGP=4;i z?>F=FY;NyAdOi8%Plca6mXVz=^ZU4F->r|cbxWUTKFVUrJLw*@*-O`##`{@5?swClQ@3(l|-j9j-gRbiHa6fN`d)D;+ zGwa8Yhw8;u6r{^1J9l*(>D2kqd-xkgv-6`j+jA){U0DO3FS&~2l2b{^aPK|i;g z(hZjN1=^pOE~!E8Ib=P)KAG`5nb?Uc$TEzvJ`KP=rxxw*Y|`KA5T|L`i7f87XH`C9!fpSNlHGJTx*yfSN#=`G~^ zHq3{HnJ;@U;~47?Pf~CET&3UF;PYhH7sn5LH_KAfY+Fe3aeCed4ZqC;vvXWW+5Xo( z(0;C0_0q;oUtjZjCTmAO+Og5<$CjAzN>=^$zg$ zU*95r+7|ZT)Ye^n{-5WU>yh#;cfM|^@Xh@mpKrE**YTk0eBaq!wysWIxvwMu)F8Mvj%cP+as<_-ig0IV9zH~C#(WB?#Lv63wzqv|hcydhP7~clqJ8EqI`zg`+#PJ;j+r+JNxH z7KNMprSD{Vo%DqXn%{SHeGw|IEh&*ohN${y|d=hzs58%&>dOpX&QPx$i$qzwyoQ9S6S9|M@F?9)No8{-Yn)(d`F+^E03G?@ooI zc{Il>S?WG*CpSR_U^2dUFhe7MDmbib6_hX>ncuxo?PxAbB{}$sj zu*A>Lp`Ma%V@mm1=YS`sL-q8CBqDvtVw+br%s+tqeqK6UaTe{?OwoTy%KR(9eFCGu zN_7`}f2ELcJ{T(J$@eRWzqx-zI6F6C^8nbb^MVKUeoQvpg3k}8EN}blmJj8QNjcwV z|4yC*_({-beDnEQd;8kY0lYt=&+TQUN0=;OutOLsq0;ke#@Ew9uPmU$HCT9&+UGUX zX{QI}JIn}!9X3u1|9&abC)jg{7<_?zptntJYx={5iXA*hKGJng^{~>T z*`&T3ln&oti1kkOYBBSn-cNm1`1Bx*z%ON-`Ex1vpE1sS4)hJFxG!>k zu6>;Ot%yEfKME^|2jeWxOX7RRSW579o=x(7baAoe{Is9Dex(-h_1lY`{~i77=Ewp+ zU0eb^|2r1hHP{Ipc`hXEf16~ygJ*&B*)pWHoPVA*$$4g!f9N)p7bIHlD!#8H;}J$q zUoV9{{bH^>Rv=OSJzAdQPEqzvK{eLZK<~3@=f`vH#EK=>T~6fM`PW*{jq`KiBG7Ox{nO=BOE zJP1eRd<4#$LvLRq@utqp^n}}{&dXIlH+6ou>PfNlE@YSY%J$O#=}}PMlbn+~w~ZCS zf}R!yzBziXa8u_i^~>SK&U^TtK<7hRU(D~Bf7{xRpkIaLajj@m=WOmDbS|UbcfP$% z?;pw00f`opAGPu?QH?9gd*6^;vGZPi-!-oPeYyHE)h{G_to%#$y#vq>yAhY0?A&_6 zKDx1~v!q{SEq30WfG=^m-^kSmSHFPS1;{1P+!_W4-L-WV^zCVYT`R;AY zw=Rbd`dLV*hv;u=x2(Ti%szi3*N?GM1v_vi>Tl>bZ;?JxNN#VVBYw}JkPNgvUpQ%d zXlLVgzckk#%8d(malRkQ;lmnwL06Gl^=C^CAG%~AS<*(|M{@YsuVnHdA+GmhIeh3R zeoi;e_ggu9%=hv(e1DX~hkoPlPsjK=a`<5P3)(xI_Vl9i&6UT@5b{lO`^9rqzU}YN zEzgx_doU`u^5>Pg_Mjf<&4F8+_SVlq1sD$r{(b8s9SgM|LvLjod|_yq&wV``^L5mi zZnSj!&RnsR{YS?KK2p2j`(s$QO~&{DKePQUG(tk7tWkE3%K47(Ct~Ic{|B+Zm$GP@ z@D4Di-^sk|Sw7qE6~0Q5WXa9=E9Jpw3F#>3JK6plyOHjjac0l=UHm?qC9lC~$@}SAeN$Exy;@Z`rND_#o7Jm6P*7oxl$fTC?wZv9Ip#tKL>oePM(B`bBaGa zc~;|CujKLAwb#HZNxnM$7(Y*Qt!4D@O2+tc9xm-auK0Yv0QLQZ_~E`@?B{L5QS#5v zeU8qg9^gBi;KZmYOF>5?&_TWOeIUO#w4kHEJg<>o&3wd{&2Q}-hPT7 z2w^ehab(s4?T?V3_SXY^p4#_faL>2)*T^P!7(ruK#zn_hH20TOfSBPDo(u1Ts z=0CDz{Tj%}LhoDp=MeEN>7gF@ z{3zZhI-c7n`id+O&3+%twe{0QOou*e_gP{-NRDj3x{&;t7HI3Aa2h1w z9FE4;fBl>}>igiEgijmz+|P0NJSSa6{ZD0m4>`gSTSu8NeR_iZKD*!l5c6gC9#@EW z@Uj8z-<6vP`+FMD8|fAo(ub)}GewAyNcw8#^L<*sC;p>bDKEbVVErB6tr;8A287L` zZ^*Gp_RoPA`_+iOTw|+dOMd-L3-rf4E9d*E^H%Wr>keQwUHDQXjPVHe!OwF%n|ndu zgMK~C^1=9&@z3}a4%2>Q{IdFwT=o*qJB@zdw@drUcYhxsT}3#`C-!Ed6*-Uq(^)W2grhaJiFCGKyMO}@iSy+OLMQj(Lg^*sv_ zx157Px{pt8Z_{aPcp-VG<{zCQ6-yYM!Ta?_WxPWuB=mQjf3aPF9n9<_fHCKS5_{{r!U&<@}CZS*n!yfS0(4Pi+ifB5-yk|-uph7+Z#Wx;(C<5uV(ggw($l2 z1)5MDVI_VJ*YWsyj?E(PvSfMC;&7ODafzMx^Zi-3yME7Oan>BQPxe`+OkXi>qTbX7 z;83R>zIGuQT1ft${i#N}+&)3?^5;K4^>tYv%-I1yCsIiGeo5PSbT9Ej?+=MBPCrN( zdWew8#q>4SKY2Xn=L!5=R+iuINAmMoncrdKjPXD7JKV0^s@3v*S#Df?-)&+aZ2Zjp zl%34)@3$5^=n%NSfpG@oLS;AerGAcU58-Sa; zQqbXU3v=g6}`a z;~DpVsi%-%%f67W7exAXRQTon$o=}!u9LP8#QQ4aa}dWcUe?)8?0cR7Z1P0AzHp1k zErMSy_SM_xcA=o&z3AI+T93*@nDt73Ttz=*>&;@QBy7K(hH>as3MRwLq(TX)%`?zH z{z~c(6ZSputEtC+egw~Zkl*G#m0e|lq@Hf1O8vVq%aG^YT>lKa%pP@L&h%x{VI@@U zyVj70pv=z6RL!6KfaVf>W~ZQM|0Hx_eGx6gc!L=0i)b;{7bQtzTW{~ALi+p^{e96S z_iBK@69dr4?NS=$?YH`RqFS|`PQ`HEnB{u+wM1+-GYw>q1f54 z>!2~dpNa5+-w+Mj`6B~p=Nk?GExPXN>(m!pR~e4#1wLq^uJ=d%uaJncONi;{&d~u) z0v+!f03A0YdrSUL=JLa$A^#5!Aiw#+R9E$5w&(K0av}c)&A(WnR{V={?SRH3|GEL- zzYf`3=y`1p|4ZdL^4~Ln{4YcH?4FBf!^f{qf)9L*_Xi5PYTHW3c&;3}7U;Nb0Ce!& z1Nt|5{Ij8BO^y$~PIf4gC-DD_;P?KVy^n47Bba~F?jQCF6>@%K=~|%(=Q3Cz9zW6F z-GB3aIr!r(-?bLL0Kt-}&UN1^4<+b+#(Vf1cr*X+dZwXXT2}f`ORnO&h}&QF2c>TK zS^0C_SBOf7rL>Dhxz`e5W7mlbJrc*`%qh7zAU_U(e{Wg9aY`AXrDZ-_I{7X9l<&-I z%4p|+#mg=3vzYoj+F4c@b`ph8jZ_>wVSC7k2)ccEf06*<$p6|_b=^S7D{^e0= z7YtrD@1wa={XT(u>#WDeIn=XO3}^#HNhr=LYBSS==e`Lz9X-o<-xl>Nn&&F^+j*|N znhyHn`*Tu%f1w5}>|c9j{P6SHBXZ6QavqiWdRb$R?@GVGF@Ls5xyk5$Vwvb=vR~;9 zN5_?p?0n7>q|3+6HIfVUW$)YWqBi+?r|f>7U9I=?q@!n4&gsF^jJKWA812=H1*e2e zALP5N%CGPw`6ugCELVC;V_C;C__)D_zjT7|6V$6b{%gA*)rw-i%+2ZRS%G2K$0aA^ z`YzEQnFo-b8ta#HbXG6xmwOovX8p3Ssc@w>rg5?38Xf0CeYL{wr^vY&gQkGZd6dtOR{^8!e_MCd0_Kqp+{Sx<}f&TEpGpH^J2Tp0cz8j-} z^q&J7X}`yltRMHEQGIBdm583zn>f}S(Q&vQwZtd<_tng5sd>QWPQ#jp4d%OKE z`XRDzZ1oLTdCDp6S!wVId%oJ>lLoIzb_R+Rn?u zGWy*oWV{GZmK6{BG2~=nZ2dKJ^{1@=P#^E+6WC+*uVucb{jQ}W)K!6p~C59Swu?kLDU3sYpB z-|$&KDJ^GztFa%Yr2&Jj-;_?7{jq*iI<4?f?RQy!iTX8k%=l4bt?CEm?EX2kZ;(9r zeCl@L&l8K4&#_!i<6L_r7o5KbKfK&;OSv}tdd(}f;&k5$rBA-o#Ms8aaMbMi1LxWP z%2|!qcMD<(K0gHAcmnzg=uboXI9Vj=;lb>3@&<1{eP|6jO{fcnYwAb?9%W1cA9yo2I z`c23WP8IZaHPB0Y8;)@tYWHLRR`ny;cNE-i`97_G*Dl=+tiqp~M)a4Uf4d2TzRSfW zu2_A8vW|jKNCq?>vVE)J(qY$s+WFFl4d%F*mfmkL=}k*NrtnbhOH8jH(m3tqJt|@^ z4gMp0&ha7b_3wK4`0y!xK3pOm__uJK zIPC_tpGb#k@^z!)w8c8FM*7P&o%2f6GecmcW1d$?ULk)`|LrpSZxKi_{z zuN=?{)A{62dRrgix1smS`dKIALh^v}7xcVL(tTeG@LYxI2LPU@Fy}phv5vr7^dzs0 zGaj_P7)&I@?Y&lDKTqKIqXQ528+6`$WJzS#wAMU5! z-z;>LrM$08-)a0#S5e<$J=XQPI%*R;Bjqp-Lytc+Ke7jQ&y4r`{CTg>uNq-qGIH)(p9&w9f3%$KgBKUG=0n$NEr+lw?4an7OmSS*ix9Fu)_KDYV1_hZQM*WM`YJIv?dfXS=Wr*Qd9%J1m|2>#Lm-#(=DXW28 zvh}8DU3${>CObdVc3#5dr2BF2Y2wY+o1|?BsMn@Os;5h?WQEUWz3EdzckE})5qmH? zt*m%Qr%?|^r&0c+wl6hm%*6WHxa5q>pNBs?p~cl6!0&>7y>$`n_#dmBeLot0*9pm* z=bPUvm)beBw)LXCyoXpH>cLW*&l8I?q5YUouGD%*<^4&(qg^{}J!p%{RVn2|9i;vJoWy)%lftPvygm5`IGsrM^BPp5fVWr`}2RueCD^xda(A> zwBP!*_4BrIafGCRAHOQ1YWG(~X(aqt_E!&zY?^-Q4=bNi^G}1HFY8@n{5#I){qH1v znD#F9@$!0;56xk_UAxNd8udEn_gvAtP&dD?_mI_ReqaB%)pv2f&(}TtzQ}Zi(Q_B& zIMOv&@#Oon+rilXg8s()#;}Wij?l+*_al70`MKLc)B?Nn1C!GAeC*7l;>h~>pyCWs zUJ2>8m0HjLOLpc@Uj;i;R{GlPjJ?O^{2Ub8RT%#Sd;pEe>+_9xlb7-NP`I{^#{Q z-ydFh{TByP!n5%M{^zS-fN|L5341;t{X_gugyLo1Kj;^x2mjyeCnMTV_9(sKiQIVX z{p5f?PTKZo4p{%Vg3q)4nFFo;Bi0MaDd*ITA6E>hp-MLj<0SYwUq1)#erPrhKY5b& z>>0-4*#4_u{FtIwzxcB483pGP?yq&Oqd0OMN^pggkGWcU$0Tw8odRKJ>Fa@p$_~#8-N&;eX2T-%Hr;VNCluK1_8lMqX}d zH|3RmuQppxVS7XE53F7rS8KmxFxwmIzhQK+yC>B1q6 zE4$gB(615ve%h_OxDZj-GyY%E zhwLqK-zoI~Z&}ho=a}H#EWZIiXmL>?)I&VzSHJ{*jK_M~YkFFjhZ3+Js(RXMdMfbV z*UBFYq8F;4E%Fe0(%#jY4*V^8-D`SXYS(LNg6cJ}$9$ZFeXlwz1U;_T$7Oylawh-k zWsT8(*K6RhApO+$QJjx_64x`H{fdz`z)$o&9Q_77|I6I-Mf#lc+4%e{>_aSX=(n$ry8Umg1Xa}cYeY`IuLk{qUdM7(`%~5I z+CP3o*1MpWQ5ci`C+*MI_O!NF?#HqIO??j8IS}e`m|%ZN`zgOdvR=`KCuujazAM$q zJ!{tQgk2jXN8H}LJyUwkPe*(4>2K)uH*ali*9y@C87EX8F~5RTh;pxydQk4)q@3)- za=m8~JGE2sl!?dId*XWk5#^7XUR364`((XX^M^{=;68&F8@yQIO68ova}}0*#r^{c z>At-hH}!u^;gPO`8gJ_w*Ep8rPyPqxwk6ghY3B;ThjyM8I&tsT27w!NGZOG$`D^n6 z$UR>k9ief*{E+}WUlkWEmQO5?P$!>a{`|J`m+h;Tb}FA^x*?BHr<`KB{DziuyKn3I zHS$f)t=srQzRLY-29xh{4_jM3+HY+B5Z|Nu8N3Ja%9r#36Agb-9wd+-}jW=QzvWH2!*6Xf9xn>$mP7qFV<6~H_~(12=MEF+Gzq*iogL0~$BYzeOA_?*yi@`#$ghKKMnr}hp zbo!NoKlUrr9?h2d8&4BV!b-ky)OeT){#_A#7YO4XQYD(XWD6ZvltYba*J-W4IE(w& z?8V4!>(>04;QKzw7y)64_)KhP<`?bYvw5o-`+G*&e%TJ@_xp0Qdw7LD1g8h}peh8^ zv*ZD;gZTS^{yi70C(g>f_fnU2fbhn}TG1N8E9UD$zCGy%V3s-rEhLa`WBVH$kjlEj zm&T(CCgJVWt8g!2FQ4vbI`E(l&_r^v$`JSEg3Hv_3hWdp`AWZH(Zlh~C`rRJGdmm%a^G!Y1a*OJBrM_I_ z!D-^0NC@@26i$2RntfSi@Z|>IYVc(S_Z!@yaJ57`)Y!@T!u$I4M{I8sv3Cu#gRlc- z$pN~qy;bDBo8|m{1n?IaHp{=V{Cm*iqTa$s|9J6iXedM4%l5=}Y?jPF(%zLyC*;)X z@0QJfn5!{tQj7m==VSBq`#IXtuC zY1a;e$+xs?r@=dwZ)w+|@IokfMH}BST-@lS`cb5K;rl+gO|H^U>5S3+* zFum9P5c5w)=~s<(-Kze{wl2;CVmaR?^Zv>F&J}_Y{c)H4NDq>a*!M0=dR$)ZY+b9@ z`8f8uwcJ^Gk2BL7(jRJOUrUFSo=|&}>WSRDXz&Vy_Zv(*otE}09M=oG7TcA@(hk)7 zS6_x-M84ykLywRq-z`-BiGQr}S+Y?1o;AOCiDT3W5`Mf!jL*~U4fKlRD~_<+ot zvh>%D+@yz`|LN4sUzT}zmi~!fTD=JQ*H?bzvohb&^q4=8L(HGOSeKeW2^lxVJ}UnX z8!Use_xHcL4*i|{mV1~@4wQ%Z^#)U3;`bX&d5RrulT%(lR?Bz*IgN{75S}F6vY<8w}-e9vc{(g#$8MzJTjmF@cn0NJ)Wvb_4%>ChJdtRjN8M^sr zHIJNMReMso`4LU&wI}+ zozj08kDk?dq-&by+t!t69JjwyzrgZYzJSiQX~NyimNSMO?rjqcvH zgfF*tl{P30yWpWye*E7%Uv!s}f&Cwlgmr05kTHMBzt{UA(F4%^KmGxk7?A%e`I+g1 zz|E&}KA^^aP)L4T?WWxGt@I8S`; zCEz3j&K=K*=B1$TVV&=-wfZ@JHtFFw8!~#>e?;#sKl@Qk55~WX(Q`aU&*=yTJyoU0 z$8qvS_G@f>F~6hc{g`yPUzzz^6KpU1DU7cdqh~CtAN)BK!Jr3sWVn5W{8#6m|AWk7 zpASEh2p{U-62YkdE~y{%dw?{X=iAu+(Ju03v`hRek$K#I;n&6fCinroKP~=P(cbq@ zQBFcE$7kYOp6=y2y8Bg*#2XHgPS}ws$oOzh$LW|}^@r-rg?X&|rSFoZ$)^AKa?Ow) zq<;`HKa%|;)ZBmc{^Ne5_b;FCxSkzizp7ci!tbf*xcOf$<{#7EHTw(wpKz4ze>UyB zv!$IBe8E;tWZ%R3-6GnJy4j81d4wm-ZuDPG*yj7`s@c|mJiq57UFj;5f5dmR-|ZCi zW$Ap(xo#%jOXfHEoNhZt|2gt&)NdXl|AYD6SclssNvMC~^`iHq9A{#GM&)FF+hpSu z6s&O{@d6Ll@T&XHnO)_$J6!sT!5nw1r7s)o{^wbPIj*Ip4)yEAPHK8uy3AmXYia3n zgE_9HrE3(Hdw?|mXxC|rPg#7@;u9K=bS2+}!j7K42ru-D+z=3nnO*!}HsgU{OYLk6>-!;Vch-$43Q z^OM&3cXK{7+5AV&W2;^@=LkPM7mFts+n;)CpY+c+E>=D~`~4}l zzp+89(Yn#3Y@g$~?oUZ!%wMqXiIDA2^`2Hfr~RiGm(FM`_c|JE`}CvS=T1lGd=2Gg ze@fd^E#0VaJpRp*^BK_Vm61OhpU*fcA6)Ty>1H7Z`Xu|x#4{h*W&Q6;#xdR-agNB+ zv;C=kq!as6aKy0>hZd$rZ(Ke;_Yr@$>zJ(SRcaL-|0?wz8V^oerUZog`#8S#a=$** zcN@Gy=?(Q!gKsf-#Nee0S4(yd;tBFOY!+pZ5YyF%^DJf`;{B=L{$9zqQL|_FO<}$c z44dU&S^hm}aWT0;ez9M+?oY8jv7b0s?33(wD4mc~>;6=q+4aS=Pf}m&{#04h^YqJp zho(<WszY42*Zx31*s`ZeSNyNHJQ`HxWFp!`F+&%5LG19{Jy`YARELEhKbajAX3 ziusBiZ_<3lj@wl~rrxjGta#e@t8UZs>F7@Ok0;q*aGpe#SuVue5eTsE z1o`=UgJ|Cgk+aW7Lw%OE+xN+kju7gJrmqngNrN4_q^~|xNMDJa4#Y+5C{OA&1 zzG&|S=I0we4+lS5?5OVQiQK#&%3K&>UJqlZ-`COnd)saA0pXCP1n>j&pnk*%sPdyl z3UG~>Q{A^m<1FBL%ISJ`-%Psh80EfhC;OS5vy*iS%Ej-|TtFn)rw7WXsR@HG*jE-_8EFn&YicoANa2n#g!1C%o2bT^x^ymVjqY7 zJV-ivl5*R(g)xLHVbJb}l=ClIFWyfT^K>E1;r>qOE^EHZ)r%R=rJbu>BZmqkgd^04 z>IVo{ALjGwyO}@TRMvcH!S;(@O}f(s)I!-;B|WAu@;x^_M_ruGk2f^BwYc6la!!8w z`{s+hA4@y5z0=>O{W{&YL-F~&(sIv>!kIn5;*kWjvn&N=-A|uq_j}gZp4m3 z{Nd13ny=dNc7>~Vt!4U=n{wOV1nhbY}OK={CN=BuAzKD*Zl<0M3f`32}t`#Bz9 z-va4!K2Yb}!#Wybe=l0s z;RFs_nNRi;w0^%wJEi#ay$O^nB)r!oY?-44o-O`%JbP5{DjTRjl_Bbn?7M0Gl}*&6 zFjUrhWS>aW!$uM${Y>r9`YTVe-_$gluB&5QOMAyPKgQYBQ9nxi4-wA<`JArXudwWc zYmELnAbwIfL^vGbb6Ia^xx>^Oms6N9e6?0N6_QUVT{7Pw{u1NZj+~QoZT?*fr_0~p zgg=5X*|_!P*FjItD_%eU>v|sQ=QMqd=o#cSII~;v!2Y+6R~%qouyINH6#~X5)SHb< zRO2N3u7UMKv>TQH^}Ag}lb2jYM1GD?$H&}v!v9dNcJThbMzT*jeyyS&`F)B$K87bK zuf`^+OhPzBxyZVV^3BIH*B`(4Ak*K@E46)c|BTA_+FLn52X%x6pV^PBzeyv(kF39; z(GtRCjQw5`e3usa3Q3uIBlmJ6wDm8u8^w-))O@Zdh2$R;#<(kHT+X?&{n@#*+Z0_k zZ*qS&%wT_Vdl_bwHKTkVL-|q3-q&UStJr-Km4y8^ecbTQXFS{Lsdn19FwA}}^J>Lc zoxfN!%6;4lr}N7OpETI$*~9jyRWb~AyH9d9m@M~$QI6g(2MeDg-)biJ(q0{Z({1}L z9@98XP~Nb=K;?c(IC5I+_w%=Sj{0Di!Oy`N)Y7naob>L}NbL>e202j9C4JD8i`lEC z=SklcR!-~^@TNV42d5E#gX*Msq1XQ2EbLP49|f*5ztfx1Z{_^l2G)67@&Aq1@AzZ9 zAG`?OLh_fSd-Z;;xY-X}Uw?<|CifoF=V6HSW%{4-1>7N@5c_wu2j$*rJSilfwDlLt z^~kip?~o=W;cdr|JqdR`rLn9tshlcz{j$NPr*Hoig~L(GKU9_zo|50xEBCiRFG|Sy zT5~XMC>=Jm-2)VzPQRU8J7shs+5uq0lwQ4B=}2F* zlJRELBK152A-$2>BJN!Vn4Je(JTH z&(ig;wBPpedwu_&`}QMUE58W^ipM=kk@qF`>i8X}Kc(p$M=_s;UcfIbOT4jbhrVwY z=Z9Y(&u9JpGw2QE_I*;YV(*^~b{tj(47H^;&*u5wQ0q1Lysr0!TFKy3=LL<7v)37X z+F<6B`%Mfcp0xCL3d=b<+L;<*@n;NXepx3rnE2$phrz5*&VvvhLhVxjM&tXi@%_HJ znx3w*^@4t`7o@$d^B9b)t?RkuH_}_ze{DTN(p%SyDF@_lU0=3!jhd37cIs|1RIwcJ z{#LvWt8zMQ`NfX=y2Fhx2Y=71o^8Emw`Qo!eVFkT4{0pljiw&f$w&E4H2G6Mrm@`9 zWbh$_PZ&%&q`k)#u5|yR;v4PadwsGGpy_f?vBhUCzCz_V+EudnI*WTPzRBW#i*L1f zmBwLYuF=hY5k@FCxo4K^#MY05E`v3@oP%Y5@q5T%zkgqNU%C90{FU#Y$zS{k6I+xY zgK8bQ?$mOww#Vu(oh3azR=?|O5A{jT@7QyzzhwHp)XOJd$CI$s%Ul2O@1T{=vA@{3 zBj<$_&i7CB!{M^vIZXYOeP``Y?za~77CYzx^*Ge2*K$9tw$s<&Q#*&4-bQ-E(ful~ zA_iy);R%lS070>T&%)!cU@5=6ftpVvzgM03 z^@q6sJHAbVFWqP72qrjQhQl03^*l&{mvwM57ZnivH_eB1$ z%0&R456{POGiUeWs}CSRLx^#7&F!{#BY{+4ns{gcJCm$J^T zb};Ryz0CG!CfF{lv!FjgN2fY(nLpn+o}(LAy*9du?>lin%xb)!IpzH@YfWEQYdqOL zEaVc)Z%3~Gqy6Cjov#G{capF5F^!RqbsX@AD@58(xtEpg_3t93{TnpDe{VTt_rq)< zzU+ROm8vhP-E+}GS8ja5`}(N=9ajonmvBD}_!f^RK0n_nqyz6C_M^R|=egVu^ICb| zDtixlh(s$cOBnn6>0rjpgCj%PYn$^n!laW2g7mm0seJ{X&(u z?3*hddC!n~aBvpm(Y>s1cb`@e^Lek-1H5H<0zUkz;N2|00Y7MQ(dI?)V}SwkZ@C|4 zl6y|_dfIw^V2jd~r$hWXO$Yv#^9Uu=>wWEdEu@Pdw6|?Nrks-AUVD3Q$Xv>ut? zX&mdd3@IMQvio6nGbyI`SdMS+pOkNNwIJH_O!va%`Sonu4|*rN_i6bQ?f)Zf|A4iB zWn25N@z#q;E+h|eo%SdbGe5mg-% z_?~QxANPbzHve2<^uMxS=s#?7mif*~Nssv#^Jy|XP%!Xe<;%ba^4a}$_{&g*m>#{i zWw#_s2or2K$}iP(+iP`vZ_}T)`%I`e@;-t37ol$BP_M7|3~*fX^~`RmKtlU{CU0o# zAB%JAz)G+A!H^4R1HA?Ct;YFl)1T&iGy5)IG#`Ja`%J!Z3*=0HI_8(&r!t}B>puTe zIEQNd108>ib^aRt_;!C9+{gNrbYKbTLX8AopzRLsk7f6na6QfWR7k$b`3tw*k_L=` z1o%b53grboZ%r0^bG`l$|4i+YpCbq#f>wzh7#~4rJYNQ1%kmuU`D@`r;{(VoK8iS` zeG<2iXS8yV#mUHjlYXRfkNJ08?5Mowuj$C&YL|JwJY9EO>6Ll7<{RzW@J(<`=6T-) z2V;DCpG;j6CBQDOHNU1u?ebp5BX&?@w8#Ce%UJ|}$9&(*nu+(sHGev4c5&Yhh5h^P z;lPlVmVQP22i|V*F@x_i_>jSGGI(5JKR*`s@7MHCC-2fYJhqqdft{A0b~hZ@Veo*K zuN^uW zGX8+`vVX#KG+u&#Hxc^q-)<3p9AHNhKac&;?}0DPGMLoMI;O$ICApXUV9-o=Fd`xDk*)6xo-uQ*+AGFTcU zAuZj-a-(xuf5qGPcIK;*pEB=ZzI}Tww)A~FE#GP_FZXI1O#SnFI^uqaei!cxoyYqy zb;<*L{#Cs9dTf01@x&CNpkGge9jARrOTKTgf#ZkmgUL8WxS#eh9Ns}Xr&u@0{4N*~ z3DC#ZZP#F)P@YEK84y=gLkO@$at%8 zn3$_^<>+dSi!=XOzdJ79ZBV|*d(-4|ZMl^{e;)L0t-ZzI#GcPF_?$iuwFjxEr9V}@ zm;HEyPZ{ibe$rsmbN{Yr&Gfv)^=A35fa+5$XVuF(sNwjh`ZQs%Fb#SPxqJow+_9&$ ze(WtD8`_GFdJsdtu zIjkaHnb+7l0^jRPd;1g~>EgXv+w6VOxIJ$c`!<>Xy+Y_fdwxOM(|ErC5^%ohq{Q)h zuF5U)zb4y{0-oP1!M;%c(vq*Mn_ia2l}_1*({{?f9^qYDt&9VFY%u$Y`0onG{DvOO{6g!C$C>?@588UR`y=4nKMCJreq%g}=Z&h5#{XCk-y?F9 zbvms_&atSS8tvL*F~>piOAY4uDen^-yxQUo7O&O&Ib!`&xtn}^eDrf)G5xQ2snB1e z9HHMQMepNw_n|$z`5f)N9sAX_lUi?n-m$*t4$T<1e_X~LU$;T|FTPybZGMdCjpAwE zyLHH5*Q0TRUC;ItuFp}vh3uR{AJemQ3X2VPJY|C&5B(o`Z^`g@dQJOl$j&!BX|ND3 zAuWB};JF6>vcfU{PK)6Xdr!O*eHx?POR%2*JpI4eZ&3M0_E`09NXgQ3gah&u_1>s@ zwp!_s^8lp3=Kj@5gIDVFYIe`mTFXD));_IgBkKYFhh_gD)D!ae@OkY|@Oz>#CgYLf zyY^;LFbM_yYA5MCSO&bF{jz?VcqW+N&uz#(9h#oD?f=}%a(6j96|@LGL4eq934rKOyb2VGE?If8ggM(Vr2#KgHw3K`Fnahwb(GN;tyt zB%8O$nm7XRAAB!=17&d<%lSD=oj3C_=QTakHA6UjLfb3n3N=PP55SX}9~9OwHLUH^ zA7QuYuYFfK?3RW~NPErh@3wx^Yj%IPK9=WZ_usx>6T)MTXAbFj%<)hj;W5W!`tg|K zp`HY@r{Ueq7k03Gc);+wo#-&UcNxAO;@Q?wPVlOe%)3<&(w^%`SN0ugakeFd>^sso z8O-r2?dex|TgR=s@fGT%EA4rMfaBPh5kd-n=r%pG-Uat?ehPW?_>D>&Kl7R zFMXDHOi%G%8}OiC2zeJmL5z!paF<5SbIev>c3xdzgvxZvL+&+Ddr+Cq1*UWr?NMd= zV$GMXqCE=J%LdaPh3Tdj?oSo*mnj9preB zUd3|WUjL3~D2(faa5tZ09Z2zBW_YdN`hB?-d;drD;XF!I?EN2^&l|i}5y^Z`>kl2P z4W31M7AUCiHKU$FVs@~fewFOQE1odzkY)^Ir{0sTbjcsY#mn$l+RrFy3@ezukDOY+ z#Jmq#3d#Q@A3Kg|#&FqF8vA}(Wyf)aD-ZlK;|G65f^8ZgVpSQJ>yq)|K0|}cZl_@;y5PjrIx`z1m~Bb#`c9e+Xp|Og|3^ZfZ6v&{e4gKldipq z>xf~9O+mkfopC$n^n|*NFKvE;+3TF2kf$GX*QEW0!*%l7~% zPdhiAogbY~`U=VI%y&8A$^5=qvSBn;d-CfeMO%FUJ%$p9g`O{EbjJJ?yOgCr6|IM( zeeV`qkgcDJe*B;yLi%zsvCVT1c3-*F*Tp7ieNBG9LsKjEeb&Ja)(-kw?t3fseH|HN zKJS(1__v3b1RCn)D~)vm?ugS@@{ zG2V{|Ub%Nt^H*wRjhpLmQn7^mcfimtApBHC+$=3Jm zJY%n&$F9w>@;vvrwRXM1r~jSp^Ye_y4Zc(9N=qM8`N%%7!N&|HU9xUtu&*DE8*Kcr zd+SEJ$d_1tsuy)-ul7ro=Y+u`4CoQ`WXCxfH;(Sr`XMK%x`nZ8ucksa*Y}*RlcXb) z(+R?voR0q=_TB|NuB$p5K9W3+EE1&H8Cx0Rcof^QB1lm}5*qbF%*ip#$5fa zwcdA~b2OF{2l&75f1dw8&tq@B`?B`lYp=cb+Sf6h3GlmZ>2e*c+LzUGs(fvdOtm~O z5&6<`AZIIyl;eE{{a%QUBeINfVtNlsy07NFcJo2(C*$vGv3nLuP$u}WrAwcOZr1(- zGsxc0*Ms|V9-jQiIaGfKR@dK-359g|wY;*h%F8P1C#KeGPY9`0x35xr zHf3MDROhoI2Re{yx-F7!v#1$u*yowLsR@d^uD5aa#)iVrwIS1bR`tJ>)8s(ji#}<6 zZBWO1+T%*U>A%!&8HLlR8M0r73GW+$|J1O#>3n*XPO99S1S`E^nJ~(E0E@qG z!5ht9P3vjx(!Neyy)LZv)-QhN!qoD-;z2=e7A>U&<-e!wXnG-~ZXk^3{;ui&38KHF>TNqg=`QFv=ap_fAl+s0EA&Cs6P7`moAh zIDv8{>%%B_vObJ@Me+JD>OE(DSO{r*EWEe-xR$cz>6sTrZBxd#XlNYnJ!9bG1p9Ft z>F^wl%Gq;QeQ1lOpZ!Gp=S4qReD#0o3vM1$|FGjf7t4NY?0?M{-gbrhzv@$;{bxC! zYX173zlx&nGpbvvTw?t#E${M-jhv$ef6RVOo!5|cJfl<2eV_D>@^=D`oY42)()~#H zQ_}0=YL}(MWm+NO!K`>odFp4C-|6srwO3O8(E2yT+w@0APRE6%nq1lm?GjG1J%+%~ zsDpfLqd)n1z0`kARerV)T9u#n`#+GeS>AWC`pWrz({rMaiJOSQ7de+>IAgO5*l}~# zSs0$Kr|KKc=jMlWo`LlG`)X1>|Av>Blsj5qD2#M^)r*?`DoG!`0ZHV32kh$!lsAgc zeWJX6?$g(O=A-uvp(nqOY;;u4j#aHUtq}(5x{a1>sGzXGn{&_ zRNvcfZlaPDI$ED^kiO2-ei!tED*9g(`tz-q{j2DpX{a07=cu2R?1OzZ&oWIT`v8X3 z?v!ca315Gg^?hr1T?bN)6t&G7?psu``ABxrdzFl=%b#yM2D(14{UH;KtGAplJn%zv zHykuK-5&jtPr4t>ay@si?ZXb39ESJyo2$PgnEHm7Z{2T9joJo{uB`V^Zl_K=pFQb( zD#&Mf;hms6voK=b`D5aj6GI;qbDhL^H>{Yuk{ItO7x}wDh00w_{|uA&Qe6H~%TM;REnMt6 z;@gNv43{eS{c^Ft44+Q>z4Jfy_n%PT?wSGPXQA?Hmg7zV=Kn)1{+#I3}iBkm%8fOwGf9x`0cooQ~Na@5?SpVys0Ki^mTch=9d zwI4-)Z|Jr3QAa<^@h#FlPP%wUNzQ2^-%03)(j_Y0a^A1q9I>yOExgb!3mNi74T0fO zJ9J~zP;XfEj4Z#ND7Dl6X`}v;Xu4u5>!7B8$_-e@i5faB-M--FMVh1P<>u7;X!D)B zZ58^m&t>({`1PMdS*=_=H=LI9gR&kY=V~p!+8G*8&efZJkO^k20@+3HFuPgwVXf9M zyXdfuXsU;^g4NzwT=o0FGEK1Y;JIHDJ;)+tf+rZg-$ZXdWlq1VF*-@VbN%7{uE*+8 zsJz$QM4xeibamey-3ImawclenelLEyHY zA+jlW<1!7E{<_}8CdR(5q3ay7e0@UKh1F8?@_0F;hIa5}=&02rTXU(=%hp_C>l`JH zqdH&IoaKE-i>LLI?scNH66H(V8y1iDM!5s^%+`q!BcHCbXo^g*#R?p4$s4_B`!RFV z{r6OMM(2!-%j5mV|72YLalPf6L-|6EzwteX3hpENII*qrR!hHwzk}`LWjXgz)BD>h z=m$IayV#}5ofcot17Mt*Kzqx6pXs&2B*yEgW5DWBntTi7gZej|d_6J8uSt&I9laJm z+>iQ)`@oNVvLBE)KAmd{RrTF))jjy75+Tt)kqO3cqJLhazrQ$v^2Bx$vE&~)_a?k zyGXe#RG`0$JpWvvoELNNBVR@#cd@-TMvVQJQf1t5+0R>(FUh#k4*877jUHm>cNgTn z0y|00Ei>HhvQFUr7%!4>WRv-)`QNMiCllbK+7llY{i1qDO%a`ceM?kRI{vBj!{2{# zKKWW^zu+tS!19f@*TaqHAtmy~?bwhQc5FyK%lXwRdHW*U1^S{$eUixM-&uHR|LM!y z)lZQ5y|0T+VBFLGpZerp^NT2F9M^`9&uVzIJOEY zPfC@C4VQUtKdgM7;l1F))U4q`<(KYn?NF#3C&qYtI&%D_IdTmCXKOBmo@|>zd*_as zo9MSL%O~MuC$YCjE3vm%JF&OtI%4$KWc>90+|2NU=0>>{#NM7qS)LJwA0a+We3Ud5qUvDzsm7z0_v|7GLH^hD%(p68p>W>D2dG^Xj(?SYOy3QeWnW^(|RnV|o9K z<$VR^?Pivz)!Ly@iHO%34l8BiF0^OmM&bi3e~EaC*y}e;OnaW|B?ZnOq`fWN`7Qek zhC|L5gn!KMoupsS^1X{VVEM+0p}*u@1@RHmquk~W6H~5p2Z>$39y09Xy_};nT&jG~ z=^NfgJ>>S<>9p7Xr0n)n>EGo`zGEFn&gYn3m;Ggy4|+`A>0x`_&hqpVL$8TE5sxwa zCc_1;|0U!2UJLhez7*RNdGgcccZB(SdybJG&hI9MPcc7?tI2%7*Zh=c?eX%j9!|~cXiuJGM=tqmwim`> z*-t~e?Sh@MEuKeDnSW{OPpzDVc-}V)|BCm&!1#GO^7L27(&OnfWko~xwFb!ttQ#bD z;uz%u7eje{T`b{;&r60;{!^~6yPS8Ob~`*` z=kPsUJ^BOZ=hgBOwQaKSG5I0f(LXH#teG9{9QU7GU$l{kOuK!(sWBo|> zi>Ys1f4Tm~x~c2mWZrv(^zUPShlxK#?E2j2)6nN(ygoU~_@7~X=(Di$FT^7jUaEYF z82WJAe#lkn-W}#2R=&>g!|Yf8NenqVo&Njzf1l4@X~X=4`yNn*cvKX?7_ z@`v$Z8|T|Qs4q(u?3a~ui>;j7DsM7(2ltUm71#&m+}Eu>$#~FDe)#&|IQd6CkQ-#U zx8D@;Zt~-R;ldQw|4RE|C!G#|UL+eIzRnfyJ!bT(`_uAsY#Jf!p*VOF#{CEWuAlsO zWONiI4+baKR<=ByX z&~S#q|M>TQR(i zO%|^E5K1>3NBuSqAiu4$@1K`HH*#In?NKjhq4IvClhoq?vCFNmKS8cb`=%`XG1r|Y zAdhlC0P*)@yiCT?Dbz3CKL;O9T4&<=!l~Dny#GAFa(n;s^(6G4WW5RfD_KAC{`6H# zFXwdFe|-H3{mIvjWWCDdR^Gp0eD4=?*Pp7_ok|sqkLBDKEPbKU%JLpDtolgI9rM%w z^bUZ2TjgJ?{M$Gz|H&6Mpnc^WsFhRBb+J5$hz}6=6MH{}e2Uyzc&P&Yo%G8l%RA|} z(BIxKw^fcBy&XC9+f&K2+{dwY-7NLd3c37;Q&#b$=1NIOYif3`zYq}T3=mv zUM#Na+uOd9?DO_5%E-=)?i0Vr%AH;GLhdKy2O*W8!)Hoje!q)-Y<>4*zI|*ht-Rxo zcQ@`uhIW6WY8~Cy$x8^=`nXXf~c%o9dk@ukeJ-@m?o|?jar}-b6e?+(kT0+(~>B@mk^m;&$SG;#T5b;yiH=akJq< zL^t;ff5JNH-an0iF9{QjfWL-zNHhlz1d*6+uP95QR=b8;D% zt3Y4u$enAryyZN`gI*}-RxsX}(cO_-ZdmT&v)$GbuQM$68F3fGpG*w8)Do;75mF@l*9d7SueKy%egC!UZFB-xR`r3>0?}wbC$#b%X2Mpz2S21 zCB)4v-wTLmEx#SP=Mx_{T+CfX+{E(GY!*I6;xThQUPk(39`)@PDy?>u$xVQ`N z5-0ulL8B+*BIWE$#NJyfG1H`URUQO)!WCyY9lOpk~rQeY&5Fa;O%)OGhiS>O2G3+WiFG4&_ zIrO8ZZi7i+>GHu1$I_3_aJe<#V_W*#Q4~^ka-qyFX_z^_YfZ=#=e4# zf4{`|$NiezafWYV{APxC5jPQc661b|+;cbkrJTz%9Cn(#XF%M}_!kkk61M5%_YWI|Le_-Kh|GfC`$i2|~i)~$`hxPj?x0x90{ZVcsG50n2dm(ba(aJ0PUkuN) zJX?vIiLWE>vGOJF?6k9e@b0P1D@pHl7Qaw|U0KZCO59}hit)QWU8HxLg_kPz7GBQ1 zhxrA}e-H6-ldmZEHsW>6{~&QE=^Y@hC!QkSL_BUdZLfZ$>RyxDuW5UAt^6*Hj*kbc z#>WMK1H$~++*WrvL3v}aIzlk z``x}CjQ#G?B-VjS6IkCA``GB0_F=uK#Pfy8IsUopzsY_s^lqWz`?=85;rM$%ukt=~ z%ehTfu0rKr!+vhU*B_GeozFC*L-|{MzEj^<^7Ea(PL~OOAM=uq)*lP|Q+9iv*vI{5 z!%Y*l2EX*C6BdntT*j0hZ&v#8brg5f2kj5f2)cbqT|X-TraI z$+$nw_&)CEe`4|(kNeHUW2DzaJW5QRW%7HqZD6)92-mam;JtdHfi$>w{6lQQHXI+%VjsLBxyo!2q%AgMMPy z2fc<9eNZ9xdB9#`*9TL?t`FX4IGG3BPwe{Opy6o42jMn++T7wUycZz4K}qKE z0kP`?ya$-*gC!Q8=!3Hjm$#f_ZeoWoV?5UfS;MXm%DJZ!yFPd_vFn4C#0QLj<=h3t zt`Ay>T_5C$2g&bd!z#~jIZb))m4g+JCeL}XJ2Jryl;?i*|5%=TiCvz149j^0xVbL4 zL!F2h%kw&7m*;k3m*-Z)i9By3c6oj^vCH!SvCDJWa3aq)6T3VQ8;&;I1h?UBaJNq} zp3Cb2VwdMb#4hiDZ@9SYpt*@WzmeGGc_*>U?+CHW_n#R~=Cyl>4|2SEkKsh#?`62l z>x|*@mIvwY@^OsV<>%YPE??gvc6s~}aVz!hBg8JB|4r=jc9hur)e&MJXAc`r`puUN zC;jTn4EO%>6^8rx`c-0|*Vg@1$FF33J7M8rJg@Dw@MK=wLp;X*v59z;xQqA@aVPOX z;&sG_iQ9>f5VsP~5a)>ph?|Lhd^?A@o_5v};%4&u3BFrreZ>2Z^^4WBiM91>z>7FYlKcPUf|5B<|;Uxs&mIUOPhEOZsEPJ;bBLKChi3 z_Id3%@g~MUMBGJukhqih0I|<&4-@;mc80i}@&ARmmH0Ejr<~XR)WT1m*Z$c2i*2){ z=kwYV#6GWmgxKe`$B2Didz3iO@-%XNxtaJ3;vULRp17Uuvx3;?waW}A^V%m9HyJ%S zm&N#9r1w z7}oJ>OVxO_;uQ8*tNyOwqu5^;%I}|LfEl(ejh9gy-b0gj zgMf$dPMWNrlMi@5Et!w4$vE*13s1(8uNf|HdC1(v{(6}4#*FTc z+&2xo{T1a-5U->B{esxXw;vH7U^#wBJVpEiVjo{;i3iE=V}_IY*b{91IT^3c;CghP z?b=T~!t}kwW5hkgqr{tteZ1--KFIJ+;={!2h>sBCy-&H9P29@x0pdKdk5}gr`*`&v z;%3Uz65?4)zaw`x@o~e&+*!mvUY$vNGuwTTc#yb6JV1=!OH%z%snQS2PN5&#fA;A5 z;n)vUKQx$rh#Fc^k1c4=Xu}}*!27}Sy9b8l{eN38irU7AdEUp*(eplF)P{BmCyp}y zLB_|qHaW*le1zd>mvG`R@r;ERCjOon?NFNdJ7Tm$IDvCmQ5)J}+XT<4wxJyg6S&V5 zwH-Hlr3swtl65DmZ+wn=6&`{qt;9Wd(U-_)*HM&14;#rB4NY0PZENh>RxrI1{(6r+ zui<=0LeFU*wR1i=4<2m@j83$n-{{KkUx9uO<7L=_*R|!HDqE*5wX870Gf@1uyF!1#FQ zAvuQ{SbHVsRF9KhGvoh)80T(6o(q%x3-g!#2Xp-#U8yo_xSVTZcqipyjP>mz9w%N$ zJWAY7JVM+`JWRZT80S*t_XCIr7~VwOPuxp)6S;xE?EaaxM<**vYv# ztaJOhxKd>k>+9$Ia8FOpGcmt*>sQG+Li9U1@4)a*3qR@n+}>8}SEoLA=i@!b60 z&y(JpxAd97)UCYF2OO@RUmuiK>MIr6&!;S1X(jYP)G=(hu*!mk&H=;G7WVf^_VTA4QwId5b6W$V;8{T{|w4M!c&oBJ{kJfbneJP4Im z$)O?n${V0Jx2?i^8qo&o;Z>j)ZGaw*_&Z+VKJX=6d6}j6`>};p?Z6wL_X{hZX}Gki z)7)~ysFic8us<(88?Q8PX_pzRx9Z<%si)r4UoSz?mOCu|O~S8rhBtENeu76#>;$4?SzaBI?iGIbqE{T3U!0;Kp--9KH6ZaAK3;^m>8H`NpO4pM0|Kh4yZH znEV-x^T|uNpS#^E{Z;Wzbd!7$KN(k%U)=w4Kjt{mV|3--_oI&+@p+wt7C#gGx8$1a zXJ&$Zn9oe~qTE}?QjIME3#>2;*__~GP?`EOiFdJ|K#`j_|1tT&9E!}S^ZQ=#$;_7n7{WZvNY z^zRt%{qz8__tOs(dp~_2vG>a2sPvQNvP;tAr+0rHY%*Tmi=Kt99 z1dJPDr3-w^VO-f(al0Gm^Gg-C%W*zmu+dNYCEiy^`sFJa?sj^Q;beTkeRrRSNqt$~ zL)5qZ#6FMfB|c!`g^G`>K2P&;b&BbGS)XxY=vDbW4#Qi;e$#Qq&!2Ck9W1{K067!+ zMtD2q7-n{C7y5YR_e=B~m&UIir{s6eSdLMaqn&t!xRrRA`0>uS#>kgmrXMBlFnB;o*8($8GKBQCndA_xqypyx-rE)OI zYk8+>XQaz7q7Z~n$&-n zVj9yHJAbCOzkV8i@i`!*iM+&IhN+ACzj zw;I@zZL;K6&Z2&q0PArw&oKWe_s^g=0lHD!r{>U)I`Cd@xEJM#HvBEpEzKLf z=t3PWg}+y5EyTTAdw)_t`5igXi{HQaH1T&phws8C_tOs|e1iAW+YSIvB3*RFxklgL zBZ;n9Y&cqqSI(mg!GP^pZjkzZ!O~A7+MM^~4oN#JpZ3Ya@!|d>$mia}aQW_T7_Yz9 z&uT`p{$y^N?}I8Y`>Yu8@1{-P0Ic-i{9}2CT<(U8So$~Y=O15r2g-Xt*e2z@*y4yi zW#y1_SS-&l@fh(S@u<1lPrJ@iNyO*x?WfC$_wG?o(+6i$SR{#z|3)Vrf7JBQtoaY| zcNvBn4%_|1A^BYfm8f+5Ddoci^4E0&=kE)o;{KkBzsIC}k?1lnMt!`z$$ek6gY!k! zSFIe&?+`}HC-=4u%e>HBzjrC;#EA#YO_#4%^3#0R%fD&zJG&5iMb{C;Tlk;gcEM+e z9BDbb7lK#Q!{RAl++R!JoA-CEx5kfJ5U;xyZQ|pj+F^1m#>(;g`o38S@lKuoe^3v` z)AFyCbTYp(f7O5f>7U`#fK0y*<8Sr%-@9@B>Uwwu$MgFUHPPQ!YU@Iun+ure@k=Z? zxD9Lxu48>wUy-$@7eCOVieB_FPc*e;=>O$GDv@MESHlbL$TVR+rQ3@Ab8DTS#bqUpr5IUEe94o`({las80)&(-?O zejpRYn^&KWYro9>_WJBYG6bDUpT+G)K84q#K;c%YuzbG1wW19F)pZTQ3iZb@8aSW) zcZAfg%1gNZPMWrq+gHlJ&GL@8zY|iOPgq3jcz~hFPSG zu7w+BkuJIxuIkCW{29H0^t+rTX7@8!{$!=V|8tVtyfP!N_{kk`P!=`U7xCY zG*TX-TajP9u1!9dn!7B(`S8jcVedRzIZR&Wk(;J;IjZFIRB}^(V^S`qJ74=hU;KZU z?f(lI=l>oOo<+a0Jbpf19_pvK{Vz50K_lfqx&^pe zF15FDXbjGgzr*HHi>Twh+CfMq<4kL-{TVJEfm=X`k5g%Vq4Z)scoE~V9I6jRr04Bl zf1~x;jrd-VkolZSUldbvt#nK#Z8un(MY~{EhkNnfO}Lx;scl^r9`0Ud@#Q?W;nMCEhRba|hTU%4EOKyE zML@v27Jucq8#$O2}URCXazmm zWqMBKeo0pv?LxY>Nau2>bX6}#Y_D>|y`a;OH+NrfzO4tSy)*asP*C4Tvv=nH9t!3m z>G=0bxgOKrxz~zqzXLqq{LSYrDwn&Zv#0U5zM%6E$kiE(Wd13nv>w&>+D-_sqCLnb zL#uJ0{4PXZyfT6QYMaPA$J6=JKaxs6WUoL9J&3jCX9V!yu-ccyN{{xJ|Gt>_n{afa z#Vp;q+1zpm=8J`u27_Y9xV6i+3%ozpwvG8zrFY&11((Zo4<+d@y zX*t^??Xs`-YbR7vBA!2x-`N433Ch`)dc)C%CUbQ<@-~&v-P@Q?Gs2fa-jx5UvYgLp zIanv@+@92Sw0DQ^>qLHBm(AKMypsOyMqb(l>n48AB~9;86}^@6o{*n2^K}_*$J95R zyw5ESBwxh#$pm=k&3~U|+g|9O9an?@g}uF2&f?XeR@&QRxP0{r^9c8DG8|nEeH8BP zB8I-&wio(i$JOY^g}uWTUc5SQ=}UVD4VSNeHt7v8J@iSqx1SjLrm%P1aPeyG)$$(c zUdYFet6yZev={9y`(hR^9H)HTi*}6OaE8ST?`;R&H$X9j_qGyuGW%G6Y>O5Yc;3Y48~UD;L=CzZ$V5}V&7vps;`%y7#zt|>1Mga`^d*9P zN`FL}3ahEWy99fFg~b*v=&L;=&@P*;eX?nkth{ccK^a>u7o9BRH2-ih`bV}F>uMR> z_0)7{1wvNb#es4{(`!q3xTo{^n(F!g5xHM2dQ0t`?z)u{SKQhQtmK-Xs7J|9b5(jP z%*}As&*^p7o_C7)P=Fbuo|N0?5&HXt(6mN&So6~^_R4F}(G=^a@zU#z|K#D)9{Oc# zWl>nZ>o{+6K52U?rS1jbm$!$9d-=2W3W?~~ON>msAK>ND_WQJ)XY}@xbD}0UJ`Sl~ zIC3@%D!T#qO1eGv6vy!&Nb(l2}i-0Z@OY+m5@y^d>|pPwVt{cyz> za{%#mk=6Azu=^!kMfs%hi1qS*p#A0oiRbHD+72(1j_&(MK3?hXyX)uevzg`TuF+mD z<>tGPpR9x8@p!&&?DqB=QJb1ywB69~12S*Ydtsu#b!J{u0lB zhxg5Z&v#fcrJUfqrqiL&_mQI85FXJ^IQ@2d^8c}RYL_CW+iBI~YA1AHZzmNS)dyN@ zac;hN$z!*h=09J%sf0hCcGLLE|MG9xZjGnWZqpY@Ci2DezTKMZ-2R~Y(f3W{-T>r~ zDpmVMHNStlI^8cIzt#Oh&h;Ces10(R^b@JDeARsSeY1-fLvKdgA)neGz}Le@PtI|h zo2|LZ=!EYuC(!b1JP+4?p;-C*HQ8^DWFJancH1Z{_1~#T;&Z=F_5Xp?IHKvkA)?~@ zMcJCQ7T^1`%a4zHQJ)0|-DjL*`bd87(L9{~^2M+RL-Icq9_pv(hoy>vuA{4)PDX|@yL-QZ{VQiSXNlhKo5!ssphuGQQ3D>4E0QPn=t^m0B4 zeC53lALmrI^qYP=T&3GCe};Fd)gtBD0DThPl_x%8bV{SGhD&!r@JjD!Lpb$HHU07) zgqNR(_~kQ@KAZr(XbtGgc}k-ntzrI~S#Q7J!uhw34_eOYZI-RrU3mXi&MD5RXVjw7 zq3sWOk6J)4YC$^Jdv4Fg?Qt2>`R^pI5jj&m{z=Mke9?fyR8>mP=D%~#DWGoSK}NT=mc{%gJ$s7pEP zu0#4*FOc8yeWn(+E8AsAg;&0~9SXqOPY+l1)7MHrb-9XiT{vem$oAZ|$*`77Nv6ls z9=$iS2z1@fc{F=qv-DXXpTh~9t2#;V?ZziFYBF_hv%t0>_oX^ea3Np`g-^)pN>}X>G{H^sKL*R;hfk|!yx!|BhrQ1X!3iE>&&eo+(h4=czoLU6D>$OpQmsr3>k2&d5g;eFuuzWT4=UDP|;t$g8P*0b#( z>cRP#{N4vK>)AF1%=xSAI|5h0N40k~zf5rIdN-x!F{;1E=hPedM@=0l7w?xvO_vfQ z|ELN2QqDbC{!tSczdd-4<*)S?76;k-PqCi8NFUd8hV=r&qP8Q%UeCk8aXk+Kr|PNn zAFZCPRrS1ePCb!-)bt9Ji|6>GrfZ3jf7J9sV&oq+J=gH|;0ANGo+9GGzWNWEeIMR2 ziuiFok5|=mwyK`Ts_J?4qMh0%+-2|DJ1Q=-+EAZ$1Kvv?YXY1p6ylj zY^|zi{-k;;{YR_kKvg}TGN+!%KWh44C|3pfM@=6kM*dOLUlAk!sOi0iw+H`du8#Yv zA+)}5J5Q)vDq|I{E#h;{*&Zo+ubea4?y z{yHw}ic7bwL&H?dBac-U@emE8I0wSmj^FRx2^j z(tP#Pl7$mpR)0TFHB?*AaciH&(RrE2uRTS4_0I(FIVryC%lYU-k7Aqnqf} zSz^e$&ujf$mAtEpaO%~he|*jS)9vhb(6Lni&~|^fw0kD_vc*rggX-t`_zJy^@gp5y z_1t{%q5t}=qS^WvQqSg1V3T_HvYt{P`Q$wkVz2im;CMXg0#1!5+CO~XZoYbtr0P9= zg=J&&^gosQPZxn*FUWgtMn7tL$O1y%^NN}tBu06nrY{hqJWNwrjP zTyIwQvrGWF8EQCa9O2iAr}6TrYY;^-(4O)p4J)l^o0B;pUz`dFUvijnBU(K|8N5I zeSgE(0bHM@_cz}6^JITR=_}oIzo_0v@09$~_X~Vncu?nkYc#d+JxBS8=eb9Wu8(JF z`ZGfB(dN0Thtun+Lo3tsT;Z= zujBO#AH82b+Pt^DYJRz0mgaq3#MfJf8jf4}!y6?t`N9h!cOmtUj!!Dzy8fxI??Woq z&)4rF>aXqvRqJxC(4XP0i2qphqIHPpbTYwDt$d}GZ59ym+(BuTO0m8d&}weDCJ*-_ z+ATF_8cw&r?i1*|yDP;PKOYly9JG9*4UkLKC$*|i_5%ApLAdV__%%KYm*?iooR5|} zQLb`3$`uWuT+t}X6%B(QQ7`x$ZL(sDK0a>u0({>hTeH~$V!b;FdCAtCXZC{c7dK+N z#m-?%)olHDi@0uAL>)*MZ5XxkX1Gq~zmL_p6ZwY|sAsvwG5Aq#!FrbKsdCG&QEz`A zrv>d3uT!?*+tqk z8Tjt{%-6Zyj*HjHo{oAn-VQgs8^6aEz8F~7M>StxPx5^_#rlP#`%&&Ju=ab6p9yeo zLCd>e_2Jvmo?!v;9*eG@t4ZkHWBK^LsHPXPCrZ01?`>$8(r(CA)P{B`?WTNjpCa6y zw*ts}Mn*5(O?hiO0?hUOSGGWY=j#`PbNa;;>feCzT>Hbg=-qG}IP~9{+dB&Sl+R4? z*M?P}DZiYbx(?TyqObINWkB=&m~?qmPiy$Ar2iEvGe)nwroqN7*N4g{m6XS87rUOF z&o1+P|3KouHNkEg!jXes2=>*wtm%iU7M_H}Hn$8*K2xDe@1rib{RK3oZ`{a4RT z=y@+ak)z|Nx_xyE&3@~X^Y7v%`0SX^stU4om&yO&yNa&-PMP7px_aA*>8pEUAfhYr z&d0bT@~$=Y`%`^&XUX3hejfIXC0u9V8RNd@cc$&YXX|o-YFKUGyoIlp|25sYw%(@x zx^6~pj_Q^R)P1#qw7VRv|Qqw$JyHk_fe%UR8Gr`S< z`)V_0C-&8zYv*eEYFp*(gvMWQCuG$Af4jvG8Gn(rP@kN;(RycVUt{f~@hTSY^^CX7 z+M}_<;@A8cWBw@3C+_ca*pAX9wCentrc^U(4|2CI{KtXPI1U_&Y3o6T_cm@|&%FhRK(PzsJH~ z$Z+(FZ0*yn-)i`qEWC^1%d9_TrJePJi-y0=!c}`{xwFQ%Z0+SSe|A}TC&QQ6%phC) zG&|3w>F=`eXEOX;p4Zx7=e0EaZVO*$;ff~=!;n+F$1wGY;CC5@KGE=74Y$YX-)i{x zVmxB_DKXw{7~@)DG5SMa?K7dzYX1QGto9FMIe%D4`8i_E@0eiMC&@e=cEm~R6O=c% zbJOFC+JUNitjY6S{U)i4P(Bm;rAG(J`wz9hE>}m{+kWg#CTM&)~D_lGB4;g?|8p3T%0$L zcz-)y_r$oG@Qrpt%s1E(X}%pM-_-U``1XSnnt_kUT8{oX;or0HzS^be=e4WR&!wNJ zM$r0x-`W-XseN)ETvVj_zeDXKR`* zy`~#Cy4YvW);uZJkNeF3bF4?s^r*(a&-`(YSM`tA``bqUlZan?eoW^F=KsIwzufel z()phGPt*TAqhFw%k_qr`rN+C*^2588ir-}zzu&C*J%;Z$I*KO@|4oed8^$}A8ot*s z-nmpfW%z?}{Qt3X;hjqj|E}Tp#^FCS3_DZ9zhxM9x8hmDu(uWex8aF6{f`aP9$h7k zB%hw+Ont+zyjELP{DrybF$RaD7U+GqyL??Byif#KzE3X1LQl9F<5su|`a#}}L;ey+ z>17$u_o?G^PX(nS^BkW0(Q#knPp>uaU>ez5zkRJN6imN~{$;p+UdHEXk2cQ_7DG2^ zO6`BIk)r!NKfdS2%1^g4pKIuPd5XZA{`?lPb9SyXkLcVHbHknO#KXiO6rDRroHsW* zcYwIl+|n%v%q?Hk1^-*7%#AMUBpxS5e&H=+#Kk4CZ*KV_(D(UfbnYgEZ|kSqW3IPDbZ)QVjNE$BKYZQ$w>iiC_vgk_OC%G( zj_Q+jEtT}FjB8RF4R)u5Uq@_pr*Q@G<^kA6Me`!^E9d~_Rrr}>sh0A-oR-Rqi0)@22vFbpiikRXWx9e(zt~`7^>0Z)b1c5N4pvgU>L2_SHSv+AUl69Lvw= z{aW7n>-%Plr}KD~k8ItrwMRPL{Pnrn(q-$~(e8Dxw|1Xqd33!%dyL$RGPzYb?@!5j z)X_ex1k&s2+8^Xzp80G1FQoV+?~um+BXj8B-htbD@;;cw8*0eUszp205X@@5{9Zx2 zUi0x=-(mPlDqrRM1H$)AaJ?2+`11_wKS_S9pLCX~bh&sRBHdrDesTFuU9S21ftIT+ zB_CR@cUZYLTDj8on!n#YAMsM{qy6jD?X$`Fcyjx6nZLGAnoqLth4?A@QbeKuc-s?l zA@9l>y@b!+uASDN>Gsv0HDCMAr?-@k|6J9+^@Y~&*H!h~X7z)9O3TUo`b`LEel6yh{+~~es{E}Lx~`9Y8++rQs>b67 zWC1H}Z+u*^=i~3|`1xPeGxD>PcArbfp>EM*I&O8#x|8xPx$mS%?IUefnO6zacv_D( zA>j1=+=1`s`Fe12ujq-EKDk!}y504dUr!HMu#}(i)&ozM>An%J=6jyx>-Tx?hqsJ# zn0NboI{uDPx7?f2FBAL-cEC;~NZ%KF0EpX==(kUi2D#1pG$vT6xMV%((QrUcnw_LFIGFs_cd-qvfw(;ufllZ ze@Nn~eCRiQ7u@td>}OUa^?vO8M>!)D(^Kkz8{#Taw%CfbvAD(&( z(>cF-N#DCu`_=iTa`_&?zVFv9@~EuxcjLnSGmyUm%Hikj zbv)RV8vm5eU&=qfzvS%`TFACMvIKdiAMy zjg+2L*YwFCtWKFMrC^bH4dKF>i+gmI>*GX%Z!rqX~wY;ZO z&pWH=Dr>c#^-Gj<_%xp%Sw2rSk957#c4Ed>>*v<{kE-bFN~6;IBcr$4=;?csr(51P z&O=Yjd#tKmzGC#z(bMIGMS+K)%( z#K*l>ukUNp<_z*GH0lmCl&eGFHEkBpXkiTc$sLDmYu&>$N$lvwq6Gq4vzkAgO zzQ^x+^;xil!}a$H%Gb76J7gs;)AHiwd)N>Z|sME z1??7{(@Xyz`VS-iIh%m@9cB6fVzfhaE!rjAH_Py0h94(Jdz3E&pVUkllYe@?K}AK^ zMMO+bbgSVfUo1~|fKPFMf;k^FV%Q8yMexJV33b;k!njss{W{RjVG;F@I?&Ex5&ZFU zK4B63i#iTjd6k})%g?ifH{`WgL0AM|n(Gbvf5Xdf#rk!0&w!}cko8k=MKd8ds}K3tV`)jrTab^Fx;-bv04ecN28l`nb!wu|@>%AMOp{AuFN=IZ;w`h^vwk9dARe!raW zQOdmnq+5D6<0E~vwArxBeY|cQzXyC2>DsO~H{^T6QO6$u@0~^XhL^(4{UP09`lEd# z|J^UX)0AEp=odcfxcJlbI4u0t8xcAmZB5x7I<9x6u=4$NQl)qwV3}3N<_G#$$oWjX z|8WaCRJarMc6&G6*=Y&g-i`YM_eZh|)k?v#ywxey{PGWO{ zXc@{^YRQ{Nd0CgarJZnnA1PY1$^7HQdEKwqW0qzil1ZNwvMn< z{?zvSjI2`(Ej(-iLkka?-9EJNAn_5pN9oR({WY}k81Zqsv-CH0F7%qLV)DLtjn-sn zA!d?NKKkctRDZ?%xZ21B-3u{3s(x#im|DKSNv)%}{Tr@9dBYZz)90mrziCZIaBjfuZ{>%k^!a1FpIAgWbbQoutDSkFxLS@M>wN-W58-^!%Q5|Wq+28DG=A7} z$UGX?!{6U=Puq$2NWI+7ZQP1@(T%KE1@fE;c;sUpd@9k{Ueod_RZXYP)H%gmME{Xn}kp~2(xv`x=v zILE6dFghOZnqFq%x?iUC_xBrgUM9oSiS7mH56+k9#YZiECg3}8_d(A$ZZmIrZxQ9% zwuj}SoR&sAErPGpbO&EFJ0!dV`N#e04wTQwqwo%tKOV2{K>d6?@b!&M@K2OSls7GZ z&q~Rk_Kz_s$@JwYZ{t;PeVtD0eN4k)#>D$b@w(=vjQGRKdBBCXPSaP5^DlairAEW$-`7Ni>Ulr?6n|ZCCU~~tK6#f( zS4#WjJuB5LdhSK%?-~#1a605K4L^tH$(GxBxoquaHjmeMIClX?^wmBA>n(ClQOl*_ zI6rfV{H638d9JM)=i0;%=RPiG_*tf}vQmC+0Zso+EAK@Ne}b*!WTiay499tw)p5A8 zDl6}aEn)aiEq#lHD?OYeSrOxJShzHW2(CHP%K#W>${t_EB9BNl&IjByS}TLa+_ zS-9p7jB_q$#~9~eU=Ka!xi#K5@^d4;@91)ywy%#$KT|nUzWezBE&p*Vk4gi|^Ao{$ z8xp-jx$=3Cujl)H0zGfMNYjZtD}}xV3$gyUpfQ%OmkFz~3sxFM&2Nk1+NYX4`F+d2 z1q-ZP`re0-GCuyH#l!j0c%ON}S&*Lv&DPFJuVC?3lOF7Yth|RUq96D=h{k(`#8dlL z^^HLLF6MjO)1AOsc{fhwJU;&`e_vFi(KU@19`hV5hO#M~=WNRO$|AVo= zaxPo@5dFUt`zznF@(w8KfqNxNM>&?Q{V)3eTO1#H?#uLt9TMwls0kbA-z9Q%y65!0 zzr=KklDT}^pHyDlUUR!u+e_ut=bzpmJ-z&1sFm04B)^w6#CL3l;@{oDc%9Y*w!biG z*<1dq7kqx@`NrdXJm2N|P(0t|I3LeMtPb>eT9Qg0&L)>mz-VD2B`Zmo?+E=pG{KESR@-6}R7GAHp zNx4eq*t{h#e=2{Tah}kT&ZPOO{iHFs%0CUiiEej*^GBspi};A1s6jiV+uin&uD4*L zJ0cy%?K7YEHA`oN2DNy@%Wsql%a^>@Y0&O>N^#=fUwO2A-a1b{30;hA{l@PJM&}^k=rYtVS_Qh9;Ack1 z`}7T~G7=>~^EpS99t%D@*vr^@OM;FTRRVzKHW}mkWRAE1v(iusyi` z;_c;jx3`P`?wOY30nv4CN1t>L3f(4tkIL(v?CW#B_B(tpH2)*fd%_R>ev|h?fAzz3 zdho-~1Ni!_&%3q$kIacLf+=4-&%DK;>_?(NzTOs}7m(;0;^C^V^>h5&zZcqO`}ADzW&hGu7TrJAQi=0_<^9R~ zBxBe6kLTUcX)%O+-~CkYhWdM~evTr%Kx=w(|MvF@BffvG^HKfO93n@&0qJcTEz`{ zKil`Qyk9nAnLzX+^m)0Z%?Kp-n=dk4zC@|0{)C<4`ZOvvn}4)ZDr(_JP!HUQD?SW?C=Wa5=pIZdCXIvk< z-4p5pqI~}QXgWSq-P>-xpzDrEcldbGapqw##4iH1#`QD|kPc4u6_i>|>9w*f6A+7m~pvFP~dUBN?Plr{Hho8f>qCJ z3u(Sjm44D~voGXR;q#@BMJG5vPP_~3G+bSxjf>+gK)=6I*!`nmnB zdPL*L;TkAk)Bw8iz4Z#(JB~;Fm*zu{rfX(D?9VHiV5nXSYA*OOB=yq#8gDRf>wnit zg*FR2bewTJ&)2z=br#AS-`7p#>_)5qd~!CGlC$ZnkzbUOuZDT#YntWC1Y1Fu{ayEO zdsFn3|34Cg*X`WKD?vYgr^)w^c&;H@1wO=f-zqe4Z1=5V`OeYSm3}PwX^iNyyuzRn z%UFWJU$=98y68XaSy~7gu#orSR-Ja(8&~!gIpyTGm0P>$4gzM*`_VpUp`bsphqjVKn z_5*|o(jEL8miNglUc`Nm_}!NRqWHR@%GXSle94xB$QRPPovryzNFF{fIQiYP1D2oL zNgDr5m2+N=i#Pjhsj!Ro`DpeKj9({2TW7OG4Tey5BOV9M_6q%a<;PmO$q_TCa%f zJ6zTYuL;a$iU%sdX zuGi1`=Juzzx3|B_&zPt%kMHr+|2fN2{5^6Gr*SwEEj_$eKoIm`P3=F=y7P`jAkht!?UY~5D8 zn@~4q`T2T*()r6D&!y90bUsWvjb@K#>n_LsRo!cBKPjEQ^G9>jUt{Ut$MjFMee!JG zrM4fOt=n$z-lWri`X_VKKiSexGCl6QX6yQGzbl>YU#s$Mw{-7fx^wMam~7pv?46i& zx-)*7;;R@ zxZ1V+e94f!Un!*I9PB9FadU5!cl?xkvR^lILQ6d)zq_JtxT;nD7q=y^&Yb(WBa&|> zzyvbQ&p)bar>9CfKi{8@x3!Az7h1e7%_d&2)Bbm?ik|k{OmO8%@p4u24tgq$bLrhu z)s9z5cqVxIN%3}9(d)E$IPa6L#}BLMWi6i8dTze8RrNqW!Fpbr-l{6OJ;&&&w9Tbg zQ^mh$TD-hQi`Su49@^)~5A36~{AfG+exk--H%D$qO%BuXA#Wq*pUBM-%GFVGA5Csl zzSH~6-x3Ay`(9~2ytS%+E2LijdxGhBKd<7e?&#@w^RWDj#~~FO`KreoAI~0p+-a1& zb-Y-`apxJJx6mB>Py0)@u3+O(bIM=)S0?yFi=*$+X;J#>@*H>8aooAa#+`IJDZZq` z#H_FG2^?oGWn8 zEK%*Q_j!fd;mQ|&!@4e@ ze2stCl6IiR11BuzaQwTL+KJ@zd#=|({oC)2DxK~+L~uLQ^Hn`?#az9}8>AN@hcMaBGt&g<>ZzB;!@ucn@O8O}FJddL&|gQSNTf5;uyPc(nU{3QJi_Ge!m-VxJv7S$t~4sw~da80L}pQHo7`s961 z=#`atw@}hSZdDq=H^uxU-AnCH)oa@F*}9E5uO;aqk5KrUpJINJZlnFF`b+g|w(bQu zH!A5gMcf}G9Vl3IN!P`6i%kz{z0iL^UGq`QPvSqv{>(~ys9wyNKcTnI{#1Xg92pe<%VU4l)BME$Qu|ZmYySMiKWTq()5m5+A+B&v)qho6__3KGhfhDhHVTe2TAAYPiPJ_N!0X zQyTx@rK*}gRhVWa`G|*n>aY5pU(BDo>2bZ({9yAj?tbl#*G7DISMGIKenZk2 zny;V7@%NOKZcpldx&QuS`u^Ii+I76gpz#N~$cji-8D_72SZ%r3&MW1UU&-%lC4FVe823i;;laZu2snU zT=6>8t&j?julfJ2w3p|r?WB^T`d=kb!~aUco!@?c(a*QUa=^~^$pxz<;CyYM;+js=C@1oOU_vxH@;-~?$oprkdWrX{WjIy|<-Zz`lOz@00oZCf{3(*FULLe4i#-nz!^3 z*O#NWw;GQ5c!$CGo|E>A&Z>T)Pu;DqTVeg!?;&gzb*cSiI*;^j&+EAZmDiB+uTr8l zTfJ~upi#RszkqzPoH{=hZ<(haq6_)cd9M2V`A(&)>#{yyIEmlaYHY}fM3Z%BXi;xp zm#eHuvo@W+m!k5S;pm|86qj4ju%0f%(O|oHYlx34evZTGMlI07ex4(0fwu7V+NcHc z7fzsFQ7RuHEBWgG$vj8rZzHL8h;m2BAIPVk)6?=K^R}ZWRHD-JyCcE})fZzaIx3GJ zk)rrLFL_7H=&Bv3-}R5t9(o^F;|=kB_R^hN3b{{9Ig)o%%_G{-Y4J9TJn0{5cc~m~ z+oN0#f(Jz~6mMK>FxV=ow7+kZzM@~@P88@tNtYMwdSZ>lJ2dmIrE&YWYiv0yAY_l; zdeI_3k5b_M#|I^TUc&vovS|)3YlLoZTgTacUn)(1V$EyoecfH@t}eZ&+3C98i1*vK zW4%Vp^}y~=J+ZMxOC#mK3s%H~QjNS|pC?aWrJ>T#w~QKpl>YELuc~RpFdz7RIW1q? zwxz44*)D6u{r)RopYeAWJYF<}`Mbvd`ND_J_Hz|_9OtG_J@Jf2sF(Qusm6PG{nG=t zkE45ftsI5Bv^At%yu8$3Nj>{fUXh*r_UC6CFS6+IeKxJfIe&XJ?|fP#?R)ii|MARe z>9_htTZS$DH2bO6r}5#N&TM==!l!%Tt`X%*(ICH1w?@X* z-s5j*ZhSlAb3ahqW9T(!<$k|16w( zz~x5ys{SjkV7<1tLw-qb1@%C*VIAaB{Pm0TV|Xl|&tbUm*YaBl9PS6d8jHZ%4=mqR zz`hUf`f9DGZ)N(u&>xLmRq09J@8c_fj4ryVoL_21eMBBV{U2w^R5tp2HXvvS4!>;XR+zE#Sdmn?3g8?SqFQl|XSeHcG?>f^iS zul_6IehWR|_l~!U%o<&~uD`YZO&@rFP|khD%CS}Yk;S8%mMg@Yv3NH&;NH&kuw*LV zR{58ge>c%h)7NtO`Ome|$yUjPNZ;vc{dsNR`cC;|@vf1`@~K={J$D`Ywc-_Q|7ce; z_(gtryggD6OE3LJzqR5j{XOEEe?9%h)%^E}YxFy2KtGnBE~CFk%BSIbq`Vey`zH9a zoc?<^zJ8+p%IYQM)_kKj^j90tCFLFoS32Q-$nR#MtGmPz$HQpLl$E2r1^2U}T?c@7 zJ!JVt_Z$Si=NpE@ec(fR%eM@d+i+h)&NU)l{QG_=cR7c9Tyjnn;X}Ch?eA%nbNJmq zH7~TC)9r5jB7egPlwZvf;|tF}m)mf^E95&U<+d-P+#LbR*YWS>`nc|LJiQw2vWI+) zwj4wGw%~UKWgmz2!FwWWMJ}yf#kKr597p&D+&go5@Za5A(E|E%&Vh7rZ>`*b`)+=2 zVXM?9FHvuz>-{R}A9(jZ+&^gb*edO3@##+AfPA!k)=xIz+(7!g8^wt1t|JHDSCr9l7#on*H9Ddy=i=|5mmBc=4AHJJ#M^~? zXVE?5!1w&v=r|wBIlPA%?t>js-tt3+qn*mRp94=cnd|r8wcSj9hklCi@pT9nfBjAx zM?(0k{8_qtFkii2u!wFv4^cTbdEmIG_L|}qok;KZ^ZnkA^4s|D{B9dTJ==b1<?|KAurCdgr~dMcr8%`VF*#qG#>@U{|#2>p--Ld#w#4yN^25m)OTW5qb6gIUe7W~Wy4S|> zRr38ExbB)4$Nnn$a(HsyELY& zJ;Q%@xVz@5vA^~VkpsqmO6;#aL)zW^Pv*y(IG*;DaAMH#>e^LtIkczv?+AC-w8ZhX zr*tn|5o7HkV!tu}^J4$y=HGvc{FlY?&bN52hF8~|6X(A|^T{lZhsNwFK#@N5b z{KuL9nX&%`=0A3d{LhHvt+aUUhF8}ti1S}1{@wMpalF-v7odM?`(0@1U@vsn{L1oC z|25{Hw{$1-{dgR&)#5=OR@eOG*WNELGXD|Q=ZA58UDxV91O4)UxA!jabrxm*_KQJdm7PGKq&}PM5XFaw*?Au zwGww%TXi*E6)3A{+*OG8fV#vRWOsF~tJ44XJ2TIF-glZ5T=2iY{e5%GhSmo@k)j!swc9^P`P?il?>_gJ+aVE^@Nq_*`K1$eO`Kv z-=i?^-KITBhAz?1$3u7O^F&BTdlhxw<)zd3E-xMRDH&R(~H|z5fp>6890Mju%s#;Q~(|9{VS8PJk zr=4W#`JcvZ3MWHTl>Lu|Hh@O+hvviY+f+Q|sd2Nyq-Vd6h1RR{EaZz=&PkA7;|4_^ z3(ZyISu9kk#xK%yypnTcq}TWsLw}(@FM;-^-6B2h629NA;z_S@ox=Pc9mlCyXuf(b zN_y%|)VWs0lV0O%6z2DRXs2SKIS%a->Cb^L_nwnp<16rs@n!0XS1e@Cvw@!aAm_$N zukmVfqrzkwv!<3(Vcb4fXpQy4bx_pkma$bV*tY1`d zq2qHDrd8CK?I!2PNYD9#+`C6ixy!v%#2km^{1`FySI$cib9|Ne%!z4N<@^{i$4NOa zK}~1 zO*FY}o0-OKZf)-|&&L^_Y;;=~o^0LHCErhGcx`KPC&Sy54G8Bi$GI)H#OdzR<+43l zF58jz`yTK&+WXR^V?38y@?CYlA5DID;U9h0gZeDb6Il-&BElc-nEs2P5&6jdHQqT1 z^F9vOfgn@mJqUh;eisAb1dGq-zVF#_&iP0> zdCt<$ffTL|orT;c^UQPeJhnET_a>;tY-d^<)URH7PAqSlvVZ*vjdy@Q7e84~7xR8O zH&KUhP838>Iuam&lW@te7WS9=$n-qdBKz=pNYC~0LA+0hDxqAxK9lF4-Z_C9^HB5 z68xTq2>M1ZNjHp%z#E~P2ty=v|2q%T{o;S;LH|1s;`qvWjh^>Zs{X}sQ{(@g2T>rD z$Nx+7All96P$#j69N%eKJ2)AH<9(WJ|72v6UKd7Ys&zv$vP{hbIKG$o&W*+*pH}G@ ze&lx?(X?Z}7!F|n52=7;WP)BF;(R100m(>(S`Wq|d)2&>=`a3$Fuh(+95m_2>Gfe` znqD79?ojhTrhD7*Y`UoPF+)FI&Ht02cM%ecyi?5&Nq@{&PSi;m`o(%Z7^&6k!N}c8 zE~G!;%b)eq>pDHKCnJmWdN6X0UJpj@Qu79;I~UIZ#mV{aWaJ#Z4vcg|HjpJqUh&CI z&S{fQt^Xn^Jr4!lI|6*)tg?-kOOGBrOSUAeE_qfV!xJ72FOBdgVXgmhO2^3(G*-k-}EQ7p1j%~wcw)^YqX zE^5x9$IxD9E4meG{X)7AkMdnD?|Ye%>3Urkxk0TLNMGVBpY1gT`9&R(*Liwf7pYV0 zI?}HWwA1B^BZgmg9Dj_Hbrr)a0&=*GjK6FTE2ePnXSf z_gU#Mok6}UymXmBIyJ>sdKC)P>tc`o86Q3QtWf#!uhz%8fpiPK@SCl4F~rDvmwZ0& z<1?yyfzEHx*M73zCH>BT9J#vF^b@Rd^txXgD5m?6uN?BP$1B%MANQq?s@|saJ!GZR z`zKj>&9&n7dYrY?>A&x5Us;EepQJCHyobo}_xkuCpL#vZ-7B5nc%PgZulK|Bx`F9W zSn0{B%&!?=X{Be*J@fSZ{-pKQ5|)a2HjNj3J^fefME*RC{A3;Rc|7Pj)w=U5piO1; ze7vXKlvmGO^>a`xk-YIm%1g5o!pVAu>n&6n`)Q?WT#1Di1md~Q!ZHi*l@4>crqhz6jqN$fsD*=RxeWR6V=oo?q8|2i6(E zGTS>3F6$J^f3q)twjcNJ_+Hr0e~C!rINm;mflS_~VLchU1Le8!MINMU!NRf*VXSb< z+;7(Z1OOcQWBr6)?i=K~{%5}O_}r8HGQWRAx&4o*Oy`#a>5p+82Kmqt zpN9&`{!o9f4$nh#D?WB>?B^rlmj{^O_{MzhA|G8QJ;D*>og1Va{qmw5$v@xYv-vCa z@yB#G`pZK)h{!94=?8r6Og>`1^u+fOGZCEFF=>~EU0+vv=a;`hWWpVYhmal;?l zTD5CJWAZ-(y5Q-{Sn_Vv^ptm>#rMlB$EQ!X`s6MBh~@Cp8Gg*G1m!!oGEepF2q%?r za$chjd?7i$-=Ocoi-o!C$Nd6*P9;4_H*orNSB1-S6=}cw5z6$k&l(Gt>;4HIn1cNM z9?wy-{qp@@;c6h{K@bi9Prh7m7Gwo2x3kWo>$9#9pZ@2nDFXxp>A9ydz zyno@R->2w@6d!V4M(%--b6s*jQT9Id?0Xw1!bxwUHo;H0^aA%XP|wN#A5{NGBf`l& z{c<0RoNpuDOPI*WKC0|T%l=!#R3on^R5r?vk$=%ErvDzISnuM=Yzgh;y%T@EwVZUl zNx$zozg&-<)GllXxi^FKdsqAEk0|<{U46>;%ZXg@{T5Qc5ji~%IZ6Hf_4+t6@MB&@ z-(RomCwk5Le-}aYpNB8!w`d1pYMldMS5KpxT>B}$beAuKj>#&e(p3~j#t;1gR*}fqDkjgDh?}`IegJc9(kVulEJ?;0CXJu5V(Y8@+W`d!T$huV(o(6x|Kp zdaFKA@2gdQY=_?m>ZzX#GCUNZr#5N*ygfj#^_ui|2lzPOqyJ`r9)pp|=h*-~GBDw# zzHrJ(uLqWT@pA%vP4~jD^riE^r+iI-K3h(%kDle|b>T%`e#O3YjMwW3ZNFLH_xkF^ zc)i}w)_bon|EOwhEx+$u=`dY^9`LE5j{i;|9ao(?{HMNj{`J+%K6>(@*KOohr+d2< z&-IEtA7ni~>C2bt_4+xxUV7Y0&n1OCA7uKORy<9of4#KSk|S#*=ksZQXTso$U1oka zTKVbqnLek=_?xW!s8Mq7vpnDMk6)Z$ah=8WD(gqb`Fe;x64NIioNM4bm>%CB(d(aD ze8-CM=|_&MaL(U1So5v%An?bxx~Xm?aT*$ush;^9P0$WV*Vdj?Mk&$N+}1N+1v{I< z#ZGE-cT3L&nogci@;!3$D{>)!sD}3(P_XTDyxr!P59&P!`H+t1HRCsa;;aJah zA0IMr@y{pNZm;*gEc55<^)k z-ndNZG*k>u^u<4)`wGIPo^M2s-|)-vGL@~pdVeotP`r59bR>3B{V?0fUUyA6{Y zjePBTO26s9OQ&bQp<{g?f#Exh^vpu)W#0?SeFw(Z;=BSxdnu=cdlXN}XK?o=DVoyiQ?{4H5Q`(4q z3iM05o_A;SRhUK!PTH^MgNAOqrjzgO$@`qrpG4n!ZZ!AdUas%KwdF5*ChrHco*XyE z)-xD5#{2A6#@An%z8mo#zq9nb{4nO!bm=mXs#JOD+Kl?oM-7zf=v}dmhm;03Fn)={8uL_sv^m0##?Bj`E%e{hr z{o=aQzrUgOBMcwZ6V7XJZdv7Di}QvnhJkyRncc@^e=QQ2!yidxo%!rKK7r}O; zW=5T7b^85gJ#D^kCHLh=ox>`=UvH0};`=_F?SLP1|10&6t~OuW8QIT}d$iKaRkZis zz1TC>b2|FD?3*0cWSD1T@PX?`Je}-6*Xq|&fc1My@*cR~4mrrr`wkC;L3!{V3Od>& zI>E9p)MM<0%mbu<)`qmA$a^W|_Z+kveY#VfNBt8vtkUOcLfNO5`*wS#>GV>L*rn0V zeS8~j_eSU*^N-gvbpA3gOLuDhh=uh=xZGpH@sm#Kf#XN2zo$+6%lpy{XHC5RdzVTE zyJOx5>(u$i!euHVZSH>)`!D^J_dwA}|2&BHWWLfaPvvWdHX8YC0ozPbhd=aV=3aB^ z39+Q>G2hRVd#PnU;_q)mXg7NvCHFh9J->jWYt`zK{Y;OCa?VZ0K{?-+{oWp!fRpy& zxoNtm^7Z&>^BK$6@A;9A{EPmIe8@*HUKoj~UAb(0)Y0$niaqH!{hssNeb)HG?;QER zCl9Fh%FhTNt;hu*zA7VeGJ>>mF=?AhuwFPx@ z!S@d19}JZ^8Q@KY`aQveq4QKn7@M9QEEISiLph5b+5!2V28zqzkHAlv_YeK?9O3H_ zf2pC%C_4S#p`VWH@RRBEdy84SJ`Gf8hbGe=2?d>7%*B1O-%R}C_a#$6ihz^ zU((z8pGqF+66!pqk}K3O!+cDA(p!S^k@@ek-9}#%Acm9j96-(@FM#vqs4y>@p2xvKckBA#sgX3rW55VB7KG#e-p04toJ;t-fRJ>cMA7S zld$JDJsx!yn(s^=P|4Nv7_Nuej>X8!(;FHeIJMrJ0tStf=O+IC%Xz1K*NXHNZP|7h zZQcrU(Cc-fe8-aaic#Xb@En)EKYyMlCVzDd*Ujj3EY}RhLWQb-cC6HT*RfoWn{sbX znchzG?$b`6Yvedx%Td-TloyxAvYt$=q}D*srn&SSFWvbX%X>p2XHRaXJj!8%zg-}b z3y}}&u?oICze-=A^GnY#e3^TS{Py-ve?Djm+BcRp z-YmWT$nN{-^%CzZqn7O|JfeL6{9`}(6M2YTr5x`^f?737LN5}UFY@_-B`7F^RhedB$4&RHG^}9T0rQTNg z^~HRT+20>I50>YI{&2RF^mpb{@@-09JGp;R>hDk2tI|n5^m>(gz|?XtzMsAi^!|E$ zo9Qbnw4Iw3*S@*eF716Mx?1;(>-G5+`JOcGJtJafeH;tv6Zqu+37;PIOs!Gr4*1jiTSH=w+Qmz0>`7Sov}ft^YhoJ<`d1 z*x#?d4TDC0_r9rmANBYT*87;Lx30gx9=hIHdF_3#UtU^HJ-gn#TsV?&KbyY4`~k{i zlWKNlUwSB4bFY!yyVG;I;iEFk$3Q?%nty-3{{DHg9voNl@YA2vZ(s1sVfZBay{i3( zR5-_L*5Gyf>$TZ>-pYC}(OC8^d(1e(@dMG2gKjT*z9IMO%Km!KEZwf@Nibw^@;wFF z-{w4%t`0s_OPxz6uFcoajc0G4OX$e=kJFFpdKTTOg1vK5Wp`;j9Upt*3|a0IE4_P$ zil9B96MH20D$0E*vG4@-e3I)Nl1lnR%k?}=o_G4&V+8f*Jd-n-elD`%Xs>^Rg;9S} zW4Sk0zT=2{MRfjm=y>5n=8xPCX8DIY*-Md6lk%L|+x0$N)cLV$NBIt9k(H0g(|m3o z^GV(Q{mnWb&cn!W)Y10=34I;v;jcIDCs7F)&+wibO+IOzkE~-#yUSI8>{rUU4%wfT zb)f8Tdh_A~S}!;c`0Xg?#d>Z$s_H58w)8=b`^gL(%jeHptmo;ZeM@hE0O4f6R`wM- z%>03LOvmrkQx1~Olc&-DGJPDt+mA{=I;i+b$voBU*V^^ze~6`L=k+$f>4RF1S^4Z& z`O5r5^dMaV!NbY<()5|S|H!?twK(TOCw6HIuu05CUQ*x1eb!A`YFa~wD zUx(Sps14nw81HD*6S}nNFNxhJub*kxdn)&+d~3t!sdRM+WPYsI4}9~0+6cMR;otRz zb39~BZG@?yKfsh%dbf+e@b5DHVU?Df$R+)#Dc4!A>2|x?zr)a*^|9=qNxE9+>QmB_ z?-xelFPnaiPH*l%^S&o%)*rIpMtasGs&=VhkBwYv%{|{<{VaZ2{*SEknNPNSy`d=W z(OKB9=D+DXTu?z?ck254=h6KfS1#A%Wlv?VYOfx%9!lS#k|ABbPJfr~7wLCtc_%V) z72dI@LiwqC^*Gv4IHK}tn5yj_$J>#>_&lu3xl#A?p3C)k*;9GUlyB_M0bM@F2|XU` z{w(b#?M(eHK|N~K=`-lLm2RwdW4cKyo!;k>``V=4{OiG^pp*5Hv|p`SZ9=Z*d2ZG| zX+Bx*i&lFf$ZIcoPLeXuNjh^2RR6Hg_ep=0e%P@|_dj`#Q)}J}=3W3qXe#?0FS8so7nhS(_$-aPZKk28V)JOIQ($icf8#UcetaRi@^fqOl zYvX|`eu!Q2 z=94DBlkJ7M$8wsB)T5X4^HY=)bM$k&lsUgrXXYF7oZFkn=;6ij4(gCY1$)gzVA-^;Ge(P{Y&=2 zJpRmg#?p7{aZ}ET$v7tCNz~E&irvccr|o@O_^v$f1g#) z$5ge|dnWxD@5Y=Lyie1mA2NDo?%z*;K-Vv^H?9ae9@Y()+V$tn3;xUfUF7?SsR!C= z^l{tB^G%N1axaCnvptS``sAnK9%qeS%eu7RtT+Ad^JKuUcW?cJnh(wwIAh}YTnH9Zx$jgFe69)d6Fbn4+0UjI z{Pu*}AK0za-L1PB_9gUrEPp$zats~kdl@9@nX1n}$$2r{qo?>jjCN&Kd`^GDUvCtO zD8<)O5J-K!=j?WUKMrCz+G$n(JsQh8&0o)7qfV)UU_IsjU7_EBGF@aL&lNp6g#PU< zm4oc-zY#fb?)_HE;nn}4a`+wE<*k&%Cq|Kj=mGVHih8mh{QG~K9=z}t*Z))h!|G4l zD((MP+Q0iLhyM%h--mqlm2+^MxA@oLwBKOglZ%{JJL$W*D?kKCdZ@NXFZ*LvTA#m9&qw6Enaq1@&Ap511~cv^bUyYzfXvrw&Ap3!ubOh< zI6_XjuS~!6-L)!%D4r_v2j>ZN>3QYKmvg=`Gk=wRe>pcg1DSFLC3NWnT5fWl(R&`C zmjiD>{c99ovi>aEq#*VQG`|hz`D(|sxQa+k)pATsyGUW#?>S6iz@?{C3aB?54K5a) zpvLD|RIgj)xww4i)h<`o!@Q4|S;+Uu#AkX>boT7YZDLu(m42hn5^)>tH zQm^zh&G*=PIYkkSrkB(w(XY}3xnq4lr}*;p%iNE4Xo|$^brI(~svSfR^|I3`GW*qX zK2@Gaoz(9R=zXzle>3f-xF8?8Jlbmh5_>J{zI2VAH_Ewa&VOcuF0neC0d>zY9s%gXgsL)7R+vrnHYYFE;h#xX${=Leog@$oDRLZabpe^>V#`c@X=! zF5zzGUAZ>gfCYB_GO#j`~U``Xk@->o`LNJN#}C z+xZ#e%RW^S`rVKDIWb*PT>G4FLG+k{- z+gq8R`p<#v0Ew)VYMo0|f~ceStHs{VHs9lms<*tbK8L=s$HmAXoZPeO$=%3%wC^?y zv7W{*%X1rfE=POB9N7-V_>nN{Ksm=13(Z38Bzzx8>#Lj(mikJ)L=Md7CZwoUw?#pZ z_PIdtKcwwu`u%#K%C?u@@0RCW+5W)WWU}Lg$g$My52qh;ReUxdt#>jWNq>(y$5s0g zLk!vT1x6l+68ib3j7PH1Bjri`Sg$I>59L*>W(%lS!UclA$<)v6L!=)WyeBZOd zjPLl)fufV|D9ZT+x%Wr*u{d6lvmM6Q@0mbeyBP}knCDLE1sV&#_VY`5FGTEIt?BnN z4tV)l``xnNCg)3;KkanX^aGiv3g78FpxAKf-O9sr4{Z?T-UQiqi$%2I?y1yvAiMvH zio(hByYznQ2lU$5)$}8pUcS%Caf&W|m$n13h@P;?xePBo`Ujmnmo8nhK;w4hsouwu zJ32L%=e1kV&TPSFjGrvX*X{00M-~p^oWT%$>3=MaEFZ-4qY*2d^7Fn=WB4yK<;uM9 z&}#iXrPBC0pax&2{_@>=dmQ5ULw!65x*^cZ^Z6Lw8>TySt)5@W_*8oB1Qjm#J&)D? za-JaST%+jIkLmW1@8w8-9jjFIN#o;F;Oi*(N}Ka8@;;4xe@f0{NVvS0D&yVj@s)m5 z>qFF8run-^*E9Wu4nLR|aa_d6d`a|~_V;Iyy}a)w{UVm9$A6pttseisSa7c5w+wnk z=ijeWcJmGsZq{2e&nMrUn#(!d#N)<}=9VFJlJ~w^`iT*3hjzONNB#Gh@x=5KIlml64L7AAmhRuQz;NX8bqHXM^Fh(eRlxeD>=6 zDK^FDZd0BaC&*U{e5E((d-Ku_?g%N;jpf=;#+5JnMEY&4m&u2CXta7&n{uXTKiy%; zzr$4#C&_=8CI7MN^@Prs^XGv4jlCmZ8{SC0OuLc(T18(m!kp6$RoZW?)_$@^`Tq92 zl5+5+@Au0=>OVvC>8+2)kb|f1cWQc(3;QLzX3V_*tNSVUGm90WyuX{8JW1hQy1Qrl zgRN&vb^hKw>vj5tSH7H|ATw}wf06I{@w_nO{qH64{Q%Jy88>AfNV$KWa`frbB)ms3 zWVCuWJ`)s%*UbK+}s|6(H-n?0%knv3P zQ=Xg0!hZl2?Dei*r58Q7^(p_b3PruA=^uBiT)2MRuj4(e=U+Yd9?^PrkEWORapgR9 z|GP~1gT^=Zv}l9IG9LHqX}$OUY=@Z_$$S{oJZCrbRmSau_-MfaWD(1WsQSlpa?}mO zaxS?v`4v@woFA6_Wd0)kn&(j~K>3XEWj`w!K3}f`Vw%sQr1m@Br+uzxF2zsbk9?I8 zU#8=^-^VV$PVCGLI-KPrsWTn#o&Okq)V$wKK1koOSJTzOWB%(%49E2ZRLkkFBGTWx z;tWULCtkjAh_lkPRD#^NTSD`+~avSW^&3JSn_p~N4FtQ8A z^3GNFx-p+~RX)0+r_2Y0@*SU8-Xw<{#q!Qk`Y|2(@I@e57x>4CnP`BRImhCkuk*W< zVjpQ&_Ctov2lRW{Th074eL$brl=qQ34(R(YJ08*RPs{$Qzq~5sQ|nv_?#PecZ#iYX z;#s~=NryRvoO3%R-OZ#^4X^CRnC;Sqam9RxH#JM2Tk7!EdkdWm_%EBrT0oD`z9-};{nq$N z^#6XeldKa_cRnW&bW2jTpUkb$XMo)XTS2g~#}j9=Jp2pRRm4 zzbxyp(({E6Jzj z2#;?)z4OMo^t)ZMmtCKpuCZSp99Lw2ll`4qmfb&J1B89>4=E3KQRnONv4Eo*>|KEw zA2EN|<0Jp#dt`;MujoKyj*A63dR#1+rpLvCm>Cz@>zMDkdb}%`uE#s%ug5zraM@>I z{jY#*MUVaVNsV{!Q`xEKJYPoEZ*;tqaiZgZ`Cj={JzhlBEv{&9GY@3>^~kqYwHn(^ ze=ok_!}_^hC*}hguprO*MwLsP@%sIPQRCG(sK=F2CMvcd~=$D&X zPu-D~+i!5uVl7Ibv-L*C6VV%a&LjKFqL;GIE%&bFAus;p{1C&&iPE^Pe;H&!cc@C+ z0huStd`9NkqM!YyUwZS}4LZH-+tz_?cC6A3`Z=S_tNV3zoI|;KKT6)imh#9O*Zs4j zB`QPNSE|F@iusj*AOy!q`g6A{GWEW029AinJhzo~LfY*6KG|&OS&%QEvWIo}I^(DJ zX#8Z8q32}OPk)c0H~z-7#v9Mne(9MkK=)_*vd>n!UJsCEX55f@$zc+~$-Nw$uS3P1 z*-bvEA*Ii-aQ>E zxwr4YezzN0)Blk1KMh~jx!kv5S#|hB{gVAy8AlGye?Vm@dQxWe=wS3dH3G_hX#cgk zT&|OtpU6kX(b6fD4$7UZJg3a)f$-6RzC}kdsBzQy@}8&ck9*JM*}ib1r?Fh!59Ay| z(L*L4%rK@l|KIidU5_1C?NYQ{$H&aQHT^Xj*TGYA(9P}?fe$*VPse2%bG@hdG(P(q z%SmFM)ekvJ|0}B3^jyy}J`eIi^a(jPumkPMmX-4bC8!ts2~*LRc}*;5D}%5u*ZfO= zK8SVYQH0ZO(C@i%MAeVuuNt2W{|9rL#Xqj+cCT{?c_`?W9QJoOSu94RN3 z_o_N47j?{g6bDQ@UZc~=^ATCMQ4d*vu`6XMO(*$ro^e81DD7`F{fKPrEz1Yo0 zjrIM8s(H|!A!n`$XSz5@L|?={#=_62_7MFMdB?(2REF$#f5b296Zz$RbVuP+kLeeg zej@AVI)syMi}878*{?6@dPKsB-KKu!0zZL&O78Gcl(Ww0`$pX@ogx>3aO_X4KliCs zKY$$RWZzTnJ!H9l`SlyRji+h)9q^csoxkE&^#t|@$j_8ddL_Rq{D70^m?F1WSdZ&( z)pI+(XRXV@x&=OpyEbV5wi@NnKJPOs|JmpNo$|l9Qa`t5`_=g7=RH$1irzmj z>O(!`{t170jS3%9^voxCPsp$?f9l{#`BkZO11E)7sPLoN`~vsI=yigeQ}Xi>SLymx zx?P3)jlW0Tud%CepYo%Qp18(xsKt)6>^vd=cwnz_Bj1i3nG4FGUV=FLE6^O&PJN#}@F3|DAOg zsABMpUG`sffVWRNS@)ef5Hr7Zn>4+DUzhuwGGAL~?D0X|+dqPEFP}-ezT6K}`4~G$ zIq@g?^Y6Dxeo=Kx7VNXeYVv zOzvME>s&`N@|<3epRdbddUel=DPQ&p8g6SjL5hR;es>1Gw_lZu`lG85QxXT2o@V!} zdMevhI%)5osXG;>9oYM(?8%6iPtcwmSK+ch>+k>Co@DLN&TU!;z&7SrC)=NP$WhL@ zD*N{-KfSgy{`^}M{aAMCh~mpnU$5v-wnyyAv`6&)^cl9RnQyb5-VS}4ZTgqk1CclR z9ePvp9T-EtT269rpPxT1-#1==E#H&tPd#G&>C62x@?0W)jefo-_l2de*U$Z8IWs|x zeu+e=y7ycwt9M#2(i>Fwb>v@x>1JTyDmtd$qoF%wVp_SDRUm8BQHUsCN(z8E6sIb)#`Rj#ZL zBwX$b;<+6@3F&WCWFEgA4H~mul>IXPQ#raXiQSg_5lYXWq7txO-icpK$9Cyx)cmbm zsq^D_#2m_&YP<#IyZHHx@t-pH5j=q~ro80>z28)4u+00s@zV4wc9|+`ye#2(Y3#J@ zqmD**8tB;X%B}QY{cU!9mwif(A1-*=Vf=$(S7DL+!SFe%{$tbgd~yi$Qr0eveq!!{ zLw+AHb4Ntr{JriUy9$d({spAF3Z0PwgvFPh7I9cBRc7j8k$d`1t&Kr&H*C!}xKV8`wqtOlP_ZAJm z%zEMY?f2N*uT?10(WWB#}p~hVQA}dGUpOgN{_T@TKQz3ql z{YM9#YFG$#Gf>(GNFORwy(o&zukk1w-+CtqhU`~t?JzG!Tyn8W!VcT(l~ z_70dy=F9as!)fJ?_`ci4{Q^H0yD#S-xW7UB#Pzw#cP0Sb4zw}nkF<*_U*sRi=Q2g6 z`avMP(S(D~K==nuIPwdG-=o9XKUj{e=Q}7|xYR7YKi^^OTWXfRzgx~X$UWB`n}q%k zs?9o92-f%P%X#Y5tn7E0&>^$!YxH5``8pltPq}#dp!m_ekV4i+etNc#&_4magF1NY zD9)o%+zF1;)HjX~wQyyoTzPMZa##Sz@>wL)5f_>Ih@LVZL^<-F9N)_X#B&+_UO;Ez zi4$CTE4daG70p0q+;7`-xE$hs>|=0heU9xX?lF`42@IIkGyfqXBWx%L43E0lpGHzNBM3Awk6dw1UbiX z8E_o&vYwUvQd6H)_2>At$2We-dnW#Q9{Z`hx8VV_JsT; z*E$$I-DyP764-J2{bqcafqqF`wp`P1L1xU4^X}K+b36+9_SfWI@Mc55b*k>She1c@ z%?}T1%;!Sf*C|@9vGlu?;ZMFx#q{hY2cySNu>ZapKFGhE=Xe5uy=%04L=<199`^kq z^8B3hH0CGuV|)GWDD7dcRr2mv=e~zaK33ol+%u@03qJ|_1aX|Wo0-C~{pgr)6a1)? zP?+JA!w!?r!N~be@r*3Jv@f4?xTG__wCfV2Bfadytv3edfZU^%F7jWrbmB#;>6Hzqa05tCuwil?-BWn{HEi*od(cF@ooVf<)i9r^l}E| zr|N6gH^L`<7xXeurJMkUi$BQRQ#W>=`RcE@iJw~ISfp>%Q?8w`j@}{WA-nUi>%ao zNcpH29s9$YQ}na3?A~FdSNdSu#qJ*mqoyC3a-<(nk6rxRVSL%&puJT6$UGlozaaMd z#bhLa<9u56BhzpCnIYT^^n*tDWgQAS{`WPMd@j@B+}C9NdA)3)U1=2Hn%}Mctt5mK zy-;?V{fO!P{m<~#oG|I8n0|zMtA1qka60ZSR{coi=F@){Nk#9c>pmd*^e{%pKAR1$!PP1Nt%yS^$T|0 zgCSSxFZ&6dwBr)UQ|YE&C_~HeWJckkG|yTIpy%*sn1FdrrrM%`W(pr&(LR;?iBh=xyd|n zG=27$JEpxKWj=L}PoLALv-PmwYVH-0dvc{e@cbB6_X8w?ll>0+J`tG@a2#3f8%IX_ zUdNzso$%+Z^Ln9Ivj5I?+#CH~$9|RW&s*oo`x~s!t^ba4XZ4~Na9+D#`7HnV|FC+j z9-|&5Q=ufr{ZD9*i%<`l$J4H!3*O~^EbiO$W>r4; z43uy9PpsGJNvFz(?;>c2@mXJmjei(%Ecvjhcj-dS2Ql3t!5TV6hCg`^VS0FhGYxSk z@rU~P`Q!S-k7bLIvQsAEx8+15VCkC06VAU+Td3 z3GAP08{%YNN-oV$4iWyy0- z3779=$a+kkOKkzIi+pw%fA(qk0mt!>EpTX|{(fsw6+`v7%5cxFQaEtbYs!!N-|as8 zLOV3{ZI+R6&HWrg2+T~A=Y znr^N+Z!6C?q$Q^dRUQV?;qdK~ zH2sL-?*d&>`@6q$Yg5V2)ZEVQ*4AX_+#PM5Tj#d7B)T@Yb#9y8)z;RMvSC|y7pPNR zb6c7>btXE~a~s+^lXFv@4Rf2jkV98zqPZ)jekZ!xI_Gqz7}e0;yfM+x(AM3Gxb5>z zXk&8jwzftCnp;y{o!t#x&26n48xvg#lM`Y$Cz@N5jc!+)yScfg#of`=){=BLb#LCB z?5u4~?rcvsbRoJW*}A2x$!**0Hn+Be$2VQCL`zFsgQ{4nrMV#qM%K1&PqZ{Qx|<*u zw=>z^nM@^HyAou%(p}xz+1B}%mfF?X+`0wDzLlAFCU$Jxo@_v_SGRS!sG_?m-IYwC zHi-P<-LJJ)SqOQEKeXj9t~J2$p8Z)@(_(cIY8w0c{6S6W4zB zZ3p_4+uqr>r8BY3ZES;_TOsvriLQnw)g!vQn%X*>yV9s;(%se!!6K&8m7d}@CAK4+ zk-M&-$?}Vkfu zsgh{db=#Ajn_Jp;KmpfKZtI~3jqAF*YT8oC=EkMY z?o8Mfv>dxSdYxOJRez_?+UBNUFB3?VYELvImp3+cLPE>8!}u|O zx3hUmQ&+vKjbWv`Tw7C++?ecaOE$*m&0nx^(c&c)OB0(K8k6X+ik}*-d}|w5v~{MM zl#N=~&T3aDQcdgIRNby?-j!q~D`C~!wwZ*>JGXSRR~ooBt9q-Tc|xR_V2I&|Ki9Xl zUAGPLM;aJu$P?MDVFy{;ieA@=R`9sGiXv#;a&5AsySX#jcwO_B)9sl!(^0DIsOEz82iC-kF4vt3`cGUxJ=(Pjs$rX-RHLw5(${NA4y~N{NGm zhwg<|QP(DwYJ%P;)AgNb@*rm`x|>_Nnp=&iQdgmIRHQnV8Es^(M!41Y74xvf*Tjv=MgbVHro>Opm*zM4eA%uV2m z1WZ9gQfH>iU?Z9~GYt++OLiJ&(TUK)FkF;TO>=8oLqk(D1}J5>l{!!Z*&~yw)LM+3 zt&n7MR~_VhszBH@w3DX43Kj><2FOjA9y(R7CPXK^GO>+@+Y}gx%I3MU1y=K4DXP)(!s z|17=axNh@im|KICFll;{orba|fw3`(nWL!k>ej~Q1Z;IGm7Uzt&SL~7uBnP@McS?|J}CtBTlyPj;Qt_gLA zv1vul$Ro-58!Wm^C|lBs*-FaI_wx>hCG<7q#QcKG_R+vw?bYZr%qrSQ~q7h|Aw zoP?WFs`q;Hiv_R#$5(#ZysZA0*Z*+w{a2s0Ve%8tR{ZmKSC^FE`1JBe>o?qcf>wa~ z2<{s-SFDFN$ee&AwCzNVOf;37Y<0IcCc2Us$-NRB?LI~Y#!z?7MrrscFKqWD=mP(>M zS$NH=5R@@Bmp$?cMVKVyQtv z_y-2wGz_FA!KO${ox-8=TF5^kwK{ zV2zE1&aqcvBj_)reSg{9^?JuMeQE+wsV2PGe^85`6eG7cW1Ya=r-Vn^oHb=GW>#t? z&DGy>uCP5THzTE2>1s)hjW2fxmg~)W{s~IcbN@UlW`S)v7h44_ErCon=~=HT#G@eF zH25u;VX2YX#~w!snJ+YASF06U6Kcbu3lTC4%2v5?xth7C<(Vp^(=6K5)&vu)b+HnG zEZ6+fP-(b~R-*K(53Uj{H#=e3l;jSIZbdIKBD>Fk!)P9D{VAd z*>t_Sg*f#}TGbajgU#Ezw{c^Usa$MDVIr1jO(oRUi^)ZI3oPxuMY6wZtj+cyriIY4 zqr$D-Kc^(KdKUG?HW?SL=!slFt1f{RH0E2`CA8PBs`?Oy+O$h}i)pvb>|1%lpop4J zT3WjaSzCL?<1aBV(=_8mLZyL@{ZZcXI|k%1tRBxGMsGHry~Z0f}3D(2G7 z?Jz-VyHFNk$(_xqu9UYk>+WdlZo!P4+nOpjtR1;8*4CMZP9?I)3swe!{Wi@VvDs~0 zpmD1aT^FX2n=!I`h|SdI?rpxfmbSKb5@5Zf3gL8L?cTfP$vIo*p!aWUX>Q#L`_$Gx zGu4&AUZYfsxxhBJHfHxXoB15TZI@NM8O~IX?&3x+x5U+kvbzggim+!}X{x(h`Ah9% zGEX0O9o=o%$d<0H3RK2YJpxFk+PXVo05)}}(lA?QhelOgg*Bjr6qbzJ@q7k#LM2d7 z7&-1Jz~XL8w5C;XN(OWwm;tfI+4gA0-bQC0F!D-mA4j`$Dqx%e6XR-Ns!*b_AF$tc=&TV&mVWTLqO}p;Wd88f$jl zRx3@dqkgWSdLo16t3)~1rn0}iKv1lvVq#;nksAkVJhjaJx)Qo|9aYN|&6sMXS7w%~ zH>>pAh&zFGd?vw7>Fe~S_?qsP78G=y(V}e5*HC|CiDbEV%PIh;X z6bxBO%}J(KDb>N#sJ5%1D_25;WE+Zv)T8BQDtILnhgo9~=WN|5i_d&!cdJ&nZB{#I zS1AoytG9~K`0J=3wSLu5x=?9UNe!BM&)DqpUnI|$w5}=Yh6J7t@JWOWM`{pNdnMVm zP&N#cQT-4x!KnwHFJVi03y0NaOe|h+GfX`Z;WUQ33!N=c_AM|oFhF>ir6-It=ge85 zCP|7nv37d5Ic?@o$;L{z@+R5B)5{fm3(u|RrmES(Yfs>j+*WiPObjav)%21~z<%L5 z3hd*uWz&|aCuMwY>3=Fx=whm{xvSDG)ZzSLLMmOi$Gr%D3NI?W-MziM9R4ldG8>PG z=C*ck!$=z68IQ;3#plNt#23aF#TUny#4F-U=f&sEn>T;nf_V$)Et{=7tUWafARb!^DE{rT@YU|Z^3-L{k3qxq6Lc=ELl*oVClm6!g&klFI=#2 z;lf1=7cX40uwvoTMe#-R7R_I@V9~-wixw?jv}94mqNR)Di{~w#zj(pog^L$0Uc7k8 z;)=yfm&BLMTQYyif+Y)=ELyU7$&w`%OO{r|E9O6sR1YA)<0#5E{`u<-KXnoH@5_N^XH2!#)@vSC!ZEv73>$Hw$#}l&e!e z=fpGri(RS@7c(g=j_~XZ_OPA-#y0Bbp(@lX(>!0D+YW_P8|`4o$qj{aLXmJZFE5;* zUl1;gjtftW6o)2-Pm7*@_8B2uUKBoa!r9T%{Ibybq1z%`!%yaXI{f$HZ-&1W{`UCq z6g(e(A^g42Q1r#{%aP&mEAB|-c=$gfe+Z4Aaq%TruY2HQAN#AG_dN8$NB-vX@BVad zUcusJmt6nv1K*0AQL=c+^>y$3>rXxT#YID>-F?q{KNguVanfls=Pj&Uy=Lv@SFdYK z-h1Eu4}A6?zVg+lzxMpipFMom*?IYen z+t~lgt2b`?%^y!(xBkN)nKO4rdF@B{A9(b!CwdQk{_nq`l{Q%|Ky<8H_gmu@P!uZ7ofygAecwHit*_mm*18fiquBWS{U9v@uEoM`0EQZ zpQ|Q^Z@wPCLC|a%f7VBy?uZ)V#Ao zXNSusyEzx;%pNx<6pzdc&ksEj`FQxRBcI6s&+s3je+s{rbE4qT&h&fke|ox4}Y99_tKkh+4$03_rLeS_kH}+pZ~&BPe1#QKl&-I5R9k}u%zi*ybbLKwrKXA#ZX)NdeXFHgu7xxi{op7+q6%Q3T47GjH5Qk*T@kax$Mp z)ZBTQ-{ik7rzq!=Xhr^o(K}8|D#@QaX?D)EqG?5$`yzLIc=EW|p7%%RMlXh_dL{oG6w{+me*KQtCU#wbm>zr7=#qS zA&DH^(GYQ+2cz}QEvN5yPMhMEjd$zIUcJb@a8`W(g>8@5Uli^gy6Bug49#|4a~FT? zME&AFgp%MUiCX8IiR+W!I&i)7y=isM(18t~dTH8?zxwHmHw_FYZg!nlZXOA}^A>!9HqV)j zm$Ae6FH}7)9xDzd(ca;3C~{utoU?8kS6NUHDv5*&Fq%a#%(=K=R!PWRf&`I#v};~r zxHMGBx9M%wX6 zS&r||WTNod;iW)QN@=J%6bT{oP=4r|P&jXV{-#j4U|inX@L9+& z6k0Mdgc76ULgyERHb+9aU@ClOIFeHwnSh_UA>1^b<3!HRDGi?!t_p|p@W%(3#ErIrETjUT9g)`O#4RC86=*g$0mVBplk96OKSEp^t`g z@?%Q2x*aCwzTmZG1r} z{M%4rBof+-@*^R)usm0_U2ZsM4w}HpgP20sP6lhpV^=P+M=L=8SWw78`!9?}bEcrS zxkb)+T#*7(;Dj!XTmx-F-RI!q$7oJ&ettOboXGog@FM2?{LsYE zk!D%MiQtvCylr_-eP$$kMrq_nVP}(pe~WXu=E$pa?T^EbIY#UpL;N&D;}8ED!i2^j z{v5(DH1yrbN8k3R?>I0=Wu1o*u5Yc=-z+oa$}`7x)m;w~UT^O7XhOOPCh)`Xr3-VHd1ZJ_W)SkFV@aAU>qG4VoI zUI;@6cl0LZIfKTpFmw_=V8Sl}AGMm)xgY1=g};Xoe!U655GNU zI`J7iaCMeQ!o_Dy=899tSHDjB4WbOlM)DP(IX4;p{rYq-4)%!-`@`=;xadaT7<$sX zQS&>D`@rckM!v89OHdvYUk+l@3SZ(g?AYt@f5g=P=+DFEHbC6fj&!_#iOwI+tY)jw z;2%`Wb4Lu{etpc~rC`yuale33@u(f=GGN{rBkp=&bG?HKivBXo7X!`&mb{2(0E-PG zjssuJAI5D3_Vd>QTw&r^7b*KOg!^^fPImJVrcx~%y)qa|1R>8 zvF;ecri~K*EW)H7etQ2n=?{Mn^fFE|JJAF285Vy+Yx}DxU%Ec^LB@lXc#&xn3E}+X z_d$dUzibofQ~q%3=;=%d=MQJP(ZbIjBb>4rjsAi$!dd6h=ocYeiFZYlm7U(b^1Z@;gShBKmUC%>iB8@nzi5Mppm{f z7va*^4uDi_k3akn!p~DV<6S8qJ;OzJ{o%d7@TH)acG!bFq(At>{q1_cFMsNdwD-fl zeEjskM7Z!k79aafwm!t`|07$zKV2`vCy)RRpHK0|4Lf|MCUIT`z34oHa(|aC=iR_E zcKX9#!8~q(NnZ?lk%OO)ffZp-=Kgk*`RG{j_PllI3=~GUQjd!z#+P}kgcqCe#xeL| z7BX&#&#=K&I$!>VxRev0V8aF-U$uJl`ZOV2T(SA#uW!x9Y`lbvPx4-r?=MgC>iEyF zdM8)#+kyRb`;o8cv_C)RHeKEkPJZFES_HzgqL8wm|82;sFKQ>H(hc3$Q&!!Ks_;%bDK8+w2E zjR+H7CgZ()&MU;(?e;6QqqM_)Ad>32;V2)0u8>!xK#>)z-4HrESYbM;#g^yWy#KI*X3Fg;i;f#g%TX?|2&i@JK7q@V|h5Iag%)-MKuJ~v$ z{~8;AEEs=`8)b0PUW1lwhl26>eL;NG!h@d<##endhzAY_@vw!DTlm8ueR_83-?-h(85*fdwbdZYVv~lXDr-j;S9bz>*uG>!d2sf@p~*hFd-OU zGBJp&Eqv5`$5HA(bb659IU|VkaiGf2UyX&U&JMdK7CvU-fmuQNy^Df)-;y96 zu<)>jk6Soi5lmlg;SCmUS{9_=XJO}(VEmpHK|Fj@5XWy0;(7}YHw5GJw+3-jOAyzz z2eF$9;(>G!kK7)_P45ZfeHI?v8;tKW_d<%k9C?2*e%Qi$%s#0{Z{eMf1k?9gxa!eh ze8$4{AGhKye9XpA2I<@XHi-BAT@c4V6U6184dUX%L44f8Lq~$~$37RtC0_{QiZ2Cm z+cdJFHg@IDLoS-AMS!Sbptyw}1EIef4;}&*t#=$=xR9m>+oOKX=Dt;+QUvA-g3-7n^aSIp! zB$!`?g*RBZ*TP3FJZ$0ep9b^aVBtMB{&|qT{g*+!$HMz8yz^H35w;#vN8B&pF@fE@N8Vm2S@DU3S zTiCram|wMp+bz7$!pAH;V&Rgjg85fjxXHr%EPTwuLl!=6;o_@<<;5*rW8o$X@3HWH z3m>uYpoK>)T)ZyGPq~GwEWE+OJ1xA=!hIGVu<(e5^YOxrU%y-nS6R5;!Wj#;_HIxt1Vn_;f#g%TR4AxFuy7bZ?N!Q3-?-h#KOh3!Tigw z58@3LuBi*gXDr-j;l1Weo7jh53lCYi_=aHqRTgfy@O}#)v+#(8OK!CCw{Vk%_geUf zg@-L%d{Z$03Jcd*xXHp93-7aVpM{TEc*w%e%|U)jEF8CRwS}83yvM@57Cvg>AqyY3 zaPcic{^Ay{v2eSE_gc8m!b28zZVi@~v9P<*ins8fh4br!^c4+3e5@&m_u<7rzkNGu z;Xw-zTlm=4VEX#jAl}dx#78VVY~hjiAbmwg5bw0`Q41fpa8qY6eQzp=M=ade6^tLW z@Q8)WyMy%Y7VfpMyFEzXWa0f5K5F6W9l`WFE!=D2_FX}G_w7M^+``4T2jlxJ9DheJ ze%QiI?+nHdS-39~j32h}zTLt2J_~QSBN$(NXAt*Uc;{Wg_#q1)c~>yL`rSe7+#SUG zEL_b4zi=|1RNWKA)%ON*&3i0-pM~!a;;Q!sal3{0JQj>E|6~vk9}41<&js-w3lCV> z{d|zV>I*?U_{AW0zZAq9EWF3UM=V^?A55RI@DU5Qe>q6M&%%8cj(;UcUvJ@^7T#;& zqZS^taQoB2{4*9lX5k?VAGdJvGr|1YpAF(879O;4$v+0^_gHwq!Xp-re=V4P=huVS zc`k_eeY@Y_LL{GA})Z(-;8VEl#`f_T4$ zU2`y3`g@awhb)4~}G@3HV+3-7b=ehVM5 z@PLJfEnG4#*j^PD-eBQ+3%6T%pM{TDc+kSfEnG4_$d7B`3Jcd*xXHrp7T#&$jD`1F z_=trEEj(o5;}$NT5Ufw!!qpbuVBsbU@3in93-7aVuZ53T_?U%)oL>~IPqBr|EnH#YY75s}xZT1T3-7aVpM{TE zc+kSb7Iun*^(nD%+``osuD9?`3-?;M&%(znJY?bH7A~F?E(ufpGPE8d9(yZdJDH(IAh_x7T#~+J_{eU@Q{V``Jq_f`oGx1B^Guq zTw&oF3)fq?-NG3Q@3ruL3-?+0sD%eCJY?Y!3p?f;xzb;YEnIHlDhtaub|t-hn^*82 zi+-Pldo6s_!h;qbw(xNa=bQIIrF{8@x8QOsK7XTLe-%Op_~bqQc4~vZ>x7z7-mjo~ zO;2gcmxH+YD?#i&7sSy-oRvZOskNoKzF2acwYY+L(kny(!09@*nF==jv*E{(#|I$7c6W1y|t*lbmJ( zcvl{s_@~b@5yq4LDWOI9!St%n!T0t-%>F}sl2B^jPCpyL(#w13;(wAvK4-G?Le9o7>W`$C_vFQw f_vERgbW*, + /// The proof of the account data + pub validity_proof: ValidityProof, + /// The account meta + pub account_meta: Option, + /// The address tree info + pub address_tree_info: Option, + /// The output state tree index + pub output_state_tree_index: u8, +} + pub const MAX_ACCOUNT_ALLOC_PER_INSTRUCTION_SIZE: u16 = 10_240; /// The counter has both mul and add instructions in order to facilitate tests where @@ -184,6 +202,26 @@ pub enum FlexiCounterInstruction { /// 1. `[signer]` The payer that created and is cancelling the task. /// 2. `[write]` Task context account. Cancel(CancelArgs), + + /// Compressed delegation of the FlexiCounter account to an ephemaral validator + /// + /// Accounts: + /// 0. `[signer]` The payer that is delegating the account. + /// 1. `[write]` The counter PDA account that will be delegated. + /// 2. `[]` The compressed delegation program id + /// 3. `[]` The CPI signer of the compressed delegation program + /// + /// 4..N `[]` Remaining accounts using by the Light program + DelegateCompressed(DelegateCompressedArgs), + + /// Commits the compressed FlexiCounter + /// + /// Accounts: + /// 0. `[signer]` The payer that created the account. + /// 1. `[write]` The counter PDA account that will be updated. + /// 2. `[]` MagicContext (used to record scheduled commit) + /// 3. `[]` MagicBlock Program (used to schedule commit) + ScheduleCommitCompressed, } pub fn create_init_ix(payer: Pubkey, label: String) -> Instruction { @@ -194,9 +232,9 @@ pub fn create_init_ix(payer: Pubkey, label: String) -> Instruction { AccountMeta::new(pda, false), AccountMeta::new_readonly(system_program::id(), false), ]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::Init { label, bump }, + &borsh::to_vec(&FlexiCounterInstruction::Init { label, bump }).unwrap(), accounts, ) } @@ -213,12 +251,13 @@ pub fn create_realloc_ix( AccountMeta::new(pda, false), AccountMeta::new_readonly(system_program::id(), false), ]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::Realloc { + &borsh::to_vec(&FlexiCounterInstruction::Realloc { bytes, invocation_count, - }, + }) + .unwrap(), accounts, ) } @@ -230,9 +269,9 @@ pub fn create_add_ix(payer: Pubkey, count: u8) -> Instruction { AccountMeta::new_readonly(payer, true), AccountMeta::new(pda, false), ]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::Add { count }, + &borsh::to_vec(&FlexiCounterInstruction::Add { count }).unwrap(), accounts, ) } @@ -241,9 +280,10 @@ pub fn create_add_unsigned_ix(payer: Pubkey, count: u8) -> Instruction { let program_id = &crate::id(); let (pda, _) = FlexiCounter::pda(&payer); let accounts = vec![AccountMeta::new(pda, false)]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::AddUnsigned { count }, + &borsh::to_vec(&FlexiCounterInstruction::AddUnsigned { count }) + .unwrap(), accounts, ) } @@ -252,9 +292,9 @@ pub fn create_add_error_ix(payer: Pubkey, count: u8) -> Instruction { let program_id = &crate::id(); let (pda, _) = FlexiCounter::pda(&payer); let accounts = vec![AccountMeta::new(pda, false)]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::AddError { count }, + &borsh::to_vec(&FlexiCounterInstruction::AddError { count }).unwrap(), accounts, ) } @@ -264,9 +304,9 @@ pub fn create_mul_ix(payer: Pubkey, multiplier: u8) -> Instruction { let (pda, _) = FlexiCounter::pda(&payer); let accounts = vec![AccountMeta::new(payer, true), AccountMeta::new(pda, false)]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::Mul { multiplier }, + &borsh::to_vec(&FlexiCounterInstruction::Mul { multiplier }).unwrap(), accounts, ) } @@ -300,9 +340,9 @@ pub fn create_delegate_ix_with_commit_frequency_ms( commit_frequency_ms, }; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::Delegate(args), + &borsh::to_vec(&FlexiCounterInstruction::Delegate(args)).unwrap(), account_metas, ) } @@ -320,9 +360,13 @@ pub fn create_add_and_schedule_commit_ix( AccountMeta::new(MAGIC_CONTEXT_ID, false), AccountMeta::new_readonly(MAGIC_PROGRAM_ID, false), ]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::AddAndScheduleCommit { count, undelegate }, + &borsh::to_vec(&FlexiCounterInstruction::AddAndScheduleCommit { + count, + undelegate, + }) + .unwrap(), accounts, ) } @@ -339,9 +383,9 @@ pub fn create_add_counter_ix( AccountMeta::new(pda_main, false), AccountMeta::new_readonly(pda_source, false), ]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::AddCounter, + &borsh::to_vec(&FlexiCounterInstruction::AddCounter).unwrap(), accounts, ) } @@ -365,15 +409,16 @@ pub fn create_intent_single_committee_ix( AccountMeta::new(counter, false), ]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::CreateIntent { + &borsh::to_vec(&FlexiCounterInstruction::CreateIntent { num_committees: 1, // Has no effect in non-undelegate case counter_diffs: vec![counter_diff], is_undelegate, compute_units, - }, + }) + .unwrap(), accounts, ) } @@ -408,15 +453,16 @@ pub fn create_intent_ix( accounts.extend(payers_meta); accounts.extend(counter_metas); - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::CreateIntent { + &borsh::to_vec(&FlexiCounterInstruction::CreateIntent { num_committees: payers.len() as u8, // Has no effect in non-undelegate case counter_diffs, is_undelegate, compute_units, - }, + }) + .unwrap(), accounts, ) } @@ -436,15 +482,16 @@ pub fn create_schedule_task_ix( AccountMeta::new(payer, true), AccountMeta::new(pda, false), ]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::Schedule(ScheduleArgs { + &borsh::to_vec(&FlexiCounterInstruction::Schedule(ScheduleArgs { task_id, execution_interval_millis, iterations, error, signer, - }), + })) + .unwrap(), accounts, ) } @@ -455,9 +502,50 @@ pub fn create_cancel_task_ix(payer: Pubkey, task_id: i64) -> Instruction { AccountMeta::new_readonly(MAGIC_PROGRAM_ID, false), AccountMeta::new(payer, true), ]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( + *program_id, + &borsh::to_vec(&FlexiCounterInstruction::Cancel(CancelArgs { + task_id, + })) + .unwrap(), + accounts, + ) +} + +pub fn create_delegate_compressed_ix( + payer: Pubkey, + remaining_accounts: &[AccountMeta], + args: DelegateCompressedArgs, +) -> Instruction { + let program_id = &crate::id(); + let (pda, _) = FlexiCounter::pda(&payer); + let mut accounts = vec![ + AccountMeta::new(payer, true), + AccountMeta::new(pda, false), + AccountMeta::new_readonly(compressed_delegation_client::ID, false), + ]; + accounts.extend(remaining_accounts.iter().cloned()); + Instruction::new_with_bytes( + *program_id, + &borsh::to_vec(&FlexiCounterInstruction::DelegateCompressed(args)) + .unwrap(), + accounts, + ) +} + +pub fn create_schedule_commit_compressed_ix(payer: Pubkey) -> Instruction { + let program_id = &crate::id(); + let (pda, _) = FlexiCounter::pda(&payer); + let accounts = vec![ + AccountMeta::new(payer, true), + AccountMeta::new(pda, false), + AccountMeta::new(MAGIC_CONTEXT_ID, false), + AccountMeta::new_readonly(MAGIC_PROGRAM_ID, false), + ]; + Instruction::new_with_bytes( *program_id, - &FlexiCounterInstruction::Cancel(CancelArgs { task_id }), + &borsh::to_vec(&FlexiCounterInstruction::ScheduleCommitCompressed) + .unwrap(), accounts, ) } diff --git a/test-integration/programs/flexi-counter/src/processor.rs b/test-integration/programs/flexi-counter/src/processor.rs index 47ad47cff..c0fed8b98 100644 --- a/test-integration/programs/flexi-counter/src/processor.rs +++ b/test-integration/programs/flexi-counter/src/processor.rs @@ -2,8 +2,15 @@ mod call_handler; mod schedule_intent; use borsh::{to_vec, BorshDeserialize}; +use compressed_delegation_client::{ + instructions::DelegateCpiBuilder, types::DelegateArgs as DelegateArgsCpi, + ExternalUndelegateArgs, + EXTERNAL_UNDELEGATE_DISCRIMINATOR as EXTERNAL_UNDELEGATE_COMPRESSED_DISCRIMINATOR, +}; use ephemeral_rollups_sdk::{ - consts::{EXTERNAL_UNDELEGATE_DISCRIMINATOR, MAGIC_PROGRAM_ID}, + consts::{ + EXTERNAL_UNDELEGATE_DISCRIMINATOR, MAGIC_CONTEXT_ID, MAGIC_PROGRAM_ID, + }, cpi::{ delegate_account, undelegate_account, DelegateAccounts, DelegateConfig, }, @@ -28,8 +35,8 @@ use solana_program::{ use crate::{ instruction::{ create_add_error_ix, create_add_ix, create_add_unsigned_ix, CancelArgs, - DelegateArgs, FlexiCounterInstruction, ScheduleArgs, - MAX_ACCOUNT_ALLOC_PER_INSTRUCTION_SIZE, + DelegateArgs, DelegateCompressedArgs, FlexiCounterInstruction, + ScheduleArgs, MAX_ACCOUNT_ALLOC_PER_INSTRUCTION_SIZE, }, processor::{ call_handler::{ @@ -52,6 +59,9 @@ pub fn process( if disc == EXTERNAL_UNDELEGATE_DISCRIMINATOR { return process_undelegate_request(accounts, data); + } else if disc == EXTERNAL_UNDELEGATE_COMPRESSED_DISCRIMINATOR { + let args = ExternalUndelegateArgs::try_from_slice(data)?; + return process_external_undelegate_compressed(accounts, args); } } @@ -94,6 +104,10 @@ pub fn process( } => process_undelegate_action_handler(accounts, amount, counter_diff), Schedule(args) => process_schedule_task(accounts, args), Cancel(args) => process_cancel_task(accounts, args), + DelegateCompressed(args) => process_delegate_compressed(accounts, args), + ScheduleCommitCompressed => { + process_schedule_commit_compressed(accounts) + } }?; Ok(()) } @@ -325,6 +339,9 @@ fn process_add_and_schedule_commit( let magic_context_info = next_account_info(account_info_iter)?; let magic_program_info = next_account_info(account_info_iter)?; + assert_magic_context(magic_context_info)?; + assert_magic_program(magic_program_info)?; + // Perform the add operation add(payer_info, counter_pda_info, count)?; @@ -415,10 +432,12 @@ fn process_schedule_task( msg!("ScheduleTask"); let account_info_iter = &mut accounts.iter(); - let _magic_program_info = next_account_info(account_info_iter)?; + let magic_program_info = next_account_info(account_info_iter)?; let payer_info = next_account_info(account_info_iter)?; let counter_pda_info = next_account_info(account_info_iter)?; + assert_magic_program(magic_program_info)?; + let (counter_pda, bump) = FlexiCounter::pda(payer_info.key); if counter_pda_info.key.ne(&counter_pda) { msg!( @@ -448,7 +467,7 @@ fn process_schedule_task( })?; let ix = Instruction::new_with_bytes( - MAGIC_PROGRAM_ID, + *magic_program_info.key, &ix_data, vec![ AccountMeta::new(*payer_info.key, true), @@ -472,9 +491,11 @@ fn process_cancel_task( msg!("CancelTask"); let account_info_iter = &mut accounts.iter(); - let _magic_program_info = next_account_info(account_info_iter)?; + let magic_program_info = next_account_info(account_info_iter)?; let payer_info = next_account_info(account_info_iter)?; + assert_magic_program(magic_program_info)?; + let ix_data = bincode::serialize(&MagicBlockInstruction::CancelTask { task_id: args.task_id, }) @@ -484,7 +505,7 @@ fn process_cancel_task( })?; let ix = Instruction::new_with_bytes( - MAGIC_PROGRAM_ID, + *magic_program_info.key, &ix_data, vec![AccountMeta::new(*payer_info.key, true)], ); @@ -493,3 +514,192 @@ fn process_cancel_task( Ok(()) } + +fn process_delegate_compressed( + accounts: &[AccountInfo], + args: DelegateCompressedArgs, +) -> ProgramResult { + msg!("DelegateCompressed"); + + let [payer_info, counter_pda_info, compressed_delegation_program_info, remaining_accounts @ ..] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if compressed_delegation_program_info + .key + .ne(&compressed_delegation_client::ID) + { + return Err(ProgramError::IncorrectProgramId); + } + + let pda_seeds = FlexiCounter::seeds(payer_info.key); + let (counter_pda, bump) = FlexiCounter::pda(payer_info.key); + if counter_pda_info.key.ne(&counter_pda) { + msg!( + "Invalid counter PDA {}, should be {}", + counter_pda_info.key, + counter_pda + ); + return Err(ProgramError::InvalidSeeds); + } + + // Send back excess lamports to the payer + let rent = Rent::get()?; + let min_rent = rent.minimum_balance(0); + **payer_info.try_borrow_mut_lamports()? += counter_pda_info + .lamports() + .checked_sub(min_rent) + .ok_or(ProgramError::ArithmeticOverflow)?; + **counter_pda_info.try_borrow_mut_lamports()? = min_rent; + + // Remove data from the delegated account and reassign ownership + let account_data = counter_pda_info.data.borrow().to_vec(); + counter_pda_info.realloc(0, false)?; + counter_pda_info.assign(compressed_delegation_program_info.key); + + // Cpi into delegation program + let bump_slice = &[bump]; + let signer_seeds = + FlexiCounter::seeds_with_bump(payer_info.key, bump_slice); + let signer = [signer_seeds.as_ref()]; + DelegateCpiBuilder::new(compressed_delegation_program_info) + .payer(payer_info) + .delegated_account(counter_pda_info) + .args(DelegateArgsCpi { + validator: args.validator, + validity_proof: args.validity_proof, + address_tree_info: args.address_tree_info, + account_meta: args.account_meta, + lamports: rent.minimum_balance(account_data.len()), + account_data, + pda_seeds: pda_seeds + .iter() + .map(|seed| seed.to_vec()) + .collect::>(), + bump, + output_state_tree_index: args.output_state_tree_index, + owner_program_id: crate::ID, + }) + .add_remaining_accounts( + &remaining_accounts + .iter() + .map(|account| { + (account, account.is_signer, account.is_writable) + }) + .collect::>(), + ) + .invoke_signed(&signer)?; + + Ok(()) +} + +fn process_schedule_commit_compressed( + accounts: &[AccountInfo], +) -> ProgramResult { + msg!("ScheduleCommitCompressed"); + + let [payer, counter, magic_context, magic_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + assert_magic_context(magic_context)?; + assert_magic_program(magic_program)?; + + let (pda, _bump) = FlexiCounter::pda(payer.key); + assert_keys_equal(counter.key, &pda, || { + format!("Invalid counter PDA {}, should be {}", counter.key, pda) + })?; + + let instruction_data = MagicBlockInstruction::ScheduleCommitAndUndelegate + .try_to_vec() + .map_err(|_| { + ProgramError::BorshIoError( + "ScheduleCommitAndUndelegate".to_string(), + ) + })?; + + let account_metas = vec![ + AccountMeta::new(*payer.key, true), + AccountMeta::new(*magic_context.key, false), + AccountMeta::new(*counter.key, false), + ]; + + let account_refs = + vec![payer.clone(), magic_context.clone(), counter.clone()]; + + let ix = Instruction { + program_id: *magic_program.key, + data: instruction_data, + accounts: account_metas, + }; + + invoke(&ix, &account_refs)?; + + Ok(()) +} + +fn process_external_undelegate_compressed( + accounts: &[AccountInfo], + args: ExternalUndelegateArgs, +) -> ProgramResult { + msg!("External Undelegate Compressed"); + + let [payer, delegated_account, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + let (pda, bump) = FlexiCounter::pda(payer.key); + if &pda != delegated_account.key { + msg!("Invalid seeds: {:?} != {:?}", pda, delegated_account.key); + return Err(ProgramError::InvalidSeeds); + } + + // Reset lamports + if args.delegation_record.lamports > delegated_account.lamports() { + invoke( + &system_instruction::transfer( + payer.key, + delegated_account.key, + args.delegation_record.lamports - delegated_account.lamports(), + ), + &[payer.clone(), delegated_account.clone()], + )?; + } else if args.delegation_record.lamports < delegated_account.lamports() { + let bump = &[bump]; + let seeds = FlexiCounter::seeds_with_bump(payer.key, bump); + invoke_signed( + &system_instruction::transfer( + delegated_account.key, + payer.key, + delegated_account.lamports() - args.delegation_record.lamports, + ), + &[delegated_account.clone(), payer.clone()], + &[&seeds], + )?; + } + + // Reset data + delegated_account.realloc(args.delegation_record.data.len(), false)?; + delegated_account + .data + .borrow_mut() + .copy_from_slice(&args.delegation_record.data); + + Ok(()) +} + +fn assert_magic_context(account_info: &AccountInfo) -> ProgramResult { + if account_info.key != &MAGIC_CONTEXT_ID { + return Err(ProgramError::InvalidAccountData); + } + Ok(()) +} + +fn assert_magic_program(account_info: &AccountInfo) -> ProgramResult { + if account_info.key != &MAGIC_PROGRAM_ID { + return Err(ProgramError::IncorrectProgramId); + } + Ok(()) +} diff --git a/test-integration/programs/flexi-counter/src/state.rs b/test-integration/programs/flexi-counter/src/state.rs index 2eb9a5255..57732b219 100644 --- a/test-integration/programs/flexi-counter/src/state.rs +++ b/test-integration/programs/flexi-counter/src/state.rs @@ -1,7 +1,18 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use light_sdk::{LightDiscriminator, LightHasher}; use solana_program::pubkey::Pubkey; -#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq)] +#[derive( + BorshSerialize, + BorshDeserialize, + Default, + Debug, + Clone, + PartialEq, + Eq, + LightDiscriminator, + LightHasher, +)] pub struct FlexiCounter { pub count: u64, pub updates: u64, diff --git a/test-integration/programs/schedulecommit/src/api.rs b/test-integration/programs/schedulecommit/src/api.rs index 8f359a56c..b8ada8fbc 100644 --- a/test-integration/programs/schedulecommit/src/api.rs +++ b/test-integration/programs/schedulecommit/src/api.rs @@ -41,9 +41,9 @@ pub fn init_account_instruction( AccountMeta::new_readonly(system_program::id(), false), ]; - Instruction::new_with_borsh( + build_instruction( program_id, - &ScheduleCommitInstruction::Init, + ScheduleCommitInstruction::Init, account_metas, ) } @@ -61,9 +61,9 @@ pub fn init_order_book_instruction( AccountMeta::new_readonly(system_program::id(), false), ]; - Instruction::new_with_borsh( + build_instruction( program_id, - &ScheduleCommitInstruction::InitOrderBook, + ScheduleCommitInstruction::InitOrderBook, account_metas, ) } @@ -82,9 +82,9 @@ pub fn grow_order_book_instruction( AccountMeta::new_readonly(system_program::id(), false), ]; - Instruction::new_with_borsh( + build_instruction( program_id, - &ScheduleCommitInstruction::GrowOrderBook(additional_space), + ScheduleCommitInstruction::GrowOrderBook(additional_space), account_metas, ) } @@ -138,9 +138,9 @@ pub fn delegate_account_cpi_instruction( delegate_metas.system_program, ]; - Instruction::new_with_borsh( + build_instruction( program_id, - &match user_seed { + match user_seed { UserSeeds::MagicScheduleCommit => { ScheduleCommitInstruction::DelegateCpi(DelegateCpiArgs { valid_until: i64::MAX, @@ -205,9 +205,9 @@ pub fn update_order_book_instruction( AccountMeta::new(order_book, false), ]; - Instruction::new_with_borsh( + build_instruction( program_id, - &ScheduleCommitInstruction::UpdateOrderBook(update), + ScheduleCommitInstruction::UpdateOrderBook(update), account_metas, ) } @@ -226,9 +226,9 @@ pub fn schedule_commit_diff_instruction_for_order_book( AccountMeta::new_readonly(magic_program_id, false), ]; - Instruction::new_with_borsh( + build_instruction( program_id, - &ScheduleCommitInstruction::ScheduleCommitForOrderBook, + ScheduleCommitInstruction::ScheduleCommitForOrderBook, account_metas, ) } @@ -303,7 +303,7 @@ fn schedule_commit_cpi_instruction_impl( commit_payer: args.commit_payer, }; let ix = ScheduleCommitInstruction::ScheduleCommitCpi(cpi_args); - Instruction::new_with_borsh(program_id, &ix, account_metas) + build_instruction(program_id, ix, account_metas) } pub fn schedule_commit_and_undelegate_cpi_with_mod_after_instruction( @@ -323,13 +323,10 @@ pub fn schedule_commit_and_undelegate_cpi_with_mod_after_instruction( account_metas.push(AccountMeta::new(*committee, false)); } - Instruction::new_with_borsh( - program_id, - &ScheduleCommitInstruction::ScheduleCommitAndUndelegateCpiModAfter( - players.to_vec(), - ), - account_metas, - ) + let ix = ScheduleCommitInstruction::ScheduleCommitAndUndelegateCpiModAfter( + players.to_vec(), + ); + build_instruction(program_id, ix, account_metas) } pub fn schedule_commit_and_undelegate_cpi_twice( @@ -349,9 +346,9 @@ pub fn schedule_commit_and_undelegate_cpi_twice( account_metas.push(AccountMeta::new(*committee, false)); } - Instruction::new_with_borsh( + build_instruction( program_id, - &ScheduleCommitInstruction::ScheduleCommitAndUndelegateCpiTwice( + ScheduleCommitInstruction::ScheduleCommitAndUndelegateCpiTwice( players.to_vec(), ), account_metas, @@ -361,9 +358,9 @@ pub fn schedule_commit_and_undelegate_cpi_twice( pub fn increase_count_instruction(committee: Pubkey) -> Instruction { let program_id = crate::id(); let account_metas = vec![AccountMeta::new(committee, false)]; - Instruction::new_with_borsh( + build_instruction( program_id, - &ScheduleCommitInstruction::IncreaseCount, + ScheduleCommitInstruction::IncreaseCount, account_metas, ) } @@ -371,9 +368,9 @@ pub fn increase_count_instruction(committee: Pubkey) -> Instruction { pub fn set_count_instruction(committee: Pubkey, count: u64) -> Instruction { let program_id = crate::id(); let account_metas = vec![AccountMeta::new(committee, false)]; - Instruction::new_with_borsh( + build_instruction( program_id, - &ScheduleCommitInstruction::SetCount(count), + ScheduleCommitInstruction::SetCount(count), account_metas, ) } @@ -406,3 +403,15 @@ pub fn pda_and_bump(acc_id: &Pubkey) -> (Pubkey, u8) { let seeds = pda_seeds(acc_id); Pubkey::find_program_address(&seeds, &program_id) } + +fn build_instruction( + program_id: Pubkey, + instruction: ScheduleCommitInstruction, + account_metas: Vec, +) -> Instruction { + Instruction::new_with_bytes( + program_id, + &borsh::to_vec(&instruction).unwrap(), + account_metas, + ) +} diff --git a/test-integration/programs/schedulecommit/src/lib.rs b/test-integration/programs/schedulecommit/src/lib.rs index 32d2e39f5..d8f408a61 100644 --- a/test-integration/programs/schedulecommit/src/lib.rs +++ b/test-integration/programs/schedulecommit/src/lib.rs @@ -32,7 +32,6 @@ use crate::{ }; pub mod api; -pub mod magicblock_program; mod order_book; mod utils; diff --git a/test-integration/programs/schedulecommit/src/magicblock_program.rs b/test-integration/programs/schedulecommit/src/magicblock_program.rs deleted file mode 100644 index 9cb90cf17..000000000 --- a/test-integration/programs/schedulecommit/src/magicblock_program.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum MagicBlockInstruction { - ModifyAccounts, - ScheduleCommit, - ScheduleCommitAndUndelegate, - ScheduledCommitSent(u64), -} - -#[allow(unused)] -impl MagicBlockInstruction { - pub(crate) fn index(&self) -> u8 { - use MagicBlockInstruction::*; - match self { - ModifyAccounts => 0, - ScheduleCommit => 1, - ScheduleCommitAndUndelegate => 2, - ScheduledCommitSent(_) => 3, - } - } - - pub(crate) fn discriminant(&self) -> [u8; 4] { - let idx = self.index(); - [idx, 0, 0, 0] - } -} diff --git a/test-integration/programs/schedulecommit/src/order_book.rs b/test-integration/programs/schedulecommit/src/order_book.rs index 5881387e4..348fa9d3d 100644 --- a/test-integration/programs/schedulecommit/src/order_book.rs +++ b/test-integration/programs/schedulecommit/src/order_book.rs @@ -53,7 +53,7 @@ impl From<&OrderBook<'_>> for OrderBookOwned { } impl borsh::de::BorshDeserialize for OrderBookOwned { - fn deserialize(buf: &mut &[u8]) -> Result { + fn deserialize(buf: &mut &[u8]) -> Result { let (book_bytes, rest) = buf.split_at(buf.len()); *buf = rest; // rest is actually empty @@ -71,12 +71,12 @@ impl borsh::de::BorshDeserialize for OrderBookOwned { slice::from_raw_parts_mut(book_bytes.as_mut_ptr(), book_bytes.len()) }) .map(|book| OrderBookOwned::from(&book)) - .map_err(|_| borsh::io::Error::from(borsh::io::ErrorKind::InvalidData)) + .map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidData)) } - fn deserialize_reader( + fn deserialize_reader( _reader: &mut R, - ) -> ::core::result::Result { + ) -> ::core::result::Result { unimplemented!("deserialize_reader() not implemented. Please use buffer version as it needs to know size of the buffer") } } diff --git a/test-integration/schedulecommit/test-scenarios/Cargo.toml b/test-integration/schedulecommit/test-scenarios/Cargo.toml index 3cda55a8f..d5b9da6d1 100644 --- a/test-integration/schedulecommit/test-scenarios/Cargo.toml +++ b/test-integration/schedulecommit/test-scenarios/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true [dev-dependencies] +borsh = { workspace = true } ephemeral-rollups-sdk = { workspace = true } integration-test-tools = { workspace = true } log = { workspace = true } @@ -17,4 +18,3 @@ solana-rpc-client-api = { workspace = true } solana-sdk = { workspace = true } test-kit = { workspace = true } rand = { workspace = true } -borsh = { workspace = true } diff --git a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs index 4c7456b43..248e0696b 100644 --- a/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs +++ b/test-integration/schedulecommit/test-scenarios/tests/01_commits.rs @@ -323,5 +323,9 @@ fn schedule_commit_cpi_illegal_owner( commit_payer: true, }; let ix = ScheduleCommitInstruction::ScheduleCommitCpi(cpi_args); - Instruction::new_with_borsh(program_id, &ix, account_metas) + Instruction::new_with_bytes( + program_id, + &borsh::to_vec(&ix).expect("Serializing instruction should never fail"), + account_metas, + ) } diff --git a/test-integration/schedulecommit/test-security/Cargo.toml b/test-integration/schedulecommit/test-security/Cargo.toml index 0429dd46b..a3254a59b 100644 --- a/test-integration/schedulecommit/test-security/Cargo.toml +++ b/test-integration/schedulecommit/test-security/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true [dev-dependencies] +borsh = { workspace = true } program-schedulecommit = { workspace = true, features = ["no-entrypoint"] } program-schedulecommit-security = { workspace = true, features = [ "no-entrypoint", diff --git a/test-integration/schedulecommit/test-security/tests/utils/mod.rs b/test-integration/schedulecommit/test-security/tests/utils/mod.rs index 21a25333b..3e4281f64 100644 --- a/test-integration/schedulecommit/test-security/tests/utils/mod.rs +++ b/test-integration/schedulecommit/test-security/tests/utils/mod.rs @@ -29,11 +29,14 @@ pub fn create_sibling_schedule_cpis_instruction( is_writable: true, }); } - Instruction::new_with_borsh( + Instruction::new_with_bytes( program_schedulecommit_security::id(), - &ScheduleCommitSecurityInstruction::SiblingScheduleCommitCpis( - player_pubkeys.to_vec(), - ), + &borsh::to_vec( + &ScheduleCommitSecurityInstruction::SiblingScheduleCommitCpis( + player_pubkeys.to_vec(), + ), + ) + .unwrap(), account_metas, ) } @@ -61,11 +64,14 @@ pub fn create_nested_schedule_cpis_instruction( is_writable: true, }); } - Instruction::new_with_borsh( + Instruction::new_with_bytes( program_schedulecommit_security::id(), - &ScheduleCommitSecurityInstruction::DirectScheduleCommitCpi( - player_pubkeys.to_vec(), - ), + &borsh::to_vec( + &ScheduleCommitSecurityInstruction::DirectScheduleCommitCpi( + player_pubkeys.to_vec(), + ), + ) + .unwrap(), account_metas, ) } @@ -74,9 +80,9 @@ pub fn create_nested_schedule_cpis_instruction( /// It could be added to confuse our algorithm to detect the invoking program. pub fn create_sibling_non_cpi_instruction(payer: Pubkey) -> Instruction { let account_metas = vec![AccountMeta::new(payer, true)]; - Instruction::new_with_borsh( + Instruction::new_with_bytes( program_schedulecommit_security::id(), - &ScheduleCommitSecurityInstruction::NonCpi, + &borsh::to_vec(&ScheduleCommitSecurityInstruction::NonCpi).unwrap(), account_metas, ) } diff --git a/test-integration/test-chainlink/Cargo.toml b/test-integration/test-chainlink/Cargo.toml index ca31c5ceb..0c1112fd4 100644 --- a/test-integration/test-chainlink/Cargo.toml +++ b/test-integration/test-chainlink/Cargo.toml @@ -4,15 +4,24 @@ version.workspace = true edition.workspace = true [dependencies] +borsh = { workspace = true } bincode = { workspace = true } +compressed-delegation-client = { workspace = true } futures = { workspace = true } +integration-test-tools = { workspace = true } +light-client = { workspace = true, features = ["v2"] } +light-compressed-account = { workspace = true } +light-hasher = { workspace = true } +light-sdk = { workspace = true, features = ["v2"] } log = { workspace = true } +magicblock-core = { workspace = true } magicblock-chainlink = { workspace = true } magicblock-config = { workspace = true } magicblock-delegation-program = { workspace = true } program-mini = { workspace = true, features = ["no-entrypoint"] } program-flexi-counter = { workspace = true, features = ["no-entrypoint"] } solana-account = { workspace = true } +solana-compute-budget-interface = { workspace = true } solana-loader-v2-interface = { workspace = true, features = ["serde"] } solana-loader-v3-interface = { workspace = true, features = ["serde"] } solana-loader-v4-interface = { workspace = true, features = ["serde"] } @@ -22,6 +31,6 @@ solana-rpc-client-api = { workspace = true } solana-sdk = { workspace = true } solana-sdk-ids = { workspace = true } solana-system-interface = { workspace = true } +solana-transaction-status = { workspace = true } spl-token = { workspace = true } -integration-test-tools = { workspace = true } tokio = { workspace = true, features = ["full"] } diff --git a/test-integration/test-chainlink/scripts/miniv2-json-from-so.js b/test-integration/test-chainlink/scripts/miniv2-json-from-so.js index 8baa272e6..2b1d78460 100644 --- a/test-integration/test-chainlink/scripts/miniv2-json-from-so.js +++ b/test-integration/test-chainlink/scripts/miniv2-json-from-so.js @@ -1,10 +1,10 @@ #!/node -const fs = require('fs') +const fs = require("fs"); const [, , inputSoFullPath, outputJsonFullPath] = process.argv; if (!inputSoFullPath || !outputJsonFullPath) { console.error( - "Usage: miniv2-json-from-so.js ", + "Usage: miniv2-json-from-so.js " ); process.exit(1); } diff --git a/test-integration/test-chainlink/src/ixtest_context.rs b/test-integration/test-chainlink/src/ixtest_context.rs index 1ef5de540..bceec2908 100644 --- a/test-integration/test-chainlink/src/ixtest_context.rs +++ b/test-integration/test-chainlink/src/ixtest_context.rs @@ -1,6 +1,17 @@ use std::sync::Arc; +use borsh::{BorshDeserialize, BorshSerialize}; +use compressed_delegation_client::{ + CommitArgs, CommitBuilder, CompressedAccountMeta, + CompressedDelegationRecord, FinalizeArgs, FinalizeBuilder, + PackedAddressTreeInfo, UndelegateArgs, UndelegateBuilder, +}; use integration_test_tools::dlp_interface; +use light_client::indexer::{ + photon_indexer::PhotonIndexer, AddressWithTree, CompressedAccount, Indexer, + ValidityProofWithContext, +}; +use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; use log::*; use magicblock_chainlink::{ accounts_bank::mock::AccountsBankStub, @@ -10,16 +21,24 @@ use magicblock_chainlink::{ native_program_accounts, remote_account_provider::{ chain_rpc_client::ChainRpcClientImpl, - chain_updates_client::ChainUpdatesClient, Endpoint, Endpoints, + chain_updates_client::ChainUpdatesClient, + photon_client::PhotonClientImpl, Endpoint, Endpoints, RemoteAccountProvider, }, submux::SubMuxClient, - testing::cloner_stub::ClonerStub, + testing::{ + cloner_stub::ClonerStub, + utils::{PHOTON_URL, RPC_URL}, + }, Chainlink, }; use magicblock_config::config::{ChainLinkConfig, LifecycleMode}; +use magicblock_core::compression::{ + derive_cda_from_pda, ADDRESS_TREE, OUTPUT_QUEUE, +}; use program_flexi_counter::state::FlexiCounter; use solana_account::AccountSharedData; +use solana_compute_budget_interface::ComputeBudgetInstruction; use solana_pubkey::Pubkey; use solana_rpc_client::nonblocking::rpc_client::RpcClient; use solana_rpc_client_api::config::RpcSendTransactionConfig; @@ -27,7 +46,7 @@ use solana_sdk::{ commitment_config::CommitmentConfig, native_token::LAMPORTS_PER_SOL, signature::Keypair, signer::Signer, transaction::Transaction, }; -use solana_sdk_ids::native_loader; +use solana_sdk_ids::{native_loader, system_program}; use tokio::task; use crate::sleep_ms; @@ -37,11 +56,13 @@ pub type IxtestChainlink = Chainlink< SubMuxClient, AccountsBankStub, ClonerStub, + PhotonClientImpl, >; #[derive(Clone)] pub struct IxtestContext { pub rpc_client: Arc, + pub photon_indexer: Arc, pub chainlink: Arc, pub bank: Arc, pub remote_account_provider: Option< @@ -49,6 +70,7 @@ pub struct IxtestContext { RemoteAccountProvider< ChainRpcClientImpl, SubMuxClient, + PhotonClientImpl, >, >, >, @@ -56,13 +78,13 @@ pub struct IxtestContext { pub validator_kp: Arc, } -const RPC_URL: &str = "http://localhost:7799"; pub const TEST_AUTHORITY: [u8; 64] = [ 251, 62, 129, 184, 107, 49, 62, 184, 1, 147, 178, 128, 185, 157, 247, 92, 56, 158, 145, 53, 51, 226, 202, 96, 178, 248, 195, 133, 133, 237, 237, 146, 13, 32, 77, 204, 244, 56, 166, 172, 66, 113, 150, 218, 112, 42, 110, 181, 98, 158, 222, 194, 130, 93, 175, 100, 190, 106, 9, 69, 156, 80, 96, 72, ]; + impl IxtestContext { pub async fn init() -> Self { Self::init_with_config(ChainlinkConfig::default_with_lifecycle_mode( @@ -79,7 +101,7 @@ impl IxtestContext { let bank = Arc::::default(); let cloner = Arc::new(ClonerStub::new(bank.clone())); let (tx, rx) = tokio::sync::mpsc::channel(100); - let (fetch_cloner, remote_account_provider) = { + let (fetch_cloner, remote_account_provider, photon_indexer) = { let endpoints_vec = vec![ Endpoint::Rpc { url: RPC_URL.to_string(), @@ -89,6 +111,10 @@ impl IxtestContext { url: "ws://localhost:7800".to_string(), label: "test-ws".to_string(), }, + Endpoint::Compression { + url: PHOTON_URL.to_string(), + api_key: None, + }, ]; let endpoints = Endpoints::from(endpoints_vec.as_slice()); // Add all native programs @@ -115,6 +141,9 @@ impl IxtestContext { ) .await; + let photon_indexer = + Arc::new(PhotonIndexer::new(PHOTON_URL.to_string(), None)); + match remote_account_provider { Ok(Some(remote_account_provider)) => { debug!("Initializing FetchCloner"); @@ -130,12 +159,13 @@ impl IxtestContext { None, )), Some(provider), + photon_indexer, ) } Err(err) => { panic!("Failed to create remote account provider: {err:?}"); } - _ => (None, None), + _ => (None, None, photon_indexer), } }; let chainlink = Chainlink::try_new( @@ -150,6 +180,7 @@ impl IxtestContext { let rpc_client = IxtestContext::get_rpc_client(commitment); Self { rpc_client: Arc::new(rpc_client), + photon_indexer, chainlink: Arc::new(chainlink), bank, remote_account_provider, @@ -353,6 +384,336 @@ impl IxtestContext { self } + pub async fn delegate_compressed_counter( + &self, + counter_auth: &Keypair, + redelegate: bool, + ) -> &Self { + debug!( + "Delegating compressed counter account {}", + counter_auth.pubkey() + ); + use program_flexi_counter::instruction::*; + + let auth = counter_auth.pubkey(); + let (pda, _bump) = FlexiCounter::pda(&auth); + let record_address = derive_cda_from_pda(&pda); + + let mut remaining_accounts = self.get_packed_accounts(); + + let ( + remaining_accounts_metas, + validity_proof, + address_tree_info, + account_meta, + output_state_tree_index, + ) = if redelegate { + let (compressed_delegated_record, proof) = + self.get_compressed_account_and_proof(&record_address).await; + + let packed_state_tree = proof + .pack_tree_infos(&mut remaining_accounts) + .state_trees + .unwrap(); + let account_meta = CompressedAccountMeta { + tree_info: packed_state_tree.packed_tree_infos[0], + address: compressed_delegated_record.address.unwrap(), + output_state_tree_index: packed_state_tree.output_tree_index, + }; + + let (remaining_accounts_metas, _, _) = + remaining_accounts.to_account_metas(); + ( + remaining_accounts_metas, + proof.proof, + None, + Some(account_meta), + packed_state_tree.output_tree_index, + ) + } else { + let rpc_result: ValidityProofWithContext = self + .photon_indexer + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: record_address.to_bytes(), + tree: ADDRESS_TREE, + }], + None, + ) + .await + .unwrap() + .value; + + // Insert trees in accounts + let address_merkle_tree_pubkey_index = + remaining_accounts.insert_or_get(ADDRESS_TREE); + let state_queue_pubkey_index = + remaining_accounts.insert_or_get(OUTPUT_QUEUE); + + let packed_address_tree_info = PackedAddressTreeInfo { + root_index: rpc_result.addresses[0].root_index, + address_merkle_tree_pubkey_index, + address_queue_pubkey_index: address_merkle_tree_pubkey_index, + }; + + let (remaining_accounts_metas, _, _) = + remaining_accounts.to_account_metas(); + + ( + remaining_accounts_metas, + rpc_result.proof, + Some(packed_address_tree_info), + None, + state_queue_pubkey_index, + ) + }; + + let delegate_ix = create_delegate_compressed_ix( + counter_auth.pubkey(), + &remaining_accounts_metas, + DelegateCompressedArgs { + validator: Some(self.validator_kp.pubkey()), + validity_proof, + account_meta, + address_tree_info, + output_state_tree_index, + }, + ); + + let latest_block_hash = + self.rpc_client.get_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(300_000), + delegate_ix, + ], + Some(&counter_auth.pubkey()), + &[&counter_auth], + latest_block_hash, + ); + debug!("Delegate transaction: {:?}", tx.signatures[0]); + self.rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ) + .await + .expect("Failed to delegate account"); + + // Wait for the indexer to index the account + sleep_ms(1500).await; + + self + } + + pub async fn undelegate_compressed_counter( + &self, + counter_auth: &Keypair, + redelegate: bool, + ) -> &Self { + debug!( + "Undelegating compressed counter account {}", + counter_auth.pubkey() + ); + let counter_pda = self.counter_pda(&counter_auth.pubkey()); + // The committor service will call this in order to have + // chainlink subscribe to account updates of the counter account + self.chainlink + .undelegation_requested(counter_pda) + .await + .unwrap(); + + // In order to make the account undelegatable we first need to + // commmit and finalize + let (pda, _bump) = FlexiCounter::pda(&counter_auth.pubkey()); + let record_address = derive_cda_from_pda(&pda); + let (compressed_account, proof) = + self.get_compressed_account_and_proof(&record_address).await; + let mut remaining_accounts = self.get_packed_accounts(); + + let packed_tree_accounts = proof + .pack_tree_infos(&mut remaining_accounts) + .state_trees + .unwrap(); + + let account_meta = CompressedAccountMeta { + tree_info: packed_tree_accounts.packed_tree_infos[0], + address: compressed_account.address.unwrap(), + output_state_tree_index: packed_tree_accounts.output_tree_index, + }; + + let (remaining_accounts_metas, _, _) = + remaining_accounts.to_account_metas(); + + let commit_ix = + CommitBuilder::new() + .validator(self.validator_kp.pubkey()) + .delegated_account(pda) + .args(CommitArgs { + current_compressed_delegated_account_data: + compressed_account.data.unwrap().data.to_vec(), + new_data: FlexiCounter::new("COUNTER".to_string()) + .try_to_vec() + .unwrap(), + account_meta, + validity_proof: proof.proof, + update_nonce: 1, + allow_undelegation: true, + }) + .add_remaining_accounts(remaining_accounts_metas.as_slice()) + .instruction(); + + let latest_block_hash = + self.rpc_client.get_latest_blockhash().await.unwrap(); + self.rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &Transaction::new_signed_with_payer( + &[commit_ix], + Some(&self.validator_kp.pubkey()), + &[&self.validator_kp], + latest_block_hash, + ), + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ) + .await + .expect("Failed to commit account"); + + // Wait for the indexer to index the account + sleep_ms(500).await; + + // Finalize + let (compressed_account, proof) = + self.get_compressed_account_and_proof(&record_address).await; + + let mut remaining_accounts = self.get_packed_accounts(); + + let packed_tree_accounts = proof + .pack_tree_infos(&mut remaining_accounts) + .state_trees + .unwrap(); + + let account_meta = CompressedAccountMeta { + tree_info: packed_tree_accounts.packed_tree_infos[0], + address: compressed_account.address.unwrap(), + output_state_tree_index: packed_tree_accounts.output_tree_index, + }; + + let (remaining_accounts_metas, _, _) = + remaining_accounts.to_account_metas(); + + let finalize_ix = + FinalizeBuilder::new() + .validator(self.validator_kp.pubkey()) + .delegated_account(pda) + .args(FinalizeArgs { + current_compressed_delegated_account_data: + compressed_account.data.unwrap().data, + account_meta, + validity_proof: proof.proof, + }) + .add_remaining_accounts(remaining_accounts_metas.as_slice()) + .instruction(); + + let latest_block_hash = + self.rpc_client.get_latest_blockhash().await.unwrap(); + self.rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &Transaction::new_signed_with_payer( + &[finalize_ix], + Some(&self.validator_kp.pubkey()), + &[&self.validator_kp], + latest_block_hash, + ), + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ) + .await + .expect("Failed to finalize account"); + + // Wait for the indexer to index the account + sleep_ms(1500).await; + + let (compressed_account, proof) = + self.get_compressed_account_and_proof(&record_address).await; + + let mut remaining_accounts = self.get_packed_accounts(); + + let packed_state_tree = proof + .pack_tree_infos(&mut remaining_accounts) + .state_trees + .unwrap(); + let account_meta = CompressedAccountMeta { + tree_info: packed_state_tree.packed_tree_infos[0], + address: compressed_account.address.unwrap(), + output_state_tree_index: packed_state_tree.output_tree_index, + }; + + let (remaining_accounts_metas, _, _) = + remaining_accounts.to_account_metas(); + + let undelegate_ix = UndelegateBuilder::new() + .payer(counter_auth.pubkey()) + .delegated_account(counter_pda) + .owner_program(program_flexi_counter::ID) + .system_program(system_program::ID) + .args(UndelegateArgs { + validity_proof: proof.proof, + delegation_record_account_meta: account_meta, + compressed_delegated_record: + CompressedDelegationRecord::try_from_slice( + &compressed_account.data.clone().unwrap().data, + ) + .unwrap(), + }) + .add_remaining_accounts(remaining_accounts_metas.as_slice()) + .instruction(); + let latest_block_hash = + self.rpc_client.get_latest_blockhash().await.unwrap(); + let tx = Transaction::new_signed_with_payer( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(250_000), + undelegate_ix, + ], + Some(&counter_auth.pubkey()), + &[&counter_auth], + latest_block_hash, + ); + debug!("Undelegate transaction: {:?}", tx.signatures[0]); + self.rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ) + .await + .expect("Failed to undelegate account"); + + // Build instructions and required signers + if redelegate { + // Wait for the indexer to index the account + sleep_ms(1500).await; + + self.delegate_compressed_counter(counter_auth, true).await + } else { + self + } + } + pub async fn top_up_ephemeral_fee_balance( &self, payer: &Keypair, @@ -390,11 +751,40 @@ impl IxtestContext { pub async fn get_remote_account( &self, pubkey: &Pubkey, - ) -> Option { + ) -> Option { self.rpc_client.get_account(pubkey).await.ok() } pub fn get_rpc_client(commitment: CommitmentConfig) -> RpcClient { RpcClient::new_with_commitment(RPC_URL.to_string(), commitment) } + + fn get_packed_accounts(&self) -> PackedAccounts { + let system_account_meta_config = + SystemAccountMetaConfig::new(compressed_delegation_client::ID); + let mut remaining_accounts = PackedAccounts::default(); + remaining_accounts + .add_system_accounts_v2(system_account_meta_config) + .unwrap(); + remaining_accounts + } + + async fn get_compressed_account_and_proof( + &self, + pubkey: &Pubkey, + ) -> (CompressedAccount, ValidityProofWithContext) { + let compressed_account = self + .photon_indexer + .get_compressed_account(pubkey.to_bytes(), None) + .await + .unwrap() + .value; + let validity_proof = self + .photon_indexer + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await + .unwrap() + .value; + (compressed_account, validity_proof) + } } diff --git a/test-integration/test-chainlink/src/programs.rs b/test-integration/test-chainlink/src/programs.rs index ee193b1c2..d60e2fb78 100644 --- a/test-integration/test-chainlink/src/programs.rs +++ b/test-integration/test-chainlink/src/programs.rs @@ -165,8 +165,8 @@ pub mod resolve_deploy { macro_rules! fetch_and_assert_loaded_program_v1_v2_v4 { ($rpc_client:expr, $program_id:expr, $expected:expr) => {{ use log::*; + use solana_account::AccountSharedData; use solana_loader_v4_interface::state::LoaderV4Status; - use solana_sdk::account::AccountSharedData; let program_account = $rpc_client .get_account(&$program_id) diff --git a/test-integration/test-chainlink/src/test_context.rs b/test-integration/test-chainlink/src/test_context.rs index 68e8726ad..d53c5a9e5 100644 --- a/test-integration/test-chainlink/src/test_context.rs +++ b/test-integration/test-chainlink/src/test_context.rs @@ -18,6 +18,7 @@ use magicblock_chainlink::{ accounts::account_shared_with_owner, cloner_stub::ClonerStub, deleg::add_delegation_record_for, + photon_client_mock::PhotonClientMock, rpc_client_mock::{ChainRpcClientMock, ChainRpcClientMockBuilder}, utils::{create_test_lru_cache, create_test_lru_cache_with_config}, }, @@ -35,6 +36,7 @@ pub type TestChainlink = Chainlink< ChainPubsubClientMock, AccountsBankStub, ClonerStub, + PhotonClientMock, >; #[derive(Clone)] @@ -44,7 +46,13 @@ pub struct TestContext { pub chainlink: Arc, pub bank: Arc, pub remote_account_provider: Option< - Arc>, + Arc< + RemoteAccountProvider< + ChainRpcClientMock, + ChainPubsubClientMock, + PhotonClientMock, + >, + >, >, pub cloner: Arc, pub validator_pubkey: Pubkey, @@ -52,13 +60,14 @@ pub struct TestContext { impl TestContext { pub async fn init(slot: Slot) -> Self { - let (rpc_client, pubsub_client) = { + let (rpc_client, pubsub_client, photon_indexer) = { let rpc_client = ChainRpcClientMockBuilder::new().slot(slot).build(); let (updates_sndr, updates_rcvr) = mpsc::channel(100); let pubsub_client = ChainPubsubClientMock::new(updates_sndr, updates_rcvr); - (rpc_client, pubsub_client) + let photon_indexer = PhotonClientMock::new(); + (rpc_client, pubsub_client, photon_indexer) }; let lifecycle_mode = LifecycleMode::Ephemeral; @@ -82,6 +91,7 @@ impl TestContext { RemoteAccountProvider::try_from_clients_and_mode( rpc_client.clone(), pubsub_client.clone(), + Some(photon_indexer.clone()), tx, &config, subscribed_accounts, diff --git a/test-integration/test-chainlink/tests/ix_01_ensure-accounts.rs b/test-integration/test-chainlink/tests/ix_01_ensure-accounts.rs index f76c5489c..9fdfd08ae 100644 --- a/test-integration/test-chainlink/tests/ix_01_ensure-accounts.rs +++ b/test-integration/test-chainlink/tests/ix_01_ensure-accounts.rs @@ -94,3 +94,43 @@ async fn ixtest_write_existing_account_valid_delegation_record() { // TODO(thlorenz): @ implement this test when we can actually delegate to a specific // authority: test_write_existing_account_other_authority + +// ----------------- +// BasicScenarios: Compressed account is initialized and already delegated to us +// ----------------- +#[tokio::test] +async fn ixtest_write_existing_account_compressed() { + init_logger(); + + let ctx = IxtestContext::init().await; + + let counter_auth = Keypair::new(); + ctx.init_counter(&counter_auth) + .await + .delegate_compressed_counter(&counter_auth, false) + .await; + + let counter_pda = ctx.counter_pda(&counter_auth.pubkey()); + let pubkeys = [counter_pda]; + + let res = ctx + .chainlink + .ensure_accounts( + &pubkeys, + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + debug!("res: {res:?}"); + + let account = ctx.cloner.get_account(&counter_pda).unwrap(); + assert_cloned_as_delegated!( + ctx.cloner, + &[counter_pda], + account.remote_slot(), + program_flexi_counter::id() + ); + assert_not_subscribed!(ctx.chainlink, &[&counter_pda]); +} diff --git a/test-integration/test-chainlink/tests/ix_03_deleg_after_sub.rs b/test-integration/test-chainlink/tests/ix_03_deleg_after_sub.rs index b6ee1dbd6..2d07efda9 100644 --- a/test-integration/test-chainlink/tests/ix_03_deleg_after_sub.rs +++ b/test-integration/test-chainlink/tests/ix_03_deleg_after_sub.rs @@ -95,3 +95,86 @@ async fn ixtest_deleg_after_subscribe_case2() { ); } } + +#[tokio::test] +async fn ixtest_deleg_after_subscribe_case2_compressed() { + init_logger(); + + let ctx = IxtestContext::init().await; + + let counter_auth = Keypair::new(); + let counter_pda = ctx.counter_pda(&counter_auth.pubkey()); + let pubkeys = [counter_pda]; + + // 1. Initially the account does not exist + { + info!("1. Initially the account does not exist"); + let res = ctx + .chainlink + .ensure_accounts( + &pubkeys, + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + + assert_not_found!(res, &pubkeys); + assert_not_cloned!(ctx.cloner, &pubkeys); + assert_not_subscribed!(ctx.chainlink, &[&counter_pda]); + } + + // 2. Account created with original owner (program) + { + info!("2. Create account owned by program_flexi_counter"); + ctx.init_counter(&counter_auth).await; + + ctx.chainlink + .ensure_accounts( + &pubkeys, + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + + // Assert cloned account state matches the remote account and slot + let account = ctx.cloner.get_account(&counter_pda).unwrap(); + assert_cloned_as_undelegated!( + ctx.cloner, + &[counter_pda], + account.remote_slot(), + program_flexi_counter::id() + ); + + assert_subscribed_without_delegation_record!(ctx.chainlink, &pubkeys); + } + + // 3. Account delegated to us + { + info!("3. Delegate account to us"); + ctx.delegate_compressed_counter(&counter_auth, false).await; + + ctx.chainlink + .ensure_accounts( + &pubkeys, + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + + let account = ctx.cloner.get_account(&counter_pda).unwrap(); + assert_cloned_as_delegated!( + ctx.cloner, + &[counter_pda], + account.remote_slot(), + program_flexi_counter::id() + ); + + assert_not_subscribed!(ctx.chainlink, &[&counter_pda]); + } +} diff --git a/test-integration/test-chainlink/tests/ix_06_redeleg_us_separate_slots.rs b/test-integration/test-chainlink/tests/ix_06_redeleg_us_separate_slots.rs index 66d589a1e..6db73a8db 100644 --- a/test-integration/test-chainlink/tests/ix_06_redeleg_us_separate_slots.rs +++ b/test-integration/test-chainlink/tests/ix_06_redeleg_us_separate_slots.rs @@ -12,6 +12,8 @@ use magicblock_chainlink::{ use solana_sdk::{signature::Keypair, signer::Signer}; use test_chainlink::{ixtest_context::IxtestContext, sleep_ms}; +const RETRIES: usize = 30; + #[tokio::test] async fn ixtest_undelegate_redelegate_to_us_in_separate_slots() { init_logger(); @@ -51,7 +53,7 @@ async fn ixtest_undelegate_redelegate_to_us_in_separate_slots() { &[counter_pda], account.remote_slot(), program_flexi_counter::id(), - 30 + RETRIES ); // Accounts delegated to us should not be tracked via subscription @@ -95,7 +97,7 @@ async fn ixtest_undelegate_redelegate_to_us_in_separate_slots() { &[counter_pda], account.remote_slot(), program_flexi_counter::id(), - 30 + RETRIES ); // Accounts delegated to us should not be tracked via subscription @@ -105,3 +107,100 @@ async fn ixtest_undelegate_redelegate_to_us_in_separate_slots() { ); } } + +#[tokio::test] +async fn ixtest_undelegate_redelegate_to_us_in_separate_slots_compressed() { + init_logger(); + + let ctx = IxtestContext::init().await; + + // Create and delegate a counter account to us + let counter_auth = Keypair::new(); + ctx.init_counter(&counter_auth) + .await + .delegate_compressed_counter(&counter_auth, false) + .await; + + let counter_pda = ctx.counter_pda(&counter_auth.pubkey()); + let pubkeys = [counter_pda]; + + // 1. Account delegated to us - readable and writable + { + info!("1. Account delegated to us"); + + ctx.chainlink + .ensure_accounts( + &pubkeys, + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + sleep_ms(1_500).await; + + // Account should be cloned as delegated + let account = ctx.cloner.get_account(&counter_pda).unwrap(); + assert_cloned_as_delegated_with_retries!( + ctx.cloner, + &[counter_pda], + account.remote_slot(), + program_flexi_counter::id(), + RETRIES + ); + + // Accounts delegated to us should not be tracked via subscription + assert_not_subscribed!(ctx.chainlink, &[&counter_pda]); + } + + // 2. Account is undelegated - writes refused, subscription set + { + info!( + "2. Account is undelegated - Would refuse write (undelegated on chain)" + ); + + ctx.undelegate_compressed_counter(&counter_auth, false) + .await; + sleep_ms(1_500).await; + + // Account should be cloned as undelegated (owned by program again) + let account = ctx.cloner.get_account(&counter_pda).unwrap(); + assert_cloned_as_undelegated!( + ctx.cloner, + &[counter_pda], + account.remote_slot(), + program_flexi_counter::id() + ); + + assert_subscribed_without_delegation_record!(ctx.chainlink, &pubkeys); + } + + // 3. Account redelegated to us (separate slot) - writes allowed again + { + info!("3. Account redelegated to us - Would allow write"); + ctx.delegate_compressed_counter(&counter_auth, true).await; + + ctx.chainlink + .ensure_accounts( + &pubkeys, + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + + // Account should be cloned as delegated back to us + let account = ctx.cloner.get_account(&counter_pda).unwrap(); + assert_cloned_as_delegated_with_retries!( + ctx.cloner, + &[counter_pda], + account.remote_slot(), + program_flexi_counter::id(), + RETRIES + ); + + // Accounts delegated to us should not be tracked via subscription + assert_not_subscribed!(ctx.chainlink, &[&counter_pda]); + } +} diff --git a/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs b/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs index be1339db3..bd01cd3d2 100644 --- a/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs +++ b/test-integration/test-chainlink/tests/ix_07_redeleg_us_same_slot.rs @@ -87,3 +87,72 @@ async fn ixtest_undelegate_redelegate_to_us_in_same_slot() { ); } } + +#[tokio::test] +async fn ixtest_undelegate_redelegate_to_us_in_same_slot_compressed() { + init_logger(); + + let ctx = IxtestContext::init().await; + + // Create and delegate a counter account to us + let counter_auth = Keypair::new(); + ctx.init_counter(&counter_auth) + .await + .delegate_compressed_counter(&counter_auth, false) + .await; + + let counter_pda = ctx.counter_pda(&counter_auth.pubkey()); + let pubkeys = [counter_pda]; + + // 1. Account delegated to us - readable and writable + { + info!("1. Account delegated to us"); + + ctx.chainlink + .ensure_accounts( + &pubkeys, + None, + AccountFetchOrigin::GetMultipleAccounts, + None, + ) + .await + .unwrap(); + sleep_ms(1_500).await; + + // Account should be cloned as delegated + let account = ctx.cloner.get_account(&counter_pda).unwrap(); + assert_cloned_as_delegated!( + ctx.cloner, + &[counter_pda], + account.remote_slot(), + program_flexi_counter::id() + ); + + // Accounts delegated to us should not be tracked via subscription + assert_not_subscribed!(ctx.chainlink, &[&counter_pda]); + } + + // 2. Account is undelegated and redelegated to us (same slot) - writes allowed again + { + info!( + "2. Account is undelegated and redelegated to us in the same slot" + ); + + ctx.undelegate_compressed_counter(&counter_auth, true).await; + + // Wait for pubsub update to trigger subscription handler + sleep_ms(1_500).await; + + // Account should still be cloned as delegated to us + let account = ctx.cloner.get_account(&counter_pda).unwrap(); + assert_cloned_as_delegated!( + ctx.cloner, + &[counter_pda], + account.remote_slot(), + program_flexi_counter::id() + ); + + // Accounts delegated to us should not be tracked via subscription + assert_not_subscribed!(ctx.chainlink, &[&counter_pda]); + } +} diff --git a/test-integration/test-chainlink/tests/ix_remote_account_provider.rs b/test-integration/test-chainlink/tests/ix_remote_account_provider.rs index 2ca142d81..903bf2c7c 100644 --- a/test-integration/test-chainlink/tests/ix_remote_account_provider.rs +++ b/test-integration/test-chainlink/tests/ix_remote_account_provider.rs @@ -3,15 +3,15 @@ use magicblock_chainlink::{ remote_account_provider::{ chain_rpc_client::ChainRpcClientImpl, chain_updates_client::ChainUpdatesClient, - config::RemoteAccountProviderConfig, Endpoint, Endpoints, - RemoteAccountProvider, RemoteAccountUpdateSource, + config::RemoteAccountProviderConfig, photon_client::PhotonClientImpl, + Endpoint, Endpoints, RemoteAccountProvider, RemoteAccountUpdateSource, }, submux::SubMuxClient, testing::utils::{ airdrop, await_next_slot, current_slot, dump_remote_account_lamports, dump_remote_account_update_source, get_remote_account_lamports, get_remote_account_update_sources, init_logger, random_pubkey, - sleep_ms, PUBSUB_URL, RPC_URL, + sleep_ms, PHOTON_URL, PUBSUB_URL, RPC_URL, }, AccountFetchOrigin, }; @@ -22,9 +22,11 @@ use solana_rpc_client_api::{ use solana_sdk::commitment_config::CommitmentConfig; use tokio::sync::mpsc; -async fn init_remote_account_provider( -) -> RemoteAccountProvider> -{ +async fn init_remote_account_provider() -> RemoteAccountProvider< + ChainRpcClientImpl, + SubMuxClient, + PhotonClientImpl, +> { let (fwd_tx, _fwd_rx) = mpsc::channel(100); let endpoints_vec = vec![ Endpoint::Rpc { @@ -35,11 +37,16 @@ async fn init_remote_account_provider( url: PUBSUB_URL.to_string(), label: "test-ws".to_string(), }, + Endpoint::Compression { + url: PHOTON_URL.to_string(), + api_key: None, + }, ]; let endpoints = Endpoints::from(endpoints_vec.as_slice()); RemoteAccountProvider::< ChainRpcClientImpl, SubMuxClient, + PhotonClientImpl, >::try_from_urls_and_config( &endpoints, CommitmentConfig::confirmed(), @@ -47,7 +54,10 @@ async fn init_remote_account_provider( &RemoteAccountProviderConfig::default_with_lifecycle_mode( LifecycleMode::Ephemeral, ), - ).await.expect("Failed to create RemoteAccountProvider").unwrap() + ) + .await + .expect("Failed to create RemoteAccountProvider") + .unwrap() } #[tokio::test] diff --git a/test-integration/test-committor-service/Cargo.toml b/test-integration/test-committor-service/Cargo.toml index c6b448d1e..86ff83eab 100644 --- a/test-integration/test-committor-service/Cargo.toml +++ b/test-integration/test-committor-service/Cargo.toml @@ -6,14 +6,21 @@ edition.workspace = true [dev-dependencies] async-trait = { workspace = true } borsh = { workspace = true } +compressed-delegation-client = { workspace = true } +light-client = { workspace = true, features = ["v2"] } +light-compressed-account = { workspace = true } +light-sdk = { workspace = true, features = ["v2"] } +light-sdk-types = { workspace = true, features = ["v2"] } log = { workspace = true } futures = { workspace = true } +magicblock-chainlink = { workspace = true } magicblock-committor-program = { workspace = true, features = [ "no-entrypoint", ] } magicblock-committor-service = { workspace = true, features = [ "dev-context-only-utils", ] } +magicblock-core = { workspace = true } magicblock-delegation-program = { workspace = true, features = [ "no-entrypoint", ] } diff --git a/test-integration/test-committor-service/tests/common.rs b/test-integration/test-committor-service/tests/common.rs index 81adae320..23532f370 100644 --- a/test-integration/test-committor-service/tests/common.rs +++ b/test-integration/test-committor-service/tests/common.rs @@ -7,6 +7,10 @@ use std::{ }; use async_trait::async_trait; +use light_client::indexer::photon_indexer::PhotonIndexer; +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +use light_sdk_types::instruction::account_meta::CompressedAccountMeta; +use magicblock_chainlink::testing::utils::PHOTON_URL; use magicblock_committor_service::{ intent_executor::{ task_info_fetcher::{ @@ -15,7 +19,7 @@ use magicblock_committor_service::{ }, IntentExecutorImpl, }, - tasks::CommitTask, + tasks::{task_builder::CompressedData, CommitTask, CompressedCommitTask}, transaction_preparator::{ delivery_preparator::DeliveryPreparator, TransactionPreparatorImpl, }, @@ -40,9 +44,16 @@ pub async fn create_test_client() -> MagicblockRpcClient { MagicblockRpcClient::new(Arc::new(rpc_client)) } +// Helper function to create a test PhotonIndexer +pub fn create_test_photon_indexer() -> Option> { + let url = PHOTON_URL.to_string(); + Some(Arc::new(PhotonIndexer::new(url, None))) +} + // Test fixture structure pub struct TestFixture { pub rpc_client: MagicblockRpcClient, + pub photon_client: Option>, pub table_mania: TableMania, pub authority: Keypair, pub compute_budget_config: ComputeBudgetConfig, @@ -58,6 +69,9 @@ impl TestFixture { pub async fn new_with_keypair(authority: Keypair) -> Self { let rpc_client = create_test_client().await; + // PhotonIndexer + let photon_client = create_test_photon_indexer(); + // TableMania let gc_config = GarbageCollectorConfig::default(); let table_mania = @@ -75,6 +89,7 @@ impl TestFixture { let compute_budget_config = ComputeBudgetConfig::new(1_000_000); Self { rpc_client, + photon_client, table_mania, authority, compute_budget_config, @@ -108,6 +123,7 @@ impl TestFixture { IntentExecutorImpl::new( self.rpc_client.clone(), + self.photon_client.clone(), transaction_preparator, self.create_task_info_fetcher(), ) @@ -127,6 +143,7 @@ impl TaskInfoFetcher for MockTaskInfoFetcher { &self, pubkeys: &[Pubkey], _: u64, + _compressed: bool, ) -> TaskInfoFetcherResult> { Ok(pubkeys.iter().map(|pubkey| (*pubkey, 0)).collect()) } @@ -194,6 +211,40 @@ pub fn create_commit_task(data: &[u8]) -> CommitTask { } } +#[allow(dead_code)] +/// Test-only helper. Uses dummy compressed_data (empty delegation record, +/// default proof/meta) and must not be used for on-chain compressed-delegation +/// transactions. +pub fn create_dummy_compressed_commit_task( + pubkey: Pubkey, + hash: [u8; 32], + data: &[u8], +) -> CompressedCommitTask { + static COMMIT_ID: AtomicU64 = AtomicU64::new(0); + CompressedCommitTask { + commit_id: COMMIT_ID.fetch_add(1, Ordering::Relaxed), + compressed_data: CompressedData { + hash, + compressed_delegation_record_bytes: vec![], + remaining_accounts: vec![], + account_meta: CompressedAccountMeta::default(), + proof: ValidityProof::default(), + }, + allow_undelegation: false, + committed_account: CommittedAccount { + pubkey, + account: Account { + lamports: 1000, + data: data.to_vec(), + owner: dlp::id(), + executable: false, + rent_epoch: 0, + }, + remote_slot: Default::default(), + }, + } +} + #[allow(dead_code)] pub fn create_committed_account(data: &[u8]) -> CommittedAccount { CommittedAccount { diff --git a/test-integration/test-committor-service/tests/test_delivery_preparator.rs b/test-integration/test-committor-service/tests/test_delivery_preparator.rs index dded722ee..f6064afc2 100644 --- a/test-integration/test-committor-service/tests/test_delivery_preparator.rs +++ b/test-integration/test-committor-service/tests/test_delivery_preparator.rs @@ -1,5 +1,9 @@ +use std::sync::Arc; + use borsh::BorshDeserialize; +use compressed_delegation_client::CompressedDelegationRecord; use futures::future::join_all; +use light_client::indexer::photon_indexer::PhotonIndexer; use magicblock_committor_program::Chunks; use magicblock_committor_service::{ persist::IntentPersisterImpl, @@ -10,11 +14,22 @@ use magicblock_committor_service::{ BaseTask, PreparationState, }, }; -use solana_sdk::signer::Signer; +use magicblock_program::validator::{ + generate_validator_authority_if_needed, validator_authority_id, +}; +use solana_sdk::{rent::Rent, signature::Keypair, signer::Signer}; +use test_kit::init_logger; -use crate::common::{create_commit_task, generate_random_bytes, TestFixture}; +use crate::{ + common::{ + create_commit_task, create_dummy_compressed_commit_task, + generate_random_bytes, TestFixture, + }, + utils::transactions::init_and_delegate_compressed_account_on_chain, +}; mod common; +mod utils; #[tokio::test] async fn test_prepare_10kb_buffer() { @@ -28,6 +43,7 @@ async fn test_prepare_10kb_buffer() { buffer_task, ))], lookup_tables_keys: vec![], + compressed: false, }; // Test preparation @@ -36,6 +52,8 @@ async fn test_prepare_10kb_buffer() { &fixture.authority, &mut strategy, &None::, + &None::>, + None, ) .await; @@ -96,6 +114,7 @@ async fn test_prepare_multiple_buffers() { let mut strategy = TransactionStrategy { optimized_tasks: buffer_tasks, lookup_tables_keys: vec![], + compressed: false, }; // Test preparation @@ -104,6 +123,8 @@ async fn test_prepare_multiple_buffers() { &fixture.authority, &mut strategy, &None::, + &None::>, + None, ) .await; @@ -176,6 +197,7 @@ async fn test_lookup_tables() { let mut strategy = TransactionStrategy { optimized_tasks: tasks, lookup_tables_keys, + compressed: false, }; let result = preparator @@ -183,6 +205,8 @@ async fn test_lookup_tables() { &fixture.authority, &mut strategy, &None::, + &None::>, + None, ) .await; assert!(result.is_ok(), "Failed to prepare lookup tables"); @@ -214,6 +238,7 @@ async fn test_already_initialized_error_handled() { buffer_task, ))], lookup_tables_keys: vec![], + compressed: false, }; // Test preparation @@ -222,6 +247,8 @@ async fn test_already_initialized_error_handled() { &fixture.authority, &mut strategy, &None::, + &None::>, + None, ) .await; assert!(result.is_ok(), "Preparation failed: {:?}", result.err()); @@ -253,6 +280,7 @@ async fn test_already_initialized_error_handled() { buffer_task, ))], lookup_tables_keys: vec![], + compressed: false, }; // Test preparation @@ -261,6 +289,8 @@ async fn test_already_initialized_error_handled() { &fixture.authority, &mut strategy, &None::, + &None::>, + None, ) .await; assert!(result.is_ok(), "Preparation failed: {:?}", result.err()); @@ -285,8 +315,6 @@ async fn test_already_initialized_error_handled() { #[tokio::test] async fn test_prepare_cleanup_and_reprepare_mixed_tasks() { - use borsh::BorshDeserialize; - let fixture = TestFixture::new().await; let preparator = fixture.create_delivery_preparator(); @@ -320,6 +348,7 @@ async fn test_prepare_cleanup_and_reprepare_mixed_tasks() { }, ], lookup_tables_keys: vec![], + compressed: false, }; // --- Step 1: initial prepare --- @@ -328,6 +357,8 @@ async fn test_prepare_cleanup_and_reprepare_mixed_tasks() { &fixture.authority, &mut strategy, &None::, + &None::>, + None, ) .await; assert!(res.is_ok(), "Initial prepare failed: {:?}", res.err()); @@ -401,7 +432,7 @@ async fn test_prepare_cleanup_and_reprepare_mixed_tasks() { .truncate(buf_b_data.len() - 5); } - // --- Step 4: re-prepare with the same logical tasks (same commit IDs, mutated data) --- + // --- Step 3: re-prepare with the same logical tasks (same commit IDs, mutated data) --- let mut strategy2 = TransactionStrategy { optimized_tasks: vec![ { @@ -420,6 +451,7 @@ async fn test_prepare_cleanup_and_reprepare_mixed_tasks() { }, ], lookup_tables_keys: vec![], + compressed: false, }; let res2 = preparator @@ -427,6 +459,8 @@ async fn test_prepare_cleanup_and_reprepare_mixed_tasks() { &fixture.authority, &mut strategy2, &None::, + &None::>, + None, ) .await; assert!( @@ -488,3 +522,66 @@ async fn test_prepare_cleanup_and_reprepare_mixed_tasks() { ); } } + +#[tokio::test] +async fn test_prepare_compressed_commit() { + let fixture = TestFixture::new().await; + let preparator = fixture.create_delivery_preparator(); + + generate_validator_authority_if_needed(); + init_logger!(); + + let counter_auth = Keypair::new(); + let (pda, _hash, account) = + init_and_delegate_compressed_account_on_chain(&counter_auth).await; + + let data = generate_random_bytes(10); + let mut task = Box::new(ArgsTask::new(ArgsTaskType::CompressedCommit( + create_dummy_compressed_commit_task( + pda, + Default::default(), + data.as_slice(), + ), + ))) as Box; + let compressed_data = task.get_compressed_data().cloned(); + + preparator + .prepare_task( + &fixture.authority, + &mut *task, + &None::, + &fixture.photon_client, + None, + ) + .await + .expect("Failed to prepare compressed commit"); + + // Verify the compressed data was updated + let new_compressed_data = task.get_compressed_data().cloned(); + assert_ne!( + new_compressed_data, compressed_data, + "Compressed data size mismatch" + ); + + // Verify the delegation record is correct + let delegation_record = CompressedDelegationRecord::from_bytes( + &new_compressed_data + .unwrap() + .compressed_delegation_record_bytes, + ) + .unwrap(); + let expected = CompressedDelegationRecord { + authority: validator_authority_id(), + pda, + delegation_slot: delegation_record.delegation_slot, + lamports: Rent::default().minimum_balance(account.data.len()), + data: account.data, + last_update_nonce: 0, + is_undelegatable: false, + owner: program_flexi_counter::id(), + }; + assert_eq!( + delegation_record, expected, + "Delegation record should be the same" + ); +} diff --git a/test-integration/test-committor-service/tests/test_intent_executor.rs b/test-integration/test-committor-service/tests/test_intent_executor.rs index edde6dcd0..c792bf407 100644 --- a/test-integration/test-committor-service/tests/test_intent_executor.rs +++ b/test-integration/test-committor-service/tests/test_intent_executor.rs @@ -12,6 +12,7 @@ use std::{ use borsh::to_vec; use dlp::{args::CommitStateArgs, pda::ephemeral_balance_pda_from_payer}; use futures::future::join_all; +use light_client::indexer::photon_indexer::PhotonIndexer; use magicblock_committor_program::pdas; use magicblock_committor_service::{ intent_executor::{ @@ -87,8 +88,10 @@ impl TestEnv { .await; let transaction_preparator = fixture.create_transaction_preparator(); - let task_info_fetcher = - Arc::new(CacheTaskInfoFetcher::new(fixture.rpc_client.clone())); + let task_info_fetcher = Arc::new(CacheTaskInfoFetcher::new( + fixture.rpc_client.clone(), + fixture.photon_client.clone(), + )); let tm = &fixture.table_mania; let mut pre_test_tablemania_state = HashMap::new(); @@ -99,6 +102,7 @@ impl TestEnv { let intent_executor = IntentExecutorImpl::new( fixture.rpc_client.clone(), + fixture.photon_client.clone(), transaction_preparator, task_info_fetcher.clone(), ); @@ -140,6 +144,7 @@ async fn test_commit_id_error_parsing() { .fetch_next_commit_ids( &intent.get_committed_pubkeys().unwrap(), remote_slot, + false, ) .await .unwrap(); @@ -154,6 +159,7 @@ async fn test_commit_id_error_parsing() { .prepare_and_execute_strategy( &mut transaction_strategy, &None::, + None, ) .await; assert!(execution_result.is_ok(), "Preparation is expected to pass!"); @@ -205,6 +211,7 @@ async fn test_undelegation_error_parsing() { .prepare_and_execute_strategy( &mut transaction_strategy, &None::, + None, ) .await; assert!(execution_result.is_ok(), "Preparation is expected to pass!"); @@ -265,6 +272,7 @@ async fn test_action_error_parsing() { .prepare_and_execute_strategy( &mut transaction_strategy, &None::, + None, ) .await; assert!(execution_result.is_ok(), "Preparation is expected to pass!"); @@ -277,7 +285,12 @@ async fn test_action_error_parsing() { execution_err, TransactionStrategyExecutionError::ActionsError(_, _) )); - assert!(execution_err.to_string().contains(EXPECTED_ERR_MSG)); + assert!( + execution_err.to_string().contains(EXPECTED_ERR_MSG), + "{} != {}", + execution_err, + EXPECTED_ERR_MSG + ); } #[tokio::test] @@ -322,6 +335,7 @@ async fn test_cpi_limits_error_parsing() { .prepare_and_execute_strategy( &mut transaction_strategy, &None::, + None, ) .await; assert!(execution_result.is_ok(), "Preparation is expected to pass!"); @@ -414,7 +428,7 @@ async fn test_commit_id_error_recovery() { // Invalidate commit nonce cache let res = task_info_fetcher - .fetch_next_commit_ids(&[committed_account.pubkey], remote_slot) + .fetch_next_commit_ids(&[committed_account.pubkey], remote_slot, false) .await; assert!(res.is_ok()); assert!(res.unwrap().contains_key(&committed_account.pubkey)); @@ -615,7 +629,7 @@ async fn test_commit_id_and_action_errors_recovery() { // Invalidate commit nonce cache let res = task_info_fetcher - .fetch_next_commit_ids(&[committed_account.pubkey], remote_slot) + .fetch_next_commit_ids(&[committed_account.pubkey], remote_slot, false) .await; assert!(res.is_ok()); assert!(res.unwrap().contains_key(&committed_account.pubkey)); @@ -821,7 +835,7 @@ async fn test_commit_id_actions_cpi_limit_errors_recovery() { // Force CommitIDError by invalidating the commit-nonce cache before running let pubkeys: Vec<_> = committed_accounts.iter().map(|c| c.pubkey).collect(); let mut invalidated_keys = task_info_fetcher - .fetch_next_commit_ids(&pubkeys, Default::default()) + .fetch_next_commit_ids(&pubkeys, Default::default(), false) .await .unwrap(); @@ -1215,13 +1229,17 @@ async fn single_flow_transaction_strategy( task_info_fetcher, intent, &None::, + &None::>, + ) + .await + .unwrap(); + let finalize_tasks = TaskBuilderImpl::finalize_tasks( + task_info_fetcher, + intent, + &None::>, ) .await .unwrap(); - let finalize_tasks = - TaskBuilderImpl::finalize_tasks(task_info_fetcher, intent) - .await - .unwrap(); tasks.extend(finalize_tasks); TaskStrategist::build_strategy( diff --git a/test-integration/test-committor-service/tests/test_ix_commit_local.rs b/test-integration/test-committor-service/tests/test_ix_commit_local.rs index 20956dc3e..c70572ce9 100644 --- a/test-integration/test-committor-service/tests/test_ix_commit_local.rs +++ b/test-integration/test-committor-service/tests/test_ix_commit_local.rs @@ -5,7 +5,12 @@ use std::{ }; use borsh::to_vec; +use compressed_delegation_client::CompressedDelegationRecord; +use light_client::indexer::{ + photon_indexer::PhotonIndexer, CompressedAccount, Indexer, +}; use log::*; +use magicblock_chainlink::testing::utils::{PHOTON_URL, RPC_URL}; use magicblock_committor_service::{ config::ChainConfig, intent_executor::ExecutionOutput, @@ -14,6 +19,7 @@ use magicblock_committor_service::{ types::{ScheduledBaseIntentWrapper, TriggerType}, BaseIntentCommittor, CommittorService, ComputeBudgetConfig, }; +use magicblock_core::compression::derive_cda_from_pda; use magicblock_program::magic_scheduled_base_intent::{ CommitAndUndelegate, CommitType, CommittedAccount, MagicBaseIntent, ScheduledBaseIntent, UndelegateType, @@ -24,8 +30,8 @@ use solana_account::{Account, ReadableAccount}; use solana_pubkey::Pubkey; use solana_rpc_client::nonblocking::rpc_client::RpcClient; use solana_sdk::{ - commitment_config::CommitmentConfig, hash::Hash, signature::Keypair, - signer::Signer, transaction::Transaction, + commitment_config::CommitmentConfig, hash::Hash, rent::Rent, + signature::Keypair, signer::Signer, transaction::Transaction, }; use test_kit::init_logger; use tokio::task::JoinSet; @@ -37,6 +43,7 @@ use crate::utils::{ transactions::{ fund_validator_auth_and_ensure_validator_fees_vault, init_and_delegate_account_on_chain, + init_and_delegate_compressed_account_on_chain, }, }; @@ -45,8 +52,16 @@ mod utils; // ----------------- // Utilities and Setup // ----------------- + type ExpectedStrategies = HashMap; +enum CommitAccountMode { + Commit, + CompressedCommit, + CommitAndUndelegate, + CompressedCommitAndUndelegate, +} + fn expect_strategies( strategies: &[(CommitStrategy, u8)], ) -> ExpectedStrategies { @@ -67,62 +82,122 @@ fn expect_strategies( #[tokio::test] async fn test_ix_commit_single_account_100_bytes() { - commit_single_account(100, CommitStrategy::StateArgs, false).await; + commit_single_account( + 100, + CommitStrategy::StateArgs, + CommitAccountMode::Commit, + ) + .await; } #[tokio::test] async fn test_ix_commit_single_account_100_bytes_and_undelegate() { - commit_single_account(100, CommitStrategy::StateArgs, true).await; + commit_single_account( + 100, + CommitStrategy::StateArgs, + CommitAccountMode::CommitAndUndelegate, + ) + .await; } #[tokio::test] async fn test_ix_commit_single_account_256_bytes() { - commit_single_account(256, CommitStrategy::StateArgs, false).await; + commit_single_account( + 256, + CommitStrategy::StateArgs, + CommitAccountMode::Commit, + ) + .await; } #[tokio::test] async fn test_ix_commit_single_account_257_bytes() { - commit_single_account(257, CommitStrategy::DiffArgs, false).await; + commit_single_account( + 257, + CommitStrategy::DiffArgs, + CommitAccountMode::Commit, + ) + .await; } #[tokio::test] async fn test_ix_commit_single_account_256_bytes_and_undelegate() { - commit_single_account(256, CommitStrategy::StateArgs, true).await; + commit_single_account( + 256, + CommitStrategy::StateArgs, + CommitAccountMode::CommitAndUndelegate, + ) + .await; } #[tokio::test] async fn test_ix_commit_single_account_257_bytes_and_undelegate() { - commit_single_account(257, CommitStrategy::DiffArgs, true).await; + commit_single_account( + 257, + CommitStrategy::DiffArgs, + CommitAccountMode::CommitAndUndelegate, + ) + .await; } #[tokio::test] async fn test_ix_commit_single_account_800_bytes() { - commit_single_account(800, CommitStrategy::DiffArgs, false).await; + commit_single_account( + 800, + CommitStrategy::DiffArgs, + CommitAccountMode::Commit, + ) + .await; } #[tokio::test] async fn test_ix_commit_single_account_800_bytes_and_undelegate() { - commit_single_account(800, CommitStrategy::DiffArgs, true).await; + commit_single_account( + 800, + CommitStrategy::DiffArgs, + CommitAccountMode::CommitAndUndelegate, + ) + .await; } #[tokio::test] async fn test_ix_commit_single_account_one_kb() { - commit_single_account(1024, CommitStrategy::DiffArgs, false).await; + commit_single_account( + 1024, + CommitStrategy::DiffArgs, + CommitAccountMode::Commit, + ) + .await; } #[tokio::test] async fn test_ix_commit_single_account_ten_kb() { - commit_single_account(10 * 1024, CommitStrategy::DiffArgs, false).await; + commit_single_account( + 10 * 1024, + CommitStrategy::DiffArgs, + CommitAccountMode::Commit, + ) + .await; } #[tokio::test] async fn test_ix_commit_order_book_change_100_bytes() { - commit_book_order_account(100, CommitStrategy::DiffArgs, false).await; + commit_book_order_account( + 100, + CommitStrategy::DiffArgs, + CommitAccountMode::Commit, + ) + .await; } #[tokio::test] async fn test_ix_commit_order_book_change_671_bytes() { - commit_book_order_account(671, CommitStrategy::DiffArgs, false).await; + commit_book_order_account( + 671, + CommitStrategy::DiffArgs, + CommitAccountMode::Commit, + ) + .await; } #[tokio::test] @@ -131,38 +206,65 @@ async fn test_ix_commit_order_book_change_673_bytes() { // of size 1644 (which is the max limit), but while the size of raw bytes for 671 is within // 1232 limit, the size for 672 exceeds by 1 (1233). That is why we used // 673 as changed_len where CommitStrategy goes from Args to FromBuffer. - commit_book_order_account(673, CommitStrategy::DiffBuffer, false).await; + commit_book_order_account( + 673, + CommitStrategy::DiffBuffer, + CommitAccountMode::Commit, + ) + .await; } #[tokio::test] async fn test_ix_commit_order_book_change_10k_bytes() { - commit_book_order_account(10 * 1024, CommitStrategy::DiffBuffer, false) - .await; + commit_book_order_account( + 10 * 1024, + CommitStrategy::DiffBuffer, + CommitAccountMode::Commit, + ) + .await; } async fn commit_single_account( bytes: usize, expected_strategy: CommitStrategy, - undelegate: bool, + mode: CommitAccountMode, ) { init_logger!(); let validator_auth = ensure_validator_authority(); fund_validator_auth_and_ensure_validator_fees_vault(&validator_auth).await; + let photon_client = + Some(Arc::new(PhotonIndexer::new(PHOTON_URL.to_string(), None))); + // Run each test with and without finalizing let service = CommittorService::try_start( validator_auth.insecure_clone(), ":memory:", ChainConfig::local(ComputeBudgetConfig::new(1_000_000)), + photon_client, ) .unwrap(); let service = CommittorServiceExt::new(Arc::new(service)); let counter_auth = Keypair::new(); - let (pubkey, mut account) = - init_and_delegate_account_on_chain(&counter_auth, bytes as u64, None) - .await; + let (pubkey, mut account) = match mode { + CommitAccountMode::Commit | CommitAccountMode::CommitAndUndelegate => { + init_and_delegate_account_on_chain( + &counter_auth, + bytes as u64, + None, + ) + .await + } + CommitAccountMode::CompressedCommit + | CommitAccountMode::CompressedCommitAndUndelegate => { + let (pubkey, _hash, account) = + init_and_delegate_compressed_account_on_chain(&counter_auth) + .await; + (pubkey, account) + } + }; let counter = FlexiCounter { label: "Counter".to_string(), @@ -179,13 +281,29 @@ async fn commit_single_account( account, remote_slot: Default::default(), }; - let base_intent = if undelegate { - MagicBaseIntent::CommitAndUndelegate(CommitAndUndelegate { - commit_action: CommitType::Standalone(vec![account]), - undelegate_action: UndelegateType::Standalone, - }) - } else { - MagicBaseIntent::Commit(CommitType::Standalone(vec![account])) + let base_intent = match mode { + CommitAccountMode::CommitAndUndelegate => { + MagicBaseIntent::CommitAndUndelegate(CommitAndUndelegate { + commit_action: CommitType::Standalone(vec![account]), + undelegate_action: UndelegateType::Standalone, + }) + } + CommitAccountMode::Commit => { + MagicBaseIntent::Commit(CommitType::Standalone(vec![account])) + } + CommitAccountMode::CompressedCommit => { + MagicBaseIntent::CompressedCommit(CommitType::Standalone(vec![ + account, + ])) + } + CommitAccountMode::CompressedCommitAndUndelegate => { + MagicBaseIntent::CompressedCommitAndUndelegate( + CommitAndUndelegate { + commit_action: CommitType::Standalone(vec![account]), + undelegate_action: UndelegateType::Standalone, + }, + ) + } }; let intent = ScheduledBaseIntentWrapper { @@ -212,7 +330,7 @@ async fn commit_single_account( async fn commit_book_order_account( changed_len: usize, expected_strategy: CommitStrategy, - undelegate: bool, + mode: CommitAccountMode, ) { init_logger!(); @@ -224,6 +342,7 @@ async fn commit_book_order_account( validator_auth.insecure_clone(), ":memory:", ChainConfig::local(ComputeBudgetConfig::new(1_000_000)), + None, ) .unwrap(); let service = CommittorServiceExt::new(Arc::new(service)); @@ -246,7 +365,11 @@ async fn commit_book_order_account( account: order_book_ac, remote_slot: Default::default(), }; - let base_intent = if undelegate { + let base_intent = if matches!( + mode, + CommitAccountMode::CommitAndUndelegate + | CommitAccountMode::CompressedCommitAndUndelegate + ) { MagicBaseIntent::CommitAndUndelegate(CommitAndUndelegate { commit_action: CommitType::Standalone(vec![account]), undelegate_action: UndelegateType::Standalone, @@ -288,7 +411,7 @@ async fn test_ix_commit_two_accounts_1kb_2kb() { commit_multiple_accounts( &[1024, 2048], 1, - false, + CommitAccountMode::Commit, expect_strategies(&[(CommitStrategy::DiffArgs, 2)]), ) .await; @@ -300,7 +423,7 @@ async fn test_ix_commit_two_accounts_512kb() { commit_multiple_accounts( &[512, 512], 1, - false, + CommitAccountMode::Commit, expect_strategies(&[(CommitStrategy::DiffArgs, 2)]), ) .await; @@ -312,7 +435,7 @@ async fn test_ix_commit_three_accounts_512kb() { commit_multiple_accounts( &[512, 512, 512], 1, - false, + CommitAccountMode::Commit, expect_strategies(&[(CommitStrategy::DiffArgs, 3)]), ) .await; @@ -324,7 +447,7 @@ async fn test_ix_commit_six_accounts_512kb() { commit_multiple_accounts( &[512, 512, 512, 512, 512, 512], 1, - false, + CommitAccountMode::Commit, expect_strategies(&[(CommitStrategy::DiffArgs, 6)]), ) .await; @@ -336,7 +459,7 @@ async fn test_ix_commit_four_accounts_1kb_2kb_5kb_10kb_single_bundle() { commit_multiple_accounts( &[1024, 2 * 1024, 5 * 1024, 10 * 1024], 1, - false, + CommitAccountMode::Commit, expect_strategies(&[(CommitStrategy::DiffArgs, 4)]), ) .await; @@ -356,7 +479,7 @@ async fn test_commit_5_accounts_1kb_bundle_size_3() { commit_5_accounts_1kb( 3, expect_strategies(&[(CommitStrategy::DiffArgs, 5)]), - false, + CommitAccountMode::Commit, ) .await; } @@ -369,7 +492,7 @@ async fn test_commit_5_accounts_1kb_bundle_size_3_undelegate_all() { // Intent fits in 1 TX only with ALT, see IntentExecutorImpl::try_unite_tasks (CommitStrategy::DiffArgs, 5), ]), - true, + CommitAccountMode::CommitAndUndelegate, ) .await; } @@ -382,7 +505,7 @@ async fn test_commit_5_accounts_1kb_bundle_size_4() { (CommitStrategy::DiffArgs, 1), (CommitStrategy::DiffBufferWithLookupTable, 4), ]), - false, + CommitAccountMode::Commit, ) .await; } @@ -395,7 +518,7 @@ async fn test_commit_5_accounts_1kb_bundle_size_4_undelegate_all() { (CommitStrategy::DiffArgs, 1), (CommitStrategy::DiffBufferWithLookupTable, 4), ]), - true, + CommitAccountMode::CommitAndUndelegate, ) .await; } @@ -405,7 +528,7 @@ async fn test_commit_5_accounts_1kb_bundle_size_5_undelegate_all() { commit_5_accounts_1kb( 5, expect_strategies(&[(CommitStrategy::DiffBufferWithLookupTable, 5)]), - true, + CommitAccountMode::CommitAndUndelegate, ) .await; } @@ -476,57 +599,207 @@ async fn test_commit_20_accounts_1kb_bundle_size_8() { .await; } +// ----------------- +// Compressed Account Commits +// ----------------- + +#[tokio::test] +async fn test_ix_commit_single_compressed_account_100_bytes() { + commit_single_account( + 100, + CommitStrategy::StateArgs, + CommitAccountMode::CompressedCommit, + ) + .await; +} + +#[tokio::test] +async fn test_ix_commit_single_compressed_account_100_bytes_and_undelegate() { + commit_single_account( + 100, + CommitStrategy::StateArgs, + CommitAccountMode::CompressedCommitAndUndelegate, + ) + .await; +} + +#[tokio::test] +async fn test_ix_commit_single_compressed_account_500_bytes() { + commit_single_account( + 500, + CommitStrategy::StateArgs, + CommitAccountMode::CompressedCommit, + ) + .await; +} + +#[tokio::test] +async fn test_ix_commit_single_compressed_account_500_bytes_and_undelegate() { + commit_single_account( + 500, + CommitStrategy::StateArgs, + CommitAccountMode::CompressedCommitAndUndelegate, + ) + .await; +} + +#[tokio::test] +async fn test_ix_commit_two_compressed_accounts_512kb() { + commit_multiple_accounts( + &[512, 512], + 1, + CommitAccountMode::CompressedCommit, + expect_strategies(&[(CommitStrategy::StateArgs, 2)]), + ) + .await; +} + +#[tokio::test] +async fn test_ix_commit_three_compressed_accounts_512kb() { + init_logger!(); + commit_multiple_accounts( + &[512, 512, 512], + 1, + CommitAccountMode::CompressedCommit, + expect_strategies(&[(CommitStrategy::StateArgs, 3)]), + ) + .await; +} + +#[tokio::test] +async fn test_ix_commit_six_compressed_accounts_512kb() { + init_logger!(); + commit_multiple_accounts( + &[512, 512, 512, 512, 512, 512], + 1, + CommitAccountMode::CompressedCommit, + expect_strategies(&[(CommitStrategy::StateArgs, 6)]), + ) + .await; +} + +#[tokio::test] +async fn test_commit_20_compressed_accounts_100bytes_bundle_size_2() { + commit_20_compressed_accounts_100bytes( + 2, + expect_strategies(&[(CommitStrategy::StateArgs, 20)]), + ) + .await; +} + +#[tokio::test] +async fn test_commit_5_compressed_accounts_100bytes_bundle_size_2() { + commit_5_compressed_accounts_100bytes( + 2, + expect_strategies(&[(CommitStrategy::StateArgs, 5)]), + CommitAccountMode::CompressedCommit, + ) + .await; +} + +#[tokio::test] +async fn test_commit_5_compressed_accounts_100bytes_bundle_size_2_undelegate_all( +) { + commit_5_compressed_accounts_100bytes( + 2, + expect_strategies(&[(CommitStrategy::StateArgs, 5)]), + CommitAccountMode::CompressedCommitAndUndelegate, + ) + .await; +} + async fn commit_5_accounts_1kb( bundle_size: usize, expected_strategies: ExpectedStrategies, - undelegate_all: bool, + mode_all: CommitAccountMode, ) { init_logger!(); let accs = (0..5).map(|_| 1024).collect::>(); + commit_multiple_accounts(&accs, bundle_size, mode_all, expected_strategies) + .await; +} + +async fn commit_5_compressed_accounts_100bytes( + bundle_size: usize, + expected_strategies: ExpectedStrategies, + mode_all: CommitAccountMode, +) { + init_logger!(); + let accs = (0..5).map(|_| 100).collect::>(); + commit_multiple_accounts(&accs, bundle_size, mode_all, expected_strategies) + .await; +} + +async fn commit_8_accounts_1kb( + bundle_size: usize, + expected_strategies: ExpectedStrategies, +) { + init_logger!(); + let accs = (0..8).map(|_| 1024).collect::>(); commit_multiple_accounts( &accs, bundle_size, - undelegate_all, + CommitAccountMode::Commit, expected_strategies, ) .await; } -async fn commit_8_accounts_1kb( +async fn commit_20_accounts_1kb( bundle_size: usize, expected_strategies: ExpectedStrategies, ) { init_logger!(); - let accs = (0..8).map(|_| 1024).collect::>(); - commit_multiple_accounts(&accs, bundle_size, false, expected_strategies) - .await; + let accs = (0..20).map(|_| 1024).collect::>(); + commit_multiple_accounts( + &accs, + bundle_size, + CommitAccountMode::Commit, + expected_strategies, + ) + .await; } -async fn commit_20_accounts_1kb( +async fn commit_20_compressed_accounts_100bytes( bundle_size: usize, expected_strategies: ExpectedStrategies, ) { init_logger!(); - let accs = (0..20).map(|_| 1024).collect::>(); - commit_multiple_accounts(&accs, bundle_size, false, expected_strategies) - .await; + let accs = (0..20).map(|_| 100).collect::>(); + commit_multiple_accounts( + &accs, + bundle_size, + CommitAccountMode::CompressedCommit, + expected_strategies, + ) + .await; } async fn create_bundles( bundle_size: usize, bytess: &[usize], + compressed: bool, ) -> Vec> { let mut join_set = JoinSet::new(); for bytes in bytess { let bytes = *bytes; join_set.spawn(async move { let counter_auth = Keypair::new(); - let (pda, mut pda_acc) = init_and_delegate_account_on_chain( - &counter_auth, - bytes as u64, - None, - ) - .await; + let (pda, mut pda_acc) = if !compressed { + init_and_delegate_account_on_chain( + &counter_auth, + bytes as u64, + None, + ) + .await + } else { + let (pda, _hash, pda_acc) = + init_and_delegate_compressed_account_on_chain( + &counter_auth, + ) + .await; + (pda, pda_acc) + }; pda_acc.owner = program_flexi_counter::id(); pda_acc.data = vec![0u8; bytes]; @@ -549,7 +822,7 @@ async fn create_bundles( async fn commit_multiple_accounts( bytess: &[usize], bundle_size: usize, - undelegate_all: bool, + mode_all: CommitAccountMode, expected_strategies: ExpectedStrategies, ) { init_logger!(); @@ -557,28 +830,55 @@ async fn commit_multiple_accounts( let validator_auth = ensure_validator_authority(); fund_validator_auth_and_ensure_validator_fees_vault(&validator_auth).await; + let photon_client = + Some(Arc::new(PhotonIndexer::new(PHOTON_URL.to_string(), None))); + let service = CommittorService::try_start( validator_auth.insecure_clone(), ":memory:", ChainConfig::local(ComputeBudgetConfig::new(1_000_000)), + photon_client, ) .unwrap(); let service = CommittorServiceExt::new(Arc::new(service)); // Create bundles of committed accounts - let bundles_of_committees = create_bundles(bundle_size, bytess).await; + let bundles_of_committees = create_bundles( + bundle_size, + bytess, + matches!( + mode_all, + CommitAccountMode::CompressedCommit + | CommitAccountMode::CompressedCommitAndUndelegate + ), + ) + .await; // Create intent for each bundle let intents = bundles_of_committees .into_iter() - .map(|committees| { - if undelegate_all { + .map(|committees| match mode_all { + CommitAccountMode::CommitAndUndelegate => { MagicBaseIntent::CommitAndUndelegate(CommitAndUndelegate { commit_action: CommitType::Standalone(committees), undelegate_action: UndelegateType::Standalone, }) - } else { + } + CommitAccountMode::Commit => { MagicBaseIntent::Commit(CommitType::Standalone(committees)) } + CommitAccountMode::CompressedCommit => { + MagicBaseIntent::CompressedCommit(CommitType::Standalone( + committees, + )) + } + CommitAccountMode::CompressedCommitAndUndelegate => { + MagicBaseIntent::CompressedCommitAndUndelegate( + CommitAndUndelegate { + commit_action: CommitType::Standalone(committees), + undelegate_action: UndelegateType::Standalone, + }, + ) + } }) .enumerate() .map(|(id, base_intent)| ScheduledBaseIntent { @@ -641,7 +941,8 @@ async fn ix_commit_local( assert_eq!(execution_outputs.len(), base_intents.len()); service.release_common_pubkeys().await.unwrap(); - let rpc_client = RpcClient::new("http://localhost:7799".to_string()); + let rpc_client = RpcClient::new(RPC_URL.to_string()); + let photon_indexer = PhotonIndexer::new(PHOTON_URL.to_string(), None); let mut strategies = ExpectedStrategies::new(); for (execution_result, base_intent) in execution_outputs.into_iter().zip(base_intents.into_iter()) @@ -661,15 +962,15 @@ async fn ix_commit_local( "No errors expected to be patched" ); assert!( - tx_logs_contain(&rpc_client, &commit_signature, "CommitState") - .await + tx_logs_contain(&rpc_client, &commit_signature, "Commit").await ); assert!( tx_logs_contain(&rpc_client, &finalize_signature, "Finalize").await ); let is_undelegate = base_intent.is_undelegate(); - if is_undelegate { + let is_compressed = base_intent.is_compressed(); + if is_undelegate && !is_compressed { // Undelegate is part of atomic Finalization Stage assert!( tx_logs_contain(&rpc_client, &finalize_signature, "Undelegate") @@ -706,26 +1007,65 @@ async fn ix_commit_local( assert_eq!(statuses.len(), committed_accounts.len()); for commit_status in statuses { - let account = committed_accounts - .remove(&commit_status.pubkey) - .expect("Account should be persisted"); - let lamports = account.account.lamports; - get_account!( - rpc_client, - account.pubkey, - "delegated state", - |acc: &Account, remaining_tries: u8| { - validate_account( - acc, - remaining_tries, - &account.account.data, - lamports, - expected_owner, - account.pubkey, - is_undelegate, - ) - } - ); + if is_compressed { + let account = committed_accounts + .remove(&commit_status.pubkey) + .expect("Account should be persisted"); + let lamports = Rent::default().minimum_balance(0); + get_account!( + rpc_client, + account.pubkey, + "delegated state", + |acc: &Account, remaining_tries: u8| { + validate_account( + acc, + remaining_tries, + &[], + lamports, + compressed_delegation_client::ID, + account.pubkey, + is_undelegate, + ) + } + ); + + let address = derive_cda_from_pda(&account.pubkey); + // NOTE: defaults to 10 retry + let compressed_account = photon_indexer + .get_compressed_account(address.to_bytes(), None) + .await + .unwrap() + .value; + assert!(validate_compressed_account( + &compressed_account, + &account.account.data, + account.account.lamports, + program_flexi_counter::id(), + account.pubkey, + is_undelegate + )); + } else { + let account = committed_accounts + .remove(&commit_status.pubkey) + .expect("Account should be persisted"); + let lamports = account.account.lamports; + get_account!( + rpc_client, + account.pubkey, + "delegated state", + |acc: &Account, remaining_tries: u8| { + validate_account( + acc, + remaining_tries, + &account.account.data, + lamports, + expected_owner, + account.pubkey, + is_undelegate, + ) + } + ); + } // Track the strategy used let strategy = commit_status.commit_strategy; @@ -853,3 +1193,53 @@ fn validate_account( } matches_all } + +fn validate_compressed_account( + acc: &CompressedAccount, + expected_data: &[u8], + expected_lamports: u64, + expected_owner: Pubkey, + account_pubkey: Pubkey, + is_undelegate: bool, +) -> bool { + let Some(data) = acc.data.as_ref().and_then(|data| { + CompressedDelegationRecord::from_bytes(&data.data).ok() + }) else { + trace!( + "Compressed account ({}) data is not present", + account_pubkey + ); + return false; + }; + let matches_data = + data.data == expected_data && data.lamports == expected_lamports; + let matches_undelegation = data.owner.eq(&expected_owner); + let matches_all = matches_data && matches_undelegation; + + if !matches_all { + if !matches_data { + trace!( + "Compressed account ({}) data {} != {} || {} != {}", + account_pubkey, + data.data.len(), + expected_data.len(), + data.lamports, + expected_lamports + ); + } + if !matches_undelegation { + trace!( + "Compressed account ({}) is {} but should be. Owner {} != {}", + account_pubkey, + if is_undelegate { + "not undelegated" + } else { + "undelegated" + }, + data.owner, + expected_owner, + ); + } + } + matches_all +} diff --git a/test-integration/test-committor-service/tests/test_transaction_preparator.rs b/test-integration/test-committor-service/tests/test_transaction_preparator.rs index b82aa3aa5..bc7c228dd 100644 --- a/test-integration/test-committor-service/tests/test_transaction_preparator.rs +++ b/test-integration/test-committor-service/tests/test_transaction_preparator.rs @@ -1,4 +1,7 @@ +use std::sync::Arc; + use borsh::BorshDeserialize; +use light_client::indexer::photon_indexer::PhotonIndexer; use magicblock_committor_program::Chunks; use magicblock_committor_service::{ persist::IntentPersisterImpl, @@ -48,6 +51,7 @@ async fn test_prepare_commit_tx_with_single_account() { let mut tx_strategy = TransactionStrategy { optimized_tasks: tasks, lookup_tables_keys: vec![], + compressed: false, }; // Test preparation @@ -56,6 +60,8 @@ async fn test_prepare_commit_tx_with_single_account() { &fixture.authority, &mut tx_strategy, &None::, + &None::>, + None, ) .await; @@ -123,6 +129,7 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { let mut tx_strategy = TransactionStrategy { optimized_tasks: tasks, lookup_tables_keys: vec![], + compressed: false, }; // Test preparation @@ -131,6 +138,8 @@ async fn test_prepare_commit_tx_with_multiple_accounts() { &fixture.authority, &mut tx_strategy, &None::, + &None::>, + None, ) .await .unwrap(); @@ -218,6 +227,7 @@ async fn test_prepare_commit_tx_with_base_actions() { let mut tx_strategy = TransactionStrategy { optimized_tasks: tasks, lookup_tables_keys: vec![], + compressed: false, }; // Test preparation @@ -226,6 +236,8 @@ async fn test_prepare_commit_tx_with_base_actions() { &fixture.authority, &mut tx_strategy, &None::, + &None::>, + None, ) .await .unwrap(); @@ -292,6 +304,7 @@ async fn test_prepare_finalize_tx_with_undelegate_with_atls() { let mut tx_strategy = TransactionStrategy { optimized_tasks: tasks, lookup_tables_keys, + compressed: false, }; // Test preparation @@ -300,6 +313,8 @@ async fn test_prepare_finalize_tx_with_undelegate_with_atls() { &fixture.authority, &mut tx_strategy, &None::, + &None::>, + None, ) .await; diff --git a/test-integration/test-committor-service/tests/utils/instructions.rs b/test-integration/test-committor-service/tests/utils/instructions.rs index 0fcb37f97..4ba7d22ce 100644 --- a/test-integration/test-committor-service/tests/utils/instructions.rs +++ b/test-integration/test-committor-service/tests/utils/instructions.rs @@ -1,3 +1,21 @@ +use std::sync::Arc; + +use compressed_delegation_client::PackedAddressTreeInfo; +use light_client::indexer::{ + photon_indexer::PhotonIndexer, AddressWithTree, Indexer, + ValidityProofWithContext, +}; +use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; +use magicblock_core::compression::{ + derive_cda_from_pda, ADDRESS_TREE, OUTPUT_QUEUE, +}; +use magicblock_program::validator::validator_authority_id; +use program_flexi_counter::{ + instruction::{ + create_delegate_compressed_ix, create_init_ix, DelegateCompressedArgs, + }, + state::FlexiCounter, +}; use solana_pubkey::Pubkey; use solana_sdk::{instruction::Instruction, rent::Rent, signature::Keypair}; use test_kit::Signer; @@ -15,7 +33,14 @@ pub struct InitAccountAndDelegateIxs { pub reallocs: Vec, pub delegate: Instruction, pub pda: Pubkey, - pub rent_excempt: u64, + pub rent_exempt: u64, +} + +pub struct InitAccountAndDelegateCompressedIxs { + pub init: Instruction, + pub delegate: Instruction, + pub pda: Pubkey, + pub address: [u8; 32], } pub fn init_account_and_delegate_ixs( @@ -47,7 +72,87 @@ pub fn init_account_and_delegate_ixs( reallocs: realloc_ixs, delegate: delegate_ix, pda, - rent_excempt: rent_exempt, + rent_exempt, + } +} + +pub async fn init_account_and_delegate_compressed_ixs( + payer: Pubkey, + photon_indexer: Arc, +) -> InitAccountAndDelegateCompressedIxs { + let (pda, _bump) = FlexiCounter::pda(&payer); + let record_address = derive_cda_from_pda(&pda); + + let init_counter_ix = create_init_ix(payer, "COUNTER".to_string()); + + let system_account_meta_config = + SystemAccountMetaConfig::new(compressed_delegation_client::ID); + let mut remaining_accounts = PackedAccounts::default(); + remaining_accounts + .add_system_accounts_v2(system_account_meta_config) + .unwrap(); + + let ( + remaining_accounts_metas, + validity_proof, + address_tree_info, + account_meta, + output_state_tree_index, + ) = { + let rpc_result: ValidityProofWithContext = photon_indexer + .get_validity_proof( + vec![], + vec![AddressWithTree { + address: record_address.to_bytes(), + tree: ADDRESS_TREE, + }], + None, + ) + .await + .unwrap() + .value; + + // Insert trees in accounts + let address_merkle_tree_pubkey_index = + remaining_accounts.insert_or_get(ADDRESS_TREE); + let state_queue_pubkey_index = + remaining_accounts.insert_or_get(OUTPUT_QUEUE); + + let packed_address_tree_info = PackedAddressTreeInfo { + root_index: rpc_result.addresses[0].root_index, + address_merkle_tree_pubkey_index, + address_queue_pubkey_index: address_merkle_tree_pubkey_index, + }; + + let (remaining_accounts_metas, _, _) = + remaining_accounts.to_account_metas(); + + ( + remaining_accounts_metas, + rpc_result.proof, + Some(packed_address_tree_info), + None, + state_queue_pubkey_index, + ) + }; + + let delegate_ix = create_delegate_compressed_ix( + payer, + &remaining_accounts_metas, + DelegateCompressedArgs { + validator: Some(validator_authority_id()), + validity_proof, + account_meta, + address_tree_info, + output_state_tree_index, + }, + ); + + InitAccountAndDelegateCompressedIxs { + init: init_counter_ix, + delegate: delegate_ix, + pda, + address: record_address.to_bytes(), } } diff --git a/test-integration/test-committor-service/tests/utils/mod.rs b/test-integration/test-committor-service/tests/utils/mod.rs index 8a049e7fc..4d9f0468f 100644 --- a/test-integration/test-committor-service/tests/utils/mod.rs +++ b/test-integration/test-committor-service/tests/utils/mod.rs @@ -21,6 +21,7 @@ pub async fn sleep_millis(millis: u64) { /// https://github.com/magicblock-labs/delegation-program/blob/7fc0ae9a59e48bea5b046b173ea0e34fd433c3c7/tests/fixtures/accounts.rs#L46 /// It is compiled in as the authority for the validator vault when we build via /// `cargo build-sbf --features=unit_test_config` +#[allow(dead_code)] pub fn get_validator_auth() -> Keypair { const VALIDATOR_AUTHORITY: [u8; 64] = [ 251, 62, 129, 184, 107, 49, 62, 184, 1, 147, 178, 128, 185, 157, 247, @@ -32,6 +33,7 @@ pub fn get_validator_auth() -> Keypair { Keypair::from_bytes(&VALIDATOR_AUTHORITY).unwrap() } +#[allow(dead_code)] pub fn ensure_validator_authority() -> Keypair { static ONCE: Once = Once::new(); diff --git a/test-integration/test-committor-service/tests/utils/transactions.rs b/test-integration/test-committor-service/tests/utils/transactions.rs index d22d21d17..c85093d52 100644 --- a/test-integration/test-committor-service/tests/utils/transactions.rs +++ b/test-integration/test-committor-service/tests/utils/transactions.rs @@ -1,4 +1,8 @@ +use std::sync::Arc; + +use light_client::indexer::{photon_indexer::PhotonIndexer, Indexer}; use log::{debug, error}; +use magicblock_chainlink::testing::utils::{PHOTON_URL, RPC_URL}; use solana_account::Account; use solana_pubkey::Pubkey; use solana_rpc_client::nonblocking::rpc_client::RpcClient; @@ -7,14 +11,16 @@ use solana_rpc_client_api::config::{ }; use solana_sdk::{ commitment_config::CommitmentConfig, + compute_budget::ComputeBudgetInstruction, native_token::LAMPORTS_PER_SOL, signature::{Keypair, Signature, Signer}, transaction::Transaction, }; use crate::utils::instructions::{ - init_account_and_delegate_ixs, init_order_book_account_and_delegate_ixs, - init_validator_fees_vault_ix, InitAccountAndDelegateIxs, + init_account_and_delegate_compressed_ixs, init_account_and_delegate_ixs, + init_order_book_account_and_delegate_ixs, init_validator_fees_vault_ix, + InitAccountAndDelegateCompressedIxs, InitAccountAndDelegateIxs, InitOrderBookAndDelegateIxs, }; @@ -68,6 +74,53 @@ macro_rules! get_account { }}; } +#[macro_export] +macro_rules! get_compressed_account { + ($photon_client:ident, $address:expr, $label:literal, $predicate:expr) => {{ + const GET_ACCOUNT_RETRIES: u8 = 12; + + let mut remaining_tries = GET_ACCOUNT_RETRIES; + loop { + let acc = $photon_client + .get_compressed_account($address, None) + .await + .ok() + .map(|acc| acc.value.clone()); + if let Some(acc) = acc { + if $predicate(&acc, remaining_tries) { + break acc; + } + remaining_tries -= 1; + if remaining_tries == 0 { + panic!( + "{} account ({:?}) does not match condition after {} retries", + $label, $address, GET_ACCOUNT_RETRIES + ); + } + $crate::utils::sleep_millis(800).await; + } else { + remaining_tries -= 1; + if remaining_tries == 0 { + panic!( + "Unable to get {} account ({:?}) matching condition after {} retries", + $label, $address, GET_ACCOUNT_RETRIES + ); + } + if remaining_tries % 10 == 0 { + debug!( + "Waiting for {} account ({:?}) to become available", + $label, $address + ); + } + $crate::utils::sleep_millis(800).await; + } + } + }}; + ($rpc_client:ident, $pubkey:expr, $label:literal) => {{ + get_compressed_account!($rpc_client, $pubkey, $label, |_: &light_client::indexer::CompressedAccount, _: u8| true) + }}; +} + #[allow(dead_code)] pub async fn fetch_tx_logs( rpc_client: &RpcClient, @@ -168,7 +221,7 @@ pub async fn init_and_delegate_account_on_chain( reallocs: realloc_ixs, delegate: delegate_ix, pda, - rent_excempt, + rent_exempt, } = init_account_and_delegate_ixs(counter_auth.pubkey(), bytes, label); let latest_block_hash = rpc_client.get_latest_blockhash().await.unwrap(); @@ -192,15 +245,12 @@ pub async fn init_and_delegate_account_on_chain( debug!("Init account: {:?}", pda); // 2. Airdrop to account for extra rent needed for reallocs - rpc_client - .request_airdrop(&pda, rent_excempt) - .await - .unwrap(); + rpc_client.request_airdrop(&pda, rent_exempt).await.unwrap(); debug!( "Airdropped to account: {:4} {}SOL to pay rent for {} bytes", pda, - rent_excempt as f64 / LAMPORTS_PER_SOL as f64, + rent_exempt as f64 / LAMPORTS_PER_SOL as f64, bytes ); @@ -249,6 +299,89 @@ pub async fn init_and_delegate_account_on_chain( (pda, pda_acc) } +/// This needs to be run for each test that required a new counter to be compressed delegated +#[allow(dead_code)] +pub async fn init_and_delegate_compressed_account_on_chain( + counter_auth: &Keypair, +) -> (Pubkey, [u8; 32], Account) { + let rpc_client = RpcClient::new(RPC_URL.to_string()); + let photon_indexer = + Arc::new(PhotonIndexer::new(PHOTON_URL.to_string(), None)); + + rpc_client + .request_airdrop(&counter_auth.pubkey(), 777 * LAMPORTS_PER_SOL) + .await + .unwrap(); + debug!("Airdropped to counter auth: {} SOL", 777 * LAMPORTS_PER_SOL); + + let InitAccountAndDelegateCompressedIxs { + init: init_counter_ix, + delegate: delegate_ix, + pda, + address, + } = init_account_and_delegate_compressed_ixs( + counter_auth.pubkey(), + photon_indexer.clone(), + ) + .await; + + let latest_block_hash = rpc_client.get_latest_blockhash().await.unwrap(); + // 1. Init account + rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &Transaction::new_signed_with_payer( + &[init_counter_ix], + Some(&counter_auth.pubkey()), + &[&counter_auth], + latest_block_hash, + ), + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ) + .await + .expect("Failed to init account"); + debug!("Init account: {:?}", pda); + + let pda_acc = get_account!(rpc_client, pda, "pda"); + + // 2. Delegate account + let tx = Transaction::new_signed_with_payer( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(250_000), + delegate_ix, + ], + Some(&counter_auth.pubkey()), + &[&counter_auth], + latest_block_hash, + ); + rpc_client + .send_and_confirm_transaction_with_spinner_and_config( + &tx, + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + skip_preflight: true, + ..Default::default() + }, + ) + .await + .inspect_err(|err| { + error!( + "Failed to delegate: {err:?}, signature: {:?}", + tx.signatures[0] + ) + }) + .expect("Failed to delegate"); + + let compressed_account = + get_compressed_account!(photon_indexer, address, "pda"); + debug!("Compressed account: {:?}", compressed_account); + + (pda, compressed_account.hash, pda_acc) +} + /// This needs to be run for each test that required a new order_book to be delegated #[allow(dead_code)] pub async fn init_and_delegate_order_book_on_chain( @@ -312,6 +445,7 @@ pub async fn init_and_delegate_order_book_on_chain( } /// This needs to be run once for all tests +#[allow(dead_code)] pub async fn fund_validator_auth_and_ensure_validator_fees_vault( validator_auth: &Keypair, ) { diff --git a/test-integration/test-runner/bin/run_tests.rs b/test-integration/test-runner/bin/run_tests.rs index 6a5a6e9c7..77ba8b5a3 100644 --- a/test-integration/test-runner/bin/run_tests.rs +++ b/test-integration/test-runner/bin/run_tests.rs @@ -9,13 +9,16 @@ use integration_test_tools::{ loaded_accounts::LoadedAccounts, toml_to_args::ProgramLoader, validator::{ - resolve_workspace_dir, start_magic_block_validator_with_config, + resolve_workspace_dir, start_light_validator_with_config, + start_magic_block_validator_with_config, start_test_validator_with_config, TestRunnerPaths, }, }; use teepee::Teepee; use test_runner::{ - cleanup::{cleanup_devnet_only, cleanup_validators}, + cleanup::{ + cleanup_devnet_only, cleanup_light_validator, cleanup_validators, + }, env_config::TestConfigViaEnvVars, signal::wait_for_ctrlc, }; @@ -152,7 +155,7 @@ fn run_restore_ledger_tests( } else { let devnet_validator = config.setup_devnet(TEST_NAME).then(start_devnet_validator); - wait_for_ctrlc(devnet_validator, None, success_output()) + wait_for_ctrlc(devnet_validator, None, None, success_output()) } } @@ -189,7 +192,7 @@ fn run_chainlink_tests( }; let start_devnet_validator = || match start_validator( "chainlink-conf.devnet.toml", - ValidatorCluster::Chain(None), + ValidatorCluster::Light, &loaded_chain_accounts, ) { Some(validator) => validator, @@ -207,16 +210,16 @@ fn run_chainlink_tests( Ok(output) => output, Err(err) => { eprintln!("Failed to run chainlink tests: {:?}", err); - cleanup_devnet_only(&mut devnet_validator); + cleanup_light_validator(&mut devnet_validator, "light"); return Err(err.into()); } }; - cleanup_devnet_only(&mut devnet_validator); + cleanup_light_validator(&mut devnet_validator, "light"); Ok(output) } else { let devnet_validator = config.setup_devnet(TEST_NAME).then(start_devnet_validator); - wait_for_ctrlc(devnet_validator, None, success_output()) + wait_for_ctrlc(None, None, devnet_validator, success_output()) } } @@ -240,7 +243,7 @@ fn run_table_mania_and_committor_tests( let start_devnet_validator = || match start_validator( "committor-conf.devnet.toml", - ValidatorCluster::Chain(None), + ValidatorCluster::Light, &loaded_chain_accounts, ) { Some(validator) => validator, @@ -303,7 +306,7 @@ fn run_table_mania_and_committor_tests( || config.setup_devnet(COMMITTOR_TEST); let devnet_validator = setup_needed.then(start_devnet_validator); Ok(( - wait_for_ctrlc(devnet_validator, None, success_output())?, + wait_for_ctrlc(devnet_validator, None, None, success_output())?, success_output(), )) } @@ -398,7 +401,12 @@ fn run_schedule_commit_tests( let ephem_validator = config.setup_ephem(TEST_NAME).then(start_ephem_validator); eprintln!("Setup validator(s)"); - wait_for_ctrlc(devnet_validator, ephem_validator, success_output())?; + wait_for_ctrlc( + devnet_validator, + ephem_validator, + None, + success_output(), + )?; Ok((success_output(), success_output())) } } @@ -474,7 +482,12 @@ fn run_cloning_tests( config.setup_devnet(TEST_NAME).then(start_devnet_validator); let ephem_validator = config.setup_ephem(TEST_NAME).then(start_ephem_validator); - wait_for_ctrlc(devnet_validator, ephem_validator, success_output()) + wait_for_ctrlc( + devnet_validator, + ephem_validator, + None, + success_output(), + ) } } @@ -531,7 +544,12 @@ fn run_magicblock_api_tests( config.setup_devnet(TEST_NAME).then(start_devnet_validator); let ephem_validator = config.setup_ephem(TEST_NAME).then(start_ephem_validator); - wait_for_ctrlc(devnet_validator, ephem_validator, success_output()) + wait_for_ctrlc( + devnet_validator, + ephem_validator, + None, + success_output(), + ) } } @@ -590,7 +608,12 @@ fn run_magicblock_pubsub_tests( config.setup_devnet(TEST_NAME).then(start_devnet_validator); let ephem_validator = config.setup_ephem(TEST_NAME).then(start_ephem_validator); - wait_for_ctrlc(devnet_validator, ephem_validator, success_output()) + wait_for_ctrlc( + devnet_validator, + ephem_validator, + None, + success_output(), + ) } } @@ -637,7 +660,7 @@ fn run_config_tests( } else { let devnet_validator = config.setup_devnet(TEST_NAME).then(start_devnet_validator); - wait_for_ctrlc(devnet_validator, None, success_output()) + wait_for_ctrlc(devnet_validator, None, None, success_output()) } } @@ -697,7 +720,12 @@ fn run_schedule_intents_tests( config.setup_devnet(TEST_NAME).then(start_devnet_validator); let ephem_validator = config.setup_ephem(TEST_NAME).then(start_ephem_validator); - wait_for_ctrlc(devnet_validator, ephem_validator, success_output()) + wait_for_ctrlc( + devnet_validator, + ephem_validator, + None, + success_output(), + ) } } @@ -746,7 +774,7 @@ fn run_task_scheduler_tests( } else { let devnet_validator = config.setup_devnet(TEST_NAME).then(start_devnet_validator); - wait_for_ctrlc(devnet_validator, None, success_output()) + wait_for_ctrlc(devnet_validator, None, None, success_output()) } } @@ -817,6 +845,7 @@ fn resolve_paths(config_file: &str) -> TestRunnerPaths { enum ValidatorCluster { Chain(Option), Ephem, + Light, } impl ValidatorCluster { @@ -824,6 +853,7 @@ impl ValidatorCluster { match self { ValidatorCluster::Chain(_) => "CHAIN", ValidatorCluster::Ephem => "EPHEM", + ValidatorCluster::Light => "LIGHT", } } } @@ -847,6 +877,12 @@ fn start_validator( log_suffix, ) } + ValidatorCluster::Light => start_light_validator_with_config( + &test_runner_paths, + None, + loaded_chain_accounts, + log_suffix, + ), _ => start_magic_block_validator_with_config( &test_runner_paths, log_suffix, diff --git a/test-integration/test-runner/src/cleanup.rs b/test-integration/test-runner/src/cleanup.rs index b595559fe..009108be6 100644 --- a/test-integration/test-runner/src/cleanup.rs +++ b/test-integration/test-runner/src/cleanup.rs @@ -9,11 +9,34 @@ pub fn cleanup_validators( kill_validators(); } +pub fn cleanup_validators_with_light( + ephem_validator: &mut Child, + light_validator: &mut Child, +) { + cleanup_validator(ephem_validator, "ephemeral"); + cleanup_light_validator(light_validator, "light"); + kill_validators(); +} + pub fn cleanup_devnet_only(devnet_validator: &mut Child) { cleanup_validator(devnet_validator, "devnet"); kill_validators(); } +pub fn cleanup_light_validator(validator: &mut Child, label: &str) { + validator.kill().unwrap_or_else(|err| { + panic!("Failed to kill {} validator ({:?})", label, err) + }); + let command = process::Command::new("light") + .arg("test-validator") + .arg("--stop") + .output() + .unwrap(); + if !command.status.success() { + panic!("Failed to stop light validator: {:?}", command); + } +} + pub fn cleanup_validator(validator: &mut Child, label: &str) { validator.kill().unwrap_or_else(|err| { panic!("Failed to kill {} validator ({:?})", label, err) diff --git a/test-integration/test-runner/src/signal.rs b/test-integration/test-runner/src/signal.rs index 75fd97c0c..90fd299b8 100644 --- a/test-integration/test-runner/src/signal.rs +++ b/test-integration/test-runner/src/signal.rs @@ -4,11 +4,12 @@ use std::{ sync::mpsc::channel, }; -use crate::cleanup::cleanup_validator; +use crate::cleanup::{cleanup_light_validator, cleanup_validator}; pub fn wait_for_ctrlc( devnet_validator: Option, ephem_validator: Option, + light_validator: Option, output: Output, ) -> Result> { let (tx, rx) = channel(); @@ -25,6 +26,8 @@ pub fn wait_for_ctrlc( if let Some(mut validator) = ephem_validator { cleanup_validator(&mut validator, "ephemeral"); } - + if let Some(mut validator) = light_validator { + cleanup_light_validator(&mut validator, "light"); + } Ok(output) } diff --git a/test-integration/test-tools/Cargo.toml b/test-integration/test-tools/Cargo.toml index 75ea22b36..6f7a25f51 100644 --- a/test-integration/test-tools/Cargo.toml +++ b/test-integration/test-tools/Cargo.toml @@ -11,6 +11,7 @@ log = { workspace = true } random-port = { workspace = true } rayon = { workspace = true } serde = { workspace = true } +shlex = { workspace = true } ureq = { workspace = true } url = { workspace = true } magicblock-core = { workspace = true } diff --git a/test-integration/test-tools/src/integration_test_context.rs b/test-integration/test-tools/src/integration_test_context.rs index 3a9103c7a..283d72289 100644 --- a/test-integration/test-tools/src/integration_test_context.rs +++ b/test-integration/test-tools/src/integration_test_context.rs @@ -726,13 +726,16 @@ impl IntegrationTestContext { } fn assert_transaction_error(res: &Result) { - assert!(matches!( - res, - Err(ClientError { - kind: ClientErrorKind::TransactionError(_), - .. - }) - )); + assert!( + matches!( + res, + Err(ClientError { + kind: ClientErrorKind::TransactionError(_), + .. + }) + ), + "Transaction error: {res:?}" + ); } #[allow(clippy::result_large_err)] diff --git a/test-integration/test-tools/src/validator.rs b/test-integration/test-tools/src/validator.rs index 6b9080a22..f3156ba53 100644 --- a/test-integration/test-tools/src/validator.rs +++ b/test-integration/test-tools/src/validator.rs @@ -83,40 +83,7 @@ pub fn start_test_validator_with_config( let mut args = config_to_args(config_path, program_loader); let accounts_dir = workspace_dir.join("configs").join("accounts"); - let accounts = [ - ( - loaded_accounts.validator_authority().to_string(), - "validator-authority.json".to_string(), - ), - ( - loaded_accounts.luzid_authority().to_string(), - "luzid-authority.json".to_string(), - ), - ( - loaded_accounts.validator_fees_vault().to_string(), - "validator-fees-vault.json".to_string(), - ), - ( - loaded_accounts.protocol_fees_vault().to_string(), - "protocol-fees-vault.json".to_string(), - ), - ( - "9yXjZTevvMp1XgZSZEaziPRgFiXtAQChpnP2oX9eCpvt".to_string(), - "non-delegated-cloneable-account1.json".to_string(), - ), - ( - "BHBuATGifAD4JbRpM5nVdyhKzPgv3p2CxLEHAqwBzAj5".to_string(), - "non-delegated-cloneable-account2.json".to_string(), - ), - ( - "2o48ieM95rmHqMWC5B3tTX4DL7cLm4m1Kuwjay3keQSv".to_string(), - "non-delegated-cloneable-account3.json".to_string(), - ), - ( - "2EmfL3MqL3YHABudGNmajjCpR13NNEn9Y4LWxbDm6SwR".to_string(), - "non-delegated-cloneable-account4.json".to_string(), - ), - ]; + let accounts = devnet_accounts(loaded_accounts); let resolved_extra_accounts = loaded_accounts.extra_accounts(workspace_dir, &accounts_dir); let accounts = accounts.iter().chain(&resolved_extra_accounts); @@ -153,6 +120,97 @@ pub fn start_test_validator_with_config( wait_for_validator(validator, port) } +pub fn start_light_validator_with_config( + test_runner_paths: &TestRunnerPaths, + program_loader: Option, + loaded_accounts: &LoadedAccounts, + log_suffix: &str, +) -> Option { + let TestRunnerPaths { + config_path, + root_dir, + workspace_dir, + } = test_runner_paths; + + let port = rpc_port_from_config(config_path); + let mut devnet_args = config_to_args(config_path, program_loader); + + // Remove args already set by light test-validator (and their values) + let args_to_remove = [ + ("--rpc-port", true), + ("--limit-ledger-size", true), + ("--log", false), + ("-r", false), + ]; + let mut filtered_devnet_args = Vec::with_capacity(devnet_args.len()); + let mut skip_next = false; + for arg in devnet_args { + if skip_next { + skip_next = false; + continue; + } + if let Some(&(_, has_value)) = + args_to_remove.iter().find(|&&(flag, _)| flag == arg) + { + skip_next = has_value; + continue; + } + filtered_devnet_args.push(arg); + } + devnet_args = filtered_devnet_args; + + // Add accounts to the validator args + let accounts_dir = workspace_dir.join("configs").join("accounts"); + let accounts = devnet_accounts(loaded_accounts); + let resolved_extra_accounts = + loaded_accounts.extra_accounts(workspace_dir, &accounts_dir); + let account_args = accounts + .iter() + .chain(&resolved_extra_accounts) + .flat_map(|(account, file)| { + let account_path = accounts_dir.join(file).canonicalize().unwrap(); + vec![ + "--account".to_string(), + account.clone(), + account_path.to_str().unwrap().to_string(), + ] + }) + .collect::>(); + devnet_args.extend(account_args); + + // Split args using shlex so that the light CLI can pass them to the validator + let validator_args = shlex::split( + format!("--validator-args=\"{}\"", devnet_args.join(" ")).as_str(), + ) + .ok_or_else(|| anyhow::anyhow!("invalid validator args")) + .unwrap(); + + let mut light_args = vec!["--rpc-port".to_string(), port.to_string()]; + light_args.extend(validator_args); + + let mut script = "#!/bin/bash\nlight test-validator".to_string(); + for arg in &light_args { + script.push_str(&format!(" \\\n {}", arg)); + } + let mut command = process::Command::new("light"); + let rust_log_style = + std::env::var("RUST_LOG_STYLE").unwrap_or(log_suffix.to_string()); + command + .arg("test-validator") + .args(light_args) + .env("RUST_LOG", "solana=warn") + .env("RUST_LOG_STYLE", rust_log_style) + .current_dir(root_dir); + + eprintln!("Starting light validator with {:?}", command); + eprintln!("{}", script); + let validator = command.spawn().expect("Failed to start validator"); + // Waiting for the prover, which is the last thing to start + // Starts by default on port 3001 + let prover_port = 3001; + wait_for_validator(validator, prover_port) +} + pub fn wait_for_validator(mut validator: Child, port: u16) -> Option { const SLEEP_DURATION: Duration = Duration::from_millis(400); let max_retries = if std::env::var("CI").is_ok() { @@ -324,6 +382,43 @@ pub fn resolve_programs( // Utilities // ----------------- +fn devnet_accounts(loaded_accounts: &LoadedAccounts) -> [(String, String); 8] { + [ + ( + loaded_accounts.validator_authority().to_string(), + "validator-authority.json".to_string(), + ), + ( + loaded_accounts.luzid_authority().to_string(), + "luzid-authority.json".to_string(), + ), + ( + loaded_accounts.validator_fees_vault().to_string(), + "validator-fees-vault.json".to_string(), + ), + ( + loaded_accounts.protocol_fees_vault().to_string(), + "protocol-fees-vault.json".to_string(), + ), + ( + "9yXjZTevvMp1XgZSZEaziPRgFiXtAQChpnP2oX9eCpvt".to_string(), + "non-delegated-cloneable-account1.json".to_string(), + ), + ( + "BHBuATGifAD4JbRpM5nVdyhKzPgv3p2CxLEHAqwBzAj5".to_string(), + "non-delegated-cloneable-account2.json".to_string(), + ), + ( + "2o48ieM95rmHqMWC5B3tTX4DL7cLm4m1Kuwjay3keQSv".to_string(), + "non-delegated-cloneable-account3.json".to_string(), + ), + ( + "2EmfL3MqL3YHABudGNmajjCpR13NNEn9Y4LWxbDm6SwR".to_string(), + "non-delegated-cloneable-account4.json".to_string(), + ), + ] +} + /// Unwraps the provided result and ensures to kill the validator before panicking /// if the result was an error #[macro_export] diff --git a/test-manual/helius-laser/configs/helius-config-template.toml b/test-manual/helius-laser/configs/helius-config-template.toml index 13cbd015d..58e43b9e5 100644 --- a/test-manual/helius-laser/configs/helius-config-template.toml +++ b/test-manual/helius-laser/configs/helius-config-template.toml @@ -1,4 +1,5 @@ lifecycle = "ephemeral" +listen = "127.0.0.1:9988" remotes = [ "https://devnet.helius-rpc.com?api-key=", @@ -8,9 +9,6 @@ remotes = [ storage = "magicblock-test-storage" -[aperture] -listen = "127.0.0.1:9988" - [chainlink] max-monitored-accounts = 100 @@ -30,8 +28,6 @@ index-size = 2048576 max-snapshots = 7 # how frequently (slot-wise) we should take snapshots snapshot-frequency = 1024 -# Wipes the accounts database on every startup. -reset = true [ledger] reset = true diff --git a/test-manual/helius-laser/configs/triton-config-template.toml b/test-manual/helius-laser/configs/triton-config-template.toml index a34e5a03d..30edd38cd 100644 --- a/test-manual/helius-laser/configs/triton-config-template.toml +++ b/test-manual/helius-laser/configs/triton-config-template.toml @@ -1,4 +1,5 @@ lifecycle = "ephemeral" +listen = "127.0.0.1:9988" remotes = [ "https://devnet.helius-rpc.com?api-key=", @@ -8,9 +9,6 @@ remotes = [ storage = "magicblock-test-storage" -[aperture] -listen = "127.0.0.1:9988" - [chainlink] max-monitored-accounts = 100 @@ -30,8 +28,6 @@ index-size = 2048576 max-snapshots = 7 # how frequently (slot-wise) we should take snapshots snapshot-frequency = 1024 -# Wipes the accounts database on every startup. -reset = true [ledger] reset = true diff --git a/test-manual/helius-laser/sh/04_start-validator.sh b/test-manual/helius-laser/sh/04_start-validator.sh index 02810638a..030a7dcd5 100755 --- a/test-manual/helius-laser/sh/04_start-validator.sh +++ b/test-manual/helius-laser/sh/04_start-validator.sh @@ -2,6 +2,6 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -RUST_LOG=warn,magicblock=info,magicblock_chainlink=debug,magicblock_chainlink::chainlink::fetch_cloner=trace,magicblock_accounts=debug \ +RUST_LOG=warn,magicblock=info,magicblock_chainlink=debug,magicblock_chainlink::fetch_cloner=trace,magicblock_accounts=debug \ cargo run --bin magicblock-validator --manifest-path=$DIR/../../../Cargo.toml \ -- /tmp/mb-test-laser.toml diff --git a/test-manual/helius-laser/sh/05_run-laser-test.sh b/test-manual/helius-laser/sh/05_run-laser-test.sh index 612bbd219..2b7458617 100755 --- a/test-manual/helius-laser/sh/05_run-laser-test.sh +++ b/test-manual/helius-laser/sh/05_run-laser-test.sh @@ -1,5 +1,3 @@ #!/usr/bin/env bash -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" - -RUST_LOG=info cargo run --bin helius-laser --manifest-path "$DIR/../Cargo.toml" +RUST_LOG=info cargo run --bin helius-laser From 4580f7f28eeb5759df8de738341c1ec113a3866e Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 11:12:19 +0100 Subject: [PATCH 02/33] feat: pass prover port --- test-integration/test-tools/src/validator.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test-integration/test-tools/src/validator.rs b/test-integration/test-tools/src/validator.rs index f3156ba53..3311c9314 100644 --- a/test-integration/test-tools/src/validator.rs +++ b/test-integration/test-tools/src/validator.rs @@ -185,7 +185,13 @@ pub fn start_light_validator_with_config( .ok_or_else(|| anyhow::anyhow!("invalid validator args")) .unwrap(); - let mut light_args = vec!["--rpc-port".to_string(), port.to_string()]; + let prover_port = 3001; + let mut light_args = vec![ + "--rpc-port".to_string(), + port.to_string(), + "--prover-port".to_string(), + prover_port.to_string(), + ]; light_args.extend(validator_args); let mut script = "#!/bin/bash\nlight test-validator".to_string(); @@ -206,8 +212,6 @@ pub fn start_light_validator_with_config( eprintln!("{}", script); let validator = command.spawn().expect("Failed to start validator"); // Waiting for the prover, which is the last thing to start - // Starts by default on port 3001 - let prover_port = 3001; wait_for_validator(validator, prover_port) } From 527e70b7ecdaed679b39e268084395c34bc090fd Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 11:16:51 +0100 Subject: [PATCH 03/33] style: trailing space --- config.example.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.example.toml b/config.example.toml index 836e55c4a..9d02b1ca6 100644 --- a/config.example.toml +++ b/config.example.toml @@ -2,7 +2,7 @@ # MagicBlock Validator Configuration # ============================================================================== # This file serves as a reference for all available configuration options. -# ALL FIELDS ARE OPTIONAL. If removed, the "Default" value listed in the +# ALL FIELDS ARE OPTIONAL. If removed, the "Default" value listed in the # comments will be used. # # CONFIGURATION PRECEDENCE (Highest to Lowest): From 79ba0fe788fba4e3875fa45f28b68dd90a039291 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 11:45:32 +0100 Subject: [PATCH 04/33] feat: log error --- .../src/intent_executor/two_stage_executor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/magicblock-committor-service/src/intent_executor/two_stage_executor.rs b/magicblock-committor-service/src/intent_executor/two_stage_executor.rs index c41c322ca..726df12e3 100644 --- a/magicblock-committor-service/src/intent_executor/two_stage_executor.rs +++ b/magicblock-committor-service/src/intent_executor/two_stage_executor.rs @@ -274,6 +274,9 @@ where }), ) .await + .inspect_err(|err| { + error!("Failed to get commit slot: {}", err) + }) .map(|tx| tx.slot) .ok() }) From 48a0ea1eeb34fe6f5e9f148093d567ceb256c129 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 11:48:51 +0100 Subject: [PATCH 05/33] feat: account size budget helper --- .../src/tasks/args_task.rs | 50 +++++++------------ 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index 3ce579972..a929b13e0 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -261,43 +261,13 @@ impl BaseTask for ArgsTask { )) } ArgsTaskType::CompressedCommit(_task) => { - total_size_budget(&[ - AccountSizeClass::Tiny, // validator - AccountSizeClass::ExtraLarge, // Light System Program - AccountSizeClass::Tiny, // CPI Signer - AccountSizeClass::Tiny, // Registered Program PDA - AccountSizeClass::Tiny, // Account Compression Authority - AccountSizeClass::Tiny, // Account Compression Program - AccountSizeClass::Tiny, // System Program - AccountSizeClass::Huge, // Batch Merkle Tree - AccountSizeClass::ExtraLarge, // Output Queue - ]) + compressed_task_accounts_size_budget() } ArgsTaskType::CompressedFinalize(_task) => { - total_size_budget(&[ - AccountSizeClass::Tiny, // validator - AccountSizeClass::ExtraLarge, // Light System Program - AccountSizeClass::Tiny, // CPI Signer - AccountSizeClass::Tiny, // Registered Program PDA - AccountSizeClass::Tiny, // Account Compression Authority - AccountSizeClass::Tiny, // Account Compression Program - AccountSizeClass::Tiny, // System Program - AccountSizeClass::Huge, // Batch Merkle Tree - AccountSizeClass::ExtraLarge, // Output Queue - ]) + compressed_task_accounts_size_budget() } ArgsTaskType::CompressedUndelegate(_task) => { - total_size_budget(&[ - AccountSizeClass::Tiny, // validator - AccountSizeClass::ExtraLarge, // Light System Program - AccountSizeClass::Tiny, // CPI Signer - AccountSizeClass::Tiny, // Registered Program PDA - AccountSizeClass::Tiny, // Account Compression Authority - AccountSizeClass::Tiny, // Account Compression Program - AccountSizeClass::Tiny, // System Program - AccountSizeClass::Huge, // Batch Merkle Tree - AccountSizeClass::ExtraLarge, // Output Queue - ]) + compressed_task_accounts_size_budget() } ArgsTaskType::BaseAction(task) => { // assume all other accounts are Small accounts. @@ -424,6 +394,20 @@ impl BaseTask for ArgsTask { } } +fn compressed_task_accounts_size_budget() -> u32 { + total_size_budget(&[ + AccountSizeClass::Tiny, // validator + AccountSizeClass::ExtraLarge, // Light System Program + AccountSizeClass::Tiny, // CPI Signer + AccountSizeClass::Tiny, // Registered Program PDA + AccountSizeClass::Tiny, // Account Compression Authority + AccountSizeClass::Tiny, // Account Compression Program + AccountSizeClass::Tiny, // System Program + AccountSizeClass::Huge, // Batch Merkle Tree + AccountSizeClass::ExtraLarge, // Output Queue + ]) +} + impl LabelValue for ArgsTask { fn value(&self) -> &str { match self.task_type { From 371b8a3ef4500a511a638ebdfcac129c1489a309 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 11:51:18 +0100 Subject: [PATCH 06/33] doc: add safety comment --- magicblock-core/src/compression/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/magicblock-core/src/compression/mod.rs b/magicblock-core/src/compression/mod.rs index 8ee734a70..3af81d64b 100644 --- a/magicblock-core/src/compression/mod.rs +++ b/magicblock-core/src/compression/mod.rs @@ -14,6 +14,7 @@ pub const OUTPUT_QUEUE: Pubkey = pub fn derive_cda_from_pda(pda: &Pubkey) -> Pubkey { // Since the PDA is already unique we use the delegation program's id // as a program id. + // SAFETY: BN254 hash of PDA must succeed for a 32-byte PDA seed let seed = hashv_to_bn254_field_size_be_const_array::<3>(&[&pda.to_bytes()]) .expect("BN254 hash of PDA must succeed for a 32-byte PDA seed"); From d6fed47a9a4e32217fec8ff66fd8e6397c3f5c4b Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 13:42:24 +0100 Subject: [PATCH 07/33] style: rename --- .../test-committor-service/tests/utils/transactions.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-integration/test-committor-service/tests/utils/transactions.rs b/test-integration/test-committor-service/tests/utils/transactions.rs index c85093d52..bec08de0a 100644 --- a/test-integration/test-committor-service/tests/utils/transactions.rs +++ b/test-integration/test-committor-service/tests/utils/transactions.rs @@ -116,8 +116,8 @@ macro_rules! get_compressed_account { } } }}; - ($rpc_client:ident, $pubkey:expr, $label:literal) => {{ - get_compressed_account!($rpc_client, $pubkey, $label, |_: &light_client::indexer::CompressedAccount, _: u8| true) + ($photon_client:ident, $pubkey:expr, $label:literal) => {{ + get_compressed_account!($photon_client, $pubkey, $label, |_: &light_client::indexer::CompressedAccount, _: u8| true) }}; } From 08e33339bc543679e7c6b969452d09b5499c6fa9 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 14:40:10 +0100 Subject: [PATCH 08/33] feat: use constant --- .../test-committor-service/tests/utils/transactions.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-integration/test-committor-service/tests/utils/transactions.rs b/test-integration/test-committor-service/tests/utils/transactions.rs index bec08de0a..983824108 100644 --- a/test-integration/test-committor-service/tests/utils/transactions.rs +++ b/test-integration/test-committor-service/tests/utils/transactions.rs @@ -208,7 +208,7 @@ pub async fn init_and_delegate_account_on_chain( bytes: u64, label: Option, ) -> (Pubkey, Account) { - let rpc_client = RpcClient::new("http://localhost:7799".to_string()); + let rpc_client = RpcClient::new(RPC_URL.to_string()); rpc_client .request_airdrop(&counter_auth.pubkey(), 777 * LAMPORTS_PER_SOL) @@ -387,7 +387,7 @@ pub async fn init_and_delegate_compressed_account_on_chain( pub async fn init_and_delegate_order_book_on_chain( payer: &Keypair, ) -> (Pubkey, Account) { - let rpc_client = RpcClient::new("http://localhost:7799".to_string()); + let rpc_client = RpcClient::new(RPC_URL.to_string()); rpc_client .request_airdrop(&payer.pubkey(), 777 * LAMPORTS_PER_SOL) @@ -449,7 +449,7 @@ pub async fn init_and_delegate_order_book_on_chain( pub async fn fund_validator_auth_and_ensure_validator_fees_vault( validator_auth: &Keypair, ) { - let rpc_client = RpcClient::new("http://localhost:7799".to_string()); + let rpc_client = RpcClient::new(RPC_URL.to_string()); rpc_client .request_airdrop(&validator_auth.pubkey(), 777 * LAMPORTS_PER_SOL) .await From fa5e1b26682f38eebcfc2ded2fb27b24225314a4 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 15:07:59 +0100 Subject: [PATCH 09/33] fix: set undelegate args --- .../src/tasks/args_task.rs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/magicblock-committor-service/src/tasks/args_task.rs b/magicblock-committor-service/src/tasks/args_task.rs index a929b13e0..ccbe97d33 100644 --- a/magicblock-committor-service/src/tasks/args_task.rs +++ b/magicblock-committor-service/src/tasks/args_task.rs @@ -1,4 +1,7 @@ -use compressed_delegation_client::types::{CommitArgs, FinalizeArgs}; +use compressed_delegation_client::{ + types::{CommitArgs, FinalizeArgs}, + CompressedDelegationRecord, UndelegateArgs, +}; use dlp::{ args::{CallHandlerArgs, CommitDiffArgs, CommitStateArgs}, compute_diff, @@ -8,6 +11,7 @@ use dlp::{ }, total_size_budget, AccountSizeClass, }; +use log::warn; use magicblock_metrics::metrics::LabelValue; use solana_account::ReadableAccount; use solana_instruction::{AccountMeta, Instruction}; @@ -153,11 +157,31 @@ impl BaseTask for ArgsTask { .instruction() } ArgsTaskType::CompressedUndelegate(value) => { + // NOTE: Undelegation should not be called as an intent + // This is because the validator would have to pay rent out of pocket. + // This could be solved by using the ephemeral payer to ensure the user can pay the rent. + // https://github.com/magicblock-labs/magicblock-validator/issues/651 + warn!("Undelegation should not be called as an intent"); compressed_delegation_client::UndelegateBuilder::new() .payer(*validator) .delegated_account(value.delegated_account) .owner_program(value.owner_program) .system_program(system_program_id()) + .args(UndelegateArgs { + validity_proof: value.compressed_data.proof, + delegation_record_account_meta: value + .compressed_data + .account_meta, + compressed_delegated_record: + CompressedDelegationRecord::from_bytes( + &value + .compressed_data + .compressed_delegation_record_bytes, + ) + .expect( + "Compressed delegation record should be valid", + ), + }) .add_remaining_accounts( &value.compressed_data.remaining_accounts, ) From 45eb6c8d40c3601a5d9058cdf658a72ad8af95d8 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 15:40:50 +0100 Subject: [PATCH 10/33] doc: explain zeroing --- compressed-delegation-client/src/utils.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/compressed-delegation-client/src/utils.rs b/compressed-delegation-client/src/utils.rs index a510ae870..8185d7799 100644 --- a/compressed-delegation-client/src/utils.rs +++ b/compressed-delegation-client/src/utils.rs @@ -9,6 +9,7 @@ impl DataHasher for CompressedDelegationRecord { fn hash(&self) -> Result<[u8; 32], HasherError> { let bytes = borsh::to_vec(self).map_err(|_| HasherError::BorshError)?; let mut hash = Sha256::hash(bytes.as_slice())?; + // Light uses 254bits field elements, so we zero out the first byte. hash[0] = 0; Ok(hash) } From 980b89007780dc60ef8cc37b63faa72fe7a4fb8a Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 16:09:00 +0100 Subject: [PATCH 11/33] feat: unused error --- .../src/transaction_preparator/delivery_preparator.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/magicblock-committor-service/src/transaction_preparator/delivery_preparator.rs b/magicblock-committor-service/src/transaction_preparator/delivery_preparator.rs index 8d2b8657b..9040b0d54 100644 --- a/magicblock-committor-service/src/transaction_preparator/delivery_preparator.rs +++ b/magicblock-committor-service/src/transaction_preparator/delivery_preparator.rs @@ -618,8 +618,6 @@ impl From for BufferExecutionError { #[derive(thiserror::Error, Debug)] pub enum InternalError { - #[error("Compressed data not found")] - CompressedDataNotFound, #[error("0 retries was requested")] ZeroRetriesRequestedError, #[error("Chunks PDA does not exist for writing. pda: {0}")] From 14abd9fc67a2249e67b5495372506fe5eac7e5db Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 16:09:34 +0100 Subject: [PATCH 12/33] style: order imports --- magicblock-core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magicblock-core/Cargo.toml b/magicblock-core/Cargo.toml index 4096e81e5..a7b60357c 100644 --- a/magicblock-core/Cargo.toml +++ b/magicblock-core/Cargo.toml @@ -10,8 +10,8 @@ edition.workspace = true [dependencies] compressed-delegation-client = { workspace = true } flume = { workspace = true } -light-sdk = { workspace = true } light-compressed-account = { workspace = true } +light-sdk = { workspace = true } magicblock-magic-program-api = { workspace = true } solana-account = { workspace = true } solana-account-decoder = { workspace = true } From e5739e6d7af745fa3400828623ca3e2dd4af94ba Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 16:10:40 +0100 Subject: [PATCH 13/33] style: use expect for consistency --- test-integration/programs/schedulecommit/src/api.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-integration/programs/schedulecommit/src/api.rs b/test-integration/programs/schedulecommit/src/api.rs index b8ada8fbc..1740c6fb7 100644 --- a/test-integration/programs/schedulecommit/src/api.rs +++ b/test-integration/programs/schedulecommit/src/api.rs @@ -411,7 +411,8 @@ fn build_instruction( ) -> Instruction { Instruction::new_with_bytes( program_id, - &borsh::to_vec(&instruction).unwrap(), + &borsh::to_vec(&instruction) + .expect("Serialization of instruction should never fail"), account_metas, ) } From 8de33869d60ac19f7dbe219eb88476cdd1f25103 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Thu, 15 Jan 2026 16:13:46 +0100 Subject: [PATCH 14/33] style: typo --- .../tests/test_ix_commit_local.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test-integration/test-committor-service/tests/test_ix_commit_local.rs b/test-integration/test-committor-service/tests/test_ix_commit_local.rs index c70572ce9..8b7411cd0 100644 --- a/test-integration/test-committor-service/tests/test_ix_commit_local.rs +++ b/test-integration/test-committor-service/tests/test_ix_commit_local.rs @@ -418,7 +418,7 @@ async fn test_ix_commit_two_accounts_1kb_2kb() { } #[tokio::test] -async fn test_ix_commit_two_accounts_512kb() { +async fn test_ix_commit_two_accounts_512_bytes() { init_logger!(); commit_multiple_accounts( &[512, 512], @@ -430,7 +430,7 @@ async fn test_ix_commit_two_accounts_512kb() { } #[tokio::test] -async fn test_ix_commit_three_accounts_512kb() { +async fn test_ix_commit_three_accounts_512_bytes() { init_logger!(); commit_multiple_accounts( &[512, 512, 512], @@ -442,7 +442,7 @@ async fn test_ix_commit_three_accounts_512kb() { } #[tokio::test] -async fn test_ix_commit_six_accounts_512kb() { +async fn test_ix_commit_six_accounts_512_bytes() { init_logger!(); commit_multiple_accounts( &[512, 512, 512, 512, 512, 512], @@ -644,7 +644,7 @@ async fn test_ix_commit_single_compressed_account_500_bytes_and_undelegate() { } #[tokio::test] -async fn test_ix_commit_two_compressed_accounts_512kb() { +async fn test_ix_commit_two_compressed_accounts_512_bytes() { commit_multiple_accounts( &[512, 512], 1, @@ -655,7 +655,7 @@ async fn test_ix_commit_two_compressed_accounts_512kb() { } #[tokio::test] -async fn test_ix_commit_three_compressed_accounts_512kb() { +async fn test_ix_commit_three_compressed_accounts_512_bytes() { init_logger!(); commit_multiple_accounts( &[512, 512, 512], @@ -667,7 +667,7 @@ async fn test_ix_commit_three_compressed_accounts_512kb() { } #[tokio::test] -async fn test_ix_commit_six_compressed_accounts_512kb() { +async fn test_ix_commit_six_compressed_accounts_512_bytes() { init_logger!(); commit_multiple_accounts( &[512, 512, 512, 512, 512, 512], From 9ae8866bda0a43789fb98a9bc845a2a3cf37dd37 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 19 Jan 2026 17:46:03 +0100 Subject: [PATCH 15/33] docs: add tracking issue --- .../src/remote_account_provider/photon_client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magicblock-chainlink/src/remote_account_provider/photon_client.rs b/magicblock-chainlink/src/remote_account_provider/photon_client.rs index f048283e1..e518b635d 100644 --- a/magicblock-chainlink/src/remote_account_provider/photon_client.rs +++ b/magicblock-chainlink/src/remote_account_provider/photon_client.rs @@ -84,8 +84,8 @@ impl PhotonClient for PhotonClientImpl { Ok(res) => res, // NOTE: @@@ this is broken, we actually are getting a `None` value // when the account is not found - // We need to wait for the light-client to provide an `Option` for that - // value + // Light released a fix for this but we can't integrate it yet. + // https://github.com/magicblock-labs/magicblock-validator/issues/869 Err(IndexerError::AccountNotFound) => { return Ok(None); } From eeadd533a8252b42d3d6c11ffe335a24c7f107ae Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 19 Jan 2026 17:54:34 +0100 Subject: [PATCH 16/33] feat: warn stale compressed account --- magicblock-chainlink/src/remote_account_provider/mod.rs | 8 ++++++-- .../src/remote_account_provider/remote_account.rs | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index 3d067d4f9..536a5a4ed 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -1482,6 +1482,7 @@ impl pubkeys: &[Pubkey], remote_accounts_results: Vec, ) -> Vec { + const STALE_SLOT_THRESHOLD: u64 = 100; let (rpc_accounts, compressed_accounts) = { if remote_accounts_results.is_empty() { return vec![]; @@ -1541,8 +1542,11 @@ impl .into_iter() .zip(compressed_accounts)) .map(|(pubkey, (rpc_acc, comp_acc))| match (rpc_acc, comp_acc) { - (Found(_), Found(comp_state)) => { - warn!("Both RPC and Compressed account found for pubkey {}. Using Compressed account.", pubkey); + (Found(rpc_state), Found(comp_state)) => { + info!("Both RPC and Compressed account found for pubkey {}. Using Compressed account.", pubkey); + if rpc_state.account.slot() > comp_state.account.slot() + STALE_SLOT_THRESHOLD { + warn!("Compressed account is stale. rpc_slot={}, comp_slot={}", rpc_state.account.slot(), comp_state.account.slot()); + } Found(comp_state) } (Found(rpc_state), NotFound(_)) => Found(rpc_state), diff --git a/magicblock-chainlink/src/remote_account_provider/remote_account.rs b/magicblock-chainlink/src/remote_account_provider/remote_account.rs index f8a64b933..6c23719bb 100644 --- a/magicblock-chainlink/src/remote_account_provider/remote_account.rs +++ b/magicblock-chainlink/src/remote_account_provider/remote_account.rs @@ -42,6 +42,13 @@ impl ResolvedAccount { .map(ResolvedAccountSharedData::Bank), } } + + pub fn slot(&self) -> Slot { + match self { + ResolvedAccount::Fresh(account) => account.remote_slot(), + ResolvedAccount::Bank((_, slot)) => *slot, + } + } } /// Same as [ResolvedAccount], but with the account data fetched from the bank. From 0a1518d6a6308a1c3c80bdc0be2c945c458bf637 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 19 Jan 2026 18:00:08 +0100 Subject: [PATCH 17/33] feat: avoid reallocations --- .../src/remote_account_provider/mod.rs | 88 ++++++++----------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index 536a5a4ed..385caff0f 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -1084,53 +1084,41 @@ impl } }; - let ( - remote_accounts_results, - found_count, - not_found_count, - compressed_found_count, - compressed_not_found_count, - ) = vec![ + let mut remote_accounts_results = Vec::with_capacity(2); + let mut found_cnt = 0; + let mut not_found_cnt = 0; + let mut compressed_found_count = 0; + let mut compressed_not_found_count = 0; + + let results = vec![ Ok(Some((rpc_accounts, found_count, not_found_count))), compressed_accounts().await, - ] - .into_iter() - .filter_map(|result| match result { - Ok(Some(result)) => Some(result), - Ok(None) => None, - Err(err) => { - error!("Failed to fetch accounts: {err:?}"); - None - } - }) - .fold( - (vec![], 0, 0, 0, 0), - |( - remote_accounts_results, - found_count, - not_found_count, - compressed_found_count, - compressed_not_found_count, - ), - (accs, found_cnt, not_found_cnt)| { - match &accs { - FetchedRemoteAccounts::Rpc(_) => ( - [remote_accounts_results, vec![accs]].concat(), - found_count + found_cnt, - not_found_count + not_found_cnt, - compressed_found_count, - compressed_not_found_count, - ), - FetchedRemoteAccounts::Compressed(_) => ( - [remote_accounts_results, vec![accs]].concat(), - found_count, - not_found_count, - compressed_found_count + found_cnt, - compressed_not_found_count + not_found_cnt, - ), + ]; + + for result in results { + match result { + Ok(Some((FetchedRemoteAccounts::Rpc(accs), fc, nfc))) => { + remote_accounts_results + .push(FetchedRemoteAccounts::Rpc(accs)); + found_cnt += fc; + not_found_cnt += nfc; } - }, - ); + Ok(Some(( + FetchedRemoteAccounts::Compressed(accs), + fc, + nfc, + ))) => { + remote_accounts_results + .push(FetchedRemoteAccounts::Compressed(accs)); + compressed_found_count += fc; + compressed_not_found_count += nfc; + } + Ok(None) => {} + Err(err) => { + error!("Failed to fetch accounts: {err:?}"); + } + } + } let remote_accounts = Self::consolidate_fetched_remote_accounts( &pubkeys, remote_accounts_results, @@ -1138,8 +1126,8 @@ impl // Update metrics for successful RPC fetch inc_account_fetches_success(pubkeys.len() as u64); - inc_account_fetches_found(fetch_origin, found_count); - inc_account_fetches_not_found(fetch_origin, not_found_count); + inc_account_fetches_found(fetch_origin, found_cnt); + inc_account_fetches_not_found(fetch_origin, not_found_cnt); if (*photon_client).is_some() { // Update metrics for successful compressed fetch @@ -1157,18 +1145,18 @@ impl // Record per-program metrics if programs were provided if let Some(program_ids) = &program_ids { for program_id in program_ids { - if found_count > 0 { + if found_cnt > 0 { inc_per_program_account_fetch_stats( &program_id.to_string(), ProgramFetchResult::Found, - found_count, + found_cnt, ); } - if not_found_count > 0 { + if not_found_cnt > 0 { inc_per_program_account_fetch_stats( &program_id.to_string(), ProgramFetchResult::NotFound, - not_found_count, + not_found_cnt, ); } } From eb494da2672836ed78f5137b9f5651c86bd283c6 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 19 Jan 2026 18:14:13 +0100 Subject: [PATCH 18/33] doc: explain nested match --- magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs index f833975bd..ae3af13d1 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs @@ -433,7 +433,11 @@ where } } } else if owned_by_compressed_delegation_program { - // If the account is compressed, the delegation record is in the account itself + // If the account is compressed, it can contain either: + // 1. The delegation record + // 2. No data in case the last update was the notif where the account was emptied + // If we fail to get the record, we need to fetch again so that we obtain the data + // from the compressed account. let delegation_record = match CompressedDelegationRecord::try_from_slice( account.data(), From 94b3d851ce1edc929fe41d3f8241953ac2f5c4d7 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 19 Jan 2026 18:14:32 +0100 Subject: [PATCH 19/33] feat: avoid unnecessary clone --- .../src/tasks/task_builder.rs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index 0ce98b8de..9d764680a 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -203,20 +203,17 @@ impl TasksBuilder for TaskBuilderImpl { let photon_client = photon_client .as_ref() .ok_or(TaskBuilderError::PhotonClientNotFound)?; - let commit_ids = commit_ids.clone(); accounts .iter() .map(|account| { - let commit_ids = commit_ids.clone(); - async move { - let commit_id = *commit_ids - .get(&account.pubkey) - .ok_or(TaskBuilderError::MissingCommitId( - account.pubkey, - ))?; + let commit_id = *commit_ids.get(&account.pubkey).ok_or( + TaskBuilderError::MissingCommitId(account.pubkey), + )?; + let account_clone = account.clone(); + Ok(async move { let compressed_data = get_compressed_data( - &account.pubkey, + &account_clone.pubkey, photon_client, None, ) @@ -225,16 +222,16 @@ impl TasksBuilder for TaskBuilderImpl { CompressedCommitTask { commit_id, allow_undelegation, - committed_account: account.clone(), + committed_account: account_clone, compressed_data, }, ); Ok::<_, TaskBuilderError>( Box::new(ArgsTask::new(task)) as Box ) - } + }) }) - .collect::>() + .collect::, TaskBuilderError>>()? .try_collect() .await? } else { From 867a25d1a106310e7dced41440a3504938b55ab8 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Mon, 19 Jan 2026 21:08:07 +0100 Subject: [PATCH 20/33] feat: remove unwrap --- .../src/remote_account_provider/mod.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index bfc20bdd9..a21b83f4e 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -1168,7 +1168,15 @@ impl pubkeys.iter().zip(remote_accounts.iter()) { let requests = { - let mut fetching = fetching_accounts.lock().unwrap(); + let mut fetching = match fetching_accounts.lock() { + Ok(guard) => guard, + Err(poisoned) => { + error!( + "fetching_accounts lock poisoned; continuing with inner state: {poisoned:?}" + ); + poisoned.into_inner() + } + }; // Remove from fetching and get pending requests // Note: the account might have been resolved by subscription update already if let Some((_, requests)) = fetching.remove(pubkey) { From 3aa48dca061084a4ca456ee8bd45199776e1dbbf Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 08:57:08 +0100 Subject: [PATCH 21/33] feat: only send metrics if accounts found --- magicblock-chainlink/src/remote_account_provider/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index a21b83f4e..dc50ec07f 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -1122,7 +1122,9 @@ impl inc_account_fetches_found(fetch_origin, found_cnt); inc_account_fetches_not_found(fetch_origin, not_found_cnt); - if (*photon_client).is_some() { + let compressed_total = + compressed_found_count + compressed_not_found_count; + if (*photon_client).is_some() && compressed_total > 0 { // Update metrics for successful compressed fetch inc_compressed_account_fetches_success(pubkeys.len() as u64); inc_compressed_account_fetches_found( From a29876e00541659ee9a37a3afcddc2713d5d712d Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 09:38:20 +0100 Subject: [PATCH 22/33] feat: async move with cloned data --- .../src/tasks/task_builder.rs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/magicblock-committor-service/src/tasks/task_builder.rs b/magicblock-committor-service/src/tasks/task_builder.rs index d6a5f1dd1..4d4a511e1 100644 --- a/magicblock-committor-service/src/tasks/task_builder.rs +++ b/magicblock-committor-service/src/tasks/task_builder.rs @@ -325,15 +325,18 @@ impl TasksBuilder for TaskBuilderImpl { .ok_or(TaskBuilderError::PhotonClientNotFound)?; committed_accounts .iter() - .map(|account| async { - Ok(Some( - get_compressed_data( - &account.pubkey, - photon_client, - None, - ) - .await?, - )) + .map(|account| { + let pubkey = account.pubkey; + async move { + Ok(Some( + get_compressed_data( + &pubkey, + photon_client, + None, + ) + .await?, + )) + } }) .collect::>() .try_collect() From b4d3875935916d8ed6d45514b27d677d07fc9697 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 09:45:43 +0100 Subject: [PATCH 23/33] feat: clarify logs --- .../src/chainlink/fetch_cloner/mod.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs index d5b1fb491..9de2a21eb 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs @@ -795,9 +795,12 @@ where let Some(record) = CompressedDelegationRecord::from_bytes(in_bank.data()).ok() else { + // The delegation record is not present in the account data because the actual account data + // has already been extracted from it. Refreshing would reset the account, losing local changes. debug!( pubkey = %pubkey, - "Account is compressed, but the delegation has already been processed (account is delegated)" + data = %format!("{:?}", in_bank.data()), + "Skip refresh for already processed compressed account" ); return RefreshDecision::No; }; @@ -812,19 +815,19 @@ where owner: record.owner, delegation_slot: record.delegation_slot, lamports: record.lamports, - commit_frequency_ms: 0, + commit_frequency_ms: 0, // TODO(dode): use the actual commit frequency once implemented }), &self.validator_pubkey, ) { debug!( pubkey = %pubkey, - "Account is compressed, but the compressed delegation record is not undelegating, refresh it" + "Refresh compressed account since the compressed delegation record is not undelegating" ); return RefreshDecision::Yes; }; debug!( pubkey = %pubkey, - "Account is compressed, the compressed delegation record is undelegating, do not refresh it" + "Skip refresh for compressed account since the compressed delegation record is undelegating" ); } else if in_bank.undelegating() { debug!( From 2ca95e49afbe7bd948168b9356bc49ed848fe3b6 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 09:50:55 +0100 Subject: [PATCH 24/33] feat: propagate errors --- .../src/chainlink/fetch_cloner/pipeline.rs | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs index 37798ea6a..1f410fa89 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs @@ -381,19 +381,22 @@ where C: Cloner, P: PhotonClient, { - Ok(owned_by_deleg_compressed.into_iter().filter_map( - |(pubkey, mut account, _)| { - let Ok(delegation_record) = - CompressedDelegationRecord::try_from_slice( - account.data(), - ) - .map_err(|err| { - error!("Failed to deserialize compressed delegation record for {pubkey}: {err}\nAccount: {:?}", account); - err - }) - else { - return None; - }; + owned_by_deleg_compressed + .into_iter() + .map(|(pubkey, mut account, _)| { + let delegation_record = + CompressedDelegationRecord::try_from_slice(account.data()) + .map_err(|err| { + error!( + "Failed to deserialize compressed delegation record for {pubkey}: {err}\nAccount: {:?}", + account + ); + ChainlinkError::DelegatedAccountResolutionsFailed( + format!( + "Failed to deserialize compressed delegation record for {pubkey}: {err}" + ), + ) + })?; account.set_compressed(true); account.set_lamports(delegation_record.lamports); @@ -414,16 +417,14 @@ where lamports: delegation_record.lamports, commit_frequency_ms: 0, }); - Some(AccountCloneRequest { + Ok(AccountCloneRequest { pubkey, account, commit_frequency_ms: None, delegated_to_other, }) - }, - ) - .collect::>() -) + }) + .collect::>>() } /// Resolves program accounts, fetching program data accounts for LoaderV3 programs From 95c7ee949f36791de05b9866a7fb735faf8dd4e5 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 09:53:23 +0100 Subject: [PATCH 25/33] test: assert not subscribed --- magicblock-chainlink/tests/03_deleg_after_sub.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/magicblock-chainlink/tests/03_deleg_after_sub.rs b/magicblock-chainlink/tests/03_deleg_after_sub.rs index f953e0ddf..4c9a595c4 100644 --- a/magicblock-chainlink/tests/03_deleg_after_sub.rs +++ b/magicblock-chainlink/tests/03_deleg_after_sub.rs @@ -233,5 +233,6 @@ async fn test_deleg_after_subscribe_case2_compressed() { assert!(updated); assert_cloned_as_delegated!(cloner, &[pubkey], slot, program_pubkey); + assert_not_subscribed!(&chainlink, &[&pubkey]); } } From 5556feeceba524f9bc374dca9d5cc11147946b1c Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 09:54:21 +0100 Subject: [PATCH 26/33] feat: explicit none --- .../src/intent_executor/single_stage_executor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/magicblock-committor-service/src/intent_executor/single_stage_executor.rs b/magicblock-committor-service/src/intent_executor/single_stage_executor.rs index edb5f5a0c..2ee81dd37 100644 --- a/magicblock-committor-service/src/intent_executor/single_stage_executor.rs +++ b/magicblock-committor-service/src/intent_executor/single_stage_executor.rs @@ -11,7 +11,7 @@ use crate::{ TransactionStrategyExecutionError, }, task_info_fetcher::TaskInfoFetcher, - ExecutionOutput, IntentExecutorImpl, + CommitSlotFn, ExecutionOutput, IntentExecutorImpl, }, persist::{IntentPersister, IntentPersisterImpl}, tasks::{ @@ -220,7 +220,7 @@ where compressed: task.is_compressed(), }, &None::, - None, + None::, ) .await .map_err(IntentExecutorError::FailedFinalizePreparationError)? From f92e8428b9b5d2afbe3c773f7101d091ad7c50bb Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 10:09:30 +0100 Subject: [PATCH 27/33] feat: use strategy compressed field --- magicblock-committor-service/src/intent_executor/mod.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/magicblock-committor-service/src/intent_executor/mod.rs b/magicblock-committor-service/src/intent_executor/mod.rs index 5bff35e88..6df00da54 100644 --- a/magicblock-committor-service/src/intent_executor/mod.rs +++ b/magicblock-committor-service/src/intent_executor/mod.rs @@ -326,12 +326,6 @@ where committed_pubkeys: &[Pubkey], strategy: &mut TransactionStrategy, ) -> Result { - // TODO(dode): Handle cases where some tasks are compressed and some are not - let compressed = strategy - .optimized_tasks - .iter() - .any(|task| task.is_compressed()); - let tasks_and_metas: Vec<_> = strategy .optimized_tasks .iter_mut() @@ -362,7 +356,7 @@ where .fetch_next_commit_ids( committed_pubkeys, min_context_slot, - compressed, + strategy.compressed, ) .await .map_err(TaskBuilderError::CommitTasksBuildError)?; From 99af6219acf72efd62574d0c45604d9f96dfa665 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 10:10:54 +0100 Subject: [PATCH 28/33] feat: photon when needed --- .../tests/test_ix_commit_local.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test-integration/test-committor-service/tests/test_ix_commit_local.rs b/test-integration/test-committor-service/tests/test_ix_commit_local.rs index 652433768..68c7972ee 100644 --- a/test-integration/test-committor-service/tests/test_ix_commit_local.rs +++ b/test-integration/test-committor-service/tests/test_ix_commit_local.rs @@ -234,8 +234,15 @@ async fn commit_single_account( let validator_auth = ensure_validator_authority(); fund_validator_auth_and_ensure_validator_fees_vault(&validator_auth).await; - let photon_client = - Some(Arc::new(PhotonIndexer::new(PHOTON_URL.to_string(), None))); + let photon_client = if matches!( + mode, + CommitAccountMode::CompressedCommit + | CommitAccountMode::CompressedCommitAndUndelegate + ) { + Some(Arc::new(PhotonIndexer::new(PHOTON_URL.to_string(), None))) + } else { + None + }; // Run each test with and without finalizing let service = CommittorService::try_start( From 32d0abe1b79f803328201b823a349bf65178686a Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 10:14:57 +0100 Subject: [PATCH 29/33] style: fmt --- .../src/chainlink/fetch_cloner/pipeline.rs | 28 +++++++++---------- .../src/remote_account_provider/mod.rs | 20 ++++++------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs index 1f410fa89..6704b06bd 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs @@ -381,22 +381,22 @@ where C: Cloner, P: PhotonClient, { - owned_by_deleg_compressed - .into_iter() - .map(|(pubkey, mut account, _)| { - let delegation_record = - CompressedDelegationRecord::try_from_slice(account.data()) - .map_err(|err| { - error!( + owned_by_deleg_compressed + .into_iter() + .map(|(pubkey, mut account, _)| { + let delegation_record = + CompressedDelegationRecord::try_from_slice(account.data()) + .map_err(|err| { + error!( "Failed to deserialize compressed delegation record for {pubkey}: {err}\nAccount: {:?}", - account - ); - ChainlinkError::DelegatedAccountResolutionsFailed( - format!( + account + ); + ChainlinkError::DelegatedAccountResolutionsFailed( + format!( "Failed to deserialize compressed delegation record for {pubkey}: {err}" - ), - ) - })?; + ), + ) + })?; account.set_compressed(true); account.set_lamports(delegation_record.lamports); diff --git a/magicblock-chainlink/src/remote_account_provider/mod.rs b/magicblock-chainlink/src/remote_account_provider/mod.rs index dc50ec07f..f2cea945e 100644 --- a/magicblock-chainlink/src/remote_account_provider/mod.rs +++ b/magicblock-chainlink/src/remote_account_provider/mod.rs @@ -1122,8 +1122,8 @@ impl inc_account_fetches_found(fetch_origin, found_cnt); inc_account_fetches_not_found(fetch_origin, not_found_cnt); - let compressed_total = - compressed_found_count + compressed_not_found_count; + let compressed_total = + compressed_found_count + compressed_not_found_count; if (*photon_client).is_some() && compressed_total > 0 { // Update metrics for successful compressed fetch inc_compressed_account_fetches_success(pubkeys.len() as u64); @@ -1170,15 +1170,15 @@ impl pubkeys.iter().zip(remote_accounts.iter()) { let requests = { - let mut fetching = match fetching_accounts.lock() { - Ok(guard) => guard, - Err(poisoned) => { - error!( + let mut fetching = match fetching_accounts.lock() { + Ok(guard) => guard, + Err(poisoned) => { + error!( "fetching_accounts lock poisoned; continuing with inner state: {poisoned:?}" - ); - poisoned.into_inner() - } - }; + ); + poisoned.into_inner() + } + }; // Remove from fetching and get pending requests // Note: the account might have been resolved by subscription update already if let Some((_, requests)) = fetching.remove(pubkey) { From 463c1c023ec7e5b4841cae8bf1665bbd6cf038a5 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 10:36:11 +0100 Subject: [PATCH 30/33] style: lint --- .../src/chainlink/fetch_cloner/pipeline.rs | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs index 6704b06bd..1064ef62c 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs @@ -305,17 +305,14 @@ where record_subs.push(delegation_record_pubkey); // If the account is delegated we set the owner and delegation state - let (commit_frequency_ms, delegated_to_other) = if let Some( - delegation_record_data, - ) = - delegation_record - { - // NOTE: failing here is fine when resolving all accounts for a transaction - // since if something is off we better not run it anyways - // However we may consider a different behavior when user is getting - // multiple accounts. - let delegation_record = - match FetchCloner::::parse_delegation_record( + let (commit_frequency_ms, delegated_to_other) = + if let Some(delegation_record_data) = delegation_record { + // NOTE: failing here is fine when resolving all accounts for a transaction + // since if something is off we better not run it anyways + // However we may consider a different behavior when user is getting + // multiple accounts. + let delegation_record = + match FetchCloner::::parse_delegation_record( delegation_record_data.data(), delegation_record_pubkey, ) { @@ -338,20 +335,21 @@ where } }; - trace!(pubkey = %pubkey, "Delegation record found"); + trace!(pubkey = %pubkey, "Delegation record found"); - let delegated_to_other = - this.get_delegated_to_other(&delegation_record); + let delegated_to_other = + this.get_delegated_to_other(&delegation_record); - let commit_freq = this.apply_delegation_record_to_account( - &mut account, - &delegation_record, - ); - (commit_freq, delegated_to_other) - } else { - missing_delegation_record.push((pubkey, account.remote_slot())); - (None, None) - }; + let commit_freq = this.apply_delegation_record_to_account( + &mut account, + &delegation_record, + ); + (commit_freq, delegated_to_other) + } else { + missing_delegation_record + .push((pubkey, account.remote_slot())); + (None, None) + }; accounts_to_clone.push(AccountCloneRequest { pubkey, account: account.into_account_shared_data(), From de75b792066b5561052e4d8db1f12db4710f8203 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 10:38:03 +0100 Subject: [PATCH 31/33] feat: set confined --- magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs index 9de2a21eb..84c0772d0 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs @@ -481,6 +481,9 @@ where account.set_owner(delegation_record.owner); account.set_data(delegation_record.data); account.set_lamports(delegation_record.lamports); + account.set_confined( + delegation_record.authority.eq(&Pubkey::default()), + ); let is_delegated_to_us = delegation_record.authority.eq(&self.validator_pubkey); From 93439417fbd4e588d7e5e38b7926ffc05e2cb956 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Tue, 20 Jan 2026 10:57:36 +0100 Subject: [PATCH 32/33] feat: silence resolution errors --- .../src/chainlink/fetch_cloner/mod.rs | 2 +- .../src/chainlink/fetch_cloner/pipeline.rs | 85 +++++++++---------- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs index 84c0772d0..c2818855d 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/mod.rs @@ -753,7 +753,7 @@ where self, owned_by_deleg_compressed, ) - .await?; + .await; accounts_to_clone.extend(compressed_delegated_accounts); // Compute sub cancellations now since we may potentially fail during a cloning step diff --git a/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs b/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs index 1064ef62c..9a9732791 100644 --- a/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs +++ b/magicblock-chainlink/src/chainlink/fetch_cloner/pipeline.rs @@ -371,7 +371,7 @@ where pub(crate) async fn resolve_compressed_delegated_accounts( this: &FetchCloner, owned_by_deleg_compressed: Vec<(Pubkey, AccountSharedData, u64)>, -) -> ChainlinkResult> +) -> Vec where T: ChainRpcClient, U: ChainPubsubClient, @@ -381,48 +381,47 @@ where { owned_by_deleg_compressed .into_iter() - .map(|(pubkey, mut account, _)| { - let delegation_record = - CompressedDelegationRecord::try_from_slice(account.data()) - .map_err(|err| { - error!( - "Failed to deserialize compressed delegation record for {pubkey}: {err}\nAccount: {:?}", - account - ); - ChainlinkError::DelegatedAccountResolutionsFailed( - format!( - "Failed to deserialize compressed delegation record for {pubkey}: {err}" - ), - ) - })?; - - account.set_compressed(true); - account.set_lamports(delegation_record.lamports); - account.set_owner(delegation_record.owner); - account.set_data(delegation_record.data); - account.set_delegated( - delegation_record - .authority - .eq(&this.validator_pubkey), - ); - account.set_confined(delegation_record.authority.eq(&Pubkey::default())); - - let delegated_to_other = - this.get_delegated_to_other(&DelegationRecord { - authority: delegation_record.authority, - owner: delegation_record.owner, - delegation_slot: delegation_record.delegation_slot, - lamports: delegation_record.lamports, - commit_frequency_ms: 0, - }); - Ok(AccountCloneRequest { - pubkey, - account, - commit_frequency_ms: None, - delegated_to_other, - }) - }) - .collect::>>() + .filter_map(|(pubkey, mut account, _)| { + match CompressedDelegationRecord::try_from_slice(account.data()) { + Ok(delegation_record) => { + account.set_compressed(true); + account.set_lamports(delegation_record.lamports); + account.set_owner(delegation_record.owner); + account.set_data(delegation_record.data); + account.set_delegated( + delegation_record + .authority + .eq(&this.validator_pubkey), + ); + account.set_confined( + delegation_record.authority.eq(&Pubkey::default()) + ); + + let delegated_to_other = this.get_delegated_to_other(&DelegationRecord { + authority: delegation_record.authority, + owner: delegation_record.owner, + delegation_slot: delegation_record.delegation_slot, + lamports: delegation_record.lamports, + commit_frequency_ms: 0, + }); + + Some(AccountCloneRequest { + pubkey, + account, + commit_frequency_ms: None, + delegated_to_other, + }) + } + Err(err) => { + error!( + "Failed to deserialize compressed delegation record for {pubkey}: {err}\nAccount: {:?}", + account + ); + None + } + } + }) + .collect::>() } /// Resolves program accounts, fetching program data accounts for LoaderV3 programs From 07a577b82042461b2842e45a2bbd9ecb994aa330 Mon Sep 17 00:00:00 2001 From: Dodecahedr0x Date: Fri, 23 Jan 2026 12:27:55 +0100 Subject: [PATCH 33/33] fix: set remote slot --- magicblock-chainlink/tests/05_redeleg_other_same_slot.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/magicblock-chainlink/tests/05_redeleg_other_same_slot.rs b/magicblock-chainlink/tests/05_redeleg_other_same_slot.rs index add53d321..d00564571 100644 --- a/magicblock-chainlink/tests/05_redeleg_other_same_slot.rs +++ b/magicblock-chainlink/tests/05_redeleg_other_same_slot.rs @@ -7,10 +7,7 @@ use magicblock_chainlink::{ assert_cloned_as_delegated, assert_cloned_as_undelegated, assert_not_subscribed, assert_remain_undelegating, assert_subscribed_without_delegation_record, - testing::{ - accounts::account_shared_with_owner, deleg::add_delegation_record_for, - init_logger, - }, + testing::{deleg::add_delegation_record_for, init_logger}, }; use solana_account::Account; use solana_program::clock::Slot; @@ -184,9 +181,10 @@ async fn test_undelegate_redelegate_to_other_in_same_slot_compressed() { .await; // Update account to be delegated on chain and send a sub update let acc = rpc_client.get_account_at_slot(&pubkey).unwrap(); - let delegated_acc = account_shared_with_owner( + let delegated_acc = account_shared_with_owner_and_slot( &acc.account, compressed_delegation_client::id(), + slot, ); let updated = ctx .send_and_receive_account_update(